@kudzujs/core 0.8.20 → 0.8.22
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 +13 -0
- package/PERFORMANCE.md +48 -0
- package/README.md +1 -1
- package/RELEASES.md +71 -0
- package/docs/next-architecture/README.md +2 -2
- package/docs/next-architecture/compiler-current-architecture.md +19 -17
- package/docs/next-architecture/goal-a-compiler-foundation.md +15 -18
- package/docs/next-architecture/versioning.md +2 -2
- package/framework/README.md +8 -4
- package/framework/build.mjs +90 -401
- package/framework/compiler/descriptor-session.mjs +11 -2
- package/framework/compiler/effect-analysis.mjs +89 -0
- package/framework/compiler/ir/module-ir.mjs +7 -1
- package/framework/compiler/list-runtime-codegen.mjs +95 -0
- package/framework/compiler/param-codegen.mjs +72 -0
- package/framework/compiler/route-capability-planner.mjs +35 -0
- package/framework/compiler/runtime-codegen.mjs +146 -0
- package/framework/compiler/worker-compiler.mjs +12 -6
- package/framework/core.d.ts +41 -24
- package/framework/core.mjs +2 -1
- package/package.json +1 -1
|
@@ -2,7 +2,7 @@ import ts from "typescript"
|
|
|
2
2
|
import { createComponentAnalysis } from "./analysis/component-analysis.mjs"
|
|
3
3
|
import { bindingNames, isFunctionLike, isReferenceIdentifier, isShadowedByParameter, isShadowedIdentifier, unwrapExpression } from "./ast-helpers.mjs"
|
|
4
4
|
import { generateCommandBehavior } from "./codegen/command-codegen.mjs"
|
|
5
|
-
import { createModuleIR, registerBinding, registerCommandHandler, registerDerived, registerKeyedBlock, registerModuleHandler } from "./ir/module-ir.mjs"
|
|
5
|
+
import { createModuleIR, registerBinding, registerCommandHandler, registerDerived, registerEffect, registerKeyedBlock, registerModuleHandler } from "./ir/module-ir.mjs"
|
|
6
6
|
|
|
7
7
|
export function createSemanticArtifact(file) {
|
|
8
8
|
return { componentAnalysis: createComponentAnalysis(file), moduleIR: createModuleIR(file) }
|
|
@@ -160,6 +160,10 @@ export function createDescriptorSession({ semantic, handlerUrl, factory, context
|
|
|
160
160
|
return registerKeyedBlock(moduleIR, descriptor)
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
+
function registerEffectResult(handler, descriptor) {
|
|
164
|
+
return registerEffect(moduleIR, { ...descriptor, setup: { exportName: handler.exportName } })
|
|
165
|
+
}
|
|
166
|
+
|
|
163
167
|
function finalize() {
|
|
164
168
|
const callbacks = [...nativeHandlers, ...effectHandlers]
|
|
165
169
|
for (const entry of callbacks) {
|
|
@@ -210,6 +214,11 @@ export function createDescriptorSession({ semantic, handlerUrl, factory, context
|
|
|
210
214
|
code: handlerLowering.lowerListExpression(entry),
|
|
211
215
|
...(source(entry.expression) ? { source: source(entry.expression) } : {})
|
|
212
216
|
})
|
|
217
|
+
for (const effect of moduleIR.effects) {
|
|
218
|
+
const handler = moduleIR.handlers.find(candidate => candidate.kind === "module-export" && candidate.role === "effect" && candidate.exportName === effect.setup.exportName)
|
|
219
|
+
if (!handler) throw new Error(`Effect handler ${JSON.stringify(effect.setup.exportName)} was not finalized`)
|
|
220
|
+
effect.setup = { handler: handler.slot }
|
|
221
|
+
}
|
|
213
222
|
const imports = [...callbacks, ...reactiveBindings].flatMap(entry => entry.imports ?? []).map(importRecord)
|
|
214
223
|
moduleIR.imports = [...new Map(imports.map(entry => [`${entry.target}:${entry.kind}:${entry.imported ?? ""}:${entry.local}`, entry])).values()]
|
|
215
224
|
moduleIR.clientModules = [...clientModules]
|
|
@@ -223,7 +232,7 @@ export function createDescriptorSession({ semantic, handlerUrl, factory, context
|
|
|
223
232
|
|
|
224
233
|
const importRecord = entry => ({ target: entry.target, kind: entry.kind, local: entry.local, ...(entry.imported ? { imported: entry.imported } : {}), package: Boolean(entry.package) })
|
|
225
234
|
|
|
226
|
-
return { compileConditional, compileEffectCallback, compileEvent, compileListConditional, compileListValue, compileReactiveBinding, finalize, registerDerived: registerDerivedResult, registerKeyedBlock: registerKeyedBlockResult }
|
|
235
|
+
return { compileConditional, compileEffectCallback, compileEvent, compileListConditional, compileListValue, compileReactiveBinding, finalize, registerDerived: registerDerivedResult, registerEffect: registerEffectResult, registerKeyedBlock: registerKeyedBlockResult }
|
|
227
236
|
}
|
|
228
237
|
|
|
229
238
|
function directStateIdentifier(expression, setters) {
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import ts from "typescript"
|
|
2
|
+
import { nearestFunction, referencesIdentifier, unwrapExpression } from "./ast-helpers.mjs"
|
|
3
|
+
import { collectionExpression } from "./collection-analysis.mjs"
|
|
4
|
+
import { referencedStateNames } from "./descriptor-session.mjs"
|
|
5
|
+
|
|
6
|
+
export function analyzeEffectDependencies({ dependencies, node, listEffect, keyedItem, setters, localDeclarations, factory, fail }) {
|
|
7
|
+
const itemDependencies = []
|
|
8
|
+
const ordinaryDependencies = []
|
|
9
|
+
let dependencyItem = listEffect ? keyedItem : undefined
|
|
10
|
+
for (const dependency of dependencies.elements) {
|
|
11
|
+
const value = unwrapExpression(dependency)
|
|
12
|
+
if (!dependencyItem && ts.isPropertyAccessExpression(value) && ts.isIdentifier(value.expression) && isDestructuredParameter(value.expression, nearestFunction(node))) dependencyItem = value.expression.text
|
|
13
|
+
const field = dependencyItem && directProperty(dependency, dependencyItem)
|
|
14
|
+
if (field) {
|
|
15
|
+
if (["__proto__", "constructor", "prototype"].includes(field)) fail(dependency, `useEffect() keyed item property "${field}" is not supported`)
|
|
16
|
+
itemDependencies.push(field)
|
|
17
|
+
} else if (dependencyItem && referencesIdentifier(dependency, dependencyItem)) {
|
|
18
|
+
fail(dependency, "useEffect() keyed item dependencies must be direct item.<field> properties")
|
|
19
|
+
} else {
|
|
20
|
+
ordinaryDependencies.push(dependency)
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
const invalidDependency = ordinaryDependencies.find(dependency => !ts.isIdentifier(dependency))
|
|
24
|
+
if (invalidDependency) fail(invalidDependency, "useEffect() dependencies must be direct state or runtime parameter identifiers")
|
|
25
|
+
|
|
26
|
+
const entries = []
|
|
27
|
+
const dependencyStates = new Map()
|
|
28
|
+
const substitutions = new Map()
|
|
29
|
+
const subscriptions = []
|
|
30
|
+
let hasDerived = false
|
|
31
|
+
const stateNames = new Set(setters.values())
|
|
32
|
+
for (const dependency of ordinaryDependencies) {
|
|
33
|
+
const declarations = localDeclarations?.get(dependency.text)
|
|
34
|
+
const initializer = declarations?.length === 1 ? declarations[0].initializer : undefined
|
|
35
|
+
const directAlias = initializer && ts.isIdentifier(unwrapExpression(initializer)) && stateNames.has(unwrapExpression(initializer).text)
|
|
36
|
+
const derivedStates = initializer && !directAlias ? referencedStateNames(initializer, setters) : new Set()
|
|
37
|
+
if (derivedStates.size) {
|
|
38
|
+
const usedStates = new Set()
|
|
39
|
+
const expression = collectionExpression(initializer, { fail, stateNames, selectorStates: usedStates })
|
|
40
|
+
if (!usedStates.size) fail(dependency, `useEffect() derived dependency "${dependency.text}" must read direct primitive state`)
|
|
41
|
+
entries.push({ kind: "derived", name: dependency.text, expression, states: usedStates, source: initializer })
|
|
42
|
+
for (const name of usedStates) {
|
|
43
|
+
subscriptions.push(factory.createIdentifier(name))
|
|
44
|
+
dependencyStates.set(name, factory.createIdentifier(name))
|
|
45
|
+
}
|
|
46
|
+
substitutions.set(dependency.text, initializer)
|
|
47
|
+
hasDerived = true
|
|
48
|
+
} else {
|
|
49
|
+
subscriptions.push(dependency)
|
|
50
|
+
entries.push({ kind: "signal", name: dependency.text })
|
|
51
|
+
dependencyStates.set(dependency.text, dependency)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (!hasDerived) dependencyStates.clear()
|
|
55
|
+
return { dependencyItem, itemDependencies, ordinaryDependencies, entries, dependencyStates, substitutions, subscriptions, hasDerived }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function validateEffectOwnedBrowserResources(callback, returns, fail) {
|
|
59
|
+
const observers = []
|
|
60
|
+
const frameAssignments = []
|
|
61
|
+
const cancellations = new Set()
|
|
62
|
+
const disconnected = new Set()
|
|
63
|
+
const insideCleanup = node => returns.cleanups.some(cleanup => {
|
|
64
|
+
for (let current = node; current; current = current.parent) if (current === cleanup) return true
|
|
65
|
+
return false
|
|
66
|
+
})
|
|
67
|
+
const visit = node => {
|
|
68
|
+
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && ts.isNewExpression(unwrapExpression(node.initializer)) && ts.isIdentifier(unwrapExpression(node.initializer).expression) && unwrapExpression(node.initializer).expression.text === "IntersectionObserver") observers.push(node)
|
|
69
|
+
if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.EqualsToken && ts.isIdentifier(unwrapExpression(node.left)) && ts.isCallExpression(unwrapExpression(node.right)) && ts.isIdentifier(unwrapExpression(node.right).expression) && unwrapExpression(node.right).expression.text === "requestAnimationFrame") frameAssignments.push(node)
|
|
70
|
+
if (insideCleanup(node) && ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === "cancelAnimationFrame" && node.arguments.length === 1 && ts.isIdentifier(unwrapExpression(node.arguments[0]))) cancellations.add(unwrapExpression(node.arguments[0]).text)
|
|
71
|
+
if (insideCleanup(node) && ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && ts.isIdentifier(node.expression.expression) && node.expression.name.text === "disconnect" && node.arguments.length === 0) disconnected.add(node.expression.expression.text)
|
|
72
|
+
ts.forEachChild(node, visit)
|
|
73
|
+
}
|
|
74
|
+
visit(callback.body)
|
|
75
|
+
for (const observer of observers) if (!disconnected.has(observer.name.text)) fail(observer, `IntersectionObserver effects must disconnect ${JSON.stringify(observer.name.text)} in cleanup`)
|
|
76
|
+
for (const assignment of frameAssignments) {
|
|
77
|
+
const name = unwrapExpression(assignment.left).text
|
|
78
|
+
if (!cancellations.has(name)) fail(assignment, `Animation loop effects must cancel ${JSON.stringify(name)} in cleanup`)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function isDestructuredParameter(identifier, fn) {
|
|
83
|
+
return fn?.parameters.some(parameter => ts.isObjectBindingPattern(parameter.name) && parameter.name.elements.some(element => ts.isIdentifier(element.name) && element.name.text === identifier.text)) ?? false
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function directProperty(expression, objectName) {
|
|
87
|
+
const value = unwrapExpression(expression)
|
|
88
|
+
return ts.isPropertyAccessExpression(value) && ts.isIdentifier(value.expression) && value.expression.text === objectName ? value.name.text : undefined
|
|
89
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export function createModuleIR(file) {
|
|
2
|
-
return { version: 1, file, signals: [], handlers: [], bindings: [], derived: [], keyedBlocks: [], imports: [], clientModules: [] }
|
|
2
|
+
return { version: 1, file, signals: [], handlers: [], bindings: [], derived: [], effects: [], keyedBlocks: [], imports: [], clientModules: [] }
|
|
3
3
|
}
|
|
4
4
|
|
|
5
5
|
export function registerCommandHandler(moduleIR, commands, source, scope = "module") {
|
|
@@ -40,6 +40,12 @@ export function registerDerived(moduleIR, descriptor) {
|
|
|
40
40
|
return derived
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
export function registerEffect(moduleIR, descriptor) {
|
|
44
|
+
const effect = { slot: moduleIR.effects.length, ...descriptor }
|
|
45
|
+
moduleIR.effects.push(effect)
|
|
46
|
+
return effect
|
|
47
|
+
}
|
|
48
|
+
|
|
43
49
|
export function registerKeyedBlock(moduleIR, descriptor) {
|
|
44
50
|
const block = { slot: moduleIR.keyedBlocks.length, ...descriptor }
|
|
45
51
|
moduleIR.keyedBlocks.push(block)
|
|
@@ -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
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -44,7 +44,8 @@ export function createWorkerCompiler({
|
|
|
44
44
|
return { worker, url, specifier: specifierNode.text, options }
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
const rewriteEffect = (callback, file, sourceFile, sourceFiles,
|
|
47
|
+
const rewriteEffect = (callback, file, sourceFile, sourceFiles, factory, context) => {
|
|
48
|
+
const workers = []
|
|
48
49
|
const visit = node => {
|
|
49
50
|
const value = candidate(node, sourceFile)
|
|
50
51
|
if (value) {
|
|
@@ -56,12 +57,17 @@ export function createWorkerCompiler({
|
|
|
56
57
|
if (!sourceFiles.has(target)) throw sourceNodeError(url.arguments[0], sourceFile, `Relative TypeScript Worker ${JSON.stringify(specifier)} must resolve to an existing .worker.ts file under src/`)
|
|
57
58
|
const identity = `${sourceRelative.replaceAll(sep, "/")}:${ts.getOriginalNode(node).getStart(sourceFile)}`
|
|
58
59
|
const placeholder = `/__kudzu_worker_${createHash("sha256").update(identity).digest("hex").slice(0, 16)}__.js`
|
|
59
|
-
|
|
60
|
+
const original = ts.getOriginalNode(node)
|
|
61
|
+
workers.push({
|
|
62
|
+
root: sourceRelative.replaceAll(sep, "/"),
|
|
63
|
+
placeholder,
|
|
64
|
+
source: { file: relative(root, sourceFile.fileName).replaceAll(sep, "/"), start: original.getStart(sourceFile), end: original.end }
|
|
65
|
+
})
|
|
60
66
|
return factory.updateNewExpression(worker, worker.expression, worker.typeArguments, [factory.createStringLiteral(placeholder), options])
|
|
61
67
|
}
|
|
62
68
|
return ts.visitEachChild(node, visit, context)
|
|
63
69
|
}
|
|
64
|
-
return ts.visitEachChild(callback, visit, context)
|
|
70
|
+
return { callback: ts.visitEachChild(callback, visit, context), workers }
|
|
65
71
|
}
|
|
66
72
|
|
|
67
73
|
const rejectConstructions = (expression, sourceFile, message) => {
|
|
@@ -124,7 +130,7 @@ export function createWorkerCompiler({
|
|
|
124
130
|
}
|
|
125
131
|
|
|
126
132
|
const emit = async (references, sourceFiles, assetsDirectory, base, minify) => {
|
|
127
|
-
const roots = [...new Set(references.map(reference => reference.root))].sort()
|
|
133
|
+
const roots = [...new Set(references.map(reference => resolve(sourceDirectory, reference.root)))].sort()
|
|
128
134
|
if (!roots.length) return new Map()
|
|
129
135
|
await validateGraphs(roots, sourceFiles)
|
|
130
136
|
const workerDirectory = resolve(assetsDirectory, "workers")
|
|
@@ -150,12 +156,12 @@ export function createWorkerCompiler({
|
|
|
150
156
|
for (const [output, metadata] of Object.entries(result.metafile.outputs)) {
|
|
151
157
|
if (!metadata.entryPoint) continue
|
|
152
158
|
const entry = resolve(root, metadata.entryPoint)
|
|
153
|
-
const rootReferences = references.filter(reference => reference.root === entry)
|
|
159
|
+
const rootReferences = references.filter(reference => resolve(sourceDirectory, reference.root) === entry)
|
|
154
160
|
const outputFile = resolve(root, output)
|
|
155
161
|
const url = assetPath(base, relative(outputDirectory, outputFile).replaceAll(sep, "/"))
|
|
156
162
|
for (const reference of rootReferences) emitted.set(reference.placeholder, url)
|
|
157
163
|
}
|
|
158
|
-
for (const reference of references) if (!emitted.has(reference.placeholder)) throw new Error(`Worker entry was not emitted: ${
|
|
164
|
+
for (const reference of references) if (!emitted.has(reference.placeholder)) throw new Error(`Worker entry was not emitted: ${reference.root}`)
|
|
159
165
|
return emitted
|
|
160
166
|
}
|
|
161
167
|
|