@nuraly/lumenjs 0.1.3 → 0.2.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/README.md +62 -282
- package/dist/auth/config.d.ts +23 -0
- package/dist/auth/config.js +115 -0
- package/dist/auth/guard.d.ts +12 -0
- package/dist/auth/guard.js +28 -0
- package/dist/auth/index.d.ts +3 -0
- package/dist/auth/index.js +1 -0
- package/dist/auth/middleware.d.ts +23 -0
- package/dist/auth/middleware.js +89 -0
- package/dist/auth/native-auth.d.ts +82 -0
- package/dist/auth/native-auth.js +340 -0
- package/dist/auth/oidc-client.d.ts +17 -0
- package/dist/auth/oidc-client.js +123 -0
- package/dist/auth/providers/google.d.ts +23 -0
- package/dist/auth/providers/google.js +25 -0
- package/dist/auth/providers/index.d.ts +2 -0
- package/dist/auth/providers/index.js +1 -0
- package/dist/auth/routes/login.d.ts +8 -0
- package/dist/auth/routes/login.js +121 -0
- package/dist/auth/routes/logout.d.ts +4 -0
- package/dist/auth/routes/logout.js +79 -0
- package/dist/auth/routes/oidc-callback.d.ts +3 -0
- package/dist/auth/routes/oidc-callback.js +70 -0
- package/dist/auth/routes/password.d.ts +5 -0
- package/dist/auth/routes/password.js +149 -0
- package/dist/auth/routes/signup.d.ts +3 -0
- package/dist/auth/routes/signup.js +81 -0
- package/dist/auth/routes/token.d.ts +4 -0
- package/dist/auth/routes/token.js +70 -0
- package/dist/auth/routes/totp.d.ts +22 -0
- package/dist/auth/routes/totp.js +232 -0
- package/dist/auth/routes/utils.d.ts +7 -0
- package/dist/auth/routes/utils.js +35 -0
- package/dist/auth/routes/verify.d.ts +3 -0
- package/dist/auth/routes/verify.js +26 -0
- package/dist/auth/routes.d.ts +8 -0
- package/dist/auth/routes.js +124 -0
- package/dist/auth/session.d.ts +8 -0
- package/dist/auth/session.js +54 -0
- package/dist/auth/token.d.ts +33 -0
- package/dist/auth/token.js +90 -0
- package/dist/auth/types.d.ts +156 -0
- package/dist/auth/types.js +2 -0
- package/dist/build/build-client.d.ts +15 -0
- package/dist/build/build-client.js +45 -0
- package/dist/build/build-prerender.d.ts +11 -0
- package/dist/build/build-prerender.js +159 -0
- package/dist/build/build-server.d.ts +18 -0
- package/dist/build/build-server.js +107 -0
- package/dist/build/build.js +60 -123
- package/dist/build/scan.d.ts +18 -0
- package/dist/build/scan.js +77 -6
- package/dist/build/serve-api.js +8 -2
- package/dist/build/serve-loaders.d.ts +4 -4
- package/dist/build/serve-loaders.js +26 -18
- package/dist/build/serve-ssr.js +38 -11
- package/dist/build/serve-static.js +3 -3
- package/dist/build/serve.js +341 -18
- package/dist/cli.js +37 -6
- package/dist/communication/encryption.d.ts +35 -0
- package/dist/communication/encryption.js +90 -0
- package/dist/communication/handlers/context.d.ts +27 -0
- package/dist/communication/handlers/context.js +1 -0
- package/dist/communication/handlers/conversation.d.ts +24 -0
- package/dist/communication/handlers/conversation.js +113 -0
- package/dist/communication/handlers/file-upload.d.ts +17 -0
- package/dist/communication/handlers/file-upload.js +62 -0
- package/dist/communication/handlers/messaging.d.ts +30 -0
- package/dist/communication/handlers/messaging.js +237 -0
- package/dist/communication/handlers/presence.d.ts +15 -0
- package/dist/communication/handlers/presence.js +76 -0
- package/dist/communication/handlers.d.ts +5 -0
- package/dist/communication/handlers.js +5 -0
- package/dist/communication/index.d.ts +9 -0
- package/dist/communication/index.js +7 -0
- package/dist/communication/link-preview.d.ts +18 -0
- package/dist/communication/link-preview.js +115 -0
- package/dist/communication/schema.d.ts +10 -0
- package/dist/communication/schema.js +101 -0
- package/dist/communication/server.d.ts +86 -0
- package/dist/communication/server.js +212 -0
- package/dist/communication/signaling.d.ts +43 -0
- package/dist/communication/signaling.js +271 -0
- package/dist/communication/store.d.ts +71 -0
- package/dist/communication/store.js +289 -0
- package/dist/communication/types.d.ts +454 -0
- package/dist/communication/types.js +1 -0
- package/dist/create.d.ts +1 -0
- package/dist/create.js +55 -0
- package/dist/db/auto-migrate.d.ts +3 -0
- package/dist/db/auto-migrate.js +100 -0
- package/dist/db/client.d.ts +3 -0
- package/dist/db/client.js +18 -0
- package/dist/db/index.d.ts +17 -13
- package/dist/db/index.js +205 -26
- package/dist/db/seed.d.ts +12 -0
- package/dist/db/seed.js +88 -0
- package/dist/db/table.d.ts +10 -0
- package/dist/db/table.js +12 -0
- package/dist/dev-server/config.d.ts +11 -0
- package/dist/dev-server/config.js +40 -20
- package/dist/dev-server/index-html.d.ts +4 -0
- package/dist/dev-server/index-html.js +21 -6
- package/dist/dev-server/nuralyui-aliases.d.ts +0 -4
- package/dist/dev-server/nuralyui-aliases.js +115 -94
- package/dist/dev-server/plugins/vite-plugin-api-routes.js +29 -5
- package/dist/dev-server/plugins/vite-plugin-auth.d.ts +6 -0
- package/dist/dev-server/plugins/vite-plugin-auth.js +223 -0
- package/dist/dev-server/plugins/vite-plugin-auto-define.d.ts +16 -0
- package/dist/dev-server/plugins/vite-plugin-auto-define.js +111 -0
- package/dist/dev-server/plugins/vite-plugin-communication.d.ts +6 -0
- package/dist/dev-server/plugins/vite-plugin-communication.js +205 -0
- package/dist/dev-server/plugins/vite-plugin-editor-api.d.ts +6 -0
- package/dist/dev-server/plugins/vite-plugin-editor-api.js +318 -0
- package/dist/dev-server/plugins/vite-plugin-i18n.js +69 -2
- package/dist/dev-server/plugins/vite-plugin-lit-dedup.d.ts +6 -0
- package/dist/dev-server/plugins/vite-plugin-lit-dedup.js +78 -34
- package/dist/dev-server/plugins/vite-plugin-lit-hmr.js +44 -2
- package/dist/dev-server/plugins/vite-plugin-llms.d.ts +2 -0
- package/dist/dev-server/plugins/vite-plugin-llms.js +92 -0
- package/dist/dev-server/plugins/vite-plugin-loaders.js +146 -13
- package/dist/dev-server/plugins/vite-plugin-routes.js +16 -5
- package/dist/dev-server/plugins/vite-plugin-socketio.d.ts +2 -0
- package/dist/dev-server/plugins/vite-plugin-socketio.js +51 -0
- package/dist/dev-server/plugins/vite-plugin-source-annotator.d.ts +2 -0
- package/dist/dev-server/plugins/vite-plugin-source-annotator.js +26 -3
- package/dist/dev-server/plugins/vite-plugin-storage.d.ts +10 -0
- package/dist/dev-server/plugins/vite-plugin-storage.js +126 -0
- package/dist/dev-server/plugins/vite-plugin-virtual-modules.js +140 -3
- package/dist/dev-server/server.js +242 -70
- package/dist/dev-server/ssr-render.d.ts +2 -1
- package/dist/dev-server/ssr-render.js +117 -50
- package/dist/editor/ai/backend.d.ts +20 -0
- package/dist/editor/ai/backend.js +113 -0
- package/dist/editor/ai/claude-code-client.d.ts +20 -0
- package/dist/editor/ai/claude-code-client.js +145 -0
- package/dist/editor/ai/deepseek-client.d.ts +7 -0
- package/dist/editor/ai/deepseek-client.js +113 -0
- package/dist/editor/ai/opencode-client.d.ts +14 -0
- package/dist/editor/ai/opencode-client.js +99 -0
- package/dist/editor/ai/snapshot-store.d.ts +22 -0
- package/dist/editor/ai/snapshot-store.js +35 -0
- package/dist/editor/ai/types.d.ts +30 -0
- package/dist/editor/ai/types.js +136 -0
- package/dist/editor/ai-chat-panel.d.ts +13 -0
- package/dist/editor/ai-chat-panel.js +613 -0
- package/dist/editor/ai-markdown.d.ts +10 -0
- package/dist/editor/ai-markdown.js +70 -0
- package/dist/editor/ai-project-panel.d.ts +11 -0
- package/dist/editor/ai-project-panel.js +332 -0
- package/dist/editor/ast-modification.d.ts +11 -0
- package/dist/editor/ast-modification.js +1 -0
- package/dist/editor/ast-service.d.ts +30 -0
- package/dist/editor/ast-service.js +180 -0
- package/dist/editor/css-rules.d.ts +54 -0
- package/dist/editor/css-rules.js +423 -0
- package/dist/editor/editor-api-client.d.ts +51 -0
- package/dist/editor/editor-api-client.js +162 -0
- package/dist/editor/editor-bridge.d.ts +1 -0
- package/dist/editor/editor-bridge.js +18 -8
- package/dist/editor/editor-toolbar.d.ts +14 -0
- package/dist/editor/editor-toolbar.js +115 -0
- package/dist/editor/file-editor.d.ts +9 -0
- package/dist/editor/file-editor.js +236 -0
- package/dist/editor/file-service.d.ts +16 -0
- package/dist/editor/file-service.js +52 -0
- package/dist/editor/i18n-key-gen.d.ts +1 -0
- package/dist/editor/i18n-key-gen.js +7 -0
- package/dist/editor/inline-text-edit.d.ts +5 -0
- package/dist/editor/inline-text-edit.js +173 -92
- package/dist/editor/overlay-events.d.ts +5 -0
- package/dist/editor/overlay-events.js +364 -0
- package/dist/editor/overlay-hmr.d.ts +2 -0
- package/dist/editor/overlay-hmr.js +76 -0
- package/dist/editor/overlay-selection.d.ts +29 -0
- package/dist/editor/overlay-selection.js +148 -0
- package/dist/editor/overlay-utils.d.ts +12 -0
- package/dist/editor/overlay-utils.js +59 -0
- package/dist/editor/properties-panel-persist.d.ts +14 -0
- package/dist/editor/properties-panel-persist.js +70 -0
- package/dist/editor/properties-panel-rows.d.ts +10 -0
- package/dist/editor/properties-panel-rows.js +349 -0
- package/dist/editor/properties-panel-styles.d.ts +4 -0
- package/dist/editor/properties-panel-styles.js +174 -0
- package/dist/editor/properties-panel.d.ts +4 -0
- package/dist/editor/properties-panel.js +148 -0
- package/dist/editor/property-registry.d.ts +16 -0
- package/dist/editor/property-registry.js +303 -0
- package/dist/editor/standalone-file-panel.d.ts +0 -0
- package/dist/editor/standalone-file-panel.js +1 -0
- package/dist/editor/standalone-overlay-dom.d.ts +0 -0
- package/dist/editor/standalone-overlay-dom.js +1 -0
- package/dist/editor/standalone-overlay-styles.d.ts +0 -0
- package/dist/editor/standalone-overlay-styles.js +1 -0
- package/dist/editor/standalone-overlay.d.ts +1 -0
- package/dist/editor/standalone-overlay.js +76 -0
- package/dist/editor/syntax-highlighter.d.ts +4 -0
- package/dist/editor/syntax-highlighter.js +81 -0
- package/dist/editor/text-toolbar.d.ts +11 -0
- package/dist/editor/text-toolbar.js +327 -0
- package/dist/editor/toolbar-styles.d.ts +4 -0
- package/dist/editor/toolbar-styles.js +198 -0
- package/dist/email/index.d.ts +32 -0
- package/dist/email/index.js +154 -0
- package/dist/email/providers/resend.d.ts +2 -0
- package/dist/email/providers/resend.js +24 -0
- package/dist/email/providers/sendgrid.d.ts +2 -0
- package/dist/email/providers/sendgrid.js +31 -0
- package/dist/email/providers/smtp.d.ts +13 -0
- package/dist/email/providers/smtp.js +125 -0
- package/dist/email/template-engine.d.ts +18 -0
- package/dist/email/template-engine.js +116 -0
- package/dist/email/templates/base.d.ts +9 -0
- package/dist/email/templates/base.js +65 -0
- package/dist/email/templates/password-reset.d.ts +5 -0
- package/dist/email/templates/password-reset.js +15 -0
- package/dist/email/templates/verify-email.d.ts +5 -0
- package/dist/email/templates/verify-email.js +15 -0
- package/dist/email/templates/welcome.d.ts +5 -0
- package/dist/email/templates/welcome.js +13 -0
- package/dist/email/types.d.ts +49 -0
- package/dist/email/types.js +1 -0
- package/dist/llms/generate.d.ts +46 -0
- package/dist/llms/generate.js +185 -0
- package/dist/permissions/guard.d.ts +28 -0
- package/dist/permissions/guard.js +30 -0
- package/dist/permissions/index.d.ts +6 -0
- package/dist/permissions/index.js +3 -0
- package/dist/permissions/service.d.ts +80 -0
- package/dist/permissions/service.js +210 -0
- package/dist/permissions/tables.d.ts +5 -0
- package/dist/permissions/tables.js +68 -0
- package/dist/permissions/types.d.ts +33 -0
- package/dist/permissions/types.js +1 -0
- package/dist/runtime/app-shell.d.ts +1 -1
- package/dist/runtime/app-shell.js +164 -0
- package/dist/runtime/auth.d.ts +10 -0
- package/dist/runtime/auth.js +30 -0
- package/dist/runtime/communication.d.ts +137 -0
- package/dist/runtime/communication.js +228 -0
- package/dist/runtime/error-boundary.d.ts +23 -0
- package/dist/runtime/error-boundary.js +120 -0
- package/dist/runtime/i18n.d.ts +6 -1
- package/dist/runtime/i18n.js +42 -21
- package/dist/runtime/island.d.ts +16 -0
- package/dist/runtime/island.js +80 -0
- package/dist/runtime/router-data.d.ts +3 -0
- package/dist/runtime/router-data.js +102 -17
- package/dist/runtime/router-hydration.js +34 -2
- package/dist/runtime/router.d.ts +19 -2
- package/dist/runtime/router.js +237 -43
- package/dist/runtime/socket-client.d.ts +2 -0
- package/dist/runtime/socket-client.js +30 -0
- package/dist/runtime/webrtc.d.ts +91 -0
- package/dist/runtime/webrtc.js +428 -0
- package/dist/shared/dom-shims.js +4 -2
- package/dist/shared/graceful-shutdown.d.ts +8 -0
- package/dist/shared/graceful-shutdown.js +36 -0
- package/dist/shared/health.d.ts +8 -0
- package/dist/shared/health.js +25 -0
- package/dist/shared/llms-txt.d.ts +31 -0
- package/dist/shared/llms-txt.js +85 -0
- package/dist/shared/logger.d.ts +32 -0
- package/dist/shared/logger.js +93 -0
- package/dist/shared/meta.d.ts +27 -0
- package/dist/shared/meta.js +71 -0
- package/dist/shared/middleware-runner.d.ts +9 -0
- package/dist/shared/middleware-runner.js +29 -0
- package/dist/shared/rate-limit.d.ts +18 -0
- package/dist/shared/rate-limit.js +71 -0
- package/dist/shared/request-id.d.ts +5 -0
- package/dist/shared/request-id.js +18 -0
- package/dist/shared/route-matching.js +16 -1
- package/dist/shared/security-headers.d.ts +18 -0
- package/dist/shared/security-headers.js +38 -0
- package/dist/shared/socket-io-setup.d.ts +11 -0
- package/dist/shared/socket-io-setup.js +51 -0
- package/dist/shared/types.d.ts +15 -0
- package/dist/shared/utils.d.ts +33 -7
- package/dist/shared/utils.js +164 -27
- package/dist/storage/adapters/local.d.ts +44 -0
- package/dist/storage/adapters/local.js +85 -0
- package/dist/storage/adapters/s3.d.ts +32 -0
- package/dist/storage/adapters/s3.js +119 -0
- package/dist/storage/adapters/types.d.ts +53 -0
- package/dist/storage/adapters/types.js +1 -0
- package/dist/storage/index.d.ts +76 -0
- package/dist/storage/index.js +83 -0
- package/package.json +45 -7
- package/templates/blog/api/posts.ts +4 -18
- package/templates/blog/data/migrations/001_init.sql +6 -5
- package/templates/blog/lumenjs.config.ts +3 -0
- package/templates/blog/package.json +14 -0
- package/templates/blog/pages/_layout.ts +25 -0
- package/templates/blog/pages/index.ts +48 -22
- package/templates/blog/pages/posts/[slug].ts +45 -20
- package/templates/blog/pages/tag/[tag].ts +44 -0
- package/templates/dashboard/api/stats.ts +8 -5
- package/templates/dashboard/lumenjs.config.ts +3 -0
- package/templates/dashboard/package.json +14 -0
- package/templates/dashboard/pages/_layout.ts +25 -0
- package/templates/dashboard/pages/index.ts +54 -23
- package/templates/dashboard/pages/settings/index.ts +29 -0
- package/templates/default/lumenjs.config.ts +3 -0
- package/templates/default/package.json +14 -0
- package/templates/default/pages/index.ts +24 -0
|
@@ -1,5 +1,65 @@
|
|
|
1
1
|
import { getI18nConfig, getLocale } from './i18n.js';
|
|
2
|
+
const PREFETCH_TTL = 30_000; // 30 seconds
|
|
3
|
+
const MAX_PREFETCH_CACHE_SIZE = 50;
|
|
4
|
+
const prefetchCache = new Map();
|
|
5
|
+
const inflightRequests = new Map();
|
|
6
|
+
// Periodic sweep of expired prefetch entries (every 60s)
|
|
7
|
+
if (typeof setInterval !== 'undefined') {
|
|
8
|
+
setInterval(() => {
|
|
9
|
+
const now = Date.now();
|
|
10
|
+
for (const [key, entry] of prefetchCache) {
|
|
11
|
+
if (now - entry.timestamp > PREFETCH_TTL)
|
|
12
|
+
prefetchCache.delete(key);
|
|
13
|
+
}
|
|
14
|
+
}, 60_000);
|
|
15
|
+
}
|
|
16
|
+
export function getCachedLoaderData(key) {
|
|
17
|
+
const entry = prefetchCache.get(key);
|
|
18
|
+
if (!entry)
|
|
19
|
+
return undefined;
|
|
20
|
+
if (Date.now() - entry.timestamp > PREFETCH_TTL) {
|
|
21
|
+
prefetchCache.delete(key);
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
return entry.data;
|
|
25
|
+
}
|
|
26
|
+
function setCachedLoaderData(key, data) {
|
|
27
|
+
// Evict oldest entry if cache is full
|
|
28
|
+
if (prefetchCache.size >= MAX_PREFETCH_CACHE_SIZE) {
|
|
29
|
+
const firstKey = prefetchCache.keys().next().value;
|
|
30
|
+
if (firstKey)
|
|
31
|
+
prefetchCache.delete(firstKey);
|
|
32
|
+
}
|
|
33
|
+
prefetchCache.set(key, { data, timestamp: Date.now() });
|
|
34
|
+
}
|
|
35
|
+
export async function prefetchLoaderData(pathname, params) {
|
|
36
|
+
const cacheKey = `page:${pathname}`;
|
|
37
|
+
const cached = getCachedLoaderData(cacheKey);
|
|
38
|
+
if (cached !== undefined)
|
|
39
|
+
return cached;
|
|
40
|
+
const data = await fetchLoaderDataRaw(pathname, params);
|
|
41
|
+
setCachedLoaderData(cacheKey, data);
|
|
42
|
+
return data;
|
|
43
|
+
}
|
|
44
|
+
export async function prefetchLayoutLoaderData(dir) {
|
|
45
|
+
const cacheKey = `layout:${dir}`;
|
|
46
|
+
const cached = getCachedLoaderData(cacheKey);
|
|
47
|
+
if (cached !== undefined)
|
|
48
|
+
return cached;
|
|
49
|
+
const data = await fetchLayoutLoaderDataRaw(dir);
|
|
50
|
+
setCachedLoaderData(cacheKey, data);
|
|
51
|
+
return data;
|
|
52
|
+
}
|
|
2
53
|
export async function fetchLoaderData(pathname, params) {
|
|
54
|
+
const cacheKey = `page:${pathname}`;
|
|
55
|
+
const cached = getCachedLoaderData(cacheKey);
|
|
56
|
+
if (cached !== undefined) {
|
|
57
|
+
prefetchCache.delete(cacheKey);
|
|
58
|
+
return cached;
|
|
59
|
+
}
|
|
60
|
+
return fetchLoaderDataRaw(pathname, params);
|
|
61
|
+
}
|
|
62
|
+
async function fetchLoaderDataRaw(pathname, params) {
|
|
3
63
|
const url = new URL(`/__nk_loader${pathname}`, location.origin);
|
|
4
64
|
if (Object.keys(params).length > 0) {
|
|
5
65
|
url.searchParams.set('__params', JSON.stringify(params));
|
|
@@ -8,30 +68,51 @@ export async function fetchLoaderData(pathname, params) {
|
|
|
8
68
|
if (config) {
|
|
9
69
|
url.searchParams.set('__locale', getLocale());
|
|
10
70
|
}
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
71
|
+
const key = url.toString();
|
|
72
|
+
const inflight = inflightRequests.get(key);
|
|
73
|
+
if (inflight)
|
|
74
|
+
return inflight;
|
|
75
|
+
const promise = fetch(key)
|
|
76
|
+
.then(async (res) => {
|
|
77
|
+
if (!res.ok)
|
|
78
|
+
throw new Error(`Loader returned ${res.status}`);
|
|
79
|
+
const data = await res.json();
|
|
80
|
+
return data?.__nk_no_loader ? undefined : data;
|
|
81
|
+
})
|
|
82
|
+
.finally(() => inflightRequests.delete(key));
|
|
83
|
+
inflightRequests.set(key, promise);
|
|
84
|
+
return promise;
|
|
19
85
|
}
|
|
20
86
|
export async function fetchLayoutLoaderData(dir) {
|
|
87
|
+
const cacheKey = `layout:${dir}`;
|
|
88
|
+
const cached = getCachedLoaderData(cacheKey);
|
|
89
|
+
if (cached !== undefined) {
|
|
90
|
+
prefetchCache.delete(cacheKey);
|
|
91
|
+
return cached;
|
|
92
|
+
}
|
|
93
|
+
return fetchLayoutLoaderDataRaw(dir);
|
|
94
|
+
}
|
|
95
|
+
async function fetchLayoutLoaderDataRaw(dir) {
|
|
21
96
|
const url = new URL(`/__nk_loader/__layout/`, location.origin);
|
|
22
97
|
url.searchParams.set('__dir', dir);
|
|
23
98
|
const config = getI18nConfig();
|
|
24
99
|
if (config) {
|
|
25
100
|
url.searchParams.set('__locale', getLocale());
|
|
26
101
|
}
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
102
|
+
const key = url.toString();
|
|
103
|
+
const inflight = inflightRequests.get(key);
|
|
104
|
+
if (inflight)
|
|
105
|
+
return inflight;
|
|
106
|
+
const promise = fetch(key)
|
|
107
|
+
.then(async (res) => {
|
|
108
|
+
if (!res.ok)
|
|
109
|
+
throw new Error(`Layout loader returned ${res.status}`);
|
|
110
|
+
const data = await res.json();
|
|
111
|
+
return data?.__nk_no_loader ? undefined : data;
|
|
112
|
+
})
|
|
113
|
+
.finally(() => inflightRequests.delete(key));
|
|
114
|
+
inflightRequests.set(key, promise);
|
|
115
|
+
return promise;
|
|
35
116
|
}
|
|
36
117
|
export function connectSubscribe(pathname, params) {
|
|
37
118
|
const url = new URL(`/__nk_subscribe${pathname}`, location.origin);
|
|
@@ -53,13 +134,17 @@ export function connectLayoutSubscribe(dir) {
|
|
|
53
134
|
}
|
|
54
135
|
return new EventSource(url.toString());
|
|
55
136
|
}
|
|
137
|
+
function escapeHtml(text) {
|
|
138
|
+
return text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
|
|
139
|
+
}
|
|
56
140
|
export function render404(pathname) {
|
|
141
|
+
const safe = escapeHtml(pathname);
|
|
57
142
|
return `<div style="display:flex;align-items:center;justify-content:center;min-height:80vh;font-family:system-ui,-apple-system,sans-serif;padding:2rem">
|
|
58
143
|
<div style="text-align:center;max-width:400px">
|
|
59
144
|
<div style="font-size:5rem;font-weight:200;letter-spacing:-2px;color:#cbd5e1;line-height:1">404</div>
|
|
60
145
|
<div style="width:32px;height:2px;background:#e2e8f0;border-radius:1px;margin:1.25rem auto"></div>
|
|
61
146
|
<h1 style="font-size:1rem;font-weight:500;color:#334155;margin:1.25rem 0 .5rem">Page not found</h1>
|
|
62
|
-
<p style="color:#94a3b8;font-size:.8125rem;line-height:1.5;margin:0 0 2rem"><code style="background:#f8fafc;padding:.125rem .375rem;border-radius:3px;font-size:.75rem;color:#64748b;border:1px solid #f1f5f9">${
|
|
147
|
+
<p style="color:#94a3b8;font-size:.8125rem;line-height:1.5;margin:0 0 2rem"><code style="background:#f8fafc;padding:.125rem .375rem;border-radius:3px;font-size:.75rem;color:#64748b;border:1px solid #f1f5f9">${safe}</code> doesn't exist</p>
|
|
63
148
|
<a href="/" style="display:inline-flex;align-items:center;gap:.375rem;padding:.4375rem 1rem;background:#f8fafc;color:#475569;border:1px solid #e2e8f0;border-radius:6px;font-size:.8125rem;font-weight:400;text-decoration:none;transition:all .15s">
|
|
64
149
|
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m15 18-6-6 6-6"/></svg>
|
|
65
150
|
Back to home
|
|
@@ -15,9 +15,26 @@ export async function hydrateInitialRoute(routes, outlet, matchRoute, onHydrated
|
|
|
15
15
|
catch { /* ignore */ }
|
|
16
16
|
i18nScript.remove();
|
|
17
17
|
}
|
|
18
|
-
//
|
|
18
|
+
// Read auth data and init before route matching
|
|
19
|
+
const authScript = document.getElementById('__nk_auth__');
|
|
20
|
+
if (authScript) {
|
|
21
|
+
try {
|
|
22
|
+
const { initAuth } = await import('@lumenjs/auth');
|
|
23
|
+
initAuth(JSON.parse(authScript.textContent || ''));
|
|
24
|
+
}
|
|
25
|
+
catch { }
|
|
26
|
+
authScript.remove();
|
|
27
|
+
}
|
|
28
|
+
// Strip Vite base path and locale prefix for route matching
|
|
29
|
+
let matchPath = location.pathname;
|
|
30
|
+
const base = import.meta.env?.BASE_URL;
|
|
31
|
+
if (base && base !== '/' && matchPath.startsWith(base)) {
|
|
32
|
+
matchPath = '/' + matchPath.slice(base.length);
|
|
33
|
+
}
|
|
19
34
|
const config = getI18nConfig();
|
|
20
|
-
|
|
35
|
+
if (config) {
|
|
36
|
+
matchPath = stripLocalePrefix(matchPath);
|
|
37
|
+
}
|
|
21
38
|
const match = matchRoute(matchPath);
|
|
22
39
|
if (!match)
|
|
23
40
|
return;
|
|
@@ -45,6 +62,19 @@ export async function hydrateInitialRoute(routes, outlet, matchRoute, onHydrated
|
|
|
45
62
|
}
|
|
46
63
|
}
|
|
47
64
|
}
|
|
65
|
+
/** Spread loader data as individual properties on an element. */
|
|
66
|
+
const BLOCKED = new Set(['__proto__', 'constructor', 'prototype',
|
|
67
|
+
'innerHTML', 'outerHTML', 'textContent',
|
|
68
|
+
'render', 'connectedCallback', 'disconnectedCallback']);
|
|
69
|
+
function spreadData(el, data) {
|
|
70
|
+
if (data && typeof data === 'object') {
|
|
71
|
+
for (const [key, value] of Object.entries(data)) {
|
|
72
|
+
if (!BLOCKED.has(key)) {
|
|
73
|
+
el[key] = value;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
48
78
|
// Load each layout module and immediately set loaderData on the
|
|
49
79
|
// existing DOM element BEFORE the next await yields to microtasks.
|
|
50
80
|
for (const layout of layouts) {
|
|
@@ -53,6 +83,7 @@ export async function hydrateInitialRoute(routes, outlet, matchRoute, onHydrated
|
|
|
53
83
|
const data = layoutDataMap.get(layout.loaderPath ?? '');
|
|
54
84
|
if (data !== undefined) {
|
|
55
85
|
existingLayout.loaderData = data;
|
|
86
|
+
spreadData(existingLayout, data);
|
|
56
87
|
}
|
|
57
88
|
}
|
|
58
89
|
if (layout.load && !customElements.get(layout.tagName)) {
|
|
@@ -66,6 +97,7 @@ export async function hydrateInitialRoute(routes, outlet, matchRoute, onHydrated
|
|
|
66
97
|
const existingPage = outlet?.querySelector(match.route.tagName);
|
|
67
98
|
if (existingPage && pageData !== undefined) {
|
|
68
99
|
existingPage.loaderData = pageData;
|
|
100
|
+
spreadData(existingPage, pageData);
|
|
69
101
|
}
|
|
70
102
|
// Load the page module (registers element, triggers hydration microtask)
|
|
71
103
|
if (match.route.load && !customElements.get(match.route.tagName)) {
|
package/dist/runtime/router.d.ts
CHANGED
|
@@ -10,6 +10,8 @@ export interface Route {
|
|
|
10
10
|
tagName: string;
|
|
11
11
|
hasLoader?: boolean;
|
|
12
12
|
hasSubscribe?: boolean;
|
|
13
|
+
hasSocket?: boolean;
|
|
14
|
+
hasMeta?: boolean;
|
|
13
15
|
load?: () => Promise<any>;
|
|
14
16
|
layouts?: LayoutInfo[];
|
|
15
17
|
pattern?: RegExp;
|
|
@@ -26,19 +28,34 @@ export declare class NkRouter {
|
|
|
26
28
|
private currentTag;
|
|
27
29
|
private currentLayoutTags;
|
|
28
30
|
private subscriptions;
|
|
31
|
+
private _sockets;
|
|
32
|
+
private siteTitle;
|
|
29
33
|
params: Record<string, string>;
|
|
30
34
|
constructor(routes: Route[], outlet: HTMLElement, hydrate?: boolean);
|
|
31
35
|
private compilePattern;
|
|
32
36
|
private cleanupSubscriptions;
|
|
33
|
-
navigate(
|
|
37
|
+
navigate(fullPath: string, pushState?: boolean): Promise<void>;
|
|
38
|
+
private setupSubscriptions;
|
|
34
39
|
private matchRoute;
|
|
35
40
|
private renderRoute;
|
|
36
41
|
private buildLayoutTree;
|
|
42
|
+
/** Spread loader data as individual properties on an element. */
|
|
43
|
+
private spreadData;
|
|
37
44
|
private createPageElement;
|
|
38
45
|
private findPageElement;
|
|
46
|
+
/**
|
|
47
|
+
* Resolve the page title from the route's meta export and update
|
|
48
|
+
* document.title, the aria-live announcer, and focus.
|
|
49
|
+
*/
|
|
50
|
+
private updatePageMeta;
|
|
39
51
|
private handleLinkClick;
|
|
40
|
-
|
|
52
|
+
prefetch(fullPath: string): Promise<void>;
|
|
53
|
+
/** Strip base and locale prefix from a path for internal route matching. */
|
|
41
54
|
private stripLocale;
|
|
42
55
|
/** Prepend locale prefix for browser-facing URLs. */
|
|
43
56
|
private withLocale;
|
|
44
57
|
}
|
|
58
|
+
/** Navigate via the client-side router. Falls back to full reload for unknown routes. */
|
|
59
|
+
export declare function navigate(href: string): void;
|
|
60
|
+
/** Programmatically prefetch a route's JS chunks and loader data. */
|
|
61
|
+
export declare function prefetch(href: string): void;
|
package/dist/runtime/router.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { fetchLoaderData, fetchLayoutLoaderData, connectSubscribe, connectLayoutSubscribe, render404 } from './router-data.js';
|
|
1
|
+
import { fetchLoaderData, fetchLayoutLoaderData, prefetchLoaderData, prefetchLayoutLoaderData, connectSubscribe, connectLayoutSubscribe, render404 } from './router-data.js';
|
|
2
2
|
import { hydrateInitialRoute } from './router-hydration.js';
|
|
3
|
-
import { getI18nConfig, getLocale, stripLocalePrefix, buildLocalePath } from './i18n.js';
|
|
3
|
+
import { getI18nConfig, getLocale, initI18n, stripLocalePrefix, buildLocalePath } from './i18n.js';
|
|
4
4
|
/**
|
|
5
5
|
* Simple client-side router for LumenJS pages.
|
|
6
6
|
* Handles popstate and link clicks for SPA navigation.
|
|
@@ -13,27 +13,79 @@ export class NkRouter {
|
|
|
13
13
|
this.currentTag = null;
|
|
14
14
|
this.currentLayoutTags = [];
|
|
15
15
|
this.subscriptions = [];
|
|
16
|
+
this._sockets = new Map();
|
|
16
17
|
this.params = {};
|
|
17
18
|
this.outlet = outlet;
|
|
19
|
+
this.siteTitle = document.title || 'LumenJS App';
|
|
18
20
|
this.routes = routes.map(r => ({
|
|
19
21
|
...r,
|
|
20
22
|
...this.compilePattern(r.path),
|
|
21
23
|
}));
|
|
24
|
+
// Initialize i18n from inlined data before any rendering
|
|
25
|
+
const i18nScript = document.getElementById('__nk_i18n__');
|
|
26
|
+
if (i18nScript) {
|
|
27
|
+
try {
|
|
28
|
+
const i18nData = JSON.parse(i18nScript.textContent || '');
|
|
29
|
+
initI18n(i18nData.config, i18nData.locale, i18nData.translations);
|
|
30
|
+
}
|
|
31
|
+
catch { /* ignore */ }
|
|
32
|
+
if (!hydrate)
|
|
33
|
+
i18nScript.remove();
|
|
34
|
+
}
|
|
22
35
|
window.addEventListener('popstate', () => {
|
|
23
36
|
const path = this.stripLocale(location.pathname);
|
|
24
37
|
this.navigate(path, false);
|
|
25
38
|
});
|
|
39
|
+
// Re-run loader when page is restored from bfcache (back/forward on mobile Safari, etc.)
|
|
40
|
+
window.addEventListener('pageshow', (e) => {
|
|
41
|
+
if (e.persisted) {
|
|
42
|
+
const path = this.stripLocale(location.pathname);
|
|
43
|
+
this.navigate(path, false);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
26
46
|
document.addEventListener('click', (e) => this.handleLinkClick(e));
|
|
47
|
+
window.__nk_navigate = (href) => {
|
|
48
|
+
const path = this.stripLocale(href);
|
|
49
|
+
if (this.matchRoute(path.split('?')[0])) {
|
|
50
|
+
this.navigate(path);
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
window.location.href = href;
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
window.__nk_prefetch = (href) => {
|
|
57
|
+
const path = this.stripLocale(href);
|
|
58
|
+
this.prefetch(path);
|
|
59
|
+
};
|
|
27
60
|
if (hydrate) {
|
|
28
61
|
hydrateInitialRoute(this.routes, this.outlet, (p) => this.matchRoute(p), (tag, layoutTags, params) => {
|
|
29
62
|
this.currentTag = tag;
|
|
30
63
|
this.currentLayoutTags = layoutTags;
|
|
31
64
|
this.params = params;
|
|
32
65
|
});
|
|
66
|
+
// Wire up SSE subscriptions after hydration
|
|
67
|
+
const path = this.stripLocale(location.pathname);
|
|
68
|
+
this.setupSubscriptions(path);
|
|
33
69
|
}
|
|
34
70
|
else {
|
|
35
|
-
|
|
36
|
-
|
|
71
|
+
// Initialize auth from inlined data before navigating (CSR path)
|
|
72
|
+
const authScript = document.getElementById('__nk_auth__');
|
|
73
|
+
if (authScript) {
|
|
74
|
+
import('@lumenjs/auth').then(({ initAuth }) => {
|
|
75
|
+
try {
|
|
76
|
+
initAuth(JSON.parse(authScript.textContent || ''));
|
|
77
|
+
}
|
|
78
|
+
catch { }
|
|
79
|
+
authScript.remove();
|
|
80
|
+
}).catch(() => { }).finally(() => {
|
|
81
|
+
const path = this.stripLocale(location.pathname);
|
|
82
|
+
this.navigate(path, false);
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
const path = this.stripLocale(location.pathname);
|
|
87
|
+
this.navigate(path, false);
|
|
88
|
+
}
|
|
37
89
|
}
|
|
38
90
|
}
|
|
39
91
|
compilePattern(path) {
|
|
@@ -49,9 +101,14 @@ export class NkRouter {
|
|
|
49
101
|
es.close();
|
|
50
102
|
}
|
|
51
103
|
this.subscriptions = [];
|
|
104
|
+
// Disconnect any active socket.io connections
|
|
105
|
+
for (const [, sock] of this._sockets)
|
|
106
|
+
sock.disconnect();
|
|
107
|
+
this._sockets.clear();
|
|
52
108
|
}
|
|
53
|
-
async navigate(
|
|
109
|
+
async navigate(fullPath, pushState = true) {
|
|
54
110
|
this.cleanupSubscriptions();
|
|
111
|
+
const pathname = fullPath.split('?')[0];
|
|
55
112
|
const match = this.matchRoute(pathname);
|
|
56
113
|
if (!match) {
|
|
57
114
|
if (this.outlet)
|
|
@@ -61,51 +118,53 @@ export class NkRouter {
|
|
|
61
118
|
return;
|
|
62
119
|
}
|
|
63
120
|
if (pushState) {
|
|
64
|
-
const localePath = this.withLocale(
|
|
121
|
+
const localePath = this.withLocale(fullPath);
|
|
65
122
|
history.pushState(null, '', localePath);
|
|
66
123
|
window.scrollTo(0, 0);
|
|
67
124
|
}
|
|
68
125
|
this.params = match.params;
|
|
69
|
-
//
|
|
70
|
-
if (match.route.
|
|
71
|
-
await match.route.load();
|
|
72
|
-
}
|
|
73
|
-
// Load layout components
|
|
74
|
-
const layouts = match.route.layouts || [];
|
|
75
|
-
for (const layout of layouts) {
|
|
76
|
-
if (layout.load && !customElements.get(layout.tagName)) {
|
|
77
|
-
await layout.load();
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
// Fetch loader data for page
|
|
81
|
-
let loaderData = undefined;
|
|
82
|
-
if (match.route.hasLoader) {
|
|
126
|
+
// Auth guard: SPA-navigate unauthenticated users to login page
|
|
127
|
+
if (match.route.__nk_has_auth) {
|
|
83
128
|
try {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const layoutDataList = [];
|
|
92
|
-
for (const layout of layouts) {
|
|
93
|
-
if (layout.hasLoader) {
|
|
94
|
-
try {
|
|
95
|
-
const data = await fetchLayoutLoaderData(layout.loaderPath || '');
|
|
96
|
-
layoutDataList.push(data);
|
|
97
|
-
}
|
|
98
|
-
catch (err) {
|
|
99
|
-
console.error('[NkRouter] Layout loader fetch failed:', err);
|
|
100
|
-
layoutDataList.push(undefined);
|
|
129
|
+
const { isAuthenticated } = await import('@lumenjs/auth');
|
|
130
|
+
if (!isAuthenticated()) {
|
|
131
|
+
const loginPath = '/auth/login';
|
|
132
|
+
const loginUrl = `${loginPath}?returnTo=${encodeURIComponent(pathname)}`;
|
|
133
|
+
history.pushState(null, '', loginUrl);
|
|
134
|
+
this.navigate(loginPath, false);
|
|
135
|
+
return;
|
|
101
136
|
}
|
|
102
137
|
}
|
|
103
|
-
|
|
104
|
-
layoutDataList.push(undefined);
|
|
105
|
-
}
|
|
138
|
+
catch { }
|
|
106
139
|
}
|
|
140
|
+
const layouts = match.route.layouts || [];
|
|
141
|
+
// Load all component JS chunks in parallel
|
|
142
|
+
await Promise.all([
|
|
143
|
+
match.route.load && !customElements.get(match.route.tagName) ? match.route.load() : undefined,
|
|
144
|
+
...layouts.map(l => l.load && !customElements.get(l.tagName) ? l.load() : undefined),
|
|
145
|
+
]);
|
|
146
|
+
// Fetch all loader data in parallel
|
|
147
|
+
const loaderPromises = [
|
|
148
|
+
match.route.hasLoader
|
|
149
|
+
? fetchLoaderData(pathname, match.params).catch(err => { console.error('[NkRouter] Loader fetch failed:', err); return undefined; })
|
|
150
|
+
: Promise.resolve(undefined),
|
|
151
|
+
...layouts.map(layout => layout.hasLoader
|
|
152
|
+
? fetchLayoutLoaderData(layout.loaderPath || '').catch(err => { console.error('[NkRouter] Layout loader fetch failed:', err); return undefined; })
|
|
153
|
+
: Promise.resolve(undefined)),
|
|
154
|
+
];
|
|
155
|
+
const [loaderData, ...layoutDataList] = await Promise.all(loaderPromises);
|
|
107
156
|
this.renderRoute(match.route, loaderData, layouts, layoutDataList);
|
|
108
|
-
//
|
|
157
|
+
// Update document.title and announce route change for screen readers
|
|
158
|
+
this.updatePageMeta(match.route, loaderData);
|
|
159
|
+
// Set up SSE subscriptions
|
|
160
|
+
this.setupSubscriptions(pathname);
|
|
161
|
+
}
|
|
162
|
+
setupSubscriptions(pathname) {
|
|
163
|
+
const match = this.matchRoute(pathname);
|
|
164
|
+
if (!match)
|
|
165
|
+
return;
|
|
166
|
+
const layouts = match.route.layouts || [];
|
|
167
|
+
// Page subscription
|
|
109
168
|
if (match.route.hasSubscribe) {
|
|
110
169
|
const es = connectSubscribe(pathname, match.params);
|
|
111
170
|
es.onmessage = (e) => {
|
|
@@ -115,7 +174,43 @@ export class NkRouter {
|
|
|
115
174
|
};
|
|
116
175
|
this.subscriptions.push(es);
|
|
117
176
|
}
|
|
118
|
-
//
|
|
177
|
+
// Page socket (bidirectional via Socket.IO)
|
|
178
|
+
if (match.route.hasSocket) {
|
|
179
|
+
import('socket.io-client').then(({ io }) => {
|
|
180
|
+
const ns = `/nk${pathname === '/' ? '/index' : pathname}`;
|
|
181
|
+
const query = {};
|
|
182
|
+
if (Object.keys(match.params).length > 0)
|
|
183
|
+
query.__params = JSON.stringify(match.params);
|
|
184
|
+
try {
|
|
185
|
+
const locale = getLocale();
|
|
186
|
+
if (locale)
|
|
187
|
+
query.__locale = locale;
|
|
188
|
+
}
|
|
189
|
+
catch { }
|
|
190
|
+
const existing = this._sockets.get(pathname);
|
|
191
|
+
if (existing)
|
|
192
|
+
existing.disconnect();
|
|
193
|
+
const socket = io(ns, { path: '/__nk_socketio/', query });
|
|
194
|
+
this._sockets.set(pathname, socket);
|
|
195
|
+
const injectEmit = () => {
|
|
196
|
+
const pageEl = this.findPageElement(match.route.tagName);
|
|
197
|
+
if (pageEl) {
|
|
198
|
+
pageEl.emit = (event, payload) => {
|
|
199
|
+
socket.emit(`nk:${event}`, payload);
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
injectEmit();
|
|
204
|
+
socket.on('nk:data', (data) => {
|
|
205
|
+
const pageEl = this.findPageElement(match.route.tagName);
|
|
206
|
+
if (pageEl) {
|
|
207
|
+
pageEl.liveData = data;
|
|
208
|
+
injectEmit();
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
}).catch(err => console.error('[NkRouter] Socket connection failed:', err));
|
|
212
|
+
}
|
|
213
|
+
// Layout subscriptions
|
|
119
214
|
for (const layout of layouts) {
|
|
120
215
|
if (layout.hasSubscribe) {
|
|
121
216
|
const es = connectLayoutSubscribe(layout.loaderPath || '');
|
|
@@ -177,6 +272,7 @@ export class NkRouter {
|
|
|
177
272
|
}
|
|
178
273
|
if (layoutDataList && layoutDataList[i] !== undefined) {
|
|
179
274
|
layoutEl.loaderData = layoutDataList[i];
|
|
275
|
+
this.spreadData(layoutEl, layoutDataList[i]);
|
|
180
276
|
}
|
|
181
277
|
parentEl = layoutEl;
|
|
182
278
|
}
|
|
@@ -201,12 +297,14 @@ export class NkRouter {
|
|
|
201
297
|
const outerLayout = document.createElement(layouts[0].tagName);
|
|
202
298
|
if (layoutDataList[0] !== undefined) {
|
|
203
299
|
outerLayout.loaderData = layoutDataList[0];
|
|
300
|
+
this.spreadData(outerLayout, layoutDataList[0]);
|
|
204
301
|
}
|
|
205
302
|
let current = outerLayout;
|
|
206
303
|
for (let i = 1; i < layouts.length; i++) {
|
|
207
304
|
const inner = document.createElement(layouts[i].tagName);
|
|
208
305
|
if (layoutDataList[i] !== undefined) {
|
|
209
306
|
inner.loaderData = layoutDataList[i];
|
|
307
|
+
this.spreadData(inner, layoutDataList[i]);
|
|
210
308
|
}
|
|
211
309
|
current.appendChild(inner);
|
|
212
310
|
current = inner;
|
|
@@ -215,6 +313,19 @@ export class NkRouter {
|
|
|
215
313
|
current.appendChild(pageEl);
|
|
216
314
|
return outerLayout;
|
|
217
315
|
}
|
|
316
|
+
/** Spread loader data as individual properties on an element. */
|
|
317
|
+
spreadData(el, data) {
|
|
318
|
+
if (data && typeof data === 'object') {
|
|
319
|
+
const BLOCKED = new Set(['__proto__', 'constructor', 'prototype',
|
|
320
|
+
'innerHTML', 'outerHTML', 'textContent',
|
|
321
|
+
'render', 'connectedCallback', 'disconnectedCallback']);
|
|
322
|
+
for (const [key, value] of Object.entries(data)) {
|
|
323
|
+
if (!BLOCKED.has(key)) {
|
|
324
|
+
el[key] = value;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
218
329
|
createPageElement(route, loaderData) {
|
|
219
330
|
const el = document.createElement(route.tagName);
|
|
220
331
|
for (const [key, value] of Object.entries(this.params)) {
|
|
@@ -222,6 +333,7 @@ export class NkRouter {
|
|
|
222
333
|
}
|
|
223
334
|
if (loaderData !== undefined) {
|
|
224
335
|
el.loaderData = loaderData;
|
|
336
|
+
this.spreadData(el, loaderData);
|
|
225
337
|
}
|
|
226
338
|
return el;
|
|
227
339
|
}
|
|
@@ -230,6 +342,49 @@ export class NkRouter {
|
|
|
230
342
|
return null;
|
|
231
343
|
return this.outlet.querySelector(tagName) ?? this.outlet.querySelector(`${tagName}:last-child`);
|
|
232
344
|
}
|
|
345
|
+
/**
|
|
346
|
+
* Resolve the page title from the route's meta export and update
|
|
347
|
+
* document.title, the aria-live announcer, and focus.
|
|
348
|
+
*/
|
|
349
|
+
async updatePageMeta(route, loaderData) {
|
|
350
|
+
let pageTitle;
|
|
351
|
+
if (route.hasMeta && route.load) {
|
|
352
|
+
try {
|
|
353
|
+
const mod = await route.load();
|
|
354
|
+
if (mod) {
|
|
355
|
+
let meta;
|
|
356
|
+
if (typeof mod.meta === 'function') {
|
|
357
|
+
meta = mod.meta({ data: loaderData, params: this.params });
|
|
358
|
+
}
|
|
359
|
+
else if (mod.meta && typeof mod.meta === 'object') {
|
|
360
|
+
meta = mod.meta;
|
|
361
|
+
}
|
|
362
|
+
if (meta?.title) {
|
|
363
|
+
pageTitle = `${meta.title} | ${this.siteTitle}`;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
catch { /* fall back to site title */ }
|
|
368
|
+
}
|
|
369
|
+
const title = pageTitle || this.siteTitle;
|
|
370
|
+
document.title = title;
|
|
371
|
+
// Let layout components adjust title (e.g. prepend badge counts)
|
|
372
|
+
window.dispatchEvent(new CustomEvent('nk-title-updated'));
|
|
373
|
+
// Announce route change to screen readers
|
|
374
|
+
const announcer = document.getElementById('nk-route-announcer');
|
|
375
|
+
if (announcer) {
|
|
376
|
+
announcer.textContent = '';
|
|
377
|
+
// Use a microtask delay so aria-live picks up the change
|
|
378
|
+
requestAnimationFrame(() => { announcer.textContent = title; });
|
|
379
|
+
}
|
|
380
|
+
// Move focus to the router outlet for keyboard/screen reader users
|
|
381
|
+
if (this.outlet) {
|
|
382
|
+
if (!this.outlet.hasAttribute('tabindex')) {
|
|
383
|
+
this.outlet.setAttribute('tabindex', '-1');
|
|
384
|
+
}
|
|
385
|
+
this.outlet.focus({ preventScroll: true });
|
|
386
|
+
}
|
|
387
|
+
}
|
|
233
388
|
handleLinkClick(event) {
|
|
234
389
|
const path = event.composedPath();
|
|
235
390
|
const anchor = path.find((el) => el instanceof HTMLElement && el.tagName === 'A');
|
|
@@ -238,11 +393,34 @@ export class NkRouter {
|
|
|
238
393
|
const href = anchor.getAttribute('href');
|
|
239
394
|
if (!href || href.startsWith('http') || href.startsWith('#') || anchor.hasAttribute('target'))
|
|
240
395
|
return;
|
|
396
|
+
// Allow modifier-key clicks to behave normally (Ctrl+Click = new tab, etc.)
|
|
397
|
+
if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey)
|
|
398
|
+
return;
|
|
241
399
|
event.preventDefault();
|
|
242
400
|
this.navigate(this.stripLocale(href));
|
|
243
401
|
}
|
|
244
|
-
|
|
402
|
+
async prefetch(fullPath) {
|
|
403
|
+
const pathname = fullPath.split('?')[0];
|
|
404
|
+
const match = this.matchRoute(pathname);
|
|
405
|
+
if (!match)
|
|
406
|
+
return;
|
|
407
|
+
const layouts = match.route.layouts || [];
|
|
408
|
+
await Promise.all([
|
|
409
|
+
// Preload component JS chunks
|
|
410
|
+
match.route.load && !customElements.get(match.route.tagName) ? match.route.load() : undefined,
|
|
411
|
+
...layouts.map(l => l.load && !customElements.get(l.tagName) ? l.load() : undefined),
|
|
412
|
+
// Prefetch loader data (cached)
|
|
413
|
+
match.route.hasLoader ? prefetchLoaderData(pathname, match.params).catch(() => { }) : undefined,
|
|
414
|
+
...layouts.map(l => l.hasLoader ? prefetchLayoutLoaderData(l.loaderPath || '').catch(() => { }) : undefined),
|
|
415
|
+
]);
|
|
416
|
+
}
|
|
417
|
+
/** Strip base and locale prefix from a path for internal route matching. */
|
|
245
418
|
stripLocale(path) {
|
|
419
|
+
// Strip Vite base path (e.g. /__app_dev/{id}/) before route matching
|
|
420
|
+
const base = import.meta.env?.BASE_URL;
|
|
421
|
+
if (base && base !== '/' && path.startsWith(base)) {
|
|
422
|
+
path = '/' + path.slice(base.length);
|
|
423
|
+
}
|
|
246
424
|
const config = getI18nConfig();
|
|
247
425
|
return config ? stripLocalePrefix(path) : path;
|
|
248
426
|
}
|
|
@@ -252,3 +430,19 @@ export class NkRouter {
|
|
|
252
430
|
return config ? buildLocalePath(getLocale(), path) : path;
|
|
253
431
|
}
|
|
254
432
|
}
|
|
433
|
+
/** Navigate via the client-side router. Falls back to full reload for unknown routes. */
|
|
434
|
+
export function navigate(href) {
|
|
435
|
+
const nav = window.__nk_navigate;
|
|
436
|
+
if (nav) {
|
|
437
|
+
nav(href);
|
|
438
|
+
}
|
|
439
|
+
else {
|
|
440
|
+
window.location.href = href;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
/** Programmatically prefetch a route's JS chunks and loader data. */
|
|
444
|
+
export function prefetch(href) {
|
|
445
|
+
const pf = window.__nk_prefetch;
|
|
446
|
+
if (pf)
|
|
447
|
+
pf(href);
|
|
448
|
+
}
|