@kudzujs/core 0.8.21 → 0.8.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/MIGRATION_ROADMAP.md +20 -0
- package/PERFORMANCE.md +48 -0
- package/README.md +1 -1
- package/RELEASES.md +71 -0
- package/docs/next-architecture/README.md +3 -3
- package/docs/next-architecture/compiler-current-architecture.md +25 -25
- package/docs/next-architecture/goal-a-compiler-foundation.md +13 -16
- package/docs/next-architecture/versioning.md +4 -3
- package/framework/README.md +5 -2
- package/framework/build.mjs +234 -3489
- package/framework/compiler/list-runtime-codegen.mjs +95 -0
- package/framework/compiler/param-codegen.mjs +72 -0
- package/framework/compiler/path-helpers.mjs +18 -0
- package/framework/compiler/route-capability-planner.mjs +35 -0
- package/framework/compiler/runtime-codegen.mjs +146 -0
- package/framework/compiler/source-compiler.mjs +2969 -0
- package/framework/compiler/source-graph.mjs +29 -0
- package/framework/compiler/worker-compiler.mjs +9 -2
- package/framework/core.d.ts +41 -38
- package/framework/core.mjs +2 -1
- package/framework/dev-server.mjs +1 -8
- package/package.json +1 -1
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { assertCapabilityIR } from "./route-capability-planner.mjs"
|
|
2
|
+
|
|
3
|
+
export function generateListRuntime(source, capabilityIR) {
|
|
4
|
+
assertCapabilityIR(capabilityIR)
|
|
5
|
+
const { lists, effects } = capabilityIR
|
|
6
|
+
let runtime = replaceRequired(source, '"./shared-runtime.js"', '"./kudzu.js"', "shared runtime import")
|
|
7
|
+
runtime = lists.calculated
|
|
8
|
+
? replaceRequired(runtime, '"./binding-runtime.js"', '"./kudzu-binding.js"', "binding runtime import")
|
|
9
|
+
: replaceRequired(runtime, /^const loadListEvaluator[^\n]+\n/m, "", "list evaluator loader")
|
|
10
|
+
runtime = lists.selectors
|
|
11
|
+
? replaceRequired(runtime, '"./collection-selector.js"', '"./kudzu-collection-selector.js"', "collection selector import")
|
|
12
|
+
: replaceRequired(runtime, /^import \{ selectCollection \}[^\n]+\n/m, "", "collection selector import")
|
|
13
|
+
if (!lists.indexes) {
|
|
14
|
+
runtime = replaceSequenceRequired(runtime, [
|
|
15
|
+
["for (const [index, item] of items.entries()) {", "for (const item of items) {", "item iteration"],
|
|
16
|
+
["const key = list.descriptor.key === null ? index : item?.[list.descriptor.key]", "const key = item?.[list.descriptor.key]", "index key"],
|
|
17
|
+
["entries.push({ item, index, key, token, value:", "entries.push({ item, key, token, value:", "indexed entry"],
|
|
18
|
+
["for (const { item, index, key, token, value } of entries) {", "for (const { item, key, token, value } of entries) {", "indexed entry loop"],
|
|
19
|
+
["fillListItem(node, item, list.descriptor.nested, index)", "fillListItem(node, item, list.descriptor.nested)", "indexed item fill", true],
|
|
20
|
+
["fillListItem(node, item, list.descriptor.nested, index, mapListItemParts", "fillListItem(node, item, list.descriptor.nested, 0, mapListItemParts", "mapped item fill"],
|
|
21
|
+
["function addListRoot(list, { item, index = list.roots.size, key, token, value })", "function addListRoot(list, { item, key, token, value })", "indexed list root"],
|
|
22
|
+
["fillListParts(root, listItemParts(root), listItems.get(owner), 0, __KUDZU_LIST_INDEXES__ ? listIndexes.get(owner) ?? 0 : 0)", "fillListParts(root, listItemParts(root), listItems.get(owner), 0)", "indexed root parts"],
|
|
23
|
+
["fillListParts(root, parts, item, revision, index, previous)", "fillListParts(root, parts, item, revision, previous)", "indexed fill call"],
|
|
24
|
+
["function fillListParts(root, parts, item, revision, index = 0, previous)", "function fillListParts(root, parts, item, revision, previous)", "indexed parts signature"],
|
|
25
|
+
["fillListExpressions(root, parts, item, revision, index)", "fillListExpressions(root, parts, item, revision)", "indexed expressions"],
|
|
26
|
+
['value?.type === "list-item" ? serializeItem(item) : value?.type === "list-index" ? index : value', 'value?.type === "list-item" ? serializeItem(item) : value', "index capture", true],
|
|
27
|
+
["evaluate(descriptor, item, index)", "evaluate(descriptor, item)", "indexed evaluator", true],
|
|
28
|
+
["updateListCondition(marker, descriptor.kind, value, item, index)", "updateListCondition(marker, descriptor.kind, value, item)", "indexed condition"],
|
|
29
|
+
["function updateListCondition(marker, kind, value, item, index)", "function updateListCondition(marker, kind, value, item)", "indexed condition signature"],
|
|
30
|
+
["fillListParts(marker, listItemParts(fragment), item, revision, index)", "fillListParts(marker, listItemParts(fragment), item, revision)", "indexed condition parts"],
|
|
31
|
+
["exports[descriptor.handler](item, index, {", "exports[descriptor.handler](item, undefined, {", "indexed state handler call"]
|
|
32
|
+
])
|
|
33
|
+
}
|
|
34
|
+
if (!lists.selectors) runtime = replaceRequired(runtime, " && !list.descriptor.selector", "", "selector guards", true)
|
|
35
|
+
if (!lists.indexes) runtime = replaceSequenceRequired(runtime, [
|
|
36
|
+
[" && !list.descriptor.indexed", "", "indexed guards", true],
|
|
37
|
+
[" && list.descriptor.key !== null", "", "key guards", true],
|
|
38
|
+
["(referenceOnly ? listItems.get(node) !== item : list.values.get(token) !== value) || list.descriptor.indexed || list.descriptor.key === null", "referenceOnly ? listItems.get(node) !== item : list.values.get(token) !== value", "indexed value comparison"]
|
|
39
|
+
])
|
|
40
|
+
if (lists.rowHooks && !lists.generalRowHooks) runtime = replaceSequenceRequired(replaceRequired(runtime, /\/\* general-row-hooks \*\/[\s\S]*?\/\* general-row-hooks-end \*\/\n/, "", "general row hooks"), [
|
|
41
|
+
["initializeGeneralRowHooks", "initializeRowStates", "general row initializer", true],
|
|
42
|
+
["if (__KUDZU_LIST_ROW_HOOKS__) for (let index = 0; index < roots.length; index++) initializeRowStates(descriptor, descriptor.keys[index], roots[index], nested?.owner)", "if (__KUDZU_LIST_ROW_HOOKS__ && descriptor.rowStates) for (let index = 0; index < roots.length; index++) initializeRowStates(descriptor, descriptor.keys[index])", "flat row initialization"],
|
|
43
|
+
["if (__KUDZU_LIST_ROW_HOOKS__) initializeRowStates(list.descriptor, key, node, list.owner)", "if (__KUDZU_LIST_ROW_HOOKS__ && list.descriptor.rowStates) initializeRowStates(list.descriptor, key, node)", "flat row add", true],
|
|
44
|
+
["for (const node of registration.list.roots.values()) deleteRowStates(registration.list.descriptor, ownershipPaths.get(node))", "for (const token of registration.list.roots.keys()) deleteFlatRowStates(registration.list.descriptor, token)", "flat registration cleanup"],
|
|
45
|
+
["deleteRowStates(list.descriptor, ownershipPaths.get(node))", "deleteFlatRowStates(list.descriptor, token)", "flat row cleanup", true],
|
|
46
|
+
[" if (__KUDZU_LIST_ROW_HOOKS__) replaceRowIds(root, rowReplacements.get(root))\n", "", "row ID replacement"],
|
|
47
|
+
[" if (!replacements) return\n", "", "row replacement guard"]
|
|
48
|
+
])
|
|
49
|
+
if (!effects.itemDependencies) runtime = replaceRequired(runtime, ", notifyListItem", "", "item notification import")
|
|
50
|
+
if (!lists.stableFastPaths) runtime = replaceRequired(runtime, /\/\* stable-list-fast-path \*\/[\s\S]*?\/\* stable-list-fast-path-end \*\/\n/, "", "stable list fast path")
|
|
51
|
+
const stylePatch = ` if (target === "style") {
|
|
52
|
+
const style = serializeStyle(value)
|
|
53
|
+
if (style) node.setAttribute("style", style)
|
|
54
|
+
else node.removeAttribute("style")
|
|
55
|
+
return
|
|
56
|
+
}`
|
|
57
|
+
runtime = replaceRequired(runtime, " /* list-style */", lists.styleCount ? stylePatch : "", "list style")
|
|
58
|
+
if (lists.styleCount) runtime = `import { serializeStyle } from "./kudzu-style.js"\n${runtime}`
|
|
59
|
+
return {
|
|
60
|
+
source: runtime,
|
|
61
|
+
define: {
|
|
62
|
+
__KUDZU_LIST_CONDITIONS__: String(lists.conditions),
|
|
63
|
+
__KUDZU_DEEP_LIST_CONDITIONS__: String(lists.deepConditions),
|
|
64
|
+
__KUDZU_LIST_TEXT_RANGES__: String(lists.textRanges),
|
|
65
|
+
__KUDZU_LIST_ATTRIBUTES__: String(lists.attributes),
|
|
66
|
+
__KUDZU_LIST_EVENTS__: String(lists.events),
|
|
67
|
+
__KUDZU_LIST_EXPRESSIONS__: String(lists.expressions),
|
|
68
|
+
__KUDZU_LIST_EXPRESSION_ATTRIBUTES__: String(lists.expressionAttributes),
|
|
69
|
+
__KUDZU_LIST_SEEDS__: String(lists.seeds),
|
|
70
|
+
__KUDZU_LIST_EFFECTS__: String(lists.effects),
|
|
71
|
+
__KUDZU_LIST_ASYNC_PARTS__: String(lists.asyncParts),
|
|
72
|
+
__KUDZU_LIST_MOUNTS__: String(lists.mounts),
|
|
73
|
+
__KUDZU_LIST_ITEM_HOOKS__: String(effects.itemDependencies),
|
|
74
|
+
__KUDZU_LIST_ROW_HOOKS__: String(lists.rowHooks),
|
|
75
|
+
__KUDZU_LIST_ROW_REFS__: String(lists.rowRefs),
|
|
76
|
+
__KUDZU_COMPLEX_LIST_ROW_STATE__: String(lists.complexRowState),
|
|
77
|
+
__KUDZU_NESTED_LISTS__: String(lists.nested),
|
|
78
|
+
__KUDZU_COLLECTION_SELECTORS__: String(lists.selectors),
|
|
79
|
+
__KUDZU_STATIC_COLLECTIONS__: String(lists.static),
|
|
80
|
+
__KUDZU_LIST_INDEXES__: String(lists.indexes),
|
|
81
|
+
__KUDZU_LIST_STABLE_FAST_PATHS__: String(lists.stableFastPaths),
|
|
82
|
+
__KUDZU_SVG_LISTS__: String(lists.svg)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function replaceRequired(source, search, replacement, label, all = false) {
|
|
88
|
+
const output = all ? source.replaceAll(search, replacement) : source.replace(search, replacement)
|
|
89
|
+
if (output === source) throw new Error(`${label} specialization did not match list-runtime.js`)
|
|
90
|
+
return output
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function replaceSequenceRequired(source, replacements) {
|
|
94
|
+
return replacements.reduce((output, [search, replacement, label, all]) => replaceRequired(output, search, replacement, label, all), source)
|
|
95
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { join } from "node:path"
|
|
2
|
+
|
|
3
|
+
export function createParamCodegen({ browserPath, inlineJson, relativeModulePath }) {
|
|
4
|
+
return function printParamEntry(schema, params, searchParams, searchParamsWritable, output, assetsDirectory, base, runtimeName, navigable) {
|
|
5
|
+
const hasSearch = searchParams.length || searchParamsWritable
|
|
6
|
+
const signature = hasSearch ? "pathname, search" : "pathname"
|
|
7
|
+
const prefix = navigable ? `export function initializeParams(${signature}) {\n${searchParamsWritable ? "globalThis.__kSetSearchParams = setSearchParams\n" : ""}` : `${schema ? "let pathname = location.pathname\n" : ""}${hasSearch ? "let search = location.search\n" : ""}`
|
|
8
|
+
const suffix = navigable ? "\n}" : ""
|
|
9
|
+
const pathname = schema ? `const base = ${inlineJson(browserPath(base).slice(1).split("/").filter(Boolean).map(segment => decodeURIComponent(segment)))}
|
|
10
|
+
const schema = ${inlineJson(schema.segments)}
|
|
11
|
+
const params = ${inlineJson(params)}
|
|
12
|
+
let path = pathname
|
|
13
|
+
if (base.length) {
|
|
14
|
+
const pathSegments = path.slice(1).split("/")
|
|
15
|
+
if (pathSegments.length < base.length || base.some((segment, index) => decodeSegment(pathSegments[index], false) !== segment)) throw new Error("Runtime route is outside the configured base")
|
|
16
|
+
path = "/" + pathSegments.slice(base.length).join("/")
|
|
17
|
+
}
|
|
18
|
+
if (path.length > 1 && path.endsWith("/")) path = path.slice(0, -1)
|
|
19
|
+
const segments = path.slice(1).split("/")
|
|
20
|
+
if (segments.length !== schema.length) throw new Error("Runtime route does not match its fallback pattern")
|
|
21
|
+
const values = Object.create(null)
|
|
22
|
+
for (let index = 0; index < schema.length; index++) {
|
|
23
|
+
const segment = schema[index]
|
|
24
|
+
const value = decodeSegment(segments[index], Boolean(segment.param))
|
|
25
|
+
if (segment.literal !== undefined && value !== segment.literal) throw new Error("Runtime route literal does not match")
|
|
26
|
+
if (segment.param) values[segment.param] = value
|
|
27
|
+
}
|
|
28
|
+
for (const param of params) {
|
|
29
|
+
const value = values[param.name]
|
|
30
|
+
browserState.set(param.id, value)
|
|
31
|
+
commitDom(param.id, value)
|
|
32
|
+
}
|
|
33
|
+
function decodeSegment(raw, param) {
|
|
34
|
+
if (param && /%(?:2f|5c)/i.test(raw)) throw new Error("Runtime route parameter contains an encoded separator")
|
|
35
|
+
let value
|
|
36
|
+
try { value = decodeURIComponent(raw) } catch { throw new Error("Runtime route parameter has malformed encoding") }
|
|
37
|
+
const decodedDots = value.replace(/%2e/gi, ".")
|
|
38
|
+
if (param && (!value || value === "." || value === ".." || decodedDots === "." || decodedDots === ".." || /[\\/?#]/.test(value) || [...value].some(character => character.charCodeAt(0) < 32 || character.charCodeAt(0) >= 127 && character.charCodeAt(0) <= 159) || /%(?:2f|5c)/i.test(value))) throw new Error("Runtime route parameter is invalid")
|
|
39
|
+
return value
|
|
40
|
+
}
|
|
41
|
+
` : ""
|
|
42
|
+
const searchInitializer = searchParamsWritable && searchParams.length ? `function initializeSearch(search) {
|
|
43
|
+
const query = new URLSearchParams(search)
|
|
44
|
+
for (const param of ${inlineJson(searchParams)}) {
|
|
45
|
+
const value = query.get(param.name)
|
|
46
|
+
browserState.set(param.id, value)
|
|
47
|
+
commitDom(param.id, value)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
` : ""
|
|
51
|
+
const query = searchParams.length ? searchParamsWritable ? "initializeSearch(search)\n" : `const query = new URLSearchParams(search)
|
|
52
|
+
for (const param of ${inlineJson(searchParams)}) {
|
|
53
|
+
const value = query.get(param.name)
|
|
54
|
+
browserState.set(param.id, value)
|
|
55
|
+
commitDom(param.id, value)
|
|
56
|
+
}
|
|
57
|
+
` : ""
|
|
58
|
+
const writer = searchParamsWritable ? `
|
|
59
|
+
function setSearchParams(update, replace) {
|
|
60
|
+
const next = update(new URLSearchParams(location.search))
|
|
61
|
+
if (!(next instanceof URLSearchParams)) throw new Error("React Router search parameter updater must return URLSearchParams")
|
|
62
|
+
const url = new URL(location.href)
|
|
63
|
+
url.search = next.toString()
|
|
64
|
+
history[replace ? "replaceState" : "pushState"](null, "", url)
|
|
65
|
+
${searchParams.length ? "initializeSearch(location.search)" : ""}
|
|
66
|
+
}
|
|
67
|
+
${navigable ? "" : `globalThis.__kSetSearchParams = setSearchParams
|
|
68
|
+
addEventListener("popstate", () => ${searchParams.length ? "initializeSearch(location.search)" : "undefined"})`}` : ""
|
|
69
|
+
return `import { browserState, commitDom } from ${JSON.stringify(relativeModulePath(output, join(assetsDirectory, runtimeName)))}
|
|
70
|
+
${searchInitializer}${prefix}${pathname}${query}${suffix}${writer}`
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { dirname, relative } from "node:path"
|
|
2
|
+
|
|
3
|
+
export function relativeModulePath(from, to) {
|
|
4
|
+
const path = relative(dirname(from), to).replaceAll("\\", "/")
|
|
5
|
+
return path.startsWith(".") ? path : `./${path}`
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function browserPath(path) {
|
|
9
|
+
return path ? new URL(path, "http://kudzu.local").pathname : ""
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function assetPath(base, path) {
|
|
13
|
+
return `${base}/${path}`
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function withBase(base, path) {
|
|
17
|
+
return base ? `${base}${path}` : path
|
|
18
|
+
}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
export function usesRouteDependencyRuntime({ plan, navigable, hasBindings, hasLists }) {
|
|
2
|
+
assertRouteIR(plan)
|
|
2
3
|
const hasDependencies = plan.effects.some(effect => effect.dependencies?.length)
|
|
3
4
|
return !navigable && hasDependencies && !plan.effects.some(effect => effect.owner) && !hasBindings && !hasLists && !plan.events.some(event => event.native)
|
|
4
5
|
}
|
|
5
6
|
|
|
6
7
|
export function planRouteCapabilities(plans, { routes = new Map(), navigationRouteCount = 0 } = {}) {
|
|
8
|
+
for (const plan of plans) assertRouteIR(plan)
|
|
7
9
|
const commandEvents = new Set()
|
|
8
10
|
const nativeEvents = new Set()
|
|
9
11
|
const bindings = { count: 0, text: false, svgConditions: false }
|
|
@@ -90,6 +92,7 @@ export function planRouteCapabilities(plans, { routes = new Map(), navigationRou
|
|
|
90
92
|
lists.mounts ||= lists.conditions || lists.nested
|
|
91
93
|
|
|
92
94
|
return {
|
|
95
|
+
version: 1,
|
|
93
96
|
routes: routeCounts,
|
|
94
97
|
events: { command: [...commandEvents].sort(), native: [...nativeEvents].sort(), hasNativeHandlers: nativeEvents.size > 0 },
|
|
95
98
|
bindings,
|
|
@@ -103,6 +106,38 @@ export function planRouteCapabilities(plans, { routes = new Map(), navigationRou
|
|
|
103
106
|
}
|
|
104
107
|
}
|
|
105
108
|
|
|
109
|
+
function assertRouteIR(plan) {
|
|
110
|
+
if (plan?.version !== 1) throw new Error(`Unsupported RouteIR version: ${JSON.stringify(plan?.version)}`)
|
|
111
|
+
if (!["states", "params", "searchParams", "events", "effects", "bindings", "conditions", "lists"].every(name => Array.isArray(plan[name])) || typeof plan.searchParamsWritable !== "boolean") throw new Error("Invalid RouteIR v1 structure")
|
|
112
|
+
if (plan.states.some((state, slot) => state?.slot !== slot || typeof state.id !== "string" || typeof state.name !== "string" || !Object.hasOwn(state, "initialValue") || state.lifetime !== undefined && !["layout", "route"].includes(state.lifetime) || state.internal !== undefined && state.internal !== true)) throw new Error("Invalid RouteIR v1 state")
|
|
113
|
+
if ([...plan.params, ...plan.searchParams].some(param => !isRecord(param) || typeof param.name !== "string" || typeof param.id !== "string")) throw new Error("Invalid RouteIR v1 parameter")
|
|
114
|
+
if (plan.events.some(event => !isRecord(event) || typeof event.event !== "string" || event.commands !== undefined && (!Array.isArray(event.commands) || event.commands.some(command => !Array.isArray(command))) || event.native !== undefined && (!isRecord(event.native) || typeof event.native.module !== "string" || typeof event.native.handler !== "string" || !isRecord(event.native.states) || !isRecord(event.native.scope)))) throw new Error("Invalid RouteIR v1 event")
|
|
115
|
+
if (plan.effects.some(effect => !isRecord(effect) || typeof effect.module !== "string" || typeof effect.handler !== "string" || !isRecord(effect.states) || !isRecord(effect.scope))) throw new Error("Invalid RouteIR v1 effect")
|
|
116
|
+
if (plan.bindings.some(binding => !isRecord(binding) || typeof binding.target !== "string") || plan.conditions.some(condition => !isRecord(condition) || condition.svg !== undefined && typeof condition.svg !== "boolean") || plan.lists.some(list => !isRouteList(list))) throw new Error("Invalid RouteIR v1 binding, condition, or list")
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function assertCapabilityIR(capabilityIR) {
|
|
120
|
+
if (capabilityIR?.version !== 1) throw new Error(`Unsupported CapabilityIR version: ${JSON.stringify(capabilityIR?.version)}`)
|
|
121
|
+
const sections = ["routes", "events", "bindings", "lists", "effects", "captures", "runtime"]
|
|
122
|
+
if (!sections.every(name => isRecord(capabilityIR[name]))) throw new Error("Invalid CapabilityIR v1 structure")
|
|
123
|
+
if (!["behaviors", "regularBehaviors", "regularStateSeeds", "dependencyStateSeeds"].every(name => isCount(capabilityIR.routes[name]))) throw new Error("Invalid CapabilityIR v1 route counts")
|
|
124
|
+
if (!["command", "native"].every(name => Array.isArray(capabilityIR.events[name]) && capabilityIR.events[name].every(event => typeof event === "string")) || typeof capabilityIR.events.hasNativeHandlers !== "boolean") throw new Error("Invalid CapabilityIR v1 events")
|
|
125
|
+
if (!isCount(capabilityIR.bindings.count) || !["text", "svgConditions"].every(name => typeof capabilityIR.bindings[name] === "boolean")) throw new Error("Invalid CapabilityIR v1 bindings")
|
|
126
|
+
const listFlags = ["conditions", "svg", "deepConditions", "textRanges", "attributes", "events", "expressions", "expressionAttributes", "seeds", "effects", "rowHooks", "rowRefs", "complexRowState", "nested", "selectors", "calculated", "static", "indexes", "stableFastPaths", "generalRowHooks", "asyncParts", "mounts"]
|
|
127
|
+
if (!isCount(capabilityIR.lists.count) || !isCount(capabilityIR.lists.styleCount) || !listFlags.every(name => typeof capabilityIR.lists[name] === "boolean")) throw new Error("Invalid CapabilityIR v1 lists")
|
|
128
|
+
if (!["any", "derivedDependencies", "itemDependencies", "captures", "navigable", "navigableOwners"].every(name => typeof capabilityIR.effects[name] === "boolean") || !["nestedState", "setter"].every(name => typeof capabilityIR.captures[name] === "boolean") || !["shared", "dependency"].every(name => typeof capabilityIR.runtime[name] === "boolean")) throw new Error("Invalid CapabilityIR v1 effect, capture, or runtime flags")
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const isRecord = value => value !== null && typeof value === "object" && !Array.isArray(value)
|
|
132
|
+
const isCount = value => Number.isSafeInteger(value) && value >= 0
|
|
133
|
+
const isRouteList = list => isRecord(list)
|
|
134
|
+
&& typeof list.id === "string"
|
|
135
|
+
&& typeof list.state === "string"
|
|
136
|
+
&& (typeof list.key === "string" || list.key === null)
|
|
137
|
+
&& Array.isArray(list.keys)
|
|
138
|
+
&& ["svg", "static", "indexed", "reducer", "mount", "nested", "effects", "conditions", "conditionHandlers", "textRanges", "attributes", "events", "expressions", "expressionAttributes", "fastRelease"].every(name => list[name] === undefined || list[name] === true)
|
|
139
|
+
&& ["selector", "children", "expressionStates", "rowStates", "rowConditions", "rowRefs"].every(name => list[name] === undefined || Array.isArray(list[name]))
|
|
140
|
+
|
|
106
141
|
function hasCaptureType(value, type) {
|
|
107
142
|
if (!value || typeof value !== "object") return false
|
|
108
143
|
if (value.type === type) return true
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { assertCapabilityIR } from "./route-capability-planner.mjs"
|
|
2
|
+
|
|
3
|
+
export function generateCoreRuntime(source, capabilityIR) {
|
|
4
|
+
assertCapabilityIR(capabilityIR)
|
|
5
|
+
const { effects, events, routes } = capabilityIR
|
|
6
|
+
let runtime = specializeRuntime(source, events.command, routes.regularStateSeeds > 0)
|
|
7
|
+
if (!effects.itemDependencies && capabilityIR.runtime.shared) runtime = replaceRequired(runtime, /\/\* list-item-hooks \*\/[\s\S]*?\/\* list-item-hooks-end \*\/\n/, "", "list item hooks", "shared-runtime.js")
|
|
8
|
+
if (effects.navigable) runtime = replaceRequired(runtime, "export function registerCommitter(commit) {\n committers.push(commit)\n}", "export function registerCommitter(commit) {\n committers.push(commit)\n return () => {\n const index = committers.indexOf(commit)\n if (index !== -1) committers.splice(index, 1)\n }\n}", "navigable committer", "shared-runtime.js")
|
|
9
|
+
if (effects.navigableOwners) runtime = replaceSequenceRequired(runtime, [
|
|
10
|
+
["export function registerMountHook(mount) {\n mountHooks.push(mount)\n}", "export function registerMountHook(mount) {\n mountHooks.push(mount)\n return () => {\n const index = mountHooks.indexOf(mount)\n if (index !== -1) mountHooks.splice(index, 1)\n }\n}", "navigable mount hook"],
|
|
11
|
+
["export function registerUnmountHook(unmount) {\n unmountHooks.push(unmount)\n}", "export function registerUnmountHook(unmount) {\n unmountHooks.push(unmount)\n return () => {\n const index = unmountHooks.indexOf(unmount)\n if (index !== -1) unmountHooks.splice(index, 1)\n }\n}", "navigable unmount hook"]
|
|
12
|
+
], "shared-runtime.js")
|
|
13
|
+
return runtime
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function generateEffectRuntime(source, capabilityIR) {
|
|
17
|
+
assertCapabilityIR(capabilityIR)
|
|
18
|
+
const { captures, effects } = capabilityIR
|
|
19
|
+
return {
|
|
20
|
+
source: effects.captures ? replaceRequired(source, '"./serialization.js"', '"./kudzu-serialization.js"', "serialization import", "effect-runtime.js") : replaceRequired(source, /^import[^\n]+\n/, "", "serialization import", "effect-runtime.js"),
|
|
21
|
+
define: {
|
|
22
|
+
"globalThis.__KUDZU_CAPTURE_SETTER__": String(captures.setter),
|
|
23
|
+
"globalThis.__KUDZU_EFFECT_CAPTURES__": String(effects.captures)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function generateBindingRuntime(source, capabilityIR, navigable) {
|
|
29
|
+
assertCapabilityIR(capabilityIR)
|
|
30
|
+
let runtime = replaceRequired(source, '"./shared-runtime.js"', '"./kudzu.js"', "shared runtime import", "binding-runtime.js")
|
|
31
|
+
runtime = replaceRequired(runtime, '"./serialization.js"', '"./kudzu-serialization.js"', "serialization import", "binding-runtime.js")
|
|
32
|
+
runtime = replaceRequired(runtime, '"./style.js"', '"./kudzu-style.js"', "style import", "binding-runtime.js")
|
|
33
|
+
if (navigable) runtime = specializeNavigationTextDescriptors(runtime)
|
|
34
|
+
return {
|
|
35
|
+
source: runtime,
|
|
36
|
+
define: {
|
|
37
|
+
"globalThis.__KUDZU_TEXT_BINDINGS__": String(capabilityIR.bindings.text),
|
|
38
|
+
"globalThis.__KUDZU_SVG_CONDITIONS__": String(capabilityIR.bindings.svgConditions),
|
|
39
|
+
"globalThis.__KUDZU_CAPTURE_STATE__": String(capabilityIR.captures.nestedState)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function generateNativeRuntime(source, capabilityIR) {
|
|
45
|
+
assertCapabilityIR(capabilityIR)
|
|
46
|
+
return {
|
|
47
|
+
source: specializeEvents(replaceRequired(replaceRequired(source, '"./shared-runtime.js"', '"./kudzu.js"', "shared runtime import", "native-runtime.js"), '"./serialization.js"', '"./kudzu-serialization.js"', "serialization import", "native-runtime.js"), capabilityIR.events.native),
|
|
48
|
+
define: { "globalThis.__KUDZU_CAPTURE_SETTER__": String(capabilityIR.captures.setter) }
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function generateNavigationRuntime(source, group) {
|
|
53
|
+
let runtime = replaceSequenceRequired(source, [
|
|
54
|
+
["__KUDZU_NAVIGATION_ROUTES__", JSON.stringify(group.records).replaceAll("<", "\\u003c").replaceAll("\u2028", "\\u2028").replaceAll("\u2029", "\\u2029"), "route records"],
|
|
55
|
+
["__KUDZU_APPLICATION_ID__", JSON.stringify(group.applicationId), "application ID"],
|
|
56
|
+
["__KUDZU_LAYOUT_ID__", JSON.stringify(group.layoutId), "layout ID"],
|
|
57
|
+
['"./shared-runtime.js"', '"./kudzu.js"', "shared runtime import"]
|
|
58
|
+
], "navigation-runtime.js")
|
|
59
|
+
runtime = specializeNavigationPatterns(runtime, group.records.some(record => record.segments))
|
|
60
|
+
return specializeNavigationEffects(runtime, group.hasEffects || group.hasParams)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function specializeRuntime(source, events, hasStateSeed) {
|
|
64
|
+
const specialized = specializeEvents(source, events)
|
|
65
|
+
if (hasStateSeed) return specialized
|
|
66
|
+
return replaceSequenceRequired(specialized, [
|
|
67
|
+
[" const initialState = document.body.dataset.kState\n", "", "initial state read"],
|
|
68
|
+
[/^ if \(initialState\).*\n/m, "", "initial state parse"]
|
|
69
|
+
], "runtime source")
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function specializeEvents(source, events) {
|
|
73
|
+
return replaceRequired(source, /const eventNames = \[[^\n]+\]/, `const eventNames = ${JSON.stringify(events)}`, "event names", "runtime source")
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function replaceRequired(source, search, replacement, label, file) {
|
|
77
|
+
const output = source.replace(search, replacement)
|
|
78
|
+
if (output === source) throw new Error(`${label} specialization did not match ${file}`)
|
|
79
|
+
return output
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function replaceSequenceRequired(source, replacements, file) {
|
|
83
|
+
return replacements.reduce((output, [search, replacement, label]) => replaceRequired(output, search, replacement, label, file), source)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function specializeNavigationEffects(source, enabled) {
|
|
87
|
+
if (enabled) return source
|
|
88
|
+
return replaceSequenceRequired(source, [
|
|
89
|
+
["const noDispose = async () => {}\nlet routeDispose = noDispose\nlet layoutDispose = noDispose\nconst ready = mountInitial()\n", "", "initial effect lifecycle"],
|
|
90
|
+
[`addEventListener("pagehide", event => {
|
|
91
|
+
if (event.persisted) return
|
|
92
|
+
++revision
|
|
93
|
+
request?.abort()
|
|
94
|
+
void (async () => {
|
|
95
|
+
await routeDispose()
|
|
96
|
+
await layoutDispose()
|
|
97
|
+
})()
|
|
98
|
+
})
|
|
99
|
+
`, "", "pagehide effect lifecycle"],
|
|
100
|
+
[`
|
|
101
|
+
async function mountInitial() {
|
|
102
|
+
try {
|
|
103
|
+
const record = matchRoute(location.pathname)
|
|
104
|
+
if (!record) throw new Error("Initial navigation route does not match")
|
|
105
|
+
const capabilities = await loadCapabilities(validate(document, record))
|
|
106
|
+
capabilities.params?.(location.pathname, location.search)
|
|
107
|
+
layoutDispose = await capabilities.effects?.mountLayoutEffects?.() ?? noDispose
|
|
108
|
+
routeDispose = await capabilities.effects?.mountRouteEffects?.() ?? noDispose
|
|
109
|
+
} catch (error) {
|
|
110
|
+
console.error(error)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
`, "", "initial effect mount"],
|
|
114
|
+
[" await ready\n", "", "initial effect readiness"],
|
|
115
|
+
[" const { incoming, parsed, capabilities } = documentResult\n", " const { incoming, parsed } = documentResult\n", "navigation capability result"],
|
|
116
|
+
[" await routeDispose()\n if (current !== revision) return\n", "", "route effect disposal"],
|
|
117
|
+
[" commit(incoming, parsed.nodes, capabilities.params, url.pathname, url.search)\n", " commit(incoming, parsed.nodes)\n", "navigation capability commit"],
|
|
118
|
+
[" routeDispose = await capabilities.effects?.mountRouteEffects?.() ?? noDispose\n", "", "route effect mount"],
|
|
119
|
+
[" return { incoming, parsed, capabilities: await loadCapabilities(parsed), record }\n", " await Promise.all(parsed.assets.filter(path => path !== navigationAsset).map(path => import(path)))\n return { incoming, parsed, record }\n", "navigation capability load"],
|
|
120
|
+
[`
|
|
121
|
+
async function loadCapabilities(parsed) {
|
|
122
|
+
const modules = await Promise.all(parsed.assets.filter(path => path !== navigationAsset).map(path => import(path)))
|
|
123
|
+
const params = modules.filter(module => typeof module.initializeParams === "function")
|
|
124
|
+
const effects = modules.filter(module => typeof module.mountRouteEffects === "function")
|
|
125
|
+
if (params.length > 1 || effects.length > 1) throw new Error("Navigation document has duplicate route capabilities")
|
|
126
|
+
return { params: params[0]?.initializeParams, effects: effects[0] }
|
|
127
|
+
}
|
|
128
|
+
`, "", "capability loader"]
|
|
129
|
+
], "navigation-runtime.js")
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function specializeNavigationPatterns(source, enabled) {
|
|
133
|
+
if (enabled) return source
|
|
134
|
+
return replaceRequired(source, /function matchRoute\(pathname\) \{[\s\S]+?\n\}\n\nfunction fallback/, `function matchRoute(pathname) {
|
|
135
|
+
return routes.find(record => record.path === pathname)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function fallback`, "exact route matcher", "navigation-runtime.js")
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function specializeNavigationTextDescriptors(source) {
|
|
142
|
+
return replaceSequenceRequired(source, [
|
|
143
|
+
["const textDescriptors = globalThis.__KUDZU_TEXT_BINDINGS__ && typeof document !== \"undefined\" ? JSON.parse(document.body.dataset.kTextBindings ?? \"[]\") : []", "const textDescriptors = () => globalThis.__KUDZU_TEXT_BINDINGS__ ? JSON.parse(document.body.dataset.kTextBindings ?? \"[]\") : []", "text descriptor reader"],
|
|
144
|
+
["const descriptor = textDescriptors[Number(node.data.slice(\"k-text:\".length))]", "const descriptor = textDescriptors()[Number(node.data.slice(\"k-text:\".length))]", "text descriptor lookup"]
|
|
145
|
+
], "binding-runtime.js")
|
|
146
|
+
}
|