- 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
128 lines
5.2 KiB
JavaScript
128 lines
5.2 KiB
JavaScript
import { hasDomain, isThunk, omit, printable, throwParseError } from "@ark/util";
|
|
import { intrinsic } from "../intrinsic.js";
|
|
import { compileSerializedValue } from "../shared/compile.js";
|
|
import { ArkErrors } from "../shared/errors.js";
|
|
import { defaultValueSerializer, implementNode } from "../shared/implement.js";
|
|
import { registeredReference } from "../shared/registry.js";
|
|
import { traverseKey } from "../shared/traversal.js";
|
|
import { BaseProp, intersectProps } from "./prop.js";
|
|
const implementation = implementNode({
|
|
kind: "optional",
|
|
hasAssociatedError: false,
|
|
intersectionIsOpen: true,
|
|
keys: {
|
|
key: {},
|
|
value: {
|
|
child: true,
|
|
parse: (schema, ctx) => ctx.$.parseSchema(schema)
|
|
},
|
|
default: {
|
|
preserveUndefined: true
|
|
}
|
|
},
|
|
normalize: schema => schema,
|
|
reduce: (inner, $) => {
|
|
if ($.resolvedConfig.exactOptionalPropertyTypes === false) {
|
|
if (!inner.value.allows(undefined)) {
|
|
return $.node("optional", { ...inner, value: inner.value.or(intrinsic.undefined) }, { prereduced: true });
|
|
}
|
|
}
|
|
},
|
|
defaults: {
|
|
description: node => `${node.compiledKey}?: ${node.value.description}`
|
|
},
|
|
intersections: {
|
|
optional: intersectProps
|
|
}
|
|
});
|
|
export class OptionalNode extends BaseProp {
|
|
constructor(...args) {
|
|
super(...args);
|
|
if ("default" in this.inner)
|
|
assertDefaultValueAssignability(this.value, this.inner.default, this.key);
|
|
}
|
|
get rawIn() {
|
|
const baseIn = super.rawIn;
|
|
if (!this.hasDefault())
|
|
return baseIn;
|
|
return this.$.node("optional", omit(baseIn.inner, { default: true }), {
|
|
prereduced: true
|
|
});
|
|
}
|
|
get outProp() {
|
|
if (!this.hasDefault())
|
|
return this;
|
|
const { default: defaultValue, ...requiredInner } = this.inner;
|
|
return this.cacheGetter("outProp", this.$.node("required", requiredInner, { prereduced: true }));
|
|
}
|
|
expression = this.hasDefault() ?
|
|
`${this.compiledKey}: ${this.value.expression} = ${printable(this.inner.default)}`
|
|
: `${this.compiledKey}?: ${this.value.expression}`;
|
|
defaultValueMorph = getDefaultableMorph(this);
|
|
defaultValueMorphRef = this.defaultValueMorph && registeredReference(this.defaultValueMorph);
|
|
}
|
|
export const Optional = {
|
|
implementation,
|
|
Node: OptionalNode
|
|
};
|
|
const defaultableMorphCache = {};
|
|
const getDefaultableMorph = (node) => {
|
|
if (!node.hasDefault())
|
|
return;
|
|
const cacheKey = `{${node.compiledKey}: ${node.value.id} = ${defaultValueSerializer(node.default)}}`;
|
|
return (defaultableMorphCache[cacheKey] ??= computeDefaultValueMorph(node.key, node.value, node.default));
|
|
};
|
|
export const computeDefaultValueMorph = (key, value, defaultInput) => {
|
|
if (typeof defaultInput === "function") {
|
|
// if the value has a morph, pipe context through it
|
|
return value.includesTransform ?
|
|
(data, ctx) => {
|
|
traverseKey(key, () => value((data[key] = defaultInput()), ctx), ctx);
|
|
return data;
|
|
}
|
|
: data => {
|
|
data[key] = defaultInput();
|
|
return data;
|
|
};
|
|
}
|
|
// non-functional defaults can be safely cached as long as the morph is
|
|
// guaranteed to be pure and the output is primitive
|
|
const precomputedMorphedDefault = value.includesTransform ? value.assert(defaultInput) : defaultInput;
|
|
return hasDomain(precomputedMorphedDefault, "object") ?
|
|
// the type signature only allows this if the value was morphed
|
|
(data, ctx) => {
|
|
traverseKey(key, () => value((data[key] = defaultInput), ctx), ctx);
|
|
return data;
|
|
}
|
|
: data => {
|
|
data[key] = precomputedMorphedDefault;
|
|
return data;
|
|
};
|
|
};
|
|
export const assertDefaultValueAssignability = (node, value, key) => {
|
|
const wrapped = isThunk(value);
|
|
if (hasDomain(value, "object") && !wrapped)
|
|
throwParseError(writeNonPrimitiveNonFunctionDefaultValueMessage(key));
|
|
// if the node has a default value, finalize it and apply JIT optimizations
|
|
// if applicable to ensure behavior + error logging is externally consistent
|
|
// (using .in here insead of .rawIn triggers finalization)
|
|
const out = node.in(wrapped ? value() : value);
|
|
if (out instanceof ArkErrors) {
|
|
if (key === null) {
|
|
// e.g. "Default must be assignable to number (was string)"
|
|
throwParseError(`Default ${out.summary}`);
|
|
}
|
|
const atPath = out.transform(e => e.transform(input => ({ ...input, prefixPath: [key] })));
|
|
// e.g. "Default for bar must be assignable to number (was string)"
|
|
// e.g. "Default for value at [0] must be assignable to number (was string)"
|
|
throwParseError(`Default for ${atPath.summary}`);
|
|
}
|
|
return value;
|
|
};
|
|
export const writeNonPrimitiveNonFunctionDefaultValueMessage = (key) => {
|
|
const keyDescription = key === null ? ""
|
|
: typeof key === "number" ? `for value at [${key}] `
|
|
: `for ${compileSerializedValue(key)} `;
|
|
return `Non-primitive default ${keyDescription}must be specified as a function like () => ({my: 'object'})`;
|
|
};
|