@kudzujs/core 0.8.52 → 0.8.55
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 +3 -1
- package/PERFORMANCE.md +13 -1
- package/README.md +1 -1
- package/RELEASES.md +113 -0
- package/docs/next-architecture/README.md +1 -1
- package/docs/next-architecture/compiler-current-architecture.md +8 -8
- package/docs/next-architecture/large-application-ai-native-roadmap.md +3 -3
- package/docs/next-architecture/versioning.md +4 -1
- package/framework/README.md +5 -1
- package/framework/build.mjs +136 -85
- package/framework/compiler/effect-codegen.mjs +17 -17
- package/framework/compiler/param-codegen.mjs +2 -2
- package/framework/compiler/route-artifact-report.mjs +133 -0
- package/framework/compiler/runtime-codegen.mjs +1 -1
- package/framework/compiler/runtime-family-planner.mjs +48 -0
- package/framework/compiler/source-compiler.mjs +11 -10
- package/framework/compiler/worker-compiler.mjs +23 -4
- package/framework/core.d.ts +2 -0
- package/framework/core.mjs +6 -4
- package/framework/dev-server.mjs +1 -1
- package/framework/navigation-runtime.js +86 -2
- package/package.json +1 -1
|
@@ -113,7 +113,7 @@ async function mountInitial() {
|
|
|
113
113
|
`, "", "initial effect mount"],
|
|
114
114
|
[" await ready\n", "", "initial effect readiness"],
|
|
115
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"],
|
|
116
|
+
[" await routeDispose()\n if (current !== revision) {\n styleUpdate.rollback()\n return\n }\n", "", "route effect disposal"],
|
|
117
117
|
[" commit(incoming, parsed.nodes, capabilities.params, url.pathname, url.search)\n", " commit(incoming, parsed.nodes)\n", "navigation capability commit"],
|
|
118
118
|
[" routeDispose = await capabilities.effects?.mountRouteEffects?.() ?? noDispose\n", "", "route effect mount"],
|
|
119
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"],
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { createHash } from "node:crypto"
|
|
2
|
+
import { assertRouteBuildRecord } from "./route-build-record.mjs"
|
|
3
|
+
import { planRouteCapabilities } from "./route-capability-planner.mjs"
|
|
4
|
+
|
|
5
|
+
export function capabilitySignature(capability) {
|
|
6
|
+
return createHash("sha256").update(JSON.stringify(capability)).digest("hex")
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function planRuntimeFamilies(records, navigationGroups = []) {
|
|
10
|
+
for (const record of records) assertRouteBuildRecord(record)
|
|
11
|
+
const grouped = new Set()
|
|
12
|
+
const scopes = []
|
|
13
|
+
for (const group of navigationGroups) {
|
|
14
|
+
const groupRecords = group.buildRecords ?? group.records ?? []
|
|
15
|
+
if (!groupRecords.length) continue
|
|
16
|
+
for (const record of groupRecords) {
|
|
17
|
+
if (!records.includes(record)) throw new Error(`Navigation runtime family contains an unknown route: ${record.route}`)
|
|
18
|
+
if (grouped.has(record)) throw new Error(`Route belongs to multiple runtime families: ${record.route}`)
|
|
19
|
+
grouped.add(record)
|
|
20
|
+
}
|
|
21
|
+
scopes.push({ navigation: true, records: groupRecords })
|
|
22
|
+
}
|
|
23
|
+
for (const record of records) if (!grouped.has(record) && record.capabilities.hasBehaviors) scopes.push({ navigation: false, records: [record] })
|
|
24
|
+
|
|
25
|
+
const familiesBySignature = new Map()
|
|
26
|
+
const signaturesById = new Map()
|
|
27
|
+
const familyByRecord = new Map()
|
|
28
|
+
for (const scope of scopes) {
|
|
29
|
+
const capability = planRouteCapabilities(scope.records, { navigationRouteCount: scope.navigation ? scope.records.length : 0 })
|
|
30
|
+
const descriptor = { version: 1, navigation: scope.navigation, capability }
|
|
31
|
+
const signature = capabilitySignature(descriptor)
|
|
32
|
+
const id = signature.slice(0, 16)
|
|
33
|
+
const existingSignature = signaturesById.get(id)
|
|
34
|
+
if (existingSignature && existingSignature !== signature) throw new Error(`Runtime family ID collision: ${id}`)
|
|
35
|
+
signaturesById.set(id, signature)
|
|
36
|
+
let family = familiesBySignature.get(signature)
|
|
37
|
+
if (!family) {
|
|
38
|
+
family = { id, signature, navigation: scope.navigation, capability, records: [] }
|
|
39
|
+
familiesBySignature.set(signature, family)
|
|
40
|
+
}
|
|
41
|
+
for (const record of scope.records) {
|
|
42
|
+
family.records.push(record)
|
|
43
|
+
familyByRecord.set(record, family)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const families = [...familiesBySignature.values()].map(family => ({ ...family, records: [...family.records].sort((left, right) => left.route.localeCompare(right.route)) })).sort((left, right) => left.id.localeCompare(right.id))
|
|
47
|
+
return { families, familyByRecord }
|
|
48
|
+
}
|
|
@@ -3059,7 +3059,7 @@ async function safeStaticFiles(files) {
|
|
|
3059
3059
|
return new Set(entries.filter(Boolean))
|
|
3060
3060
|
}
|
|
3061
3061
|
|
|
3062
|
-
function orderSourceStyles(
|
|
3062
|
+
function orderSourceStyles(entryFiles, sourceFiles, sourceIndex, staticFiles) {
|
|
3063
3063
|
const ordered = []
|
|
3064
3064
|
const seenStyles = new Set()
|
|
3065
3065
|
const seenSources = new Set()
|
|
@@ -3069,11 +3069,13 @@ function orderSourceStyles(cssFiles, sourceFiles, sourceIndex, staticFiles) {
|
|
|
3069
3069
|
seenSources.add(file)
|
|
3070
3070
|
const sourceFile = parseSourceFile(file, sourceIndex.get(file))
|
|
3071
3071
|
for (const statement of sourceFile.statements) {
|
|
3072
|
-
if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier) || !statement.moduleSpecifier.text.startsWith(".")) continue
|
|
3072
|
+
if ((!ts.isImportDeclaration(statement) && !ts.isExportDeclaration(statement)) || !runtimeModuleReference(statement) || !statement.moduleSpecifier || !ts.isStringLiteral(statement.moduleSpecifier) || !statement.moduleSpecifier.text.startsWith(".")) continue
|
|
3073
3073
|
const specifier = statement.moduleSpecifier.text
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3074
|
+
const queryIndex = specifier.indexOf("?")
|
|
3075
|
+
const query = queryIndex === -1 ? "" : specifier.slice(queryIndex + 1)
|
|
3076
|
+
if (ts.isImportDeclaration(statement) && staticImportExtension(specifier) === ".css") {
|
|
3077
|
+
if (query) continue
|
|
3078
|
+
const target = resolveStaticImport(file, specifier, staticFiles)
|
|
3077
3079
|
if (!seenStyles.has(target)) {
|
|
3078
3080
|
seenStyles.add(target)
|
|
3079
3081
|
ordered.push(target)
|
|
@@ -3081,12 +3083,11 @@ function orderSourceStyles(cssFiles, sourceFiles, sourceIndex, staticFiles) {
|
|
|
3081
3083
|
continue
|
|
3082
3084
|
}
|
|
3083
3085
|
if (isStaticImport(specifier)) continue
|
|
3084
|
-
|
|
3086
|
+
visit(resolveSourceImport(file, specifier, sourceSet))
|
|
3085
3087
|
}
|
|
3086
3088
|
}
|
|
3087
|
-
for (const file of
|
|
3088
|
-
|
|
3089
|
-
return [...ordered, ...cssFiles.filter(file => !seenStyles.has(file))]
|
|
3089
|
+
for (const file of entryFiles) visit(file)
|
|
3090
|
+
return ordered
|
|
3090
3091
|
}
|
|
3091
3092
|
|
|
3092
3093
|
function staticImportEntry(node, sourceFile, file, staticFiles, importedAssets, cssModules, base, factory) {
|
|
@@ -3105,7 +3106,7 @@ function staticImportEntry(node, sourceFile, file, staticFiles, importedAssets,
|
|
|
3105
3106
|
const extension = staticImportExtension(specifier)
|
|
3106
3107
|
if (query === "url") {
|
|
3107
3108
|
if (!node.importClause?.name || node.importClause.isTypeOnly || node.importClause.namedBindings) throw sourceNodeError(node, sourceFile, "Static assets require one default import")
|
|
3108
|
-
|
|
3109
|
+
importedAssets.add(target)
|
|
3109
3110
|
const value = factory.createStringLiteral(assetPath(base, `assets/${relative(sourceDirectory, target).replaceAll(sep, "/")}`))
|
|
3110
3111
|
return staticImportReplacement(node.importClause.name.text, value, factory)
|
|
3111
3112
|
}
|
|
@@ -130,7 +130,7 @@ export function createWorkerCompiler({
|
|
|
130
130
|
|
|
131
131
|
const emit = async (references, sourceFiles, assetsDirectory, base, minify) => {
|
|
132
132
|
const roots = [...new Set(references.map(reference => resolve(sourceDirectory, reference.root)))].sort()
|
|
133
|
-
if (!roots.length) return new Map()
|
|
133
|
+
if (!roots.length) return { assets: new Map(), outputs: new Map() }
|
|
134
134
|
await validateGraphs(roots, sourceFiles)
|
|
135
135
|
const workerDirectory = resolve(assetsDirectory, "workers")
|
|
136
136
|
await mkdir(workerDirectory, { recursive: true })
|
|
@@ -152,16 +152,35 @@ export function createWorkerCompiler({
|
|
|
152
152
|
logLevel: "silent"
|
|
153
153
|
})
|
|
154
154
|
const emitted = new Map()
|
|
155
|
+
const outputUrls = new Map(Object.keys(result.metafile.outputs).map(output => {
|
|
156
|
+
const outputFile = resolve(root, output)
|
|
157
|
+
return [outputFile, assetPath(base, relative(resolve(assetsDirectory, ".."), outputFile).replaceAll(sep, "/"))]
|
|
158
|
+
}))
|
|
159
|
+
const outputImports = new Map(Object.entries(result.metafile.outputs).map(([output, metadata]) => {
|
|
160
|
+
const outputFile = resolve(root, output)
|
|
161
|
+
return [outputUrls.get(outputFile), (metadata.imports ?? []).filter(entry => !entry.external).map(entry => outputUrls.get(resolve(root, entry.path)) ?? outputUrls.get(resolve(outputFile, "..", entry.path))).filter(Boolean)]
|
|
162
|
+
}))
|
|
163
|
+
const outputs = new Map()
|
|
155
164
|
for (const [output, metadata] of Object.entries(result.metafile.outputs)) {
|
|
156
165
|
if (!metadata.entryPoint) continue
|
|
157
166
|
const entry = resolve(root, metadata.entryPoint)
|
|
158
167
|
const rootReferences = references.filter(reference => resolve(sourceDirectory, reference.root) === entry)
|
|
159
168
|
const outputFile = resolve(root, output)
|
|
160
|
-
const url =
|
|
161
|
-
|
|
169
|
+
const url = outputUrls.get(outputFile)
|
|
170
|
+
const closure = new Set()
|
|
171
|
+
const visit = path => {
|
|
172
|
+
if (closure.has(path)) return
|
|
173
|
+
closure.add(path)
|
|
174
|
+
for (const imported of outputImports.get(path) ?? []) visit(imported)
|
|
175
|
+
}
|
|
176
|
+
visit(url)
|
|
177
|
+
for (const reference of rootReferences) {
|
|
178
|
+
emitted.set(reference.placeholder, url)
|
|
179
|
+
outputs.set(reference.placeholder, { entry: url, chunks: [...closure].filter(path => path !== url).sort() })
|
|
180
|
+
}
|
|
162
181
|
}
|
|
163
182
|
for (const reference of references) if (!emitted.has(reference.placeholder)) throw new Error(`Worker entry was not emitted: ${reference.root}`)
|
|
164
|
-
return emitted
|
|
183
|
+
return { assets: emitted, outputs }
|
|
165
184
|
}
|
|
166
185
|
|
|
167
186
|
return { candidate, emit, rejectConstructions, rejectOrdinaryImports, rewriteEffect }
|
package/framework/core.d.ts
CHANGED
package/framework/core.mjs
CHANGED
|
@@ -486,9 +486,11 @@ export async function renderPage(component, metadata = {}, props = {}, layout) {
|
|
|
486
486
|
const title = escapeHtml(metadata.title ?? "Kudzu")
|
|
487
487
|
const head = renderMetadata(metadata)
|
|
488
488
|
const capability = metadata.navigationAsset ? " data-k-capability" : ""
|
|
489
|
+
const managedStyles = new Set(metadata.managedStyles ?? [])
|
|
490
|
+
const styleAnchor = metadata.navigationAsset ? "<meta data-k-style-anchor>" : ""
|
|
489
491
|
const styles = metadata.styles === false
|
|
490
492
|
? ""
|
|
491
|
-
: (Array.isArray(metadata.styles) ? metadata.styles : [assetPath(metadata.base, "assets/style.css")]).map(href => `<link rel="stylesheet" href="${escapeAttribute(href)}">`).join("")
|
|
493
|
+
: (Array.isArray(metadata.styles) ? metadata.styles : [assetPath(metadata.base, "assets/style.css")]).map(href => `<link rel="stylesheet" href="${escapeAttribute(href)}"${managedStyles.has(href) ? " data-k-route-style" : ""}>`).join("")
|
|
492
494
|
const runtime = renderContext.hasBehaviors
|
|
493
495
|
? `<script type="module"${capability} src="${escapeAttribute(metadata.runtimeAsset ?? assetPath(metadata.base, "assets/kudzu.js"))}"></script>`
|
|
494
496
|
: ""
|
|
@@ -499,10 +501,10 @@ export async function renderPage(component, metadata = {}, props = {}, layout) {
|
|
|
499
501
|
? `<script type="module"${capability} src="${escapeAttribute(metadata.paramAsset)}"></script>`
|
|
500
502
|
: ""
|
|
501
503
|
const bindingRuntime = renderContext.hasBindings
|
|
502
|
-
? `<script type="module"${capability} src="${assetPath(metadata.base, "assets/kudzu-binding.js")}"></script>`
|
|
504
|
+
? `<script type="module"${capability} src="${escapeAttribute(metadata.bindingAsset ?? assetPath(metadata.base, "assets/kudzu-binding.js"))}"></script>`
|
|
503
505
|
: ""
|
|
504
506
|
const listRuntime = renderContext.hasLists
|
|
505
|
-
? `<script type="module"${capability} src="${assetPath(metadata.base, "assets/kudzu-list.js")}"></script>`
|
|
507
|
+
? `<script type="module"${capability} src="${escapeAttribute(metadata.listAsset ?? assetPath(metadata.base, "assets/kudzu-list.js"))}"></script>`
|
|
506
508
|
: ""
|
|
507
509
|
const effectRuntime = renderContext.hasEffects
|
|
508
510
|
? `<script type="module"${capability} src="${escapeAttribute(metadata.effectAsset)}"></script>`
|
|
@@ -526,7 +528,7 @@ export async function renderPage(component, metadata = {}, props = {}, layout) {
|
|
|
526
528
|
: ""
|
|
527
529
|
|
|
528
530
|
return {
|
|
529
|
-
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)}" data-k-route="${escapeAttribute(metadata.routeId)}"` : ""}>${body}</body></html>`,
|
|
531
|
+
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}${styleAnchor}${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)}" data-k-route="${escapeAttribute(metadata.routeId)}"` : ""}>${body}</body></html>`,
|
|
530
532
|
hasBehaviors: renderContext.hasBehaviors,
|
|
531
533
|
hasEffects: renderContext.hasEffects,
|
|
532
534
|
hasParams: renderContext.hasParams,
|
package/framework/dev-server.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import { extname, join, resolve, sep } from "node:path"
|
|
|
5
5
|
import { browserPath, withBase } from "./compiler/path-helpers.mjs"
|
|
6
6
|
import { stateSchema } from "./dev-state.js"
|
|
7
7
|
|
|
8
|
-
const devClient = (session, revision, schema) => `<script>(()=>{const show=event=>{let box=document.getElementById("__kudzu_error");if(!box){box=document.createElement("div");box.id="__kudzu_error";box.setAttribute("role","alert");box.setAttribute("aria-live","assertive");box.style.cssText="position:fixed;inset:0;z-index:2147483647;overflow:auto;padding:2rem;background:#200;color:#fff;font:16px/1.5 ui-monospace,monospace";const title=document.createElement("strong"),text=document.createElement("pre");title.textContent="Kudzu build error";text.style.whiteSpace="pre-wrap";box.append(title,text);document.body.append(box)}box.querySelector("pre").textContent=event.data};const schema=${inlineJson(schema)},route=location.pathname+location.search+location.hash,urls=[...document.querySelectorAll('script[type="module"][src]')].map(node=>node.src).filter(url=>/\/assets\/kudzu(?:-
|
|
8
|
+
const devClient = (session, revision, schema) => `<script>(()=>{const show=event=>{let box=document.getElementById("__kudzu_error");if(!box){box=document.createElement("div");box.id="__kudzu_error";box.setAttribute("role","alert");box.setAttribute("aria-live","assertive");box.style.cssText="position:fixed;inset:0;z-index:2147483647;overflow:auto;padding:2rem;background:#200;color:#fff;font:16px/1.5 ui-monospace,monospace";const title=document.createElement("strong"),text=document.createElement("pre");title.textContent="Kudzu build error";text.style.whiteSpace="pre-wrap";box.append(title,text);document.body.append(box)}box.querySelector("pre").textContent=event.data};const schema=${inlineJson(schema)},route=location.pathname+location.search+location.hash,urls=[...document.querySelectorAll('script[type="module"][src]')].map(node=>node.src).filter(url=>/\/assets\/runtime\/[a-f0-9]+\/kudzu(?:-deps|-binding|-list|-native)?\.js$/.test(new URL(url).pathname));const devImport=import("/__kudzu_dev.js"),runtimeImports=Promise.allSettled(urls.map(url=>import(url)));const ready=(async()=>{const dev=await devImport,modules=await runtimeImports,runtime=modules.find(result=>result.status==="fulfilled"&&result.value.browserState instanceof Map&&typeof result.value.commitDom==="function")?.value;try{dev.restoreState(sessionStorage,route,runtime?.browserState,schema,runtime?.commitDom)}catch{}return{dev,runtime}})().catch(()=>({}));const events=new EventSource("/__kudzu_reload?session=${session}&revision=${revision}");let reloading=false;events.addEventListener("reload",async()=>{if(reloading)return;reloading=true;try{const{dev,runtime}=await ready;dev?.snapshotState(sessionStorage,route,runtime?.browserState,schema)}catch{}location.reload()});events.addEventListener("build-error",show)})()</script>`
|
|
9
9
|
|
|
10
10
|
export function parseDevPort(value) {
|
|
11
11
|
if (value === undefined || value.trim() === "") return 3000
|
|
@@ -12,6 +12,7 @@ status.style.cssText = "position:fixed;top:0;left:0;width:1px;height:1px;padding
|
|
|
12
12
|
document.body.append(status)
|
|
13
13
|
|
|
14
14
|
let request
|
|
15
|
+
let pendingStyleUpdate
|
|
15
16
|
let revision = 0
|
|
16
17
|
const documents = new Map()
|
|
17
18
|
let observer
|
|
@@ -115,9 +116,12 @@ async function navigate(url, push) {
|
|
|
115
116
|
const record = matchRoute(url.pathname)
|
|
116
117
|
if (!record) return fallback(url, push)
|
|
117
118
|
const current = ++revision
|
|
119
|
+
pendingStyleUpdate?.rollback()
|
|
120
|
+
pendingStyleUpdate = undefined
|
|
118
121
|
request?.abort()
|
|
119
122
|
request = new AbortController()
|
|
120
123
|
let committed = false
|
|
124
|
+
let styleUpdate
|
|
121
125
|
try {
|
|
122
126
|
let documentResult
|
|
123
127
|
const cached = documents.get(url.href)
|
|
@@ -128,8 +132,20 @@ async function navigate(url, push) {
|
|
|
128
132
|
documents.set(url.href, Promise.resolve(documentResult))
|
|
129
133
|
const { incoming, parsed, capabilities } = documentResult
|
|
130
134
|
if (current !== revision) return
|
|
135
|
+
styleUpdate = prepareStyles(parsed.styles)
|
|
136
|
+
pendingStyleUpdate = styleUpdate
|
|
137
|
+
await styleUpdate.ready
|
|
138
|
+
if (current !== revision) {
|
|
139
|
+
styleUpdate.rollback()
|
|
140
|
+
return
|
|
141
|
+
}
|
|
131
142
|
await routeDispose()
|
|
132
|
-
if (current !== revision)
|
|
143
|
+
if (current !== revision) {
|
|
144
|
+
styleUpdate.rollback()
|
|
145
|
+
return
|
|
146
|
+
}
|
|
147
|
+
styleUpdate.commit()
|
|
148
|
+
if (pendingStyleUpdate === styleUpdate) pendingStyleUpdate = undefined
|
|
133
149
|
commit(incoming, parsed.nodes, capabilities.params, url.pathname, url.search)
|
|
134
150
|
committed = true
|
|
135
151
|
routeDispose = await capabilities.effects?.mountRouteEffects?.() ?? noDispose
|
|
@@ -139,6 +155,8 @@ async function navigate(url, push) {
|
|
|
139
155
|
status.textContent = `Navigated to ${document.title}`
|
|
140
156
|
discover()
|
|
141
157
|
} catch (error) {
|
|
158
|
+
styleUpdate?.rollback()
|
|
159
|
+
if (pendingStyleUpdate === styleUpdate) pendingStyleUpdate = undefined
|
|
142
160
|
if (current !== revision || error.name === "AbortError") return
|
|
143
161
|
fallback(url, push)
|
|
144
162
|
if (committed) return
|
|
@@ -173,7 +191,73 @@ function validate(incoming, record) {
|
|
|
173
191
|
return url.pathname
|
|
174
192
|
})
|
|
175
193
|
if (!assets.includes(navigationAsset)) throw new Error("Navigation capability asset is missing")
|
|
176
|
-
|
|
194
|
+
const styles = [...incoming.head.querySelectorAll('link[data-k-route-style][rel="stylesheet"][href]')]
|
|
195
|
+
const styleUrls = styles.map(link => {
|
|
196
|
+
const url = new URL(link.href)
|
|
197
|
+
if (url.origin !== location.origin) throw new Error("Navigation stylesheet must be same-origin")
|
|
198
|
+
return url.href
|
|
199
|
+
})
|
|
200
|
+
if (new Set(styleUrls).size !== styleUrls.length) throw new Error("Navigation document has duplicate route stylesheets")
|
|
201
|
+
return { nodes, assets: [...new Set(assets)], styles }
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function prepareStyles(incoming) {
|
|
205
|
+
const anchor = document.head.querySelector("meta[data-k-style-anchor]")
|
|
206
|
+
if (!anchor || document.head.querySelectorAll("meta[data-k-style-anchor]").length !== 1) throw new Error("Current navigation style anchor is invalid")
|
|
207
|
+
const current = [...document.head.querySelectorAll('link[data-k-route-style][rel="stylesheet"][href]')]
|
|
208
|
+
const place = links => {
|
|
209
|
+
let previous = anchor
|
|
210
|
+
for (const link of links) {
|
|
211
|
+
if (link.previousSibling !== previous) previous.after(link)
|
|
212
|
+
previous = link
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
const byUrl = new Map(current.map(link => [new URL(link.href).href, link]))
|
|
216
|
+
const next = []
|
|
217
|
+
const created = []
|
|
218
|
+
const loads = []
|
|
219
|
+
for (const source of incoming) {
|
|
220
|
+
const href = new URL(source.href).href
|
|
221
|
+
let link = byUrl.get(href)
|
|
222
|
+
if (link) byUrl.delete(href)
|
|
223
|
+
else {
|
|
224
|
+
link = document.importNode(source, true)
|
|
225
|
+
created.push(link)
|
|
226
|
+
loads.push(new Promise((resolve, reject) => {
|
|
227
|
+
link.addEventListener("load", resolve, { once: true })
|
|
228
|
+
link.addEventListener("error", () => reject(new Error("Navigation stylesheet failed to load")), { once: true })
|
|
229
|
+
}))
|
|
230
|
+
}
|
|
231
|
+
next.push(link)
|
|
232
|
+
}
|
|
233
|
+
let settled = false
|
|
234
|
+
let cancel
|
|
235
|
+
const update = {
|
|
236
|
+
ready: Promise.race([
|
|
237
|
+
Promise.all(loads),
|
|
238
|
+
new Promise((resolve, reject) => {
|
|
239
|
+
cancel = () => {
|
|
240
|
+
const error = new Error("Navigation stylesheet load was cancelled")
|
|
241
|
+
error.name = "AbortError"
|
|
242
|
+
reject(error)
|
|
243
|
+
}
|
|
244
|
+
})
|
|
245
|
+
]),
|
|
246
|
+
commit() {
|
|
247
|
+
if (settled) return
|
|
248
|
+
settled = true
|
|
249
|
+
for (const link of byUrl.values()) link.remove()
|
|
250
|
+
},
|
|
251
|
+
rollback() {
|
|
252
|
+
if (settled) return
|
|
253
|
+
settled = true
|
|
254
|
+
place(current)
|
|
255
|
+
for (const link of created) link.remove()
|
|
256
|
+
cancel()
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
place(next)
|
|
260
|
+
return update
|
|
177
261
|
}
|
|
178
262
|
|
|
179
263
|
function commit(incoming, incomingNodes, initializeParams, pathname, search) {
|