- Delete old Vite+Svelte frontend - Initialize new SvelteKit project with TypeScript - Configure Tailwind CSS v4 + DaisyUI - Implement JWT authentication with auto-refresh - Create login page with form validation (Zod) - Add protected route guards - Update Docker configuration for single-stage build - Add E2E tests with Playwright (6/11 passing) - Fix Svelte 5 reactivity with $state() runes Known issues: - 5 E2E tests failing (timing/async issues) - Token refresh implementation needs debugging - Validation error display timing
151 lines
5.5 KiB
JavaScript
151 lines
5.5 KiB
JavaScript
import { domainOf } from "./domain.js";
|
|
import { serializePrimitive } from "./primitive.js";
|
|
import { stringAndSymbolicEntriesOf } from "./records.js";
|
|
import { isDotAccessible, register } from "./registry.js";
|
|
export const snapshot = (data, opts = {}) => _serialize(data, {
|
|
onUndefined: `$ark.undefined`,
|
|
onBigInt: n => `$ark.bigint-${n}`,
|
|
...opts
|
|
}, []);
|
|
export const print = (data, opts) => console.log(printable(data, opts));
|
|
export const printable = (data, opts) => {
|
|
switch (domainOf(data)) {
|
|
case "object":
|
|
const o = data;
|
|
const ctorName = o.constructor?.name ?? "Object";
|
|
return (ctorName === "Object" || ctorName === "Array" ?
|
|
opts?.quoteKeys === false ?
|
|
stringifyUnquoted(o, opts?.indent ?? 0, "")
|
|
: JSON.stringify(_serialize(o, printableOpts, []), null, opts?.indent)
|
|
: stringifyUnquoted(o, opts?.indent ?? 0, ""));
|
|
case "symbol":
|
|
return printableOpts.onSymbol(data);
|
|
default:
|
|
return serializePrimitive(data);
|
|
}
|
|
};
|
|
const stringifyUnquoted = (value, indent, currentIndent) => {
|
|
if (typeof value === "function")
|
|
return printableOpts.onFunction(value);
|
|
if (typeof value !== "object" || value === null)
|
|
return serializePrimitive(value);
|
|
const nextIndent = currentIndent + " ".repeat(indent);
|
|
if (Array.isArray(value)) {
|
|
if (value.length === 0)
|
|
return "[]";
|
|
const items = value
|
|
.map(item => stringifyUnquoted(item, indent, nextIndent))
|
|
.join(",\n" + nextIndent);
|
|
return indent ? `[\n${nextIndent}${items}\n${currentIndent}]` : `[${items}]`;
|
|
}
|
|
const ctorName = value.constructor?.name ?? "Object";
|
|
if (ctorName === "Object") {
|
|
const keyValues = stringAndSymbolicEntriesOf(value).map(([key, val]) => {
|
|
const stringifiedKey = typeof key === "symbol" ? printableOpts.onSymbol(key)
|
|
: isDotAccessible(key) ? key
|
|
: JSON.stringify(key);
|
|
const stringifiedValue = stringifyUnquoted(val, indent, nextIndent);
|
|
return `${nextIndent}${stringifiedKey}: ${stringifiedValue}`;
|
|
});
|
|
if (keyValues.length === 0)
|
|
return "{}";
|
|
return indent ?
|
|
`{\n${keyValues.join(",\n")}\n${currentIndent}}`
|
|
: `{${keyValues.join(", ")}}`;
|
|
}
|
|
if (value instanceof Date)
|
|
return describeCollapsibleDate(value);
|
|
if ("expression" in value && typeof value.expression === "string")
|
|
return value.expression;
|
|
return ctorName;
|
|
};
|
|
const printableOpts = {
|
|
onCycle: () => "(cycle)",
|
|
onSymbol: v => `Symbol(${register(v)})`,
|
|
onFunction: v => `Function(${register(v)})`
|
|
};
|
|
const _serialize = (data, opts, seen) => {
|
|
switch (domainOf(data)) {
|
|
case "object": {
|
|
const o = data;
|
|
if ("toJSON" in o && typeof o.toJSON === "function")
|
|
return o.toJSON();
|
|
if (typeof o === "function")
|
|
return printableOpts.onFunction(o);
|
|
if (seen.includes(o))
|
|
return "(cycle)";
|
|
const nextSeen = [...seen, o];
|
|
if (Array.isArray(o))
|
|
return o.map(item => _serialize(item, opts, nextSeen));
|
|
if (o instanceof Date)
|
|
return o.toDateString();
|
|
const result = {};
|
|
for (const k in o)
|
|
result[k] = _serialize(o[k], opts, nextSeen);
|
|
for (const s of Object.getOwnPropertySymbols(o)) {
|
|
result[opts.onSymbol?.(s) ?? s.toString()] = _serialize(o[s], opts, nextSeen);
|
|
}
|
|
return result;
|
|
}
|
|
case "symbol":
|
|
return printableOpts.onSymbol(data);
|
|
case "bigint":
|
|
return opts.onBigInt?.(data) ?? `${data}n`;
|
|
case "undefined":
|
|
return opts.onUndefined ?? "undefined";
|
|
case "string":
|
|
return data.replace(/\\/g, "\\\\");
|
|
default:
|
|
return data;
|
|
}
|
|
};
|
|
/**
|
|
* Converts a Date instance to a human-readable description relative to its precision
|
|
*/
|
|
export const describeCollapsibleDate = (date) => {
|
|
const year = date.getFullYear();
|
|
const month = date.getMonth();
|
|
const dayOfMonth = date.getDate();
|
|
const hours = date.getHours();
|
|
const minutes = date.getMinutes();
|
|
const seconds = date.getSeconds();
|
|
const milliseconds = date.getMilliseconds();
|
|
if (month === 0 &&
|
|
dayOfMonth === 1 &&
|
|
hours === 0 &&
|
|
minutes === 0 &&
|
|
seconds === 0 &&
|
|
milliseconds === 0)
|
|
return `${year}`;
|
|
const datePortion = `${months[month]} ${dayOfMonth}, ${year}`;
|
|
if (hours === 0 && minutes === 0 && seconds === 0 && milliseconds === 0)
|
|
return datePortion;
|
|
let timePortion = date.toLocaleTimeString();
|
|
const suffix = timePortion.endsWith(" AM") || timePortion.endsWith(" PM") ?
|
|
timePortion.slice(-3)
|
|
: "";
|
|
if (suffix)
|
|
timePortion = timePortion.slice(0, -suffix.length);
|
|
if (milliseconds)
|
|
timePortion += `.${pad(milliseconds, 3)}`;
|
|
else if (timeWithUnnecessarySeconds.test(timePortion))
|
|
timePortion = timePortion.slice(0, -3);
|
|
return `${timePortion + suffix}, ${datePortion}`;
|
|
};
|
|
const months = [
|
|
"January",
|
|
"February",
|
|
"March",
|
|
"April",
|
|
"May",
|
|
"June",
|
|
"July",
|
|
"August",
|
|
"September",
|
|
"October",
|
|
"November",
|
|
"December"
|
|
];
|
|
const timeWithUnnecessarySeconds = /:\d\d:00$/;
|
|
const pad = (value, length) => String(value).padStart(length, "0");
|