@adaas/are-html 0.0.23 → 0.0.24
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/browser/index.d.mts +18 -2
- package/dist/browser/index.mjs +35 -10
- package/dist/browser/index.mjs.map +1 -1
- package/dist/node/directives/AreDirectiveIf.directive.d.mts +17 -1
- package/dist/node/directives/AreDirectiveIf.directive.d.ts +17 -1
- package/dist/node/directives/AreDirectiveIf.directive.js +29 -6
- package/dist/node/directives/AreDirectiveIf.directive.js.map +1 -1
- package/dist/node/directives/AreDirectiveIf.directive.mjs +29 -6
- package/dist/node/directives/AreDirectiveIf.directive.mjs.map +1 -1
- package/dist/node/engine/AreHTML.compiler.d.mts +3 -1
- package/dist/node/engine/AreHTML.compiler.d.ts +3 -1
- package/dist/node/engine/AreHTML.compiler.js +7 -4
- package/dist/node/engine/AreHTML.compiler.js.map +1 -1
- package/dist/node/engine/AreHTML.compiler.mjs +7 -4
- package/dist/node/engine/AreHTML.compiler.mjs.map +1 -1
- package/examples/for-perf/dist/index.html +1 -1
- package/examples/for-perf/dist/{mqj1mpf2-z4aokv.js → mqp8i2py-vltsx0.js} +2488 -2373
- package/examples/lazy-loading/README.md +76 -0
- package/examples/lazy-loading/concept.ts +55 -0
- package/examples/lazy-loading/containers/UI.container.ts +215 -0
- package/examples/lazy-loading/dist/app.js +3803 -0
- package/examples/{for-perf/dist/mqj1mpff-4fr7mw.js → lazy-loading/dist/chunks/chunk-6K72IBO4.js} +2688 -5897
- package/examples/lazy-loading/dist/index.html +36 -0
- package/examples/lazy-loading/dist/lazy/about-page.js +59 -0
- package/examples/lazy-loading/dist/lazy/reports-page.js +65 -0
- package/examples/lazy-loading/dist/lazy/settings-page.js +54 -0
- package/examples/lazy-loading/public/index.html +36 -0
- package/examples/lazy-loading/src/components/AppShell.component.ts +44 -0
- package/examples/lazy-loading/src/components/HomePage.component.ts +59 -0
- package/examples/lazy-loading/src/components/LazyOutlet.component.ts +108 -0
- package/examples/lazy-loading/src/components/NavBar.component.ts +98 -0
- package/examples/lazy-loading/src/concept.ts +116 -0
- package/examples/lazy-loading/src/lazy/AboutPage.component.ts +54 -0
- package/examples/lazy-loading/src/lazy/ReportsPage.component.ts +56 -0
- package/examples/lazy-loading/src/lazy/SettingsPage.component.ts +45 -0
- package/examples/lazy-loading/src/runtime/ComponentManifest.fragment.ts +61 -0
- package/examples/lazy-loading/src/runtime/LazyComponentResolver.fragment.ts +77 -0
- package/examples/os-desktop/README.md +91 -0
- package/examples/os-desktop/concept.ts +54 -0
- package/examples/os-desktop/containers/OS.container.ts +198 -0
- package/examples/os-desktop/containers/apps/AppBackend.ts +29 -0
- package/examples/os-desktop/containers/apps/GanttApp.backend.ts +56 -0
- package/examples/os-desktop/containers/apps/MarketingApp.backend.ts +68 -0
- package/examples/os-desktop/dist/app.js +4410 -0
- package/examples/os-desktop/dist/apps/gantt/app.js +271 -0
- package/examples/os-desktop/dist/apps/marketing/app.js +346 -0
- package/examples/os-desktop/dist/chunks/chunk-6K72IBO4.js +12455 -0
- package/examples/os-desktop/dist/chunks/chunk-EIIGUL6N.js +30 -0
- package/examples/os-desktop/dist/chunks/chunk-WOH7L5UR.js +30 -0
- package/examples/os-desktop/dist/index.html +33 -0
- package/examples/os-desktop/public/index.html +33 -0
- package/examples/os-desktop/src/apps/gantt/GanttApp.component.ts +41 -0
- package/examples/os-desktop/src/apps/gantt/GanttChart.component.ts +126 -0
- package/examples/os-desktop/src/apps/gantt/GanttStore.ts +47 -0
- package/examples/os-desktop/src/apps/gantt/GanttToolbar.component.ts +73 -0
- package/examples/os-desktop/src/apps/gantt/index.ts +13 -0
- package/examples/os-desktop/src/apps/marketing/MarketingApp.component.ts +53 -0
- package/examples/os-desktop/src/apps/marketing/MarketingStore.ts +34 -0
- package/examples/os-desktop/src/apps/marketing/PostEditor.component.ts +153 -0
- package/examples/os-desktop/src/apps/marketing/PostPreview.component.ts +110 -0
- package/examples/os-desktop/src/apps/marketing/index.ts +16 -0
- package/examples/os-desktop/src/concept.ts +126 -0
- package/examples/os-desktop/src/os/AppStage.component.ts +112 -0
- package/examples/os-desktop/src/os/AppWindow.component.ts +102 -0
- package/examples/os-desktop/src/os/Desktop.component.ts +106 -0
- package/examples/os-desktop/src/os/Dock.component.ts +174 -0
- package/examples/os-desktop/src/os/Hud.component.ts +83 -0
- package/examples/os-desktop/src/os/Launchpad.component.ts +191 -0
- package/examples/os-desktop/src/os/MenuBar.component.ts +156 -0
- package/examples/os-desktop/src/runtime/AppComponentResolver.fragment.ts +121 -0
- package/examples/os-desktop/src/runtime/AppRegistry.fragment.ts +104 -0
- package/examples/os-desktop/src/signals/MouseState.signal.ts +34 -0
- package/examples/os-desktop/src/signals/OSRoute.signal.ts +37 -0
- package/examples/os-desktop/src/signals/SelectionState.signal.ts +34 -0
- package/examples/signal-routing/dist/index.html +1 -1
- package/examples/signal-routing/dist/{mqiwo23h-bhcolu.js → mqp8hgce-4d6rh0.js} +2911 -2708
- package/package.json +11 -7
- package/src/directives/AreDirectiveIf.directive.ts +33 -4
- package/src/engine/AreHTML.compiler.ts +12 -2
- package/tests/PropPropagation.test.ts +181 -0
- package/tests/jest.setup.ts +11 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# ARE · Lazy Loading example
|
|
2
|
+
|
|
3
|
+
Runtime, on-demand loading of components that are **served by the backend** and
|
|
4
|
+
were **not part of the initial bundle**. The browser fetches a component's JS
|
|
5
|
+
the first time its route is visited, registers the returned class into the live
|
|
6
|
+
DI scope, and renders it into an already-mounted tree — no full reload, no
|
|
7
|
+
pre-registration at bootstrap.
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm run example:lazy-loading
|
|
11
|
+
# → http://localhost:8083
|
|
12
|
+
# manifest → http://localhost:8083/api/components
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Open DevTools → Network and click an **About / Settings / Reports** link: the
|
|
16
|
+
matching `/lazy/*.js` bundle loads exactly once, the first time.
|
|
17
|
+
|
|
18
|
+
## How it works
|
|
19
|
+
|
|
20
|
+
1. **Discover** — on boot the app fetches `GET /api/components` (served by the
|
|
21
|
+
`UIContainer` backend) to learn which components exist and where to load
|
|
22
|
+
the lazy ones (`ComponentManifest` fragment).
|
|
23
|
+
2. **Navigate** — `NavBar` dispatches an `AreRoute` signal on the bus.
|
|
24
|
+
3. **Resolve + fetch** — `LazyOutlet` (the dynamic slot) maps the route to a
|
|
25
|
+
component. If it's lazy and not yet loaded, it does a dynamic
|
|
26
|
+
`import(url)`.
|
|
27
|
+
4. **Register** — the returned class is registered into the live scope via
|
|
28
|
+
`scope.register(Class)` (this bumps the resolution version so the engine can
|
|
29
|
+
immediately resolve the new tag).
|
|
30
|
+
5. **Render** — the outlet sets its content to `<the-tag>` and runs the
|
|
31
|
+
`tokenize → init → load → transform → compile → mount` subtree build.
|
|
32
|
+
|
|
33
|
+
### Code-splitting = shared runtime singletons
|
|
34
|
+
|
|
35
|
+
The app bundle and every lazy bundle are built in **one esbuild pass with
|
|
36
|
+
`splitting: true`**. esbuild hoists the framework code (`@adaas/are`,
|
|
37
|
+
`@adaas/a-concept`, …) into shared chunks that both the app and each lazy bundle
|
|
38
|
+
import **by URL**. When the browser later `import()`s a lazy bundle it reuses the
|
|
39
|
+
already-evaluated shared chunk — so the lazy `class X extends Are` references the
|
|
40
|
+
**same** `Are` and the **same** DI context the host app is running. Without this,
|
|
41
|
+
the dynamically-loaded class would fail `instanceof` / DI lookups.
|
|
42
|
+
|
|
43
|
+
## Files
|
|
44
|
+
|
|
45
|
+
| File | Role |
|
|
46
|
+
| --- | --- |
|
|
47
|
+
| `concept.ts` | Node entry — runs the backend (`UIContainer`). |
|
|
48
|
+
| `containers/UI.container.ts` | Builds app + lazy bundles (code-split), serves static files, exposes `/api/components`. The `COMPONENTS` array is the single source of truth. |
|
|
49
|
+
| `public/index.html` | SPA shell with the outer `<are-root id="app">`. |
|
|
50
|
+
| `src/concept.ts` | Browser app bootstrap — fetches the manifest, wires the `AreContainer`. |
|
|
51
|
+
| `src/components/AppShell` · `NavBar` · `HomePage` | **Eager** UI (in the app bundle). |
|
|
52
|
+
| `src/components/LazyOutlet` | The dynamic slot that fetches + registers + renders lazy components. |
|
|
53
|
+
| `src/lazy/*` | **Lazy** components — each its own esbuild entry, served on demand. |
|
|
54
|
+
| `src/runtime/ComponentManifest.fragment.ts` | Holds the backend manifest + "already loaded" set. |
|
|
55
|
+
|
|
56
|
+
## What this surfaced about the engine
|
|
57
|
+
|
|
58
|
+
Building `LazyOutlet` is essentially a **fork of `AreRoot`** with an async
|
|
59
|
+
fetch+register step inserted. That duplication points at three primitives the
|
|
60
|
+
**engine** (adaas-are) should own so are-html/apps don't reinvent them:
|
|
61
|
+
|
|
62
|
+
- **(A) Runtime component-registration entry point.** We call
|
|
63
|
+
`node.scope.register(Class)` directly; there's no documented
|
|
64
|
+
`context.registerComponent(Class)` that targets the scope root nodes resolve
|
|
65
|
+
against.
|
|
66
|
+
- **(B) Async unresolved-component resolver hook.** Resolution is synchronous
|
|
67
|
+
today — a tag miss becomes a plain element forever. We had to intercept
|
|
68
|
+
*before* building. The load path should be able to consult a pluggable async
|
|
69
|
+
provider (`AreComponentResolver`) when `node.component` is undefined.
|
|
70
|
+
- **(C) One reusable build+mount subtree primitive.** The
|
|
71
|
+
`tokenize → init/load/transform/compile/mount` loop in `LazyOutlet.render()`
|
|
72
|
+
is copy-pasted from `AreRoot.onSignal` — exactly the duplication that has
|
|
73
|
+
caused mount-point regressions. It belongs in `node.render()` /
|
|
74
|
+
`engine.renderSubtree(node)`.
|
|
75
|
+
|
|
76
|
+
Dynamic `import()` itself (network + bundling) stays an **are-html/app** concern.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { A_Concept, A_Context } from "@adaas/a-concept";
|
|
2
|
+
import { UIContainer } from "./containers/UI.container";
|
|
3
|
+
import { A_Logger } from "@adaas/a-utils/a-logger";
|
|
4
|
+
import { A_Polyfill } from "@adaas/a-utils/a-polyfill";
|
|
5
|
+
import { A_Config, ENVConfigReader } from "@adaas/a-utils/a-config";
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Node-side entry point for the Lazy Loading example.
|
|
10
|
+
*
|
|
11
|
+
* The UIContainer below is a *backend*: it
|
|
12
|
+
* 1. builds the browser app bundle AND a separate JS bundle per "lazy"
|
|
13
|
+
* component (code-split so they share the framework runtime singletons),
|
|
14
|
+
* 2. serves the static SPA,
|
|
15
|
+
* 3. exposes a `/api/components` manifest endpoint so the browser app can
|
|
16
|
+
* discover, at runtime, which components exist and where to fetch them.
|
|
17
|
+
*
|
|
18
|
+
* The browser then loads each lazy component on demand (dynamic `import()`),
|
|
19
|
+
* registers the returned class into the live scope, and renders it into an
|
|
20
|
+
* already-mounted tree — no full reload, no pre-registration at bootstrap.
|
|
21
|
+
*/
|
|
22
|
+
(async () => {
|
|
23
|
+
try {
|
|
24
|
+
const Application = new UIContainer({
|
|
25
|
+
name: 'ARE Lazy Loading',
|
|
26
|
+
components: [
|
|
27
|
+
A_Polyfill,
|
|
28
|
+
ENVConfigReader,
|
|
29
|
+
A_Logger,
|
|
30
|
+
],
|
|
31
|
+
fragments: [
|
|
32
|
+
new A_Config({
|
|
33
|
+
defaults: {
|
|
34
|
+
PORT: 8083,
|
|
35
|
+
CONFIG_VERBOSE: true,
|
|
36
|
+
DEV_MODE: true,
|
|
37
|
+
}
|
|
38
|
+
}),
|
|
39
|
+
]
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const concept = new A_Concept({
|
|
43
|
+
name: 'adaas-are-example-lazy-loading',
|
|
44
|
+
components: [A_Logger, A_Polyfill, ENVConfigReader],
|
|
45
|
+
containers: [Application],
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
await concept.load();
|
|
49
|
+
await concept.start();
|
|
50
|
+
|
|
51
|
+
} catch (error) {
|
|
52
|
+
const logger = A_Context.root.resolve<A_Logger>(A_Logger)!;
|
|
53
|
+
logger.error(error);
|
|
54
|
+
}
|
|
55
|
+
})();
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { A_Concept, A_Inject } from "@adaas/a-concept";
|
|
2
|
+
import { A_Config } from "@adaas/a-utils/a-config";
|
|
3
|
+
import { A_Logger } from "@adaas/a-utils/a-logger";
|
|
4
|
+
import { A_Service } from "@adaas/a-utils/a-service";
|
|
5
|
+
import { build } from "esbuild";
|
|
6
|
+
import fs from "fs";
|
|
7
|
+
import http from "http";
|
|
8
|
+
import path from "path";
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Single source of truth for "which components exist".
|
|
13
|
+
*
|
|
14
|
+
* - `entry === null` → the component is part of the eager app bundle
|
|
15
|
+
* (registered at bootstrap). The home page is eager so something paints
|
|
16
|
+
* immediately.
|
|
17
|
+
* - `entry !== null` → the component is a *lazy* component. It is built into
|
|
18
|
+
* its own JS file and is NOT registered at bootstrap. The browser fetches it
|
|
19
|
+
* on demand the first time its route is visited.
|
|
20
|
+
*
|
|
21
|
+
* This array drives BOTH the esbuild entry points AND the `/api/components`
|
|
22
|
+
* manifest the browser app consumes — so adding a backend component is a
|
|
23
|
+
* one-line change here.
|
|
24
|
+
*/
|
|
25
|
+
type ComponentDescriptor = {
|
|
26
|
+
/** Route that activates this component. */
|
|
27
|
+
route: string;
|
|
28
|
+
/** Custom-element tag (kebab-case of the class name). */
|
|
29
|
+
tag: string;
|
|
30
|
+
/** Class name (PascalCase). */
|
|
31
|
+
name: string;
|
|
32
|
+
/** Source entry for lazy components, or null when bundled eagerly. */
|
|
33
|
+
entry: string | null;
|
|
34
|
+
/** Public URL the browser dynamically imports (lazy components only). */
|
|
35
|
+
url: string | null;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const COMPONENTS: ComponentDescriptor[] = [
|
|
39
|
+
{ route: '/', tag: 'home-page', name: 'HomePage', entry: null, url: null },
|
|
40
|
+
{ route: '/about', tag: 'about-page', name: 'AboutPage', entry: 'src/lazy/AboutPage.component.ts', url: '/lazy/about-page.js' },
|
|
41
|
+
{ route: '/settings', tag: 'settings-page', name: 'SettingsPage', entry: 'src/lazy/SettingsPage.component.ts', url: '/lazy/settings-page.js' },
|
|
42
|
+
{ route: '/reports', tag: 'reports-page', name: 'ReportsPage', entry: 'src/lazy/ReportsPage.component.ts', url: '/lazy/reports-page.js' },
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
export class UIContainer extends A_Service {
|
|
47
|
+
|
|
48
|
+
protected server!: http.Server;
|
|
49
|
+
|
|
50
|
+
@A_Concept.Build()
|
|
51
|
+
async build(
|
|
52
|
+
@A_Inject(A_Logger) logger: A_Logger,
|
|
53
|
+
): Promise<void> {
|
|
54
|
+
logger.log('Building Lazy Loading example...');
|
|
55
|
+
|
|
56
|
+
const distDir = path.resolve(__dirname, "../dist");
|
|
57
|
+
const appEntry = path.resolve(__dirname, "../src/concept.ts");
|
|
58
|
+
|
|
59
|
+
if (fs.existsSync(distDir)) {
|
|
60
|
+
fs.rmSync(distDir, { recursive: true, force: true });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Build the eager app + every lazy component in ONE esbuild pass with
|
|
64
|
+
// code-splitting enabled. This is the key to runtime lazy loading:
|
|
65
|
+
// esbuild hoists the shared framework code (@adaas/are, @adaas/a-concept,
|
|
66
|
+
// ...) into shared chunks that BOTH the app and each lazy bundle import
|
|
67
|
+
// by URL. When the browser later `import()`s a lazy bundle, it reuses the
|
|
68
|
+
// already-evaluated shared chunk module — so the lazy component's
|
|
69
|
+
// `class X extends Are` references the SAME `Are` (and the SAME DI
|
|
70
|
+
// context) the host app is running. Without shared singletons, the
|
|
71
|
+
// dynamically-loaded class would fail `instanceof` / DI lookups.
|
|
72
|
+
const entryPoints: Record<string, string> = {
|
|
73
|
+
app: appEntry,
|
|
74
|
+
};
|
|
75
|
+
for (const comp of COMPONENTS) {
|
|
76
|
+
if (comp.entry && comp.url) {
|
|
77
|
+
// 'lazy/about-page' → dist/lazy/about-page.js
|
|
78
|
+
const key = comp.url.replace(/^\//, '').replace(/\.js$/, '');
|
|
79
|
+
entryPoints[key] = path.resolve(__dirname, "..", comp.entry);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
await build({
|
|
84
|
+
entryPoints,
|
|
85
|
+
outdir: distDir,
|
|
86
|
+
bundle: true,
|
|
87
|
+
splitting: true,
|
|
88
|
+
format: "esm",
|
|
89
|
+
target: "es2020",
|
|
90
|
+
keepNames: true,
|
|
91
|
+
minify: false,
|
|
92
|
+
sourcemap: false,
|
|
93
|
+
entryNames: '[dir]/[name]',
|
|
94
|
+
chunkNames: 'chunks/[name]-[hash]',
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
logger.log('green', 'App + lazy component bundles built (code-split).');
|
|
98
|
+
|
|
99
|
+
// index.html → dist (no BUNDLE_ID needed; the app entry has a stable name)
|
|
100
|
+
const indexHtml = await fs.promises.readFile(
|
|
101
|
+
path.resolve(__dirname, "../public/index.html"), 'utf-8'
|
|
102
|
+
);
|
|
103
|
+
await fs.promises.writeFile(path.join(distDir, "index.html"), indexHtml);
|
|
104
|
+
|
|
105
|
+
// Copy any remaining static assets (excluding index.html, handled above)
|
|
106
|
+
const publicDir = path.resolve(__dirname, "../public");
|
|
107
|
+
const entries = await fs.promises.readdir(publicDir, { withFileTypes: true });
|
|
108
|
+
for (const entry of entries) {
|
|
109
|
+
if (entry.isFile() && entry.name !== 'index.html') {
|
|
110
|
+
await fs.promises.copyFile(
|
|
111
|
+
path.join(publicDir, entry.name),
|
|
112
|
+
path.join(distDir, entry.name),
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
logger.log('green', 'Static assets copied.');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
@A_Concept.Load()
|
|
121
|
+
async preLoadBuild(
|
|
122
|
+
@A_Inject(A_Logger) logger: A_Logger,
|
|
123
|
+
) {
|
|
124
|
+
await this.build(logger);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
@A_Concept.Start()
|
|
128
|
+
async startStaticServer(
|
|
129
|
+
@A_Inject(A_Logger) logger: A_Logger,
|
|
130
|
+
@A_Inject(A_Config) config: A_Config
|
|
131
|
+
) {
|
|
132
|
+
this.server = http.createServer(this.handleRequest.bind(this));
|
|
133
|
+
const PORT = config.get('PORT') || 8083;
|
|
134
|
+
this.server.listen(PORT, () => {
|
|
135
|
+
logger.log('green', `Lazy Loading example running at http://localhost:${PORT}`);
|
|
136
|
+
logger.log('blue', `Component manifest at http://localhost:${PORT}/api/components`);
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Routes requests:
|
|
142
|
+
* - GET /api/components → JSON manifest the browser app discovers at runtime
|
|
143
|
+
* - everything else → static files (with SPA fallback to index.html)
|
|
144
|
+
*/
|
|
145
|
+
protected handleRequest(
|
|
146
|
+
req: http.IncomingMessage,
|
|
147
|
+
res: http.ServerResponse,
|
|
148
|
+
) {
|
|
149
|
+
const url = req.url || '/';
|
|
150
|
+
|
|
151
|
+
if (url === '/api/components') {
|
|
152
|
+
const manifest = COMPONENTS.map(({ route, tag, name, url }) => ({
|
|
153
|
+
route, tag, name, url,
|
|
154
|
+
lazy: url !== null,
|
|
155
|
+
}));
|
|
156
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
157
|
+
res.end(JSON.stringify(manifest));
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
this.serveStaticFile(url, res);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
protected serveStaticFile(
|
|
165
|
+
url: string,
|
|
166
|
+
res: http.ServerResponse,
|
|
167
|
+
) {
|
|
168
|
+
const distDir = path.resolve(__dirname, "../dist");
|
|
169
|
+
let filePath = path.join(distDir, url === '/' ? 'index.html' : url);
|
|
170
|
+
|
|
171
|
+
const logger = this.scope.resolve<A_Logger>(A_Logger)!;
|
|
172
|
+
|
|
173
|
+
const mimeTypes: Record<string, string> = {
|
|
174
|
+
'.html': 'text/html',
|
|
175
|
+
'.js': 'text/javascript',
|
|
176
|
+
'.css': 'text/css',
|
|
177
|
+
'.json': 'application/json',
|
|
178
|
+
'.png': 'image/png',
|
|
179
|
+
'.svg': 'image/svg+xml',
|
|
180
|
+
};
|
|
181
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
182
|
+
|
|
183
|
+
if (!fs.existsSync(filePath)) {
|
|
184
|
+
// SPA fallback — serve index.html for unknown *non-asset* paths so
|
|
185
|
+
// deep links (e.g. /about) still boot the app. Asset misses (.js)
|
|
186
|
+
// should 404 honestly so loading bugs are visible.
|
|
187
|
+
if (ext && ext !== '.html') {
|
|
188
|
+
logger.log('red', `404: ${filePath}`);
|
|
189
|
+
res.writeHead(404);
|
|
190
|
+
res.end(`Not Found: ${url}`);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
filePath = path.join(distDir, 'index.html');
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Resolve the content type from the FILE WE WILL ACTUALLY SERVE, not the
|
|
197
|
+
// requested URL. A deep link like /about is extensionless → without this
|
|
198
|
+
// the fallback index.html would be sent as application/octet-stream and
|
|
199
|
+
// the browser would download it instead of rendering the SPA.
|
|
200
|
+
const servedExt = path.extname(filePath).toLowerCase();
|
|
201
|
+
const contentType = mimeTypes[servedExt] || 'application/octet-stream';
|
|
202
|
+
|
|
203
|
+
logger.log('blue', `Serving: ${filePath}`);
|
|
204
|
+
|
|
205
|
+
fs.readFile(filePath, (err, content) => {
|
|
206
|
+
if (err) {
|
|
207
|
+
res.writeHead(500);
|
|
208
|
+
res.end(`Server Error: ${err.code}`);
|
|
209
|
+
} else {
|
|
210
|
+
res.writeHead(200, { 'Content-Type': contentType });
|
|
211
|
+
res.end(content);
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
}
|