@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
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build Utilities
|
|
3
|
+
*
|
|
4
|
+
* Standalone client bundling — extracted from Runtime so that build is a
|
|
5
|
+
* separate concern from storage. Call `buildClientBundles()` before
|
|
6
|
+
* `createEmrouteServer()` to produce emroute.js + app.js.
|
|
7
|
+
*
|
|
8
|
+
* Requires esbuild as a devDependency in the consumer project.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { readFile } from 'node:fs/promises';
|
|
12
|
+
import { createRequire } from 'node:module';
|
|
13
|
+
import { resolve } from 'node:path';
|
|
14
|
+
import type { Runtime } from '../runtime/abstract.runtime.ts';
|
|
15
|
+
import { DEFAULT_ROUTES_DIR, DEFAULT_WIDGETS_DIR } from '../runtime/abstract.runtime.ts';
|
|
16
|
+
import { createManifestPlugin } from './esbuild-manifest.plugin.ts';
|
|
17
|
+
import { createRuntimeLoaderPlugin } from '../runtime/bun/esbuild-runtime-loader.plugin.ts';
|
|
18
|
+
import { generateMainTs } from './codegen.util.ts';
|
|
19
|
+
import type { SpaMode } from '../src/type/widget.type.ts';
|
|
20
|
+
|
|
21
|
+
export const EMROUTE_EXTERNALS = [
|
|
22
|
+
'@emkodev/emroute/spa',
|
|
23
|
+
'@emkodev/emroute/overlay',
|
|
24
|
+
'@emkodev/emroute',
|
|
25
|
+
'@emkodev/emroute/server',
|
|
26
|
+
'@emkodev/emroute/runtime/fetch',
|
|
27
|
+
] as const;
|
|
28
|
+
|
|
29
|
+
/** esbuild namespace for virtual `emroute:routes` / `emroute:widgets` modules. */
|
|
30
|
+
export const EMROUTE_VIRTUAL_NS = 'emroute';
|
|
31
|
+
|
|
32
|
+
export interface BuildOptions {
|
|
33
|
+
/** Runtime instance to read manifests and source files from. */
|
|
34
|
+
runtime: Runtime;
|
|
35
|
+
/** Filesystem root for esbuild resolution (e.g. process.cwd() or the app directory). */
|
|
36
|
+
root: string;
|
|
37
|
+
/** SPA mode — skips bundling when 'none'. */
|
|
38
|
+
spa: SpaMode;
|
|
39
|
+
/** Consumer's SPA entry point (e.g. '/main.ts'). When absent, auto-generates one. */
|
|
40
|
+
entryPoint?: string;
|
|
41
|
+
/** Output paths for the bundles. */
|
|
42
|
+
bundlePaths?: { emroute: string; app: string };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const DEFAULT_BUNDLE_PATHS = { emroute: '/emroute.js', app: '/app.js' };
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Build client bundles and write them into the runtime.
|
|
49
|
+
*
|
|
50
|
+
* Produces:
|
|
51
|
+
* - emroute.js — pre-built from dist/ (no esbuild needed for this)
|
|
52
|
+
* - app.js — consumer entry point with routeTree, FetchRuntime, createEmrouteApp
|
|
53
|
+
* - index.html — shell with import map + script tags (if not already present)
|
|
54
|
+
*/
|
|
55
|
+
export async function buildClientBundles(options: BuildOptions): Promise<void> {
|
|
56
|
+
const { runtime, root, spa, entryPoint } = options;
|
|
57
|
+
if (spa === 'none') return;
|
|
58
|
+
|
|
59
|
+
const paths = options.bundlePaths ?? DEFAULT_BUNDLE_PATHS;
|
|
60
|
+
|
|
61
|
+
// Copy pre-built emroute.js from the package dist/
|
|
62
|
+
const consumerRequire = createRequire(root + '/');
|
|
63
|
+
const emrouteJsPath = resolvePrebuiltBundle(consumerRequire);
|
|
64
|
+
const emrouteJs = await readFile(emrouteJsPath);
|
|
65
|
+
await runtime.command(paths.emroute, { body: emrouteJs });
|
|
66
|
+
|
|
67
|
+
// App bundle — generate main.ts if absent, virtual plugin resolves manifests
|
|
68
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
69
|
+
const esbuild = await loadEsbuild() as any;
|
|
70
|
+
const ep = entryPoint ?? '/main.ts';
|
|
71
|
+
if ((await runtime.query(ep)).status === 404) {
|
|
72
|
+
const hasRoutes = (await runtime.query((runtime.config.routesDir ?? DEFAULT_ROUTES_DIR) + '/')).status !== 404;
|
|
73
|
+
const hasWidgets = (await runtime.query((runtime.config.widgetsDir ?? DEFAULT_WIDGETS_DIR) + '/')).status !== 404;
|
|
74
|
+
const code = generateMainTs(spa, hasRoutes, hasWidgets, '@emkodev/emroute');
|
|
75
|
+
await runtime.command(ep, { body: code });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const manifestPlugin = createManifestPlugin({ runtime, resolveDir: root });
|
|
79
|
+
const runtimeLoader = createRuntimeLoaderPlugin({ runtime, root });
|
|
80
|
+
|
|
81
|
+
const result = await esbuild.build({
|
|
82
|
+
bundle: true,
|
|
83
|
+
write: false,
|
|
84
|
+
format: 'esm' as const,
|
|
85
|
+
platform: 'browser' as const,
|
|
86
|
+
entryPoints: [`${root}${ep}`],
|
|
87
|
+
outfile: `${root}${paths.app}`,
|
|
88
|
+
external: [...EMROUTE_EXTERNALS],
|
|
89
|
+
plugins: [manifestPlugin, runtimeLoader],
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
for (const file of result.outputFiles) {
|
|
93
|
+
const runtimePath = file.path.startsWith(root)
|
|
94
|
+
? file.path.slice(root.length)
|
|
95
|
+
: '/' + file.path;
|
|
96
|
+
await runtime.command(runtimePath, { body: file.contents as unknown as BodyInit });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Write shell (index.html) if not already present
|
|
100
|
+
await writeShell(runtime, paths, ep);
|
|
101
|
+
|
|
102
|
+
await esbuild.stop();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Resolve the pre-built dist/emroute.js from the consumer's node_modules.
|
|
107
|
+
* Falls back to the local dist/ when running from the source repo.
|
|
108
|
+
*/
|
|
109
|
+
function resolvePrebuiltBundle(require: NodeRequire): string {
|
|
110
|
+
try {
|
|
111
|
+
const spaEntry = require.resolve('@emkodev/emroute/spa');
|
|
112
|
+
// Compiled: .../dist/src/renderer/spa/mod.js → .../dist/emroute.js
|
|
113
|
+
const distMatch = spaEntry.match(/^(.+\/dist\/)src\/renderer\/spa\/mod\.js$/);
|
|
114
|
+
if (distMatch) return distMatch[1] + 'emroute.js';
|
|
115
|
+
// Source (Bun): .../src/renderer/spa/mod.ts → .../dist/emroute.js
|
|
116
|
+
const srcMatch = spaEntry.match(/^(.+\/)src\/renderer\/spa\/mod\.ts$/);
|
|
117
|
+
if (srcMatch) return srcMatch[1] + 'dist/emroute.js';
|
|
118
|
+
} catch { /* not installed as dependency */ }
|
|
119
|
+
// Last resort
|
|
120
|
+
return resolve(process.cwd(), 'dist/emroute.js');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ── Shell generation ──────────────────────────────────────────────────
|
|
124
|
+
|
|
125
|
+
async function writeShell(
|
|
126
|
+
runtime: Runtime,
|
|
127
|
+
paths: { emroute: string; app: string },
|
|
128
|
+
entryPoint: string,
|
|
129
|
+
): Promise<void> {
|
|
130
|
+
if ((await runtime.query('/index.html')).status !== 404) return;
|
|
131
|
+
|
|
132
|
+
const imports: Record<string, string> = {};
|
|
133
|
+
for (const pkg of EMROUTE_EXTERNALS) {
|
|
134
|
+
imports[pkg] = paths.emroute;
|
|
135
|
+
}
|
|
136
|
+
const importMap = JSON.stringify({ imports }, null, 2);
|
|
137
|
+
|
|
138
|
+
const scripts = [
|
|
139
|
+
`<script type="importmap">\n${importMap}\n </script>`,
|
|
140
|
+
];
|
|
141
|
+
if (entryPoint) {
|
|
142
|
+
scripts.push(`<script type="module" src="${paths.app}"></script>`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const html = `<!DOCTYPE html>
|
|
146
|
+
<html>
|
|
147
|
+
<head>
|
|
148
|
+
<meta charset="utf-8">
|
|
149
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
150
|
+
<title>emroute</title>
|
|
151
|
+
<style>@view-transition { navigation: auto; } router-slot { display: contents; }</style>
|
|
152
|
+
</head>
|
|
153
|
+
<body>
|
|
154
|
+
<router-slot></router-slot>
|
|
155
|
+
${scripts.join('\n ')}
|
|
156
|
+
</body>
|
|
157
|
+
</html>`;
|
|
158
|
+
|
|
159
|
+
await runtime.command('/index.html', { body: html });
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ── esbuild loader ────────────────────────────────────────────────────
|
|
163
|
+
|
|
164
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
165
|
+
async function loadEsbuild(): Promise<any> {
|
|
166
|
+
const consumerRequire = createRequire(process.cwd() + '/');
|
|
167
|
+
return consumerRequire('esbuild');
|
|
168
|
+
}
|
package/server/codegen.util.ts
CHANGED
|
@@ -12,6 +12,9 @@ import type { SpaMode } from '../src/type/widget.type.ts';
|
|
|
12
12
|
/**
|
|
13
13
|
* Generate a main.ts entry point for SPA bootstrapping.
|
|
14
14
|
*
|
|
15
|
+
* For `root`/`only` modes: creates an EmrouteServer with FetchRuntime
|
|
16
|
+
* in the browser and wires it to Navigation API via `createEmrouteApp`.
|
|
17
|
+
*
|
|
15
18
|
* Imports route tree and widget manifests from virtual `emroute:` specifiers
|
|
16
19
|
* that the esbuild manifest plugin resolves at bundle time.
|
|
17
20
|
*/
|
|
@@ -27,23 +30,23 @@ export function generateMainTs(
|
|
|
27
30
|
|
|
28
31
|
const spaImport = `${importPath}/spa`;
|
|
29
32
|
|
|
30
|
-
if (hasRoutes) {
|
|
31
|
-
imports.push(`import { routeTree, moduleLoaders } from 'emroute:routes';`);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
33
|
if (hasWidgets) {
|
|
35
|
-
imports.push(`import { ComponentElement } from '${spaImport}';`);
|
|
34
|
+
imports.push(`import { ComponentElement, WidgetRegistry } from '${spaImport}';`);
|
|
36
35
|
imports.push(`import { widgetsManifest } from 'emroute:widgets';`);
|
|
36
|
+
body.push('const widgets = new WidgetRegistry();');
|
|
37
37
|
body.push('for (const entry of widgetsManifest.widgets) {');
|
|
38
38
|
body.push(
|
|
39
39
|
' const mod = await widgetsManifest.moduleLoaders![entry.modulePath]() as Record<string, unknown>;',
|
|
40
40
|
);
|
|
41
41
|
body.push(' for (const exp of Object.values(mod)) {');
|
|
42
42
|
body.push(" if (exp && typeof exp === 'object' && 'getData' in exp) {");
|
|
43
|
+
body.push(' widgets.add(exp as any);');
|
|
43
44
|
body.push(' ComponentElement.register(exp as any, entry.files);');
|
|
44
45
|
body.push(' break;');
|
|
45
46
|
body.push(' }');
|
|
46
47
|
body.push(" if (typeof exp === 'function' && exp.prototype?.getData) {");
|
|
48
|
+
body.push(' const instance = new (exp as new () => any)();');
|
|
49
|
+
body.push(' widgets.add(instance);');
|
|
47
50
|
body.push(
|
|
48
51
|
' ComponentElement.registerClass(exp as new () => any, entry.name, entry.files);',
|
|
49
52
|
);
|
|
@@ -54,14 +57,29 @@ export function generateMainTs(
|
|
|
54
57
|
}
|
|
55
58
|
|
|
56
59
|
if ((spa === 'root' || spa === 'only') && hasRoutes) {
|
|
57
|
-
imports.push(`import {
|
|
58
|
-
imports.push(`import {
|
|
60
|
+
imports.push(`import { routeTree, moduleLoaders } from 'emroute:routes';`);
|
|
61
|
+
imports.push(`import { createEmrouteServer } from '${importPath}/server';`);
|
|
62
|
+
imports.push(`import { FetchRuntime } from '${importPath}/runtime/fetch';`);
|
|
63
|
+
imports.push(`import { createEmrouteApp } from '${spaImport}';`);
|
|
64
|
+
|
|
65
|
+
body.push('const runtime = new FetchRuntime(location.origin);');
|
|
66
|
+
|
|
67
|
+
// Merge route + widget module loaders so the browser never calls runtime.loadModule()
|
|
68
|
+
const loadersExpr = hasWidgets
|
|
69
|
+
? '{ ...moduleLoaders, ...widgetsManifest.moduleLoaders }'
|
|
70
|
+
: 'moduleLoaders';
|
|
59
71
|
|
|
60
|
-
|
|
72
|
+
const configParts = ['routeTree', `moduleLoaders: ${loadersExpr}`];
|
|
73
|
+
if (hasWidgets) configParts.push('widgets');
|
|
74
|
+
if (basePath) {
|
|
75
|
+
configParts.push(`basePath: { html: '${basePath.html}', md: '${basePath.md}', app: '${basePath.app}' }`);
|
|
76
|
+
}
|
|
77
|
+
body.push(`const server = await createEmrouteServer({ ${configParts.join(', ')} }, runtime);`);
|
|
61
78
|
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
79
|
+
const appOpts = basePath
|
|
80
|
+
? `{ basePath: { html: '${basePath.html}', md: '${basePath.md}', app: '${basePath.app}' } }`
|
|
81
|
+
: '';
|
|
82
|
+
body.push(`await createEmrouteApp(server${appOpts ? ', ' + appOpts : ''});`);
|
|
65
83
|
}
|
|
66
84
|
|
|
67
85
|
return `/** Auto-generated entry point — do not edit. */\n${imports.join('\n')}\n\n${
|
package/server/emroute.server.ts
CHANGED
|
@@ -37,6 +37,7 @@ import type { WidgetManifestEntry } from '../src/type/widget.type.ts';
|
|
|
37
37
|
import { WidgetRegistry } from '../src/widget/widget.registry.ts';
|
|
38
38
|
import type { WidgetComponent } from '../src/component/widget.component.ts';
|
|
39
39
|
import { escapeHtml } from '../src/util/html.util.ts';
|
|
40
|
+
import { rewriteMdLinks } from '../src/util/md.util.ts';
|
|
40
41
|
import {
|
|
41
42
|
ROUTES_MANIFEST_PATH,
|
|
42
43
|
Runtime,
|
|
@@ -58,7 +59,8 @@ function createModuleLoaders(
|
|
|
58
59
|
const paths = new Set<string>();
|
|
59
60
|
|
|
60
61
|
function walk(node: RouteNode): void {
|
|
61
|
-
|
|
62
|
+
const modulePath = node.files?.ts ?? node.files?.js;
|
|
63
|
+
if (modulePath) paths.add(modulePath);
|
|
62
64
|
if (node.redirect) paths.add(node.redirect);
|
|
63
65
|
if (node.errorBoundary) paths.add(node.errorBoundary);
|
|
64
66
|
|
|
@@ -106,6 +108,7 @@ async function importWidgets(
|
|
|
106
108
|
widgetFiles: Record<string, { html?: string; md?: string; css?: string }>;
|
|
107
109
|
}> {
|
|
108
110
|
const registry = new WidgetRegistry();
|
|
111
|
+
const widgetFiles: Record<string, { html?: string; md?: string; css?: string }> = {};
|
|
109
112
|
|
|
110
113
|
for (const entry of entries) {
|
|
111
114
|
try {
|
|
@@ -117,8 +120,17 @@ async function importWidgets(
|
|
|
117
120
|
const instance = extractWidgetExport(mod);
|
|
118
121
|
if (!instance) continue;
|
|
119
122
|
registry.add(instance);
|
|
123
|
+
|
|
124
|
+
// Prefer inlined __files from merged module over manifest paths
|
|
125
|
+
const inlined = mod.__files;
|
|
126
|
+
if (inlined && typeof inlined === 'object') {
|
|
127
|
+
widgetFiles[entry.name] = inlined as { html?: string; md?: string; css?: string };
|
|
128
|
+
} else if (entry.files) {
|
|
129
|
+
widgetFiles[entry.name] = entry.files;
|
|
130
|
+
}
|
|
120
131
|
} catch (e) {
|
|
121
132
|
console.error(`[emroute] Failed to load widget ${entry.modulePath}:`, e);
|
|
133
|
+
if (entry.files) widgetFiles[entry.name] = entry.files;
|
|
122
134
|
}
|
|
123
135
|
}
|
|
124
136
|
|
|
@@ -128,21 +140,17 @@ async function importWidgets(
|
|
|
128
140
|
}
|
|
129
141
|
}
|
|
130
142
|
|
|
131
|
-
const widgetFiles: Record<string, { html?: string; md?: string; css?: string }> = {};
|
|
132
|
-
for (const entry of entries) {
|
|
133
|
-
if (entry.files) widgetFiles[entry.name] = entry.files;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
143
|
return { registry, widgetFiles };
|
|
137
144
|
}
|
|
138
145
|
|
|
139
146
|
// ── HTML shell ─────────────────────────────────────────────────────────
|
|
140
147
|
|
|
141
148
|
/** Build a default HTML shell. */
|
|
142
|
-
function buildHtmlShell(title: string): string {
|
|
149
|
+
function buildHtmlShell(title: string, htmlBase: string): string {
|
|
150
|
+
const baseTag = htmlBase ? `\n <base href="${escapeHtml(htmlBase)}/">` : '';
|
|
143
151
|
return `<!DOCTYPE html>
|
|
144
152
|
<html>
|
|
145
|
-
<head
|
|
153
|
+
<head>${baseTag}
|
|
146
154
|
<meta charset="utf-8">
|
|
147
155
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
148
156
|
<title>${escapeHtml(title)}</title>
|
|
@@ -178,10 +186,11 @@ function injectSsrContent(
|
|
|
178
186
|
async function resolveShell(
|
|
179
187
|
runtime: Runtime,
|
|
180
188
|
title: string,
|
|
189
|
+
htmlBase: string,
|
|
181
190
|
): Promise<string> {
|
|
182
191
|
const response = await runtime.query('/index.html');
|
|
183
192
|
if (response.status !== 404) return await response.text();
|
|
184
|
-
return buildHtmlShell(title);
|
|
193
|
+
return buildHtmlShell(title, htmlBase);
|
|
185
194
|
}
|
|
186
195
|
|
|
187
196
|
// ── More path helpers ─────────────────────────────────────────────────
|
|
@@ -201,10 +210,7 @@ export async function createEmrouteServer(
|
|
|
201
210
|
spa = 'root',
|
|
202
211
|
} = config;
|
|
203
212
|
|
|
204
|
-
|
|
205
|
-
runtime.config.spa = spa;
|
|
206
|
-
|
|
207
|
-
const { html: htmlBase, md: mdBase } = config.basePath ?? DEFAULT_BASE_PATH;
|
|
213
|
+
const { html: htmlBase, md: mdBase, app: appBase } = config.basePath ?? DEFAULT_BASE_PATH;
|
|
208
214
|
|
|
209
215
|
// ── Route tree (read from runtime) ──────────────────────────────────
|
|
210
216
|
|
|
@@ -223,7 +229,7 @@ export async function createEmrouteServer(
|
|
|
223
229
|
routeTree = await manifestResponse.json();
|
|
224
230
|
}
|
|
225
231
|
|
|
226
|
-
const moduleLoaders = createModuleLoaders(routeTree, runtime);
|
|
232
|
+
const moduleLoaders = config.moduleLoaders ?? createModuleLoaders(routeTree, runtime);
|
|
227
233
|
const resolver = new RouteTrie(routeTree);
|
|
228
234
|
|
|
229
235
|
// ── Widgets (read from runtime) ────────────────────────────────────
|
|
@@ -235,9 +241,17 @@ export async function createEmrouteServer(
|
|
|
235
241
|
const widgetsResponse = await runtime.query(WIDGETS_MANIFEST_PATH);
|
|
236
242
|
if (widgetsResponse.status !== 404) {
|
|
237
243
|
discoveredWidgetEntries = await widgetsResponse.json();
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
244
|
+
if (config.widgets) {
|
|
245
|
+
// Widgets pre-provided (e.g. browser bundle) — just collect file paths
|
|
246
|
+
widgets = config.widgets;
|
|
247
|
+
for (const entry of discoveredWidgetEntries) {
|
|
248
|
+
if (entry.files) widgetFiles[entry.name] = entry.files;
|
|
249
|
+
}
|
|
250
|
+
} else {
|
|
251
|
+
const imported = await importWidgets(discoveredWidgetEntries, runtime);
|
|
252
|
+
widgets = imported.registry;
|
|
253
|
+
widgetFiles = imported.widgetFiles;
|
|
254
|
+
}
|
|
241
255
|
}
|
|
242
256
|
|
|
243
257
|
// ── SSR routers ──────────────────────────────────────────────────────
|
|
@@ -254,7 +268,6 @@ export async function createEmrouteServer(
|
|
|
254
268
|
|
|
255
269
|
ssrHtmlRouter = new SsrHtmlRouter(resolver, {
|
|
256
270
|
fileReader: (path) => runtime.query(path, { as: 'text' }),
|
|
257
|
-
basePath: htmlBase,
|
|
258
271
|
moduleLoaders,
|
|
259
272
|
markdownRenderer: config.markdownRenderer,
|
|
260
273
|
extendContext: config.extendContext,
|
|
@@ -264,7 +277,6 @@ export async function createEmrouteServer(
|
|
|
264
277
|
|
|
265
278
|
ssrMdRouter = new SsrMdRouter(resolver, {
|
|
266
279
|
fileReader: (path) => runtime.query(path, { as: 'text' }),
|
|
267
|
-
basePath: mdBase,
|
|
268
280
|
moduleLoaders,
|
|
269
281
|
extendContext: config.extendContext,
|
|
270
282
|
widgets,
|
|
@@ -274,14 +286,10 @@ export async function createEmrouteServer(
|
|
|
274
286
|
|
|
275
287
|
buildSsrRouters();
|
|
276
288
|
|
|
277
|
-
// ── Bundling (runtime decides whether/how to bundle) ────────────────
|
|
278
|
-
|
|
279
|
-
await runtime.bundle();
|
|
280
|
-
|
|
281
289
|
// ── HTML shell ───────────────────────────────────────────────────────
|
|
282
290
|
|
|
283
291
|
const title = config.title ?? 'emroute';
|
|
284
|
-
let shell = await resolveShell(runtime, title);
|
|
292
|
+
let shell = await resolveShell(runtime, title, htmlBase);
|
|
285
293
|
|
|
286
294
|
// Auto-discover main.css and inject <link> into <head>
|
|
287
295
|
if ((await runtime.query('/main.css')).status !== 404) {
|
|
@@ -296,6 +304,7 @@ export async function createEmrouteServer(
|
|
|
296
304
|
|
|
297
305
|
const mdPrefix = mdBase + '/';
|
|
298
306
|
const htmlPrefix = htmlBase + '/';
|
|
307
|
+
const appPrefix = appBase + '/';
|
|
299
308
|
|
|
300
309
|
// SSR Markdown: /md/*
|
|
301
310
|
if (
|
|
@@ -309,12 +318,13 @@ export async function createEmrouteServer(
|
|
|
309
318
|
return Response.redirect(new URL(canonical, url.origin), 301);
|
|
310
319
|
}
|
|
311
320
|
try {
|
|
312
|
-
const
|
|
321
|
+
const routeUrl = new URL(routePath + url.search, url.origin);
|
|
322
|
+
const { content, status, redirect } = await ssrMdRouter.render(routeUrl, req.signal);
|
|
313
323
|
if (redirect) {
|
|
314
324
|
const target = redirect.startsWith('/') ? mdBase + redirect : redirect;
|
|
315
325
|
return Response.redirect(new URL(target, url.origin), status);
|
|
316
326
|
}
|
|
317
|
-
return new Response(content, {
|
|
327
|
+
return new Response(rewriteMdLinks(content, mdBase, [mdBase, htmlBase]), {
|
|
318
328
|
status,
|
|
319
329
|
headers: { 'Content-Type': 'text/markdown; charset=utf-8; variant=CommonMark' },
|
|
320
330
|
});
|
|
@@ -336,7 +346,8 @@ export async function createEmrouteServer(
|
|
|
336
346
|
return Response.redirect(new URL(canonical, url.origin), 301);
|
|
337
347
|
}
|
|
338
348
|
try {
|
|
339
|
-
const
|
|
349
|
+
const routeUrl = new URL(routePath + url.search, url.origin);
|
|
350
|
+
const result = await ssrHtmlRouter.render(routeUrl, req.signal);
|
|
340
351
|
if (result.redirect) {
|
|
341
352
|
const target = result.redirect.startsWith('/') ? htmlBase + result.redirect : result.redirect;
|
|
342
353
|
return Response.redirect(new URL(target, url.origin), result.status);
|
|
@@ -353,7 +364,15 @@ export async function createEmrouteServer(
|
|
|
353
364
|
}
|
|
354
365
|
}
|
|
355
366
|
|
|
356
|
-
// /
|
|
367
|
+
// /app/* — serve shell (browser JS takes over via createEmrouteApp)
|
|
368
|
+
if (pathname.startsWith(appPrefix) || pathname === appBase) {
|
|
369
|
+
return new Response(shell, {
|
|
370
|
+
status: 200,
|
|
371
|
+
headers: { 'Content-Type': 'text/html; charset=utf-8' },
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// /html/* or /md/* that wasn't handled by SSR (e.g. 'only' mode) — serve shell
|
|
357
376
|
if (
|
|
358
377
|
pathname.startsWith(htmlPrefix) || pathname === htmlBase ||
|
|
359
378
|
pathname.startsWith(mdPrefix) || pathname === mdBase
|
|
@@ -372,9 +391,10 @@ export async function createEmrouteServer(
|
|
|
372
391
|
return null;
|
|
373
392
|
}
|
|
374
393
|
|
|
375
|
-
// Bare paths — redirect to /
|
|
394
|
+
// Bare paths — redirect to /app/* in root/only modes, /html/* otherwise.
|
|
395
|
+
const base = (spa === 'root' || spa === 'only') ? appBase : htmlBase;
|
|
376
396
|
const bare = pathname === '/' ? '' : pathname.slice(1).replace(/\/$/, '');
|
|
377
|
-
return Response.redirect(new URL(`${
|
|
397
|
+
return Response.redirect(new URL(`${base}/${bare}`, url.origin), 302);
|
|
378
398
|
}
|
|
379
399
|
|
|
380
400
|
// ── Return ───────────────────────────────────────────────────────────
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
import type { Runtime } from '../runtime/abstract.runtime.ts';
|
|
13
13
|
import { ROUTES_MANIFEST_PATH, WIDGETS_MANIFEST_PATH } from '../runtime/abstract.runtime.ts';
|
|
14
|
+
import { EMROUTE_VIRTUAL_NS } from './build.util.ts';
|
|
14
15
|
|
|
15
16
|
/** Escape a string for use inside a single-quoted JS/TS string literal. */
|
|
16
17
|
function esc(value: string): string {
|
|
@@ -46,12 +47,12 @@ export function createManifestPlugin(options: ManifestPluginOptions): EsbuildPlu
|
|
|
46
47
|
build.onResolve(
|
|
47
48
|
{ filter: /^emroute:/ },
|
|
48
49
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
49
|
-
(args: any) => ({ path: args.path, namespace:
|
|
50
|
+
(args: any) => ({ path: args.path, namespace: EMROUTE_VIRTUAL_NS }),
|
|
50
51
|
);
|
|
51
52
|
|
|
52
53
|
// ── Load virtual modules ────────────────────────────────────────
|
|
53
54
|
build.onLoad(
|
|
54
|
-
{ filter: /.*/, namespace:
|
|
55
|
+
{ filter: /.*/, namespace: EMROUTE_VIRTUAL_NS },
|
|
55
56
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
56
57
|
async (args: any) => {
|
|
57
58
|
if (args.path === 'emroute:routes') {
|
|
@@ -108,7 +109,8 @@ ${moduleLoadersCode}
|
|
|
108
109
|
*/
|
|
109
110
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
110
111
|
function collectModulePaths(node: any, paths: Set<string>): void {
|
|
111
|
-
|
|
112
|
+
const modulePath = node.files?.ts ?? node.files?.js;
|
|
113
|
+
if (modulePath) paths.add(modulePath);
|
|
112
114
|
if (node.errorBoundary) paths.add(node.errorBoundary);
|
|
113
115
|
if (node.redirect) paths.add(node.redirect);
|
|
114
116
|
if (node.children) {
|
|
@@ -58,6 +58,13 @@ export interface EmrouteServerConfig {
|
|
|
58
58
|
|
|
59
59
|
/** Enrich every ComponentContext with app-level services. */
|
|
60
60
|
extendContext?: ContextProvider;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Pre-bundled module loaders (route + widget modules).
|
|
64
|
+
* When provided, skips `runtime.loadModule()` — used in the browser
|
|
65
|
+
* where modules are already bundled into app.js.
|
|
66
|
+
*/
|
|
67
|
+
moduleLoaders?: Record<string, () => Promise<unknown>>;
|
|
61
68
|
}
|
|
62
69
|
|
|
63
70
|
/**
|
|
@@ -17,7 +17,7 @@ import { escapeHtml } from '../util/html.util.ts';
|
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
19
|
* Context passed to components during rendering.
|
|
20
|
-
* Extends RouteInfo (pathname, pattern, params
|
|
20
|
+
* Extends RouteInfo (pathname, pattern, params)
|
|
21
21
|
* with pre-loaded file content and an abort signal.
|
|
22
22
|
*
|
|
23
23
|
* Consumers can extend this interface via module augmentation
|
|
@@ -27,12 +27,14 @@ import { escapeHtml } from '../util/html.util.ts';
|
|
|
27
27
|
export type FileContents = { html?: string; md?: string; css?: string };
|
|
28
28
|
|
|
29
29
|
export interface ComponentContext extends RouteInfo {
|
|
30
|
+
/** @deprecated Use context.url.pathname */
|
|
31
|
+
readonly pathname: string;
|
|
32
|
+
/** @deprecated Use context.url.searchParams */
|
|
33
|
+
readonly searchParams: URLSearchParams;
|
|
30
34
|
readonly files?: Readonly<FileContents>;
|
|
31
35
|
readonly signal?: AbortSignal;
|
|
32
36
|
/** True when this component is the leaf (matched) route, false when rendered as a layout parent. */
|
|
33
37
|
readonly isLeaf?: boolean;
|
|
34
|
-
/** Base path for SSR HTML links (e.g. '/html'). */
|
|
35
|
-
readonly basePath?: string;
|
|
36
38
|
}
|
|
37
39
|
|
|
38
40
|
/**
|
|
@@ -155,11 +155,12 @@ export class ComponentElement<TParams, TData> extends HTMLElementBase {
|
|
|
155
155
|
const files = await this.loadFiles();
|
|
156
156
|
if (signal.aborted) return;
|
|
157
157
|
|
|
158
|
+
const currentUrl = globalThis.location ? new URL(location.href) : new URL('http://localhost/');
|
|
158
159
|
const base: ComponentContext = {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
160
|
+
url: currentUrl,
|
|
161
|
+
pathname: currentUrl.pathname,
|
|
162
|
+
searchParams: currentUrl.searchParams,
|
|
163
|
+
params: this.params ?? {},
|
|
163
164
|
files: (files.html || files.md || files.css) ? files : undefined,
|
|
164
165
|
};
|
|
165
166
|
this.context = ComponentElement.extendContext ? ComponentElement.extendContext(base) : base;
|
package/src/renderer/spa/mod.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* SPA (Browser) Module
|
|
3
3
|
*
|
|
4
4
|
* Everything needed for the browser bundle:
|
|
5
|
-
* -
|
|
5
|
+
* - EmrouteApp: Navigation API glue wired to an EmrouteServer
|
|
6
6
|
* - Custom elements for rendering and hydrating SSR islands
|
|
7
7
|
* - Widget registry (built-in widgets are opt-in)
|
|
8
8
|
*/
|
|
@@ -12,13 +12,7 @@ import { MarkdownElement } from '../../element/markdown.element.ts';
|
|
|
12
12
|
import { ComponentElement } from '../../element/component.element.ts';
|
|
13
13
|
import { WidgetRegistry } from '../../widget/widget.registry.ts';
|
|
14
14
|
|
|
15
|
-
export {
|
|
16
|
-
export {
|
|
17
|
-
createHashRouter,
|
|
18
|
-
type HashRouteConfig,
|
|
19
|
-
HashRouter,
|
|
20
|
-
type HashRouterOptions,
|
|
21
|
-
} from './hash.renderer.ts';
|
|
15
|
+
export { createEmrouteApp, EmrouteApp, type EmrouteAppOptions } from './thin-client.ts';
|
|
22
16
|
export { ComponentElement, MarkdownElement, RouterSlot, WidgetRegistry };
|
|
23
17
|
export type { SpaMode, WidgetsManifest } from '../../type/widget.type.ts';
|
|
24
18
|
|