@kudzujs/core 0.5.10 → 0.6.0
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/GOAL_A.md +175 -0
- package/README.md +69 -7
- package/framework/README.md +6 -1
- package/framework/build.mjs +510 -28
- package/framework/core.d.ts +7 -3
- package/framework/core.mjs +100 -29
- package/framework/effect-runtime.js +3 -1
- package/framework/list-runtime.js +13 -6
- package/framework/navigation-runtime.js +220 -0
- package/package.json +2 -1
package/framework/core.d.ts
CHANGED
|
@@ -54,8 +54,12 @@ export function renderPage<Props = Record<string, never>>(
|
|
|
54
54
|
effectAsset?: string
|
|
55
55
|
paramAsset?: string
|
|
56
56
|
runtimeParams?: string[]
|
|
57
|
+
navigationAsset?: string
|
|
58
|
+
applicationId?: string
|
|
59
|
+
layoutId?: string
|
|
57
60
|
},
|
|
58
|
-
props?: Props
|
|
61
|
+
props?: Props,
|
|
62
|
+
layout?: (props: { children: unknown }) => unknown | Promise<unknown>
|
|
59
63
|
): Promise<{
|
|
60
64
|
html: string
|
|
61
65
|
hasBehaviors: boolean
|
|
@@ -66,14 +70,14 @@ export function renderPage<Props = Record<string, never>>(
|
|
|
66
70
|
hasListStyles: boolean
|
|
67
71
|
hasStateSeed: boolean
|
|
68
72
|
plan: {
|
|
69
|
-
states: Array<{ id: string; name: string; initialValue: unknown }>
|
|
73
|
+
states: Array<{ id: string; name: string; initialValue: unknown; lifetime?: "layout" | "route" }>
|
|
70
74
|
params: Array<{ name: string; id: string }>
|
|
71
75
|
events: Array<{
|
|
72
76
|
event: string
|
|
73
77
|
commands?: Array<[string, string, unknown]>
|
|
74
78
|
native?: { module: string; handler: string; states: Record<string, string>; scope: Record<string, unknown> }
|
|
75
79
|
}>
|
|
76
|
-
effects: Array<{ module: string; handler: string; states: Record<string, string>; scope: Record<string, unknown>; dependencies?: string[]; cleanup?: true }>
|
|
80
|
+
effects: Array<{ module: string; handler: string; states: Record<string, string>; scope: Record<string, unknown>; lifetime?: "layout" | "route"; dependencies?: string[]; cleanup?: true; owner?: string; list?: true }>
|
|
77
81
|
bindings: Array<{
|
|
78
82
|
target: string
|
|
79
83
|
state?: string
|
package/framework/core.mjs
CHANGED
|
@@ -14,6 +14,7 @@ const listConditionalMarker = Symbol("kudzu.listConditional")
|
|
|
14
14
|
const refMarker = Symbol("kudzu.ref")
|
|
15
15
|
const contextMarker = Symbol("kudzu.context")
|
|
16
16
|
const contextProviderMarker = Symbol("kudzu.contextProvider")
|
|
17
|
+
const routeScopeMarker = Symbol("kudzu.routeScope")
|
|
17
18
|
const noSelectValue = Symbol("kudzu.no-select-value")
|
|
18
19
|
|
|
19
20
|
let renderContext
|
|
@@ -23,23 +24,24 @@ export function useState(initialValue, name) {
|
|
|
23
24
|
throw new Error("useState() can only run while rendering a Kudzu component")
|
|
24
25
|
}
|
|
25
26
|
|
|
26
|
-
const id =
|
|
27
|
+
const id = nextRenderId("s")
|
|
27
28
|
const signal = createSignal(id, initialValue)
|
|
28
29
|
|
|
29
30
|
const setter = () => {
|
|
30
31
|
throw new Error("State setters are compiled into ordered browser behaviors")
|
|
31
32
|
}
|
|
32
33
|
Object.defineProperty(setter, setterMarker, { value: id })
|
|
33
|
-
renderContext.states[id] = { name: name ?? id, initialValue }
|
|
34
|
+
renderContext.states[id] = { name: name ?? id, initialValue, ...(renderContext.scoped ? { lifetime: renderContext.renderScope } : {}) }
|
|
34
35
|
return [signal, setter]
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
export function useParams() {
|
|
39
|
+
if (renderContext?.renderScope === "layout") throw new Error("useParams() is only supported in route scope")
|
|
38
40
|
if (!renderContext?.runtimeParamNames?.length) throw new Error("useParams() requires export const runtimeParams = true on a bracket page")
|
|
39
41
|
if (!renderContext.params) {
|
|
40
42
|
const params = Object.create(null)
|
|
41
|
-
renderContext.paramEntries = renderContext.runtimeParamNames.map(
|
|
42
|
-
const id =
|
|
43
|
+
renderContext.paramEntries = renderContext.runtimeParamNames.map(name => {
|
|
44
|
+
const id = nextRenderId("p")
|
|
43
45
|
params[name] = createSignal(id, "")
|
|
44
46
|
return { name, id }
|
|
45
47
|
})
|
|
@@ -71,7 +73,28 @@ export function useEffect(callback, dependencies, module, handler, states, scope
|
|
|
71
73
|
if (!dependency?.[signalMarker] || !validEffectDependency(dependency.value)) throw new Error(`${source} useEffect() dependencies must be primitive Kudzu state or runtime parameter identifiers`)
|
|
72
74
|
return dependency.id
|
|
73
75
|
})
|
|
74
|
-
|
|
76
|
+
let owner
|
|
77
|
+
let list = false
|
|
78
|
+
if (renderContext.listDepth) {
|
|
79
|
+
const effects = renderContext.listRoot?.effects
|
|
80
|
+
if (!effects) throw new Error(`${source} useEffect() inside keyed lists must belong to the direct row component`)
|
|
81
|
+
const index = effects.length
|
|
82
|
+
if (renderContext.listTemplate) {
|
|
83
|
+
owner = nextRenderId("e")
|
|
84
|
+
renderContext.listEffectOwners.push(owner)
|
|
85
|
+
list = true
|
|
86
|
+
} else {
|
|
87
|
+
owner = renderContext.listEffectOwners[index]
|
|
88
|
+
if (!owner) throw new Error(`${source} Keyed row effects must have the same hook order for every item`)
|
|
89
|
+
}
|
|
90
|
+
effects.push(owner)
|
|
91
|
+
} else if (renderContext.conditionDepth) {
|
|
92
|
+
const owners = renderContext.effectOwners.at(-1)
|
|
93
|
+
if (!owners) throw new Error(`${source} useEffect() inside conditional DOM must belong to a rendered function component`)
|
|
94
|
+
owner = nextRenderId("e")
|
|
95
|
+
owners.push(owner)
|
|
96
|
+
}
|
|
97
|
+
if (!renderContext.listDepth || list) renderContext.effects.push({ module, handler, states, scope, source, renderScope: renderContext.renderScope, ...(dependencyIds.length ? { dependencies: dependencyIds } : {}), ...(cleanup ? { cleanup: true } : {}), ...(owner ? { owner } : {}), ...(list ? { list: true } : {}) })
|
|
75
98
|
renderContext.hasBehaviors = true
|
|
76
99
|
renderContext.hasEffects = true
|
|
77
100
|
}
|
|
@@ -83,7 +106,7 @@ function validEffectDependency(value) {
|
|
|
83
106
|
export function useRef(initialValue) {
|
|
84
107
|
if (!renderContext) throw new Error("useRef() can only run while rendering a Kudzu component")
|
|
85
108
|
if (initialValue !== null) throw new Error("Kudzu DOM refs must initialize with null")
|
|
86
|
-
return { [refMarker]: true, id:
|
|
109
|
+
return { [refMarker]: true, id: nextRenderId("r"), current: null }
|
|
87
110
|
}
|
|
88
111
|
|
|
89
112
|
export function createContext(defaultValue) {
|
|
@@ -267,19 +290,26 @@ function serializeCapture(name, value, seen) {
|
|
|
267
290
|
}
|
|
268
291
|
}
|
|
269
292
|
|
|
270
|
-
export async function renderPage(component, metadata = {}, props = {}) {
|
|
271
|
-
renderContext = { nextState: 0, nextRef: 0, nextCondition: 0, nextList: 0, conditionDepth: 0, listDepth: 0, listRoot: undefined, listTemplate: false, listInitialMarkers: false, listConditionalBranch: false, listFields: undefined, contexts: [], states: {}, textStates: new Set(), conditionStates: new Set(), events: [], effects: [], bindings: [], textBindings: [], conditions: [], lists: [], runtimeParamNames: metadata.runtimeParams, paramEntries: [], params: undefined, hasBehaviors: false, hasNativeBehaviors: false, hasEffects: false, hasParams: false, hasBindings: false, hasLists: false, hasListStyles: false }
|
|
293
|
+
export async function renderPage(component, metadata = {}, props = {}, layout) {
|
|
294
|
+
renderContext = { scoped: Boolean(layout), renderScope: layout ? "layout" : "route", counters: { layout: { s: 0, r: 0, c: 0, l: 0, e: 0, p: 0 }, route: { s: 0, r: 0, c: 0, l: 0, e: 0, p: 0 } }, nextState: 0, nextRef: 0, nextCondition: 0, nextList: 0, nextEffect: 0, nextParam: 0, conditionDepth: 0, listDepth: 0, listRoot: undefined, listTemplate: false, listInitialMarkers: false, listConditionalBranch: false, listFields: undefined, listEffectOwners: [], effectOwners: [], contexts: [], states: {}, textStates: new Set(), conditionStates: new Set(), events: [], effects: [], bindings: [], textBindings: [], conditions: [], lists: [], runtimeParamNames: metadata.runtimeParams, paramEntries: [], params: undefined, hasBehaviors: false, hasNativeBehaviors: false, hasEffects: false, hasParams: false, hasBindings: false, hasLists: false, hasListStyles: false }
|
|
272
295
|
|
|
273
296
|
try {
|
|
274
|
-
const
|
|
297
|
+
const page = { [routeScopeMarker]: true, component, props }
|
|
298
|
+
const body = await renderNode(layout ? { type: layout, props: { children: page } } : { type: component, props })
|
|
275
299
|
renderContext.effects = renderContext.effects.map(effect => {
|
|
276
300
|
try {
|
|
301
|
+
if (metadata.navigationAsset && effect.owner) throw new Error("useEffect() inside conditional or keyed DOM is not supported in a configured navigation group yet; move the effect to the layout or route component body, or remove the route from navigation")
|
|
302
|
+
const descriptor = nativeDescriptor(effect.states.map(([name, read]) => [name, read()]), effect.scope.map(([name, read]) => [name, typeof read === "function" ? read() : read]))
|
|
277
303
|
return {
|
|
278
304
|
module: effect.module,
|
|
279
305
|
handler: effect.handler,
|
|
280
306
|
...(effect.dependencies ? { dependencies: effect.dependencies } : {}),
|
|
281
307
|
...(effect.cleanup ? { cleanup: true } : {}),
|
|
282
|
-
...
|
|
308
|
+
...(effect.owner ? { owner: effect.owner } : {}),
|
|
309
|
+
...(effect.list ? { list: true } : {}),
|
|
310
|
+
...(renderContext.scoped ? { lifetime: effect.renderScope } : {}),
|
|
311
|
+
states: descriptor.states,
|
|
312
|
+
scope: descriptor.scope
|
|
283
313
|
}
|
|
284
314
|
} catch (error) {
|
|
285
315
|
throw new Error(`${effect.source} ${error.message}`)
|
|
@@ -287,26 +317,30 @@ export async function renderPage(component, metadata = {}, props = {}) {
|
|
|
287
317
|
})
|
|
288
318
|
const title = escapeHtml(metadata.title ?? "Kudzu")
|
|
289
319
|
const head = renderMetadata(metadata)
|
|
320
|
+
const capability = metadata.navigationAsset ? " data-k-capability" : ""
|
|
290
321
|
const styles = metadata.styles === false
|
|
291
322
|
? ""
|
|
292
323
|
: (Array.isArray(metadata.styles) ? metadata.styles : [assetPath(metadata.base, "assets/style.css")]).map(href => `<link rel="stylesheet" href="${escapeAttribute(href)}">`).join("")
|
|
293
324
|
const runtime = renderContext.hasBehaviors
|
|
294
|
-
? `<script type="module" src="${escapeAttribute(metadata.runtimeAsset ?? assetPath(metadata.base, "assets/kudzu.js"))}"></script>`
|
|
325
|
+
? `<script type="module"${capability} src="${escapeAttribute(metadata.runtimeAsset ?? assetPath(metadata.base, "assets/kudzu.js"))}"></script>`
|
|
295
326
|
: ""
|
|
296
327
|
const nativeRuntime = renderContext.hasNativeBehaviors
|
|
297
|
-
? `<script type="module" src="${assetPath(metadata.base, "assets/kudzu-native.js")}"></script>`
|
|
328
|
+
? `<script type="module"${capability} src="${assetPath(metadata.base, "assets/kudzu-native.js")}"></script>`
|
|
298
329
|
: ""
|
|
299
330
|
const paramRuntime = renderContext.hasParams
|
|
300
|
-
? `<script type="module" src="${escapeAttribute(metadata.paramAsset)}"></script>`
|
|
331
|
+
? `<script type="module"${capability} src="${escapeAttribute(metadata.paramAsset)}"></script>`
|
|
301
332
|
: ""
|
|
302
333
|
const bindingRuntime = renderContext.hasBindings
|
|
303
|
-
? `<script type="module" src="${assetPath(metadata.base, "assets/kudzu-binding.js")}"></script>`
|
|
334
|
+
? `<script type="module"${capability} src="${assetPath(metadata.base, "assets/kudzu-binding.js")}"></script>`
|
|
304
335
|
: ""
|
|
305
336
|
const listRuntime = renderContext.hasLists
|
|
306
|
-
? `<script type="module" src="${assetPath(metadata.base, "assets/kudzu-list.js")}"></script>`
|
|
337
|
+
? `<script type="module"${capability} src="${assetPath(metadata.base, "assets/kudzu-list.js")}"></script>`
|
|
307
338
|
: ""
|
|
308
339
|
const effectRuntime = renderContext.hasEffects
|
|
309
|
-
? `<script type="module" src="${escapeAttribute(metadata.effectAsset)}"></script>`
|
|
340
|
+
? `<script type="module"${capability} src="${escapeAttribute(metadata.effectAsset)}"></script>`
|
|
341
|
+
: ""
|
|
342
|
+
const navigationRuntime = metadata.navigationAsset
|
|
343
|
+
? `<script type="module" data-k-capability src="${escapeAttribute(metadata.navigationAsset)}"></script>`
|
|
310
344
|
: ""
|
|
311
345
|
const listStates = new Set(renderContext.lists.map(list => list.state))
|
|
312
346
|
const seededListStates = new Set(renderContext.lists.filter(list => list.seed && !renderContext.textStates.has(list.state) && !renderContext.conditionStates.has(list.state)).map(list => list.state))
|
|
@@ -324,7 +358,7 @@ export async function renderPage(component, metadata = {}, props = {}) {
|
|
|
324
358
|
: ""
|
|
325
359
|
|
|
326
360
|
return {
|
|
327
|
-
html: `<!doctype html><html lang="${escapeAttribute(metadata.lang ?? "en")}"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>${title}</title>${head}${styles}${runtime}${paramRuntime}${bindingRuntime}${listRuntime}${nativeRuntime}${effectRuntime}</head><body${state}${textBindings}>${body}</body></html>`,
|
|
361
|
+
html: `<!doctype html><html lang="${escapeAttribute(metadata.lang ?? "en")}"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>${title}</title>${head}${styles}${runtime}${paramRuntime}${bindingRuntime}${listRuntime}${nativeRuntime}${effectRuntime}${navigationRuntime}</head><body${state}${textBindings}${metadata.applicationId ? ` data-k-application="${escapeAttribute(metadata.applicationId)}" data-k-layout="${escapeAttribute(metadata.layoutId)}"` : ""}>${body}</body></html>`,
|
|
328
362
|
hasBehaviors: renderContext.hasBehaviors,
|
|
329
363
|
hasEffects: renderContext.hasEffects,
|
|
330
364
|
hasParams: renderContext.hasParams,
|
|
@@ -349,16 +383,17 @@ export async function renderPage(component, metadata = {}, props = {}) {
|
|
|
349
383
|
|
|
350
384
|
function renderMetadata(metadata) {
|
|
351
385
|
const tags = []
|
|
386
|
+
const owned = metadata.navigationAsset ? " data-k-head" : ""
|
|
352
387
|
const meta = (name, content, property = false) => {
|
|
353
|
-
if (content) tags.push(`<meta ${property ? "property" : "name"}="${escapeAttribute(name)}" content="${escapeAttribute(content)}">`)
|
|
388
|
+
if (content) tags.push(`<meta${owned} ${property ? "property" : "name"}="${escapeAttribute(name)}" content="${escapeAttribute(content)}">`)
|
|
354
389
|
}
|
|
355
390
|
|
|
356
391
|
if (metadata.description) meta("description", metadata.description)
|
|
357
392
|
if (metadata.themeColor) meta("theme-color", metadata.themeColor)
|
|
358
|
-
if (metadata.url) tags.push(`<link rel="canonical" href="${escapeAttribute(metadata.url)}">`)
|
|
359
|
-
if (metadata.icon) tags.push(`<link rel="icon" href="${escapeAttribute(baseUrl(metadata.base, metadata.icon))}">`)
|
|
360
|
-
if (metadata.appleTouchIcon) tags.push(`<link rel="apple-touch-icon" href="${escapeAttribute(baseUrl(metadata.base, metadata.appleTouchIcon))}">`)
|
|
361
|
-
if (metadata.manifest) tags.push(`<link rel="manifest" href="${escapeAttribute(baseUrl(metadata.base, metadata.manifest))}">`)
|
|
393
|
+
if (metadata.url) tags.push(`<link${owned} rel="canonical" href="${escapeAttribute(metadata.url)}">`)
|
|
394
|
+
if (metadata.icon) tags.push(`<link${owned} rel="icon" href="${escapeAttribute(baseUrl(metadata.base, metadata.icon))}">`)
|
|
395
|
+
if (metadata.appleTouchIcon) tags.push(`<link${owned} rel="apple-touch-icon" href="${escapeAttribute(baseUrl(metadata.base, metadata.appleTouchIcon))}">`)
|
|
396
|
+
if (metadata.manifest) tags.push(`<link${owned} rel="manifest" href="${escapeAttribute(baseUrl(metadata.base, metadata.manifest))}">`)
|
|
362
397
|
|
|
363
398
|
meta("og:title", metadata.title, true)
|
|
364
399
|
meta("og:description", metadata.description, true)
|
|
@@ -404,6 +439,16 @@ async function renderNode(node, namespace, selectValue = noSelectValue) {
|
|
|
404
439
|
return escapeHtml(node)
|
|
405
440
|
}
|
|
406
441
|
if (node instanceof Promise) return renderNode(await node, namespace, selectValue)
|
|
442
|
+
if (node?.[routeScopeMarker]) {
|
|
443
|
+
const previousScope = renderContext.renderScope
|
|
444
|
+
renderContext.renderScope = "route"
|
|
445
|
+
try {
|
|
446
|
+
const html = await renderNode({ type: node.component, props: node.props }, namespace, selectValue)
|
|
447
|
+
return `<template data-k-route-start></template>${html}<template data-k-route-end></template>`
|
|
448
|
+
} finally {
|
|
449
|
+
renderContext.renderScope = previousScope
|
|
450
|
+
}
|
|
451
|
+
}
|
|
407
452
|
if (node?.[contextProviderMarker]) {
|
|
408
453
|
renderContext.contexts.push([node.context, node.value])
|
|
409
454
|
try {
|
|
@@ -418,7 +463,7 @@ async function renderNode(node, namespace, selectValue = noSelectValue) {
|
|
|
418
463
|
if (!stateIds.size) return renderNode(node.value ? node.truthy() : node.falsy(), namespace, selectValue)
|
|
419
464
|
if (namespace) throw new Error(`Reactive conditional DOM is not supported inside ${namespace}`)
|
|
420
465
|
|
|
421
|
-
const id =
|
|
466
|
+
const id = nextRenderId("c")
|
|
422
467
|
renderContext.conditionDepth++
|
|
423
468
|
const truthy = await renderNode(node.truthy(), namespace, selectValue)
|
|
424
469
|
const falsy = await renderNode(node.falsy(), namespace, selectValue)
|
|
@@ -479,7 +524,19 @@ async function renderNode(node, namespace, selectValue = noSelectValue) {
|
|
|
479
524
|
}
|
|
480
525
|
|
|
481
526
|
if (node.type === Symbol.for("kudzu.fragment")) return renderNode(node.props.children, namespace, selectValue)
|
|
482
|
-
if (typeof node.type === "function")
|
|
527
|
+
if (typeof node.type === "function") {
|
|
528
|
+
if (!renderContext.conditionDepth) return renderNode(await node.type(node.props), namespace, selectValue)
|
|
529
|
+
const owners = []
|
|
530
|
+
renderContext.effectOwners.push(owners)
|
|
531
|
+
let result
|
|
532
|
+
try {
|
|
533
|
+
result = await node.type(node.props)
|
|
534
|
+
} finally {
|
|
535
|
+
renderContext.effectOwners.pop()
|
|
536
|
+
}
|
|
537
|
+
const html = await renderNode(result, namespace, selectValue)
|
|
538
|
+
return owners.map(owner => `<template data-k-effect="${owner}"></template>`).join("") + html
|
|
539
|
+
}
|
|
483
540
|
|
|
484
541
|
const tag = node.type
|
|
485
542
|
const props = node.props ?? {}
|
|
@@ -508,6 +565,10 @@ async function renderNode(node, namespace, selectValue = noSelectValue) {
|
|
|
508
565
|
const root = renderContext.listRoot
|
|
509
566
|
renderContext.listRoot = undefined
|
|
510
567
|
if (root.template) attributes += ` data-k-list-root="${root.id}"`
|
|
568
|
+
if (root.effects.length) {
|
|
569
|
+
attributes += ` data-k-effects='${escapeJsonAttribute(root.effects)}'`
|
|
570
|
+
if (!root.template) attributes += ` data-k-effect-item='${escapeJsonAttribute(root.item)}'`
|
|
571
|
+
}
|
|
511
572
|
}
|
|
512
573
|
|
|
513
574
|
for (const [rawName, value] of Object.entries(props)) {
|
|
@@ -520,7 +581,7 @@ async function renderNode(node, namespace, selectValue = noSelectValue) {
|
|
|
520
581
|
}
|
|
521
582
|
if (rawName === "selected" && selectValue !== noSelectValue) continue
|
|
522
583
|
if (/^on/i.test(rawName) && !/^on[A-Z]/.test(rawName)) throw new Error(`${rawName} must use a camelCase event handler`)
|
|
523
|
-
if (rawName.toLowerCase().startsWith("data-k-")) throw new Error(`${rawName} uses Kudzu's reserved data-k-* prefix`)
|
|
584
|
+
if (rawName.toLowerCase().startsWith("data-k-") && rawName.toLowerCase() !== "data-k-native") throw new Error(`${rawName} uses Kudzu's reserved data-k-* prefix`)
|
|
524
585
|
if (["ref", "dangerouslysetinnerhtml"].includes(rawName.toLowerCase()) && (value?.[signalMarker] || value?.[bindingMarker])) {
|
|
525
586
|
throw new Error(`Reactive ${rawName} is not supported`)
|
|
526
587
|
}
|
|
@@ -613,16 +674,19 @@ async function renderNode(node, namespace, selectValue = noSelectValue) {
|
|
|
613
674
|
|
|
614
675
|
async function renderList(node, namespace, selectValue) {
|
|
615
676
|
if (namespace) throw new Error(`Reactive keyed lists are not supported inside ${namespace}`)
|
|
616
|
-
const id =
|
|
677
|
+
const id = nextRenderId("l")
|
|
617
678
|
const descriptor = { id, state: node.items.id, key: node.keyField, keys: node.items.value.map(item => item[node.keyField]) }
|
|
618
679
|
renderContext.listDepth++
|
|
619
680
|
const previousListFields = renderContext.listFields
|
|
681
|
+
const previousListEffectOwners = renderContext.listEffectOwners
|
|
620
682
|
try {
|
|
621
683
|
renderContext.listTemplate = true
|
|
684
|
+
renderContext.listEffectOwners = []
|
|
622
685
|
renderContext.listFields = new Set([node.keyField])
|
|
623
|
-
renderContext.listRoot = { id, template: true }
|
|
686
|
+
renderContext.listRoot = { id, template: true, effects: [], item: {} }
|
|
624
687
|
const template = await renderNode(node.render({}), namespace, selectValue)
|
|
625
|
-
if (template.includes("data-k-native-")) descriptor.mount = true
|
|
688
|
+
if (template.includes("data-k-native-") || template.includes("data-k-effects=")) descriptor.mount = true
|
|
689
|
+
if (template.includes("data-k-effects=")) descriptor.effects = true
|
|
626
690
|
if (template.includes("data-k-list-condition")) descriptor.conditions = true
|
|
627
691
|
if (template.includes("data-k-list-text-end")) descriptor.textRanges = true
|
|
628
692
|
if (template.includes("data-k-list-attrs")) descriptor.attributes = true
|
|
@@ -635,7 +699,7 @@ async function renderList(node, namespace, selectValue) {
|
|
|
635
699
|
renderContext.listTemplate = false
|
|
636
700
|
renderContext.listInitialMarkers = Boolean(descriptor.conditions)
|
|
637
701
|
for (const item of node.items.value) {
|
|
638
|
-
renderContext.listRoot = { id, key: item[node.keyField], template: false }
|
|
702
|
+
renderContext.listRoot = { id, key: item[node.keyField], template: false, effects: [], item }
|
|
639
703
|
current += await renderNode(node.render(item), namespace, selectValue)
|
|
640
704
|
}
|
|
641
705
|
renderContext.lists.push(descriptor)
|
|
@@ -647,10 +711,17 @@ async function renderList(node, namespace, selectValue) {
|
|
|
647
711
|
renderContext.listTemplate = false
|
|
648
712
|
renderContext.listInitialMarkers = false
|
|
649
713
|
renderContext.listFields = previousListFields
|
|
714
|
+
renderContext.listEffectOwners = previousListEffectOwners
|
|
650
715
|
renderContext.listDepth--
|
|
651
716
|
}
|
|
652
717
|
}
|
|
653
718
|
|
|
719
|
+
function nextRenderId(kind) {
|
|
720
|
+
if (renderContext.scoped) return `${renderContext.renderScope === "layout" ? "l" : "r"}${kind}${renderContext.counters[renderContext.renderScope][kind]++}`
|
|
721
|
+
const counters = { s: "nextState", r: "nextRef", c: "nextCondition", l: "nextList", e: "nextEffect", p: "nextParam" }
|
|
722
|
+
return `${kind}${renderContext[counters[kind]]++}`
|
|
723
|
+
}
|
|
724
|
+
|
|
654
725
|
function optionValue(props) {
|
|
655
726
|
if (props.value != null) return bindingValue(props.value)
|
|
656
727
|
return Array.isArray(props.children) ? props.children.join("") : props.children ?? ""
|
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
import { deserialize } from "./serialization.js"
|
|
2
2
|
|
|
3
|
-
export function createEffectContext(state, stateIds, commit, serializedScope = {}) {
|
|
3
|
+
export function createEffectContext(state, stateIds, commit, serializedScope = {}, active = () => true) {
|
|
4
4
|
const changed = new Set()
|
|
5
5
|
let scheduled = false
|
|
6
6
|
|
|
7
7
|
const flush = () => {
|
|
8
8
|
scheduled = false
|
|
9
|
+
if (!active()) return changed.clear()
|
|
9
10
|
const ids = [...changed]
|
|
10
11
|
changed.clear()
|
|
11
12
|
for (const id of ids) commit(id, state.get(id))
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
const setId = (id, value) => {
|
|
16
|
+
if (!active()) return
|
|
15
17
|
const current = state.get(id)
|
|
16
18
|
state.set(id, typeof value === "function" ? value(current) : value)
|
|
17
19
|
changed.add(id)
|
|
@@ -7,7 +7,7 @@ const imports = __KUDZU_LIST_ASYNC_PARTS__ ? new Map() : undefined
|
|
|
7
7
|
const revisions = __KUDZU_LIST_ASYNC_PARTS__ ? new WeakMap() : undefined
|
|
8
8
|
const itemParts = new WeakMap()
|
|
9
9
|
const conditionOwners = __KUDZU_LIST_CONDITIONS__ ? new WeakMap() : undefined
|
|
10
|
-
const itemPartsSelector = `[data-k-list-text]${__KUDZU_LIST_ATTRIBUTES__ ? ",[data-k-list-attrs]" : ""}${__KUDZU_LIST_EVENTS__ ? ",[data-k-list-events]" : ""}${__KUDZU_LIST_EXPRESSIONS__ ? ",[data-k-list-expression]" : ""}${__KUDZU_LIST_EXPRESSION_ATTRIBUTES__ ? ",[data-k-list-expression-attrs]" : ""}${__KUDZU_LIST_CONDITIONS__ ? ",[data-k-list-condition]" : ""}`
|
|
10
|
+
const itemPartsSelector = `[data-k-list-text]${__KUDZU_LIST_ATTRIBUTES__ ? ",[data-k-list-attrs]" : ""}${__KUDZU_LIST_EVENTS__ ? ",[data-k-list-events]" : ""}${__KUDZU_LIST_EXPRESSIONS__ ? ",[data-k-list-expression]" : ""}${__KUDZU_LIST_EXPRESSION_ATTRIBUTES__ ? ",[data-k-list-expression-attrs]" : ""}${__KUDZU_LIST_CONDITIONS__ ? ",[data-k-list-condition]" : ""}${__KUDZU_LIST_EFFECTS__ ? ",[data-k-effects]" : ""}`
|
|
11
11
|
|
|
12
12
|
function commitLists(id) {
|
|
13
13
|
const lists = listTargets.get(id)
|
|
@@ -114,8 +114,11 @@ function updateList(list) {
|
|
|
114
114
|
} else node.remove()
|
|
115
115
|
}
|
|
116
116
|
if (added) {
|
|
117
|
-
if (__KUDZU_LIST_MOUNTS__ && list.descriptor.mount)
|
|
118
|
-
|
|
117
|
+
if (__KUDZU_LIST_MOUNTS__ && list.descriptor.mount) {
|
|
118
|
+
const addedNodes = [...additions.childNodes]
|
|
119
|
+
parent.insertBefore(additions, list.boundary)
|
|
120
|
+
for (const node of addedNodes) mountDom(node)
|
|
121
|
+
} else parent.insertBefore(additions, list.boundary)
|
|
119
122
|
list.container ??= parent
|
|
120
123
|
}
|
|
121
124
|
let anchor = list.boundary
|
|
@@ -149,6 +152,7 @@ function fillListItem(root, item) {
|
|
|
149
152
|
}
|
|
150
153
|
|
|
151
154
|
function fillListParts(root, parts, item, revision) {
|
|
155
|
+
if (__KUDZU_LIST_EFFECTS__) for (const node of parts.effects) node.dataset.kEffectItem = JSON.stringify(item)
|
|
152
156
|
for (const [node, field] of parts.directTexts) {
|
|
153
157
|
const text = item?.[field]
|
|
154
158
|
const value = text == null ? "" : String(text)
|
|
@@ -203,7 +207,7 @@ function fillListParts(root, parts, item, revision) {
|
|
|
203
207
|
function listItemParts(root) {
|
|
204
208
|
let parts = itemParts.get(root)
|
|
205
209
|
if (parts) return parts
|
|
206
|
-
parts = { directTexts: [], texts: [], attributes: [], events: [], expressions: [], expressionAttributes: [], conditions: [] }
|
|
210
|
+
parts = { directTexts: [], texts: [], attributes: [], events: [], expressions: [], expressionAttributes: [], conditions: [], effects: [] }
|
|
207
211
|
for (const node of matching(root, itemPartsSelector)) {
|
|
208
212
|
if (node.hasAttribute("data-k-list-text")) (node.tagName === "TEMPLATE" ? parts.texts : parts.directTexts).push([node, node.dataset.kListText])
|
|
209
213
|
if (__KUDZU_LIST_ATTRIBUTES__ && node.hasAttribute("data-k-list-attrs")) parts.attributes.push([node, JSON.parse(node.dataset.kListAttrs)])
|
|
@@ -214,6 +218,7 @@ function listItemParts(root) {
|
|
|
214
218
|
parts.conditions.push([node, JSON.parse(node.dataset.kListCondition)])
|
|
215
219
|
conditionOwners.set(node, root)
|
|
216
220
|
}
|
|
221
|
+
if (__KUDZU_LIST_EFFECTS__ && node.hasAttribute("data-k-effects")) parts.effects.push(node)
|
|
217
222
|
}
|
|
218
223
|
itemParts.set(root, parts)
|
|
219
224
|
return parts
|
|
@@ -230,7 +235,8 @@ function listItemPartPlan(template) {
|
|
|
230
235
|
events: __KUDZU_LIST_EVENTS__ ? parts.events.map(([node, events]) => [indexes.get(node), events]) : [],
|
|
231
236
|
expressions: __KUDZU_LIST_EXPRESSIONS__ ? parts.expressions.map(([node, descriptor]) => [indexes.get(node), descriptor]) : [],
|
|
232
237
|
expressionAttributes: __KUDZU_LIST_EXPRESSION_ATTRIBUTES__ ? parts.expressionAttributes.map(([node, attributes]) => [indexes.get(node), attributes]) : [],
|
|
233
|
-
conditions: __KUDZU_LIST_CONDITIONS__ ? parts.conditions.map(([node, descriptor]) => [indexes.get(node), descriptor]) : []
|
|
238
|
+
conditions: __KUDZU_LIST_CONDITIONS__ ? parts.conditions.map(([node, descriptor]) => [indexes.get(node), descriptor]) : [],
|
|
239
|
+
effects: __KUDZU_LIST_EFFECTS__ ? parts.effects.map(node => indexes.get(node)) : []
|
|
234
240
|
}
|
|
235
241
|
}
|
|
236
242
|
|
|
@@ -246,7 +252,8 @@ function mapListItemParts(parts, root) {
|
|
|
246
252
|
conditions: __KUDZU_LIST_CONDITIONS__ ? parts.conditions.map(([index, descriptor]) => {
|
|
247
253
|
conditionOwners.set(target[index], root)
|
|
248
254
|
return [target[index], descriptor]
|
|
249
|
-
}) : []
|
|
255
|
+
}) : [],
|
|
256
|
+
effects: __KUDZU_LIST_EFFECTS__ ? parts.effects.map(index => target[index]) : []
|
|
250
257
|
})
|
|
251
258
|
}
|
|
252
259
|
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { browserState, mountDom, unmountDom } from "./shared-runtime.js"
|
|
2
|
+
|
|
3
|
+
const routes = new Set(__KUDZU_NAVIGATION_ROUTES__)
|
|
4
|
+
const applicationId = __KUDZU_APPLICATION_ID__
|
|
5
|
+
const layoutId = __KUDZU_LAYOUT_ID__
|
|
6
|
+
const navigationAsset = new URL(import.meta.url).pathname
|
|
7
|
+
const status = document.createElement("div")
|
|
8
|
+
status.dataset.kNavigationStatus = ""
|
|
9
|
+
status.setAttribute("role", "status")
|
|
10
|
+
status.setAttribute("aria-live", "polite")
|
|
11
|
+
status.style.cssText = "position:fixed;top:0;left:0;width:1px;height:1px;padding:0;margin:0;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0"
|
|
12
|
+
document.body.append(status)
|
|
13
|
+
|
|
14
|
+
let request
|
|
15
|
+
let revision = 0
|
|
16
|
+
const documents = new Map()
|
|
17
|
+
let observer
|
|
18
|
+
let idle
|
|
19
|
+
let idleAnchors
|
|
20
|
+
const noDispose = async () => {}
|
|
21
|
+
let routeDispose = noDispose
|
|
22
|
+
let layoutDispose = noDispose
|
|
23
|
+
const ready = mountInitial()
|
|
24
|
+
|
|
25
|
+
document.addEventListener("click", event => {
|
|
26
|
+
const anchor = event.target.closest?.("a[href]")
|
|
27
|
+
if (!eligibleClick(event, anchor)) return
|
|
28
|
+
const url = new URL(anchor.href)
|
|
29
|
+
event.preventDefault()
|
|
30
|
+
navigate(url, true)
|
|
31
|
+
})
|
|
32
|
+
document.addEventListener("pointerover", event => prefetchAnchor(event.target.closest?.("a[href]")))
|
|
33
|
+
document.addEventListener("focusin", event => prefetchAnchor(event.target.closest?.("a[href]")))
|
|
34
|
+
|
|
35
|
+
addEventListener("popstate", () => navigate(new URL(location.href), false))
|
|
36
|
+
addEventListener("pagehide", event => {
|
|
37
|
+
if (event.persisted) return
|
|
38
|
+
++revision
|
|
39
|
+
request?.abort()
|
|
40
|
+
void (async () => {
|
|
41
|
+
await routeDispose()
|
|
42
|
+
await layoutDispose()
|
|
43
|
+
})()
|
|
44
|
+
})
|
|
45
|
+
discover()
|
|
46
|
+
|
|
47
|
+
async function mountInitial() {
|
|
48
|
+
try {
|
|
49
|
+
const effects = await loadCapabilities(validate(document))
|
|
50
|
+
layoutDispose = await effects?.mountLayoutEffects?.() ?? noDispose
|
|
51
|
+
routeDispose = await effects?.mountRouteEffects?.() ?? noDispose
|
|
52
|
+
} catch (error) {
|
|
53
|
+
console.error(error)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function eligibleClick(event, anchor) {
|
|
58
|
+
if (!anchor || event.defaultPrevented || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return false
|
|
59
|
+
return eligibleAnchor(anchor)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function eligibleAnchor(anchor) {
|
|
63
|
+
if (anchor.hasAttribute("download") || anchor.hasAttribute("data-k-native") || !["", "_self"].includes(anchor.target)) return false
|
|
64
|
+
if (anchor.relList?.contains("external")) return false
|
|
65
|
+
const url = new URL(anchor.href)
|
|
66
|
+
if (url.hash && url.pathname === location.pathname && url.search === location.search) return false
|
|
67
|
+
return url.origin === location.origin && routes.has(url.pathname)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function discover() {
|
|
71
|
+
const anchors = [...document.querySelectorAll("a[href]")].filter(eligibleAnchor)
|
|
72
|
+
idleAnchors = anchors
|
|
73
|
+
prune(anchors)
|
|
74
|
+
observer?.disconnect()
|
|
75
|
+
if ("IntersectionObserver" in globalThis) {
|
|
76
|
+
observer ??= new IntersectionObserver(entries => {
|
|
77
|
+
for (const entry of entries) if (entry.isIntersecting) {
|
|
78
|
+
observer.unobserve(entry.target)
|
|
79
|
+
prefetchAnchor(entry.target)
|
|
80
|
+
}
|
|
81
|
+
}, { rootMargin: "200px" })
|
|
82
|
+
for (const anchor of anchors) observer.observe(anchor)
|
|
83
|
+
} else if (idle === undefined) {
|
|
84
|
+
const schedule = globalThis.requestIdleCallback ?? (callback => setTimeout(callback, 0))
|
|
85
|
+
idle = schedule(() => {
|
|
86
|
+
idle = undefined
|
|
87
|
+
for (const anchor of idleAnchors) prefetchAnchor(anchor)
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function prefetchAnchor(anchor) {
|
|
93
|
+
if (!eligibleAnchor(anchor)) return
|
|
94
|
+
const url = new URL(anchor.href)
|
|
95
|
+
prune([...document.querySelectorAll("a[href]")].filter(eligibleAnchor))
|
|
96
|
+
if (documents.has(url.href)) return
|
|
97
|
+
const pending = fetchDocument(url)
|
|
98
|
+
documents.set(url.href, pending)
|
|
99
|
+
pending.catch(() => {
|
|
100
|
+
if (documents.get(url.href) === pending) documents.delete(url.href)
|
|
101
|
+
})
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function prune(anchors) {
|
|
105
|
+
const retained = new Set([location.href, ...anchors.map(anchor => anchor.href)])
|
|
106
|
+
for (const key of documents.keys()) if (!retained.has(key)) documents.delete(key)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function navigate(url, push) {
|
|
110
|
+
await ready
|
|
111
|
+
const current = ++revision
|
|
112
|
+
request?.abort()
|
|
113
|
+
request = new AbortController()
|
|
114
|
+
let committed = false
|
|
115
|
+
try {
|
|
116
|
+
let documentResult
|
|
117
|
+
const cached = documents.get(url.href)
|
|
118
|
+
if (cached) {
|
|
119
|
+
try { documentResult = await cached }
|
|
120
|
+
catch { documentResult = await fetchDocument(url, request.signal) }
|
|
121
|
+
} else documentResult = await fetchDocument(url, request.signal)
|
|
122
|
+
documents.set(url.href, Promise.resolve(documentResult))
|
|
123
|
+
const { incoming, parsed } = documentResult
|
|
124
|
+
const effects = await loadCapabilities(parsed)
|
|
125
|
+
if (current !== revision) return
|
|
126
|
+
await routeDispose()
|
|
127
|
+
if (current !== revision) return
|
|
128
|
+
commit(incoming, parsed.nodes)
|
|
129
|
+
routeDispose = await effects?.mountRouteEffects?.() ?? noDispose
|
|
130
|
+
committed = true
|
|
131
|
+
if (push) history.pushState(null, "", url)
|
|
132
|
+
updateHead(incoming)
|
|
133
|
+
focusAndScroll(url)
|
|
134
|
+
status.textContent = `Navigated to ${document.title}`
|
|
135
|
+
discover()
|
|
136
|
+
} catch (error) {
|
|
137
|
+
if (current !== revision || error.name === "AbortError") return
|
|
138
|
+
if (push) location.assign(url.href)
|
|
139
|
+
else location.reload()
|
|
140
|
+
if (committed) return
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function loadCapabilities(parsed) {
|
|
145
|
+
const modules = await Promise.all(parsed.assets.filter(path => path !== navigationAsset).map(path => import(path)))
|
|
146
|
+
return modules.find(module => typeof module.mountRouteEffects === "function")
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function fetchDocument(url, signal) {
|
|
150
|
+
const response = await fetch(url, { signal, redirect: "manual", headers: { accept: "text/html" } })
|
|
151
|
+
if (!response.ok || response.redirected || response.type === "opaqueredirect" || !response.headers.get("content-type")?.toLowerCase().includes("text/html")) throw new Error("Navigation response is not successful nonredirected HTML")
|
|
152
|
+
const incoming = new DOMParser().parseFromString(await response.text(), "text/html")
|
|
153
|
+
return { incoming, parsed: validate(incoming) }
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function validate(incoming) {
|
|
157
|
+
if (incoming.body.dataset.kApplication !== applicationId || incoming.body.dataset.kLayout !== layoutId) throw new Error("Navigation document identity does not match")
|
|
158
|
+
const starts = incoming.querySelectorAll("template[data-k-route-start]")
|
|
159
|
+
const ends = incoming.querySelectorAll("template[data-k-route-end]")
|
|
160
|
+
if (starts.length !== 1 || ends.length !== 1) throw new Error("Navigation document must contain exactly one route marker pair")
|
|
161
|
+
const nodes = between(starts[0], ends[0])
|
|
162
|
+
const assets = [...incoming.querySelectorAll("script[data-k-capability][src]")].map(script => {
|
|
163
|
+
const url = new URL(script.src)
|
|
164
|
+
if (url.origin !== location.origin) throw new Error("Navigation capability asset must be same-origin")
|
|
165
|
+
return url.pathname
|
|
166
|
+
})
|
|
167
|
+
if (!assets.includes(navigationAsset)) throw new Error("Navigation capability asset is missing")
|
|
168
|
+
return { nodes, assets: [...new Set(assets)] }
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function commit(incoming, incomingNodes) {
|
|
172
|
+
const start = document.querySelector("template[data-k-route-start]")
|
|
173
|
+
const end = document.querySelector("template[data-k-route-end]")
|
|
174
|
+
if (!start || !end || document.querySelectorAll("template[data-k-route-start],template[data-k-route-end]").length !== 2) throw new Error("Current route markers are invalid")
|
|
175
|
+
const outgoing = between(start, end)
|
|
176
|
+
for (const node of outgoing) unmountDom(node)
|
|
177
|
+
for (const node of outgoing) node.remove()
|
|
178
|
+
for (const id of [...browserState.keys()]) if (id.startsWith("r")) browserState.delete(id)
|
|
179
|
+
for (const [id, value, compact] of JSON.parse(incoming.body.dataset.kState ?? "[]")) if (id.startsWith("r")) browserState.set(id, compact ? value[1].map(row => Object.fromEntries(value[0].map((field, index) => [field, row[index]]))) : value)
|
|
180
|
+
if (incoming.body.dataset.kTextBindings === undefined) delete document.body.dataset.kTextBindings
|
|
181
|
+
else document.body.dataset.kTextBindings = incoming.body.dataset.kTextBindings
|
|
182
|
+
const nodes = incomingNodes.map(node => document.importNode(node, true))
|
|
183
|
+
end.before(...nodes)
|
|
184
|
+
for (const node of nodes) mountDom(node)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function between(start, end) {
|
|
188
|
+
if (start.parentNode !== end.parentNode) throw new Error("Route markers must share a parent")
|
|
189
|
+
const nodes = []
|
|
190
|
+
for (let node = start.nextSibling; node && node !== end; node = node.nextSibling) nodes.push(node)
|
|
191
|
+
if (!nodes.length && start.nextSibling !== end) throw new Error("Route marker pair is invalid")
|
|
192
|
+
return nodes
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function updateHead(incoming) {
|
|
196
|
+
document.title = incoming.title
|
|
197
|
+
document.head.querySelectorAll("[data-k-head]").forEach(node => node.remove())
|
|
198
|
+
document.head.append(...[...incoming.head.querySelectorAll("[data-k-head]")].map(node => document.importNode(node, true)))
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function focusAndScroll(url) {
|
|
202
|
+
const hashTarget = url.hash && document.getElementById(decodeURIComponent(url.hash.slice(1)))
|
|
203
|
+
const target = hashTarget ?? routeElement("h1") ?? routeElement("main")
|
|
204
|
+
if (target) {
|
|
205
|
+
if (!target.hasAttribute("tabindex")) target.setAttribute("tabindex", "-1")
|
|
206
|
+
target.focus({ preventScroll: true })
|
|
207
|
+
}
|
|
208
|
+
if (hashTarget) hashTarget.scrollIntoView()
|
|
209
|
+
else scrollTo(0, 0)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function routeElement(selector) {
|
|
213
|
+
const start = document.querySelector("template[data-k-route-start]")
|
|
214
|
+
const end = document.querySelector("template[data-k-route-end]")
|
|
215
|
+
for (const node of between(start, end)) {
|
|
216
|
+
if (node.matches?.(selector)) return node
|
|
217
|
+
const match = node.querySelector?.(selector)
|
|
218
|
+
if (match) return match
|
|
219
|
+
}
|
|
220
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kudzujs/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "HTML-first TSX framework with synchronous state semantics and no virtual DOM",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
"files": [
|
|
25
25
|
"bin/",
|
|
26
26
|
"framework/",
|
|
27
|
+
"GOAL_A.md",
|
|
27
28
|
"README.md",
|
|
28
29
|
"LICENSE"
|
|
29
30
|
],
|