@emkodev/emroute 1.6.6-beta.2 → 1.6.6-beta.4
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/dist/emroute.js +2757 -0
- package/dist/emroute.js.map +7 -0
- package/dist/runtime/abstract.runtime.d.ts +0 -28
- package/dist/runtime/abstract.runtime.js +10 -58
- package/dist/runtime/abstract.runtime.js.map +1 -1
- package/dist/runtime/bun/esbuild-runtime-loader.plugin.js +3 -0
- package/dist/runtime/bun/esbuild-runtime-loader.plugin.js.map +1 -1
- package/dist/runtime/bun/fs/bun-fs.runtime.d.ts +0 -5
- package/dist/runtime/bun/fs/bun-fs.runtime.js +1 -95
- package/dist/runtime/bun/fs/bun-fs.runtime.js.map +1 -1
- package/dist/runtime/bun/sqlite/bun-sqlite.runtime.d.ts +0 -5
- package/dist/runtime/bun/sqlite/bun-sqlite.runtime.js +2 -96
- package/dist/runtime/bun/sqlite/bun-sqlite.runtime.js.map +1 -1
- package/dist/runtime/fetch.runtime.d.ts +26 -0
- package/dist/runtime/fetch.runtime.js +55 -0
- package/dist/runtime/fetch.runtime.js.map +1 -0
- package/dist/runtime/sitemap.generator.d.ts +4 -4
- package/dist/runtime/sitemap.generator.js +32 -11
- package/dist/runtime/sitemap.generator.js.map +1 -1
- package/dist/runtime/universal/fs/universal-fs.runtime.d.ts +0 -5
- package/dist/runtime/universal/fs/universal-fs.runtime.js +1 -95
- package/dist/runtime/universal/fs/universal-fs.runtime.js.map +1 -1
- package/dist/server/build.util.d.ts +38 -0
- package/dist/server/build.util.js +133 -0
- package/dist/server/build.util.js.map +1 -0
- package/dist/server/codegen.util.d.ts +3 -0
- package/dist/server/codegen.util.js +28 -10
- package/dist/server/codegen.util.js.map +1 -1
- package/dist/server/emroute.server.js +53 -29
- package/dist/server/emroute.server.js.map +1 -1
- package/dist/server/esbuild-manifest.plugin.js +6 -4
- package/dist/server/esbuild-manifest.plugin.js.map +1 -1
- package/dist/server/server-api.type.d.ts +6 -0
- package/dist/src/component/abstract.component.d.ts +5 -3
- package/dist/src/component/abstract.component.js.map +1 -1
- package/dist/src/element/component.element.js +5 -4
- package/dist/src/element/component.element.js.map +1 -1
- package/dist/src/renderer/spa/mod.d.ts +2 -3
- package/dist/src/renderer/spa/mod.js +2 -3
- package/dist/src/renderer/spa/mod.js.map +1 -1
- package/dist/src/renderer/spa/thin-client.d.ts +34 -0
- package/dist/src/renderer/spa/thin-client.js +138 -0
- package/dist/src/renderer/spa/thin-client.js.map +1 -0
- package/dist/src/renderer/ssr/html.renderer.d.ts +3 -3
- package/dist/src/renderer/ssr/html.renderer.js +6 -6
- package/dist/src/renderer/ssr/html.renderer.js.map +1 -1
- package/dist/src/renderer/ssr/md.renderer.d.ts +3 -3
- package/dist/src/renderer/ssr/md.renderer.js +12 -7
- package/dist/src/renderer/ssr/md.renderer.js.map +1 -1
- package/dist/src/renderer/ssr/ssr.renderer.d.ts +7 -6
- package/dist/src/renderer/ssr/ssr.renderer.js +42 -44
- package/dist/src/renderer/ssr/ssr.renderer.js.map +1 -1
- package/dist/src/route/route.core.d.ts +16 -6
- package/dist/src/route/route.core.js +44 -23
- package/dist/src/route/route.core.js.map +1 -1
- package/dist/src/type/route-tree.type.d.ts +2 -0
- package/dist/src/type/route.type.d.ts +6 -24
- package/dist/src/util/md.util.d.ts +8 -0
- package/dist/src/util/md.util.js +28 -0
- package/dist/src/util/md.util.js.map +1 -0
- package/dist/src/util/widget-resolve.util.js +6 -1
- package/dist/src/util/widget-resolve.util.js.map +1 -1
- package/dist/src/widget/breadcrumb.widget.d.ts +0 -1
- package/dist/src/widget/breadcrumb.widget.js +4 -15
- package/dist/src/widget/breadcrumb.widget.js.map +1 -1
- package/package.json +13 -2
- package/runtime/abstract.runtime.ts +9 -82
- package/runtime/bun/esbuild-runtime-loader.plugin.ts +2 -0
- package/runtime/bun/fs/bun-fs.runtime.ts +0 -109
- package/runtime/bun/sqlite/bun-sqlite.runtime.ts +1 -112
- package/runtime/fetch.runtime.ts +70 -0
- package/runtime/sitemap.generator.ts +37 -12
- package/runtime/universal/fs/universal-fs.runtime.ts +0 -109
- package/server/build.util.ts +168 -0
- package/server/codegen.util.ts +29 -11
- package/server/emroute.server.ts +50 -30
- package/server/esbuild-manifest.plugin.ts +5 -3
- package/server/server-api.type.ts +7 -0
- package/src/component/abstract.component.ts +5 -3
- package/src/element/component.element.ts +5 -4
- package/src/renderer/spa/mod.ts +2 -8
- package/src/renderer/spa/thin-client.ts +165 -0
- package/src/renderer/ssr/html.renderer.ts +6 -5
- package/src/renderer/ssr/md.renderer.ts +12 -6
- package/src/renderer/ssr/ssr.renderer.ts +54 -48
- package/src/route/route.core.ts +49 -28
- package/src/type/route-tree.type.ts +2 -0
- package/src/type/route.type.ts +7 -32
- package/src/util/md.util.ts +31 -0
- package/src/util/widget-resolve.util.ts +6 -1
- package/src/widget/breadcrumb.widget.ts +4 -16
- package/server/scanner.util.ts +0 -243
- package/src/renderer/spa/base.renderer.ts +0 -186
- package/src/renderer/spa/hash.renderer.ts +0 -238
- package/src/renderer/spa/html.renderer.ts +0 -399
- package/src/route/route.matcher.ts +0 -260
- package/src/web-doc/index.md +0 -15
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
export interface RouteFiles {
|
|
14
14
|
/** TypeScript module (e.g. "routes/about.page.ts") */
|
|
15
15
|
ts?: string;
|
|
16
|
+
/** JavaScript module — merged module with inlined companions (e.g. "routes/about.page.js") */
|
|
17
|
+
js?: string;
|
|
16
18
|
/** HTML template (e.g. "routes/about.page.html") */
|
|
17
19
|
html?: string;
|
|
18
20
|
/** Markdown content (e.g. "routes/about.page.md") */
|
package/src/type/route.type.ts
CHANGED
|
@@ -10,17 +10,11 @@ export type RouteParams = Readonly<Record<string, string>>;
|
|
|
10
10
|
|
|
11
11
|
/** Immutable route context built once per navigation, shared across the render pipeline. */
|
|
12
12
|
export interface RouteInfo {
|
|
13
|
-
/**
|
|
14
|
-
readonly
|
|
13
|
+
/** The URL being rendered. Components read pathname, searchParams, hash from this. */
|
|
14
|
+
readonly url: URL;
|
|
15
15
|
|
|
16
|
-
/**
|
|
17
|
-
readonly pattern: string;
|
|
18
|
-
|
|
19
|
-
/** URL parameters extracted by the router */
|
|
16
|
+
/** URL parameters extracted by the trie match. */
|
|
20
17
|
readonly params: RouteParams;
|
|
21
|
-
|
|
22
|
-
/** Query string parameters */
|
|
23
|
-
readonly searchParams: URLSearchParams;
|
|
24
18
|
}
|
|
25
19
|
|
|
26
20
|
/** Supported file patterns in file-based routing */
|
|
@@ -37,6 +31,9 @@ export interface RouteFiles {
|
|
|
37
31
|
/** TypeScript module path (.page.ts) */
|
|
38
32
|
ts?: string;
|
|
39
33
|
|
|
34
|
+
/** JavaScript module — merged module with inlined companions (.page.js) */
|
|
35
|
+
js?: string;
|
|
36
|
+
|
|
40
37
|
/** HTML template path (.page.html) */
|
|
41
38
|
html?: string;
|
|
42
39
|
|
|
@@ -75,12 +72,6 @@ export interface MatchedRoute {
|
|
|
75
72
|
|
|
76
73
|
/** Extracted URL parameters */
|
|
77
74
|
readonly params: RouteParams;
|
|
78
|
-
|
|
79
|
-
/** Query string parameters from the matched URL */
|
|
80
|
-
readonly searchParams?: URLSearchParams;
|
|
81
|
-
|
|
82
|
-
/** The URLPatternResult from matching */
|
|
83
|
-
readonly patternResult?: URLPatternResult;
|
|
84
75
|
}
|
|
85
76
|
|
|
86
77
|
/** Error boundary configuration */
|
|
@@ -92,23 +83,7 @@ export interface ErrorBoundary {
|
|
|
92
83
|
modulePath: string;
|
|
93
84
|
}
|
|
94
85
|
|
|
95
|
-
|
|
96
|
-
export interface RoutesManifest {
|
|
97
|
-
/** All page routes */
|
|
98
|
-
routes: RouteConfig[];
|
|
99
|
-
|
|
100
|
-
/** Error boundaries by pattern prefix */
|
|
101
|
-
errorBoundaries: ErrorBoundary[];
|
|
102
|
-
|
|
103
|
-
/** Status-specific pages (404, 401, 403) */
|
|
104
|
-
statusPages: Map<number, RouteConfig>;
|
|
105
|
-
|
|
106
|
-
/** Generic error handler */
|
|
107
|
-
errorHandler?: RouteConfig;
|
|
108
|
-
|
|
109
|
-
/** Pre-bundled module loaders keyed by module path (for SPA bundles) */
|
|
110
|
-
moduleLoaders?: Record<string, () => Promise<unknown>>;
|
|
111
|
-
}
|
|
86
|
+
export type { RouteNode } from './route-tree.type.ts';
|
|
112
87
|
|
|
113
88
|
/** Router state for history management */
|
|
114
89
|
export interface RouterState {
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Markdown Link Rewriting
|
|
3
|
+
*
|
|
4
|
+
* Rewrites internal absolute links in markdown to include a base path prefix.
|
|
5
|
+
* Skips fenced code blocks and links already under a known base path.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** Rewrite internal absolute links in markdown to include the base path prefix. */
|
|
9
|
+
export function rewriteMdLinks(markdown: string, base: string, skipPrefixes: string[]): string {
|
|
10
|
+
const prefix = base + '/';
|
|
11
|
+
// Negative lookahead: skip links already under a known base path
|
|
12
|
+
const skip = skipPrefixes.map((p) => p.slice(1) + '/').join('|');
|
|
13
|
+
const inlineRe = new RegExp(`\\]\\(\\/(?!${skip})`, 'g');
|
|
14
|
+
const refRe = new RegExp(`^(\\[[^\\]]+\\]:\\s+)\\/(?!${skip})`, 'g');
|
|
15
|
+
|
|
16
|
+
const lines = markdown.split('\n');
|
|
17
|
+
let inCodeBlock = false;
|
|
18
|
+
|
|
19
|
+
for (let i = 0; i < lines.length; i++) {
|
|
20
|
+
if (lines[i].startsWith('```')) {
|
|
21
|
+
inCodeBlock = !inCodeBlock;
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
if (inCodeBlock) continue;
|
|
25
|
+
|
|
26
|
+
lines[i] = lines[i].replaceAll(inlineRe, `](${prefix}`);
|
|
27
|
+
lines[i] = lines[i].replaceAll(refRe, `$1${prefix}`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return lines.join('\n');
|
|
31
|
+
}
|
|
@@ -118,7 +118,12 @@ export function resolveWidgetTags(
|
|
|
118
118
|
files = await loadFiles(widgetName, widget.files);
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
-
const baseContext = {
|
|
121
|
+
const baseContext = {
|
|
122
|
+
...routeInfo,
|
|
123
|
+
pathname: routeInfo.url.pathname,
|
|
124
|
+
searchParams: routeInfo.url.searchParams,
|
|
125
|
+
files,
|
|
126
|
+
};
|
|
122
127
|
const context = contextProvider ? contextProvider(baseContext) : baseContext;
|
|
123
128
|
|
|
124
129
|
const data = await widget.getData({ params, context });
|
|
@@ -17,7 +17,6 @@
|
|
|
17
17
|
import { WidgetComponent } from '../component/widget.component.ts';
|
|
18
18
|
import { escapeHtml } from '../util/html.util.ts';
|
|
19
19
|
import type { ComponentContext } from '../component/abstract.component.ts';
|
|
20
|
-
import { DEFAULT_BASE_PATH } from '../route/route.core.ts';
|
|
21
20
|
|
|
22
21
|
const DEFAULT_HTML_SEPARATOR = ' \u203A ';
|
|
23
22
|
const DEFAULT_MD_SEPARATOR = ' > ';
|
|
@@ -42,20 +41,14 @@ export class BreadcrumbWidget extends WidgetComponent<BreadcrumbParams, Breadcru
|
|
|
42
41
|
override getData(
|
|
43
42
|
args: { params: BreadcrumbParams; signal?: AbortSignal; context: ComponentContext },
|
|
44
43
|
): Promise<BreadcrumbData | null> {
|
|
45
|
-
const
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
// Skip basePath segments for display — only show route segments
|
|
49
|
-
const barePathname = htmlBase && pathname.startsWith(htmlBase)
|
|
50
|
-
? pathname.slice(htmlBase.length) || '/'
|
|
51
|
-
: pathname;
|
|
52
|
-
const parts = barePathname.split('/').filter(Boolean);
|
|
44
|
+
const pathname = args.context.pathname || '/';
|
|
45
|
+
const parts = pathname.split('/').filter(Boolean);
|
|
53
46
|
|
|
54
47
|
const segments: BreadcrumbSegment[] = [
|
|
55
|
-
{ label: 'Home', href:
|
|
48
|
+
{ label: 'Home', href: '/' },
|
|
56
49
|
];
|
|
57
50
|
|
|
58
|
-
let accumulated =
|
|
51
|
+
let accumulated = '';
|
|
59
52
|
for (const part of parts) {
|
|
60
53
|
accumulated += '/' + part;
|
|
61
54
|
segments.push({
|
|
@@ -67,11 +60,6 @@ export class BreadcrumbWidget extends WidgetComponent<BreadcrumbParams, Breadcru
|
|
|
67
60
|
return Promise.resolve({ segments });
|
|
68
61
|
}
|
|
69
62
|
|
|
70
|
-
private resolvePathname(htmlBase: string): string {
|
|
71
|
-
if (typeof globalThis.location === 'undefined') return htmlBase + '/';
|
|
72
|
-
return location.pathname;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
63
|
override renderHTML(
|
|
76
64
|
args: { data: BreadcrumbData | null; params: BreadcrumbParams; context: ComponentContext },
|
|
77
65
|
): string {
|
package/server/scanner.util.ts
DELETED
|
@@ -1,243 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Standalone Scanning Utilities
|
|
3
|
-
*
|
|
4
|
-
* Runtime-agnostic route and widget scanning. Works with any Runtime
|
|
5
|
-
* that implements query() — used by tests with mock runtimes and by
|
|
6
|
-
* the CLI generate command.
|
|
7
|
-
*
|
|
8
|
-
* Note: BunFsRuntime has these as instance methods with lazy caching.
|
|
9
|
-
* These standalone functions are the same logic without caching.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import type { Runtime } from '../runtime/abstract.runtime.ts';
|
|
13
|
-
import {
|
|
14
|
-
filePathToPattern,
|
|
15
|
-
getPageFileType,
|
|
16
|
-
getRouteType,
|
|
17
|
-
sortRoutesBySpecificity,
|
|
18
|
-
} from '../src/route/route.matcher.ts';
|
|
19
|
-
import type {
|
|
20
|
-
ErrorBoundary,
|
|
21
|
-
RouteConfig,
|
|
22
|
-
RouteFiles,
|
|
23
|
-
RoutesManifest,
|
|
24
|
-
} from '../src/type/route.type.ts';
|
|
25
|
-
import type { WidgetManifestEntry } from '../src/type/widget.type.ts';
|
|
26
|
-
|
|
27
|
-
export interface GeneratorResult extends RoutesManifest {
|
|
28
|
-
warnings: string[];
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/** Walk directory recursively and collect files via Runtime. */
|
|
32
|
-
async function* walkDirectory(runtime: Runtime, dir: string): AsyncGenerator<string> {
|
|
33
|
-
const trailingDir = dir.endsWith('/') ? dir : dir + '/';
|
|
34
|
-
const response = await runtime.query(trailingDir);
|
|
35
|
-
const entries: string[] = await response.json();
|
|
36
|
-
|
|
37
|
-
for (const entry of entries) {
|
|
38
|
-
const path = `${trailingDir}${entry}`;
|
|
39
|
-
if (entry.endsWith('/')) {
|
|
40
|
-
yield* walkDirectory(runtime, path);
|
|
41
|
-
} else {
|
|
42
|
-
yield path;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/** Generate routes manifest by scanning a directory via Runtime. */
|
|
48
|
-
export async function generateRoutesManifest(
|
|
49
|
-
routesDir: string,
|
|
50
|
-
runtime: Runtime,
|
|
51
|
-
): Promise<GeneratorResult> {
|
|
52
|
-
const pageFiles: Array<{
|
|
53
|
-
path: string;
|
|
54
|
-
pattern: string;
|
|
55
|
-
fileType: 'ts' | 'html' | 'md' | 'css';
|
|
56
|
-
}> = [];
|
|
57
|
-
const redirects: RouteConfig[] = [];
|
|
58
|
-
const errorBoundaries: ErrorBoundary[] = [];
|
|
59
|
-
const statusPages = new Map<number, RouteConfig>();
|
|
60
|
-
let errorHandler: RouteConfig | undefined;
|
|
61
|
-
|
|
62
|
-
const allFiles: string[] = [];
|
|
63
|
-
for await (const file of walkDirectory(runtime, routesDir)) {
|
|
64
|
-
allFiles.push(file);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
for (const filePath of allFiles) {
|
|
68
|
-
const relativePath = filePath.replace(`${routesDir}/`, '');
|
|
69
|
-
const filename = relativePath.split('/').pop() ?? '';
|
|
70
|
-
|
|
71
|
-
if (filename === 'index.error.ts' && relativePath === 'index.error.ts') {
|
|
72
|
-
errorHandler = {
|
|
73
|
-
pattern: '/',
|
|
74
|
-
type: 'error',
|
|
75
|
-
modulePath: filePath,
|
|
76
|
-
};
|
|
77
|
-
continue;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const cssFileType = getPageFileType(filename);
|
|
81
|
-
if (cssFileType === 'css') {
|
|
82
|
-
const pattern = filePathToPattern(relativePath);
|
|
83
|
-
pageFiles.push({ path: filePath, pattern, fileType: 'css' });
|
|
84
|
-
continue;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const routeType = getRouteType(filename);
|
|
88
|
-
if (!routeType) continue;
|
|
89
|
-
|
|
90
|
-
const statusMatch = filename.match(/^(\d{3})\.page\.(ts|html|md)$/);
|
|
91
|
-
if (statusMatch) {
|
|
92
|
-
const statusCode = parseInt(statusMatch[1], 10);
|
|
93
|
-
const fileType = getPageFileType(filename);
|
|
94
|
-
if (fileType) {
|
|
95
|
-
const existing = statusPages.get(statusCode);
|
|
96
|
-
if (existing) {
|
|
97
|
-
existing.files ??= {};
|
|
98
|
-
existing.files[fileType] = filePath;
|
|
99
|
-
existing.modulePath = existing.files.ts ?? existing.files.html ?? existing.files.md ?? '';
|
|
100
|
-
} else {
|
|
101
|
-
const files: RouteFiles = { [fileType]: filePath };
|
|
102
|
-
statusPages.set(statusCode, {
|
|
103
|
-
pattern: `/${statusCode}`,
|
|
104
|
-
type: 'page',
|
|
105
|
-
modulePath: filePath,
|
|
106
|
-
statusCode,
|
|
107
|
-
files,
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
continue;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const pattern = filePathToPattern(relativePath);
|
|
115
|
-
|
|
116
|
-
if (routeType === 'error') {
|
|
117
|
-
const boundaryPattern = pattern.replace(/\/[^/]+$/, '') || '/';
|
|
118
|
-
errorBoundaries.push({ pattern: boundaryPattern, modulePath: filePath });
|
|
119
|
-
continue;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
if (routeType === 'redirect') {
|
|
123
|
-
redirects.push({ pattern, type: 'redirect', modulePath: filePath });
|
|
124
|
-
continue;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
const fileType = getPageFileType(filename);
|
|
128
|
-
if (fileType) {
|
|
129
|
-
pageFiles.push({ path: filePath, pattern, fileType });
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// Group files by pattern
|
|
134
|
-
const groups = new Map<string, { pattern: string; files: RouteFiles; parent?: string }>();
|
|
135
|
-
for (const { path, pattern, fileType } of pageFiles) {
|
|
136
|
-
let group = groups.get(pattern);
|
|
137
|
-
if (!group) {
|
|
138
|
-
group = { pattern, files: {} };
|
|
139
|
-
const segments = pattern.split('/').filter(Boolean);
|
|
140
|
-
if (segments.length > 1) {
|
|
141
|
-
group.parent = '/' + segments.slice(0, -1).join('/');
|
|
142
|
-
}
|
|
143
|
-
groups.set(pattern, group);
|
|
144
|
-
}
|
|
145
|
-
const existing = group.files[fileType];
|
|
146
|
-
if (existing?.includes('/index.page.') && !path.includes('/index.page.')) {
|
|
147
|
-
continue;
|
|
148
|
-
}
|
|
149
|
-
group.files[fileType] = path;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// Detect collisions
|
|
153
|
-
const warnings: string[] = [];
|
|
154
|
-
for (const [pattern, group] of groups) {
|
|
155
|
-
const filePaths = Object.values(group.files).filter(Boolean);
|
|
156
|
-
const hasIndex = filePaths.some((p) => p?.includes('/index.page.'));
|
|
157
|
-
const hasFlat = filePaths.some((p) => p && !p.includes('/index.page.'));
|
|
158
|
-
if (hasIndex && hasFlat) {
|
|
159
|
-
warnings.push(
|
|
160
|
-
`Warning: Mixed file structure for ${pattern}:\n` +
|
|
161
|
-
filePaths.map((p) => ` ${p}`).join('\n') +
|
|
162
|
-
`\n Both folder/index and flat files detected`,
|
|
163
|
-
);
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// Convert groups to RouteConfig array
|
|
168
|
-
const routes: RouteConfig[] = [];
|
|
169
|
-
for (const [_, group] of groups) {
|
|
170
|
-
const modulePath = group.files.ts ?? group.files.html ?? group.files.md ?? '';
|
|
171
|
-
if (!modulePath) continue;
|
|
172
|
-
const route: RouteConfig = {
|
|
173
|
-
pattern: group.pattern,
|
|
174
|
-
type: 'page',
|
|
175
|
-
modulePath,
|
|
176
|
-
files: group.files,
|
|
177
|
-
};
|
|
178
|
-
if (group.parent) route.parent = group.parent;
|
|
179
|
-
routes.push(route);
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
routes.push(...redirects);
|
|
183
|
-
const sortedRoutes = sortRoutesBySpecificity(routes);
|
|
184
|
-
|
|
185
|
-
return {
|
|
186
|
-
routes: sortedRoutes,
|
|
187
|
-
errorBoundaries,
|
|
188
|
-
statusPages,
|
|
189
|
-
errorHandler,
|
|
190
|
-
warnings,
|
|
191
|
-
};
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
/**
|
|
195
|
-
* Discover widget modules and companion files by scanning a directory.
|
|
196
|
-
*/
|
|
197
|
-
export async function discoverWidgets(
|
|
198
|
-
widgetsDir: string,
|
|
199
|
-
runtime: Runtime,
|
|
200
|
-
pathPrefix?: string,
|
|
201
|
-
): Promise<WidgetManifestEntry[]> {
|
|
202
|
-
const COMPANION_EXTENSIONS = ['html', 'md', 'css'] as const;
|
|
203
|
-
const WIDGET_FILE_SUFFIX = '.widget.ts';
|
|
204
|
-
const entries: WidgetManifestEntry[] = [];
|
|
205
|
-
|
|
206
|
-
const trailingDir = widgetsDir.endsWith('/') ? widgetsDir : widgetsDir + '/';
|
|
207
|
-
const response = await runtime.query(trailingDir);
|
|
208
|
-
const listing: string[] = await response.json();
|
|
209
|
-
|
|
210
|
-
for (const item of listing) {
|
|
211
|
-
if (!item.endsWith('/')) continue;
|
|
212
|
-
|
|
213
|
-
const name = item.slice(0, -1);
|
|
214
|
-
const moduleFile = `${name}${WIDGET_FILE_SUFFIX}`;
|
|
215
|
-
const modulePath = `${trailingDir}${name}/${moduleFile}`;
|
|
216
|
-
|
|
217
|
-
if ((await runtime.query(modulePath)).status === 404) continue;
|
|
218
|
-
|
|
219
|
-
const prefix = pathPrefix ? `${pathPrefix}/` : '';
|
|
220
|
-
const entry: WidgetManifestEntry = {
|
|
221
|
-
name,
|
|
222
|
-
modulePath: `${prefix}${name}/${moduleFile}`,
|
|
223
|
-
tagName: `widget-${name}`,
|
|
224
|
-
};
|
|
225
|
-
|
|
226
|
-
const files: { html?: string; md?: string; css?: string } = {};
|
|
227
|
-
let hasFiles = false;
|
|
228
|
-
for (const ext of COMPANION_EXTENSIONS) {
|
|
229
|
-
const companionFile = `${name}.widget.${ext}`;
|
|
230
|
-
const companionPath = `${trailingDir}${name}/${companionFile}`;
|
|
231
|
-
if ((await runtime.query(companionPath)).status !== 404) {
|
|
232
|
-
files[ext] = `${prefix}${name}/${companionFile}`;
|
|
233
|
-
hasFiles = true;
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
if (hasFiles) entry.files = files;
|
|
238
|
-
entries.push(entry);
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
242
|
-
return entries;
|
|
243
|
-
}
|
|
@@ -1,186 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Base Renderer
|
|
3
|
-
*
|
|
4
|
-
* Shared rendering logic for SPA and Hash routers:
|
|
5
|
-
* - Route hierarchy traversal with nested slot rendering
|
|
6
|
-
* - Component loading, data fetching, and HTML rendering
|
|
7
|
-
* - Markdown render waiting
|
|
8
|
-
* - Document title updates
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import type { MatchedRoute, RouteConfig, RouteInfo } from '../../type/route.type.ts';
|
|
12
|
-
import defaultPageComponent, { type PageComponent } from '../../component/page.component.ts';
|
|
13
|
-
import { DEFAULT_ROOT_ROUTE, RouteCore } from '../../route/route.core.ts';
|
|
14
|
-
import { logger } from '../../util/logger.util.ts';
|
|
15
|
-
|
|
16
|
-
const MARKDOWN_RENDER_TIMEOUT = 5000;
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Abstract base for renderers that share the page rendering pipeline.
|
|
20
|
-
* Subclasses provide navigation mechanics (Navigation API, hashchange, etc.).
|
|
21
|
-
*/
|
|
22
|
-
export abstract class BaseRenderer {
|
|
23
|
-
protected core: RouteCore;
|
|
24
|
-
protected slot: Element | null = null;
|
|
25
|
-
|
|
26
|
-
constructor(core: RouteCore) {
|
|
27
|
-
this.core = core;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Render a matched page route with nested route support.
|
|
32
|
-
*/
|
|
33
|
-
protected async renderPage(
|
|
34
|
-
routeInfo: RouteInfo,
|
|
35
|
-
matched: MatchedRoute,
|
|
36
|
-
signal: AbortSignal,
|
|
37
|
-
): Promise<void> {
|
|
38
|
-
if (!this.slot) return;
|
|
39
|
-
|
|
40
|
-
try {
|
|
41
|
-
const hierarchy = this.core.buildRouteHierarchy(routeInfo.pattern);
|
|
42
|
-
logger.render('page', routeInfo.pattern, `hierarchy: ${hierarchy.join(' > ')}`);
|
|
43
|
-
|
|
44
|
-
let currentSlot: Element = this.slot;
|
|
45
|
-
let pageTitle: string | undefined;
|
|
46
|
-
|
|
47
|
-
for (let i = 0; i < hierarchy.length; i++) {
|
|
48
|
-
if (signal.aborted) return;
|
|
49
|
-
|
|
50
|
-
const routePattern = hierarchy[i];
|
|
51
|
-
const isLeaf = i === hierarchy.length - 1;
|
|
52
|
-
|
|
53
|
-
let route = this.core.findRoute(routePattern);
|
|
54
|
-
|
|
55
|
-
if (!route && routePattern === '/') {
|
|
56
|
-
route = DEFAULT_ROOT_ROUTE;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
if (!route) {
|
|
60
|
-
logger.render('skip', routePattern, 'route not found');
|
|
61
|
-
continue;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const routeType = isLeaf ? 'leaf' : 'layout';
|
|
65
|
-
logger.render(routeType, routePattern, `${route.files?.ts ?? 'default'} → slot`);
|
|
66
|
-
|
|
67
|
-
// Skip wildcard route appearing as its own parent (prevents double-render)
|
|
68
|
-
if (route === matched.route && routePattern !== matched.route.pattern) {
|
|
69
|
-
continue;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const { html, title } = await this.renderRouteContent(routeInfo, route, signal, isLeaf);
|
|
73
|
-
if (signal.aborted) return;
|
|
74
|
-
|
|
75
|
-
currentSlot.setHTMLUnsafe(html);
|
|
76
|
-
|
|
77
|
-
if (title) {
|
|
78
|
-
pageTitle = title;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// Wait for <mark-down> to finish rendering its content
|
|
82
|
-
// (must happen before attributing slots — router-slot may be inside markdown)
|
|
83
|
-
const markDown = currentSlot.querySelector<HTMLElement>('mark-down');
|
|
84
|
-
if (markDown) {
|
|
85
|
-
await this.waitForMarkdownRender(markDown);
|
|
86
|
-
if (signal.aborted) return;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// Attribute bare <router-slot> tags with this route's pattern
|
|
90
|
-
for (const slot of currentSlot.querySelectorAll('router-slot:not([pattern])')) {
|
|
91
|
-
slot.setAttribute('pattern', routePattern);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
if (!isLeaf) {
|
|
95
|
-
const nestedSlot = currentSlot.querySelector(
|
|
96
|
-
`router-slot[pattern="${CSS.escape(routePattern)}"]`,
|
|
97
|
-
);
|
|
98
|
-
if (nestedSlot) {
|
|
99
|
-
currentSlot = nestedSlot;
|
|
100
|
-
} else {
|
|
101
|
-
logger.warn(
|
|
102
|
-
`Route "${routePattern}" has no <router-slot> ` +
|
|
103
|
-
`for child routes to render into. ` +
|
|
104
|
-
`Add <router-slot></router-slot> to the parent template.`,
|
|
105
|
-
);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
if (signal.aborted) return;
|
|
111
|
-
|
|
112
|
-
this.updateTitle(pageTitle);
|
|
113
|
-
|
|
114
|
-
this.core.emit({
|
|
115
|
-
type: 'load',
|
|
116
|
-
pathname: routeInfo.pathname,
|
|
117
|
-
params: routeInfo.params,
|
|
118
|
-
});
|
|
119
|
-
} catch (error) {
|
|
120
|
-
if (signal.aborted) return;
|
|
121
|
-
throw error;
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* Render a single route's content.
|
|
127
|
-
*/
|
|
128
|
-
protected async renderRouteContent(
|
|
129
|
-
routeInfo: RouteInfo,
|
|
130
|
-
route: RouteConfig,
|
|
131
|
-
signal: AbortSignal,
|
|
132
|
-
isLeaf?: boolean,
|
|
133
|
-
): Promise<{ html: string; title?: string }> {
|
|
134
|
-
if (route.modulePath === DEFAULT_ROOT_ROUTE.modulePath) {
|
|
135
|
-
return { html: `<router-slot pattern="${route.pattern}"></router-slot>` };
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
const files = route.files ?? {};
|
|
139
|
-
|
|
140
|
-
const component: PageComponent = files.ts
|
|
141
|
-
? (await this.core.loadModule<{ default: PageComponent }>(files.ts)).default
|
|
142
|
-
: defaultPageComponent;
|
|
143
|
-
|
|
144
|
-
const context = await this.core.buildComponentContext(routeInfo, route, signal, isLeaf);
|
|
145
|
-
const data = await component.getData({ params: routeInfo.params, signal, context });
|
|
146
|
-
const html = component.renderHTML({ data, params: routeInfo.params, context });
|
|
147
|
-
const title = component.getTitle({ data, params: routeInfo.params, context });
|
|
148
|
-
return { html, title };
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
* Wait for a <mark-down> element to finish rendering.
|
|
153
|
-
*/
|
|
154
|
-
protected waitForMarkdownRender(element: HTMLElement): Promise<void> {
|
|
155
|
-
return new Promise((resolve) => {
|
|
156
|
-
if (element.children.length > 0) {
|
|
157
|
-
resolve();
|
|
158
|
-
return;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
const timeout = setTimeout(() => {
|
|
162
|
-
observer.disconnect();
|
|
163
|
-
resolve();
|
|
164
|
-
}, MARKDOWN_RENDER_TIMEOUT);
|
|
165
|
-
|
|
166
|
-
const observer = new MutationObserver(() => {
|
|
167
|
-
if (element.children.length > 0) {
|
|
168
|
-
clearTimeout(timeout);
|
|
169
|
-
observer.disconnect();
|
|
170
|
-
resolve();
|
|
171
|
-
}
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
observer.observe(element, { childList: true });
|
|
175
|
-
});
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
/**
|
|
179
|
-
* Update document.title from getTitle() result.
|
|
180
|
-
*/
|
|
181
|
-
protected updateTitle(pageTitle?: string): void {
|
|
182
|
-
if (pageTitle) {
|
|
183
|
-
document.title = pageTitle;
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
}
|