@nifrajs/web 0.1.0-alpha.1
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/LICENSE +21 -0
- package/README.md +18 -0
- package/css-modules.d.ts +34 -0
- package/dist/build.d.ts +110 -0
- package/dist/build.d.ts.map +1 -0
- package/dist/build.js +238 -0
- package/dist/build.js.map +1 -0
- package/dist/client.d.ts +32 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +204 -0
- package/dist/client.js.map +1 -0
- package/dist/deferred.d.ts +90 -0
- package/dist/deferred.d.ts.map +1 -0
- package/dist/deferred.js +310 -0
- package/dist/deferred.js.map +1 -0
- package/dist/dev-error.d.ts +17 -0
- package/dist/dev-error.d.ts.map +1 -0
- package/dist/dev-error.js +59 -0
- package/dist/dev-error.js.map +1 -0
- package/dist/dev.d.ts +25 -0
- package/dist/dev.d.ts.map +1 -0
- package/dist/dev.js +83 -0
- package/dist/dev.js.map +1 -0
- package/dist/draft.d.ts +58 -0
- package/dist/draft.d.ts.map +1 -0
- package/dist/draft.js +55 -0
- package/dist/draft.js.map +1 -0
- package/dist/fonts-google.d.ts +109 -0
- package/dist/fonts-google.d.ts.map +1 -0
- package/dist/fonts-google.js +291 -0
- package/dist/fonts-google.js.map +1 -0
- package/dist/fonts.d.ts +67 -0
- package/dist/fonts.d.ts.map +1 -0
- package/dist/fonts.js +103 -0
- package/dist/fonts.js.map +1 -0
- package/dist/fs.d.ts +10 -0
- package/dist/fs.d.ts.map +1 -0
- package/dist/fs.js +24 -0
- package/dist/fs.js.map +1 -0
- package/dist/index.d.ts +280 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +784 -0
- package/dist/index.js.map +1 -0
- package/dist/islands.d.ts +37 -0
- package/dist/islands.d.ts.map +1 -0
- package/dist/islands.js +107 -0
- package/dist/islands.js.map +1 -0
- package/dist/isr.d.ts +154 -0
- package/dist/isr.d.ts.map +1 -0
- package/dist/isr.js +331 -0
- package/dist/isr.js.map +1 -0
- package/dist/manifest.d.ts +178 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +213 -0
- package/dist/manifest.js.map +1 -0
- package/dist/plugins/css-modules.d.ts +40 -0
- package/dist/plugins/css-modules.d.ts.map +1 -0
- package/dist/plugins/css-modules.js +0 -0
- package/dist/plugins/css-modules.js.map +1 -0
- package/dist/plugins/kit.d.ts +44 -0
- package/dist/plugins/kit.d.ts.map +1 -0
- package/dist/plugins/kit.js +105 -0
- package/dist/plugins/kit.js.map +1 -0
- package/dist/plugins/scss.d.ts +27 -0
- package/dist/plugins/scss.d.ts.map +1 -0
- package/dist/plugins/scss.js +78 -0
- package/dist/plugins/scss.js.map +1 -0
- package/dist/prerender.d.ts +77 -0
- package/dist/prerender.d.ts.map +1 -0
- package/dist/prerender.js +139 -0
- package/dist/prerender.js.map +1 -0
- package/dist/query.d.ts +62 -0
- package/dist/query.d.ts.map +1 -0
- package/dist/query.js +184 -0
- package/dist/query.js.map +1 -0
- package/dist/router.d.ts +169 -0
- package/dist/router.d.ts.map +1 -0
- package/dist/router.js +531 -0
- package/dist/router.js.map +1 -0
- package/dist/vite.d.ts +49 -0
- package/dist/vite.d.ts.map +1 -0
- package/dist/vite.js +144 -0
- package/dist/vite.js.map +1 -0
- package/package.json +127 -0
- package/src/build.ts +375 -0
- package/src/client.ts +218 -0
- package/src/deferred.ts +339 -0
- package/src/dev-error.ts +62 -0
- package/src/dev.ts +108 -0
- package/src/draft.ts +82 -0
- package/src/fonts-google.ts +397 -0
- package/src/fonts.ts +147 -0
- package/src/fs.ts +26 -0
- package/src/index.ts +1202 -0
- package/src/islands.ts +121 -0
- package/src/isr.ts +461 -0
- package/src/manifest.ts +366 -0
- package/src/plugins/css-modules.ts +0 -0
- package/src/plugins/kit.ts +123 -0
- package/src/plugins/scss.ts +108 -0
- package/src/prerender.ts +209 -0
- package/src/query.ts +250 -0
- package/src/router.ts +676 -0
- package/src/vite.ts +212 -0
package/dist/vite.js
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@nifrajs/web/vite` — a **dev-only** server with true HMR, backed by Vite in middleware mode. nifra's
|
|
3
|
+
* production pipeline stays Bun-native (`@nifrajs/web/build` → `Bun.build`); this is purely for the dev
|
|
4
|
+
* loop. Bun's own HMR is dev-server-only and DCE's `import.meta.hot` under `Bun.build` (nifra's bundler),
|
|
5
|
+
* so HMR comes from Vite + the framework's official Vite plugin (React Fast Refresh, Vue/Svelte/Solid
|
|
6
|
+
* HMR) — which you inject via `plugins` (the same structural-injection idiom as the Bun/codec plugins).
|
|
7
|
+
*
|
|
8
|
+
* Flow: Vite serves + HMR-swaps the client modules (nifra's codegen'd entry + the route files); nifra
|
|
9
|
+
* still **SSRs** each request, and the rendered HTML is run through `vite.transformIndexHtml` so Vite
|
|
10
|
+
* injects its HMR client + the framework's refresh preamble. Node `http` (not `Bun.serve`) because
|
|
11
|
+
* Vite's `middlewares` are Connect-style — it runs fine under Bun.
|
|
12
|
+
*/
|
|
13
|
+
import { writeFileSync } from "node:fs";
|
|
14
|
+
import { createServer as createHttpServer, } from "node:http";
|
|
15
|
+
import { relative, resolve as resolvePath } from "node:path";
|
|
16
|
+
import { renderDevErrorOverlay } from "./dev-error.js";
|
|
17
|
+
import { discoverRoutes } from "./fs.js";
|
|
18
|
+
import { generateClientEntry } from "./index.js";
|
|
19
|
+
// The codegen'd client entry is written here (at the Vite root) so Vite serves + HMRs it.
|
|
20
|
+
const DEV_ENTRY = ".nifra-vite-entry.tsx";
|
|
21
|
+
const readNodeBody = async (req) => {
|
|
22
|
+
if (req.method === "GET" || req.method === "HEAD")
|
|
23
|
+
return undefined;
|
|
24
|
+
const chunks = [];
|
|
25
|
+
for await (const chunk of req)
|
|
26
|
+
chunks.push(chunk);
|
|
27
|
+
return Buffer.concat(chunks);
|
|
28
|
+
};
|
|
29
|
+
/** Build a Web `Request` from a Node `IncomingMessage` (+ already-read body) for nifra's `app.fetch`. */
|
|
30
|
+
function toWebRequest(req, body) {
|
|
31
|
+
const headers = new Headers();
|
|
32
|
+
for (const [key, value] of Object.entries(req.headers)) {
|
|
33
|
+
if (typeof value === "string")
|
|
34
|
+
headers.set(key, value);
|
|
35
|
+
else if (Array.isArray(value))
|
|
36
|
+
for (const v of value)
|
|
37
|
+
headers.append(key, v);
|
|
38
|
+
}
|
|
39
|
+
const url = `http://${req.headers.host ?? "localhost"}${req.url ?? "/"}`;
|
|
40
|
+
// Assemble init via an `unknown`-typed `body` slot: a Buffer is a valid request body at runtime
|
|
41
|
+
// (Bun/undici), but the named `BodyInit` type only exists under the DOM lib (the root program is
|
|
42
|
+
// DOM-free). `ConstructorParameters<typeof Request>[1]` derives the init type from whichever lib is
|
|
43
|
+
// active, so this type-checks in both the bun and DOM tsconfig programs.
|
|
44
|
+
const init = {
|
|
45
|
+
method: req.method ?? "GET",
|
|
46
|
+
headers,
|
|
47
|
+
};
|
|
48
|
+
if (body !== undefined)
|
|
49
|
+
init.body = body;
|
|
50
|
+
return new Request(url, init);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Start the Vite-backed dev server: Vite serves/HMRs the client; nifra SSRs each request and Vite
|
|
54
|
+
* injects its HMR client + the framework refresh preamble via `transformIndexHtml`.
|
|
55
|
+
*/
|
|
56
|
+
export async function createViteDevServer(options) {
|
|
57
|
+
const root = resolvePath(options.root ?? process.cwd());
|
|
58
|
+
const routesDir = resolvePath(options.routesDir);
|
|
59
|
+
const port = options.port ?? 3000;
|
|
60
|
+
// Codegen the client entry with Vite-servable, root-relative specifiers (e.g. `/routes/index.tsx`).
|
|
61
|
+
const manifest = discoverRoutes(routesDir);
|
|
62
|
+
const toUrl = (file) => `/${relative(root, `${routesDir}/${file}`)}`;
|
|
63
|
+
writeFileSync(resolvePath(root, DEV_ENTRY), generateClientEntry(manifest, { clientModule: options.clientModule, resolve: toUrl }));
|
|
64
|
+
const entryUrl = `/${DEV_ENTRY}`;
|
|
65
|
+
// Create our HTTP server FIRST, then hand it to Vite as the HMR WebSocket host (`hmr.server`). In
|
|
66
|
+
// middleware mode Vite would otherwise open a *separate* ws port (24678) — fragile: it conflicts
|
|
67
|
+
// across restarts ("Port undefined is already in use" → the client never connects). Sharing one
|
|
68
|
+
// port means HMR rides the same origin as the app, robust across restarts. The handler closes over
|
|
69
|
+
// `vite`, which is assigned just below before the server starts listening.
|
|
70
|
+
let vite;
|
|
71
|
+
let version = 0;
|
|
72
|
+
let app;
|
|
73
|
+
const server = createHttpServer((req, res) => {
|
|
74
|
+
vite.middlewares(req, res, () => {
|
|
75
|
+
// Not a Vite asset → nifra SSR. (`next` runs after Vite declines, so the body is still readable.)
|
|
76
|
+
void (async () => {
|
|
77
|
+
try {
|
|
78
|
+
const body = await readNodeBody(req);
|
|
79
|
+
const nifraRes = await app.fetch(toWebRequest(req, body));
|
|
80
|
+
const contentType = nifraRes.headers.get("content-type") ?? "";
|
|
81
|
+
res.statusCode = nifraRes.status;
|
|
82
|
+
if (!contentType.includes("text/html")) {
|
|
83
|
+
// Data / redirect / asset response — pass through untouched.
|
|
84
|
+
for (const [key, value] of nifraRes.headers)
|
|
85
|
+
res.setHeader(key, value);
|
|
86
|
+
res.end(Buffer.from(await nifraRes.arrayBuffer()));
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
// Inject Vite's HMR client + the framework's refresh preamble into the SSR'd HTML.
|
|
90
|
+
const html = await vite.transformIndexHtml(req.url ?? "/", await nifraRes.text());
|
|
91
|
+
res.setHeader("content-type", "text/html; charset=utf-8");
|
|
92
|
+
res.end(html);
|
|
93
|
+
}
|
|
94
|
+
catch (err) {
|
|
95
|
+
// Source-map the stack first (Vite maps the bundled frames back to your `.ts`), then render
|
|
96
|
+
// the readable dev overlay instead of a bare text dump. Dev-only — production maps to `_error`.
|
|
97
|
+
if (err instanceof Error)
|
|
98
|
+
vite.ssrFixStacktrace(err);
|
|
99
|
+
res.statusCode = 500;
|
|
100
|
+
res.setHeader("content-type", "text/html; charset=utf-8");
|
|
101
|
+
res.end(renderDevErrorOverlay(err, { method: req.method ?? "GET", url: req.url ?? "/" }));
|
|
102
|
+
}
|
|
103
|
+
})();
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
// `conditions: ["bun"]` makes Vite resolve nifra's workspace packages (`@nifrajs/web-react/client`, …) to
|
|
107
|
+
// their TS **source** — so the dev server needs no prior `dist` build of the adapter packages.
|
|
108
|
+
const usePolling = options.poll ?? process.env.CHOKIDAR_USEPOLLING === "1";
|
|
109
|
+
const { createServer } = (await import("vite"));
|
|
110
|
+
vite = await createServer({
|
|
111
|
+
root,
|
|
112
|
+
appType: "custom",
|
|
113
|
+
server: {
|
|
114
|
+
middlewareMode: true,
|
|
115
|
+
hmr: { server },
|
|
116
|
+
// Explicit watch config; poll when native fs events aren't delivered (containers/sandboxes).
|
|
117
|
+
watch: usePolling ? { usePolling: true, interval: 80 } : {},
|
|
118
|
+
},
|
|
119
|
+
plugins: [...(options.plugins ?? [])],
|
|
120
|
+
resolve: {
|
|
121
|
+
conditions: [...(options.conditions ?? []), "bun", "module", "browser", "development"],
|
|
122
|
+
},
|
|
123
|
+
...(options.define ? { define: options.define } : {}),
|
|
124
|
+
});
|
|
125
|
+
app = await options.createApp(entryUrl, `v=${version}`);
|
|
126
|
+
// Keep server-render fresh on edits (Vite already HMRs the client live; this is for a hard reload).
|
|
127
|
+
vite.watcher.on("change", () => {
|
|
128
|
+
version += 1;
|
|
129
|
+
Promise.resolve(options.createApp(entryUrl, `v=${version}`))
|
|
130
|
+
.then((next) => {
|
|
131
|
+
app = next;
|
|
132
|
+
})
|
|
133
|
+
.catch((err) => console.error("[nifra/web/vite] app re-create failed:", err));
|
|
134
|
+
});
|
|
135
|
+
await new Promise((done) => server.listen(port, done));
|
|
136
|
+
return {
|
|
137
|
+
port,
|
|
138
|
+
stop: async () => {
|
|
139
|
+
server.close();
|
|
140
|
+
await vite.close();
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
//# sourceMappingURL=vite.js.map
|
package/dist/vite.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vite.js","sourceRoot":"","sources":["../src/vite.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nifrajs/web",
|
|
3
|
+
"version": "0.1.0-alpha.1",
|
|
4
|
+
"description": "Framework-agnostic SSR core for nifra — the render seam + HTML document orchestration.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"bun",
|
|
7
|
+
"typescript",
|
|
8
|
+
"nifra",
|
|
9
|
+
"web-framework",
|
|
10
|
+
"full-stack",
|
|
11
|
+
"edge",
|
|
12
|
+
"ssr",
|
|
13
|
+
"react",
|
|
14
|
+
"vue",
|
|
15
|
+
"svelte",
|
|
16
|
+
"solid",
|
|
17
|
+
"preact",
|
|
18
|
+
"file-routing",
|
|
19
|
+
"islands",
|
|
20
|
+
"streaming-ssr"
|
|
21
|
+
],
|
|
22
|
+
"type": "module",
|
|
23
|
+
"main": "./dist/index.js",
|
|
24
|
+
"module": "./dist/index.js",
|
|
25
|
+
"types": "./dist/index.d.ts",
|
|
26
|
+
"exports": {
|
|
27
|
+
".": {
|
|
28
|
+
"bun": "./src/index.ts",
|
|
29
|
+
"types": "./dist/index.d.ts",
|
|
30
|
+
"default": "./dist/index.js"
|
|
31
|
+
},
|
|
32
|
+
"./fs": {
|
|
33
|
+
"bun": "./src/fs.ts",
|
|
34
|
+
"types": "./dist/fs.d.ts",
|
|
35
|
+
"default": "./dist/fs.js"
|
|
36
|
+
},
|
|
37
|
+
"./client": {
|
|
38
|
+
"bun": "./src/client.ts",
|
|
39
|
+
"types": "./dist/client.d.ts",
|
|
40
|
+
"default": "./dist/client.js"
|
|
41
|
+
},
|
|
42
|
+
"./islands": {
|
|
43
|
+
"bun": "./src/islands.ts",
|
|
44
|
+
"types": "./dist/islands.d.ts",
|
|
45
|
+
"default": "./dist/islands.js"
|
|
46
|
+
},
|
|
47
|
+
"./build": {
|
|
48
|
+
"bun": "./src/build.ts",
|
|
49
|
+
"types": "./dist/build.d.ts",
|
|
50
|
+
"default": "./dist/build.js"
|
|
51
|
+
},
|
|
52
|
+
"./fonts": {
|
|
53
|
+
"bun": "./src/fonts-google.ts",
|
|
54
|
+
"types": "./dist/fonts-google.d.ts",
|
|
55
|
+
"default": "./dist/fonts-google.js"
|
|
56
|
+
},
|
|
57
|
+
"./dev": {
|
|
58
|
+
"bun": "./src/dev.ts",
|
|
59
|
+
"types": "./dist/dev.d.ts",
|
|
60
|
+
"default": "./dist/dev.js"
|
|
61
|
+
},
|
|
62
|
+
"./vite": {
|
|
63
|
+
"bun": "./src/vite.ts",
|
|
64
|
+
"types": "./dist/vite.d.ts",
|
|
65
|
+
"default": "./dist/vite.js"
|
|
66
|
+
},
|
|
67
|
+
"./plugins/kit": {
|
|
68
|
+
"bun": "./src/plugins/kit.ts",
|
|
69
|
+
"types": "./dist/plugins/kit.d.ts",
|
|
70
|
+
"default": "./dist/plugins/kit.js"
|
|
71
|
+
},
|
|
72
|
+
"./plugins/css-modules": {
|
|
73
|
+
"bun": "./src/plugins/css-modules.ts",
|
|
74
|
+
"types": "./dist/plugins/css-modules.d.ts",
|
|
75
|
+
"default": "./dist/plugins/css-modules.js"
|
|
76
|
+
},
|
|
77
|
+
"./plugins/scss": {
|
|
78
|
+
"bun": "./src/plugins/scss.ts",
|
|
79
|
+
"types": "./dist/plugins/scss.d.ts",
|
|
80
|
+
"default": "./dist/plugins/scss.js"
|
|
81
|
+
},
|
|
82
|
+
"./css-modules": {
|
|
83
|
+
"types": "./css-modules.d.ts"
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
"files": [
|
|
87
|
+
"dist",
|
|
88
|
+
"src",
|
|
89
|
+
"css-modules.d.ts"
|
|
90
|
+
],
|
|
91
|
+
"sideEffects": false,
|
|
92
|
+
"scripts": {
|
|
93
|
+
"build": "tsc -p tsconfig.build.json && bun run ../../scripts/fix-dts.ts dist"
|
|
94
|
+
},
|
|
95
|
+
"license": "MIT",
|
|
96
|
+
"publishConfig": {
|
|
97
|
+
"access": "public"
|
|
98
|
+
},
|
|
99
|
+
"dependencies": {
|
|
100
|
+
"@nifrajs/core": "workspace:*"
|
|
101
|
+
},
|
|
102
|
+
"peerDependencies": {
|
|
103
|
+
"sass": "^1.0.0",
|
|
104
|
+
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0"
|
|
105
|
+
},
|
|
106
|
+
"peerDependenciesMeta": {
|
|
107
|
+
"sass": {
|
|
108
|
+
"optional": true
|
|
109
|
+
},
|
|
110
|
+
"vite": {
|
|
111
|
+
"optional": true
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
"devDependencies": {
|
|
115
|
+
"sass": "^1.77.0",
|
|
116
|
+
"vite": "^8.0.0"
|
|
117
|
+
},
|
|
118
|
+
"repository": {
|
|
119
|
+
"type": "git",
|
|
120
|
+
"url": "git+https://github.com/nifrajs/nifra.git",
|
|
121
|
+
"directory": "packages/web"
|
|
122
|
+
},
|
|
123
|
+
"bugs": {
|
|
124
|
+
"url": "https://github.com/nifrajs/nifra/issues"
|
|
125
|
+
},
|
|
126
|
+
"homepage": "https://github.com/nifrajs/nifra#readme"
|
|
127
|
+
}
|
package/src/build.ts
ADDED
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@nifrajs/web/build` — the production build (Bun-only, build-time). `buildClient` codegens + bundles
|
|
3
|
+
* the client entry (content-hashed, code-split); `buildServer` codegens the static-import server
|
|
4
|
+
* manifest + bundles a self-contained **worker** for the disk-less edge (Cloudflare Workers). Both
|
|
5
|
+
* are Bun-specific and never on the request path (own subpath, like `@nifrajs/web/fs`); the *output*
|
|
6
|
+
* runs on any runtime.
|
|
7
|
+
*/
|
|
8
|
+
import { mkdirSync, writeFileSync } from "node:fs"
|
|
9
|
+
import { dirname, relative, resolve as resolvePath } from "node:path"
|
|
10
|
+
import type { BunPlugin } from "bun"
|
|
11
|
+
import { discoverRoutes } from "./fs.ts"
|
|
12
|
+
import { generateClientEntry, generateServerManifest } from "./index.ts"
|
|
13
|
+
|
|
14
|
+
// Build-time SSG: prerender opted-in static + dynamic routes to `index.html` (+ static `_data.json`),
|
|
15
|
+
// run after `buildClient`.
|
|
16
|
+
export {
|
|
17
|
+
type CloudflarePagesRoutes,
|
|
18
|
+
type CloudflarePagesRoutesOptions,
|
|
19
|
+
cloudflarePagesRoutes,
|
|
20
|
+
dataFileFor,
|
|
21
|
+
htmlFileFor,
|
|
22
|
+
type PrerenderApp,
|
|
23
|
+
type PrerenderEntry,
|
|
24
|
+
type PrerenderOptions,
|
|
25
|
+
type PrerenderResult,
|
|
26
|
+
prerenderRoutes,
|
|
27
|
+
} from "./prerender.ts"
|
|
28
|
+
|
|
29
|
+
export interface BuildClientOptions {
|
|
30
|
+
/** The `routes/` directory to discover (absolute path). */
|
|
31
|
+
readonly routesDir: string
|
|
32
|
+
/** Output directory for the bundle + `manifest.json` (absolute path). */
|
|
33
|
+
readonly outDir: string
|
|
34
|
+
/** The adapter's client runtime (exports `mountRouter`), e.g. `"@nifrajs/web-solid/client"`. */
|
|
35
|
+
readonly clientModule: string
|
|
36
|
+
/** Route/layout file → import specifier (default: `${routesDir}/${file}`). */
|
|
37
|
+
readonly resolve?: (file: string) => string
|
|
38
|
+
/** Adapter build plugins (e.g. `solidBunPlugin("dom")`). */
|
|
39
|
+
readonly plugins?: readonly BunPlugin[]
|
|
40
|
+
/** `Bun.build` export conditions (e.g. `["bun", "solid", "browser"]`). */
|
|
41
|
+
readonly conditions?: readonly string[]
|
|
42
|
+
/** Compile-time replacements (e.g. `{ "process.env.NODE_ENV": '"production"' }`). */
|
|
43
|
+
readonly define?: Readonly<Record<string, string>>
|
|
44
|
+
/** Minify the output (default `true`). */
|
|
45
|
+
readonly minify?: boolean
|
|
46
|
+
/** URL prefix the assets are served under (default `"/assets/"`); also Bun's chunk `publicPath`. */
|
|
47
|
+
readonly publicPath?: string
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** The built asset map — the server reads `entry` for the client script + serves `assets`. */
|
|
51
|
+
export interface BuildManifest {
|
|
52
|
+
/** URL of the client entry module (content-hashed). */
|
|
53
|
+
readonly entry: string
|
|
54
|
+
/** URLs of every emitted asset (entry + chunks) — for serving + preloading. */
|
|
55
|
+
readonly assets: readonly string[]
|
|
56
|
+
/** `routeId → [layout chunk URLs…, own chunk URL]` — the chunks a route needs, for `createWebApp`'s
|
|
57
|
+
* `routePreload` (`<link rel="modulepreload">` the matched route alongside the entry). Each route +
|
|
58
|
+
* layout is also a build entrypoint, so it gets a named chunk the bootstrap's lazy import dedupes to. */
|
|
59
|
+
readonly routes: Readonly<Record<string, readonly string[]>>
|
|
60
|
+
/** The app's bundled, content-hashed stylesheet(s) — the bootstrap's **aggregate** CSS (every
|
|
61
|
+
* `import './x.css'` reachable from the app). The complete stylesheet regardless of which file
|
|
62
|
+
* imported the CSS; the always-safe fallback `createWebApp` links when a route has no per-route entry
|
|
63
|
+
* in {@link routeStyles}. Omitted when the app imports no CSS. */
|
|
64
|
+
readonly css?: readonly string[]
|
|
65
|
+
/** `routeId → [chain CSS URLs]` — only the stylesheets the matched route's layout chain + own file
|
|
66
|
+
* actually use (Bun emits a per-entrypoint CSS bundle per route/layout, with shared-component CSS
|
|
67
|
+
* inlined into each consumer). `createWebApp` links these instead of the aggregate, so a page ships
|
|
68
|
+
* only its own CSS. A route is omitted (→ aggregate fallback) when its `[name]` collides with another
|
|
69
|
+
* route's basename (ambiguous CSS↔route) or the build emitted orphan shared-chunk CSS — correctness
|
|
70
|
+
* over minimality. Absent entirely when the app imports no CSS. */
|
|
71
|
+
readonly routeStyles?: Readonly<Record<string, readonly string[]>>
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** The slice of Bun's build metafile nifra reads: per JS output, its source `entryPoint` + the
|
|
75
|
+
* `cssBundle` Bun emitted for that entry. Used to map each route file → its stylesheet (collision-safe,
|
|
76
|
+
* keyed by source path). Not yet in `@types/bun`; shape per the Bun bundler docs. */
|
|
77
|
+
interface BunMetafile {
|
|
78
|
+
readonly outputs: Readonly<
|
|
79
|
+
Record<string, { readonly entryPoint?: string; readonly cssBundle?: string }>
|
|
80
|
+
>
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const basename = (path: string): string => path.slice(path.lastIndexOf("/") + 1)
|
|
84
|
+
/** `[name]` Bun derives for an entrypoint: basename without extension (`users/[id].tsx` → `[id]`). */
|
|
85
|
+
const entryName = (file: string): string => {
|
|
86
|
+
const base = basename(file)
|
|
87
|
+
const dot = base.lastIndexOf(".")
|
|
88
|
+
return dot === -1 ? base : base.slice(0, dot)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Build the client bundle for a file-routed app. Writes the hashed assets + `manifest.json` to
|
|
93
|
+
* `outDir` and returns the manifest. Throws (with the bundler logs) on build failure — never
|
|
94
|
+
* silently ships a broken bundle.
|
|
95
|
+
*/
|
|
96
|
+
export async function buildClient(options: BuildClientOptions): Promise<BuildManifest> {
|
|
97
|
+
const { routesDir, outDir, clientModule } = options
|
|
98
|
+
const resolve = options.resolve ?? ((file: string) => `${routesDir}/${file}`)
|
|
99
|
+
const publicPath = options.publicPath ?? "/assets/"
|
|
100
|
+
mkdirSync(outDir, { recursive: true })
|
|
101
|
+
|
|
102
|
+
const routeManifest = discoverRoutes(routesDir)
|
|
103
|
+
// The bootstrap's filename is `_velo`-namespaced (not `entry.ts`) so its `[name]` can't collide with
|
|
104
|
+
// a user route named `entry.tsx` when the CSS mapping below excludes the bootstrap's aggregate CSS.
|
|
105
|
+
const entryFile = `${outDir}/_nifra-entry.ts`
|
|
106
|
+
writeFileSync(entryFile, generateClientEntry(routeManifest, { clientModule, resolve }))
|
|
107
|
+
|
|
108
|
+
// Every unique route/layout/`_404` file (sorted, stable), as ADDITIONAL entrypoints — Bun emits a
|
|
109
|
+
// named chunk per file that the bootstrap's lazy `import()` dedupes to (verified), so the manifest
|
|
110
|
+
// can map each route to its chunk URLs for matched-route preload. `resolve(file)` is the same
|
|
111
|
+
// specifier the bootstrap imports, so the entrypoint + lazy import are the same module (dedup).
|
|
112
|
+
const routeFiles = [
|
|
113
|
+
...new Set([
|
|
114
|
+
...routeManifest.routes.map((r) => r.file),
|
|
115
|
+
...Object.values(routeManifest.layouts).map((l) => l.file),
|
|
116
|
+
...(routeManifest.notFound ? [routeManifest.notFound.file] : []),
|
|
117
|
+
]),
|
|
118
|
+
].sort()
|
|
119
|
+
|
|
120
|
+
// `metafile: true` asks Bun for the input/output graph — specifically `outputs[js].entryPoint`
|
|
121
|
+
// (the source file) + `outputs[js].cssBundle` (that entry's emitted stylesheet). It's the robust
|
|
122
|
+
// entry→CSS link for per-route splitting: keyed by the unique source path, so it survives
|
|
123
|
+
// same-basename collisions (`index.tsx` + `blog/index.tsx`) that a filename match can't. Not yet in
|
|
124
|
+
// `@types/bun`'s `BuildConfig`, so spread it in (spread props skip the excess-property check).
|
|
125
|
+
const buildExtras = { metafile: true }
|
|
126
|
+
const result = await Bun.build({
|
|
127
|
+
entrypoints: [entryFile, ...routeFiles.map(resolve)],
|
|
128
|
+
outdir: outDir,
|
|
129
|
+
target: "browser",
|
|
130
|
+
naming: "[name]-[hash].[ext]",
|
|
131
|
+
publicPath,
|
|
132
|
+
splitting: true, // one chunk per lazily-imported route; shared deps deduped into shared chunks
|
|
133
|
+
// `import "./x.css"` in a route/component → bundled, minified, content-hashed `.css` asset (Bun
|
|
134
|
+
// strips the import from the JS; CSS bundling is on by default since Bun 1.2). Mapped to routes
|
|
135
|
+
// below — both the aggregate and per-route — via the metafile, for `<link>` injection.
|
|
136
|
+
...buildExtras,
|
|
137
|
+
minify: options.minify ?? true,
|
|
138
|
+
plugins: [...(options.plugins ?? [])],
|
|
139
|
+
...(options.conditions ? { conditions: [...options.conditions] } : {}),
|
|
140
|
+
...(options.define ? { define: { ...options.define } } : {}),
|
|
141
|
+
})
|
|
142
|
+
if (!result.success) {
|
|
143
|
+
throw new Error(
|
|
144
|
+
`[nifra/web] client build failed:\n${result.logs.map((l) => String(l)).join("\n")}`,
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const toUrl = (path: string): string => `${publicPath}${basename(path)}`
|
|
149
|
+
// Entry-point outputs come back in entrypoint order: [bootstrap, ...routeFiles]. Map each route file
|
|
150
|
+
// to its chunk URL by that order (guarded against drift), then a route's chunks = its layout chain +
|
|
151
|
+
// own file.
|
|
152
|
+
const entryPoints = result.outputs.filter((o) => o.kind === "entry-point")
|
|
153
|
+
const bootstrap = entryPoints[0]
|
|
154
|
+
if (bootstrap === undefined) throw new Error("[nifra/web] build produced no entry-point output")
|
|
155
|
+
if (entryPoints.length !== routeFiles.length + 1) {
|
|
156
|
+
throw new Error(
|
|
157
|
+
`[nifra/web] expected ${routeFiles.length + 1} entry-point outputs (bootstrap + ${routeFiles.length} routes), got ${entryPoints.length}`,
|
|
158
|
+
)
|
|
159
|
+
}
|
|
160
|
+
const fileToChunk = new Map<string, string>()
|
|
161
|
+
routeFiles.forEach((file, i) => {
|
|
162
|
+
const out = entryPoints[i + 1] // in range — length checked above
|
|
163
|
+
if (out !== undefined) fileToChunk.set(file, toUrl(out.path))
|
|
164
|
+
})
|
|
165
|
+
const chunksFor = (chainFiles: readonly string[]): string[] =>
|
|
166
|
+
chainFiles.map((f) => fileToChunk.get(f)).filter((u): u is string => u !== undefined)
|
|
167
|
+
const routes: Record<string, string[]> = {}
|
|
168
|
+
for (const route of routeManifest.routes) {
|
|
169
|
+
routes[route.id] = chunksFor([
|
|
170
|
+
...route.layoutIds.map((id) => routeManifest.layouts[id]?.file ?? ""),
|
|
171
|
+
route.file,
|
|
172
|
+
])
|
|
173
|
+
}
|
|
174
|
+
if (routeManifest.notFound) routes._404 = chunksFor([routeManifest.notFound.file])
|
|
175
|
+
|
|
176
|
+
// CSS — aggregate: an `import "./x.css"` anywhere → a content-hashed `.css` asset (Bun strips the
|
|
177
|
+
// import from the JS). The bootstrap lazily imports every route, so its **aggregate** stylesheet is
|
|
178
|
+
// the whole app's CSS — the always-safe fallback `createWebApp` links when a route has no per-route
|
|
179
|
+
// entry below. Fallback to all CSS assets if Bun emitted no distinct aggregate.
|
|
180
|
+
const bootstrapName = entryName(entryFile) // `_nifra-entry`
|
|
181
|
+
const cssNameOf = (path: string): string => {
|
|
182
|
+
const base = basename(path)
|
|
183
|
+
return base.slice(0, base.lastIndexOf("-")) // strip `-${hash}.css`
|
|
184
|
+
}
|
|
185
|
+
const cssAssets = result.outputs.filter((o) => o.kind === "asset" && o.path.endsWith(".css"))
|
|
186
|
+
const aggregate = cssAssets
|
|
187
|
+
.filter((o) => cssNameOf(o.path) === bootstrapName)
|
|
188
|
+
.map((o) => toUrl(o.path))
|
|
189
|
+
const css: readonly string[] =
|
|
190
|
+
aggregate.length > 0 ? aggregate : cssAssets.map((o) => toUrl(o.path))
|
|
191
|
+
|
|
192
|
+
// CSS — per-route: each route/layout file is its own entrypoint, so the build metafile records its
|
|
193
|
+
// `cssBundle` — exactly the CSS that file's subtree uses (shared-component CSS is inlined into each
|
|
194
|
+
// consumer; verified). Keyed by the metafile's unique source `entryPoint`, so it survives
|
|
195
|
+
// same-basename collisions (`index.tsx` + `blog/index.tsx`) that a filename match can't. A page then
|
|
196
|
+
// links only its layout chain + own CSS (deduped); an empty array means the page needs no CSS at all.
|
|
197
|
+
// Absent (→ aggregate fallback) only if Bun emits no metafile/cssBundle — never silently incomplete.
|
|
198
|
+
const meta = (result as unknown as { metafile?: BunMetafile }).metafile
|
|
199
|
+
const cwd = process.cwd()
|
|
200
|
+
const cssByEntry = new Map<string, string>()
|
|
201
|
+
for (const out of Object.values(meta?.outputs ?? {})) {
|
|
202
|
+
if (out.entryPoint !== undefined && out.cssBundle !== undefined) {
|
|
203
|
+
cssByEntry.set(resolvePath(cwd, out.entryPoint), toUrl(out.cssBundle))
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
const stylesFor = (chainFiles: readonly string[]): readonly string[] => {
|
|
207
|
+
const urls = chainFiles
|
|
208
|
+
.map((f) => (f ? cssByEntry.get(resolvePath(resolve(f))) : undefined))
|
|
209
|
+
.filter((u): u is string => u !== undefined)
|
|
210
|
+
return [...new Set(urls)]
|
|
211
|
+
}
|
|
212
|
+
const routeStyles: Record<string, readonly string[]> = {}
|
|
213
|
+
if (css.length > 0 && cssByEntry.size > 0) {
|
|
214
|
+
for (const route of routeManifest.routes) {
|
|
215
|
+
routeStyles[route.id] = stylesFor([
|
|
216
|
+
...route.layoutIds.map((id) => routeManifest.layouts[id]?.file ?? ""),
|
|
217
|
+
route.file,
|
|
218
|
+
])
|
|
219
|
+
}
|
|
220
|
+
if (routeManifest.notFound) routeStyles._404 = stylesFor([routeManifest.notFound.file])
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const manifest: BuildManifest = {
|
|
224
|
+
entry: toUrl(bootstrap.path),
|
|
225
|
+
assets: result.outputs.map((o) => toUrl(o.path)),
|
|
226
|
+
routes,
|
|
227
|
+
...(css.length > 0 ? { css } : {}),
|
|
228
|
+
...(Object.keys(routeStyles).length > 0 ? { routeStyles } : {}),
|
|
229
|
+
}
|
|
230
|
+
writeFileSync(`${outDir}/manifest.json`, JSON.stringify(manifest, null, 2))
|
|
231
|
+
return manifest
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export interface BuildServerOptions {
|
|
235
|
+
/** The `routes/` directory to discover (absolute path). */
|
|
236
|
+
readonly routesDir: string
|
|
237
|
+
/** The worker entry module (absolute path) — your `worker.ts`. It imports `{ manifest, clientEntry }`
|
|
238
|
+
* from the generated `./server-manifest`, builds `createWebApp`, and `export default toFetchHandler(app)`. */
|
|
239
|
+
readonly serverEntry: string
|
|
240
|
+
/** Output directory for the bundled worker (absolute path). */
|
|
241
|
+
readonly outDir: string
|
|
242
|
+
/** The content-hashed client entry URL (from `buildClient`'s manifest) — **baked** into the generated
|
|
243
|
+
* server manifest, since a disk-less worker can't read `manifest.json` at runtime. */
|
|
244
|
+
readonly clientEntry: string
|
|
245
|
+
/** Route/layout file → import specifier in the generated manifest (default: a relative path from the
|
|
246
|
+
* manifest's location — written next to `serverEntry` — to `routesDir`). */
|
|
247
|
+
readonly resolve?: (file: string) => string
|
|
248
|
+
/** Filename for the generated server-manifest module, written next to `serverEntry` (default
|
|
249
|
+
* `"server-manifest.ts"`); your `serverEntry` imports it as `./server-manifest`. */
|
|
250
|
+
readonly manifestFile?: string
|
|
251
|
+
/** Adapter build plugins (e.g. `solidBunPlugin("ssr")` — Solid routes need their SSR transform at
|
|
252
|
+
* build time; React's JSX is Bun-native and needs none). */
|
|
253
|
+
readonly plugins?: readonly BunPlugin[]
|
|
254
|
+
/** `Bun.build` resolution conditions (default `["workerd", "edge-light", "browser"]`) — selects each
|
|
255
|
+
* dependency's edge build. */
|
|
256
|
+
readonly conditions?: readonly string[]
|
|
257
|
+
/** Compile-time replacements (default `{ "process.env.NODE_ENV": '"production"' }` → production
|
|
258
|
+
* React/Solid on the edge). Pass an explicit object to override (e.g. `{}` to opt out). */
|
|
259
|
+
readonly define?: Readonly<Record<string, string>>
|
|
260
|
+
/** Minify the output (default `true`). */
|
|
261
|
+
readonly minify?: boolean
|
|
262
|
+
/** `Bun.build` target (default `"browser"` — the right shape for edge runtimes: Cloudflare Workers,
|
|
263
|
+
* Vercel Edge, Deno, Deno Deploy). Use `"node"` for a `@nifrajs/node` server (so `node:*` built-ins
|
|
264
|
+
* stay external), or `"bun"` for a Bun server. The default `conditions` + the edge resolve shims
|
|
265
|
+
* only apply to the `"browser"` target; `"node"`/`"bun"` resolve their own renderer builds via the
|
|
266
|
+
* matching condition. */
|
|
267
|
+
readonly target?: "browser" | "node" | "bun"
|
|
268
|
+
/** **Lazy/code-split routes** (default `false`): emit `() => import(route)` loaders + bundle with
|
|
269
|
+
* `splitting`, so each route is its own chunk loaded on first request (smaller cold-start parse)
|
|
270
|
+
* instead of all parsed at boot. The output becomes the worker entry **+ chunk files** in `outDir`
|
|
271
|
+
* — on Cloudflare, ship them with wrangler's `no_bundle` + `find_additional_modules` + an ESModule
|
|
272
|
+
* `rule` (Node/Deno import the chunks natively). Eager (one self-contained file) stays the default. */
|
|
273
|
+
readonly lazy?: boolean
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** The built worker bundle — point your `wrangler.toml`'s `main` at `worker`. */
|
|
277
|
+
export interface ServerBuild {
|
|
278
|
+
/** Path to the bundled, self-contained worker entry. */
|
|
279
|
+
readonly worker: string
|
|
280
|
+
/** Paths of every emitted output (entry + any code-split chunks) — what to ship to the platform. */
|
|
281
|
+
readonly outputs: readonly string[]
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* react-dom's `exports["./server"]` maps the `bun` condition to a Bun-API server build that crashes
|
|
286
|
+
* on workerd, and `Bun.build` always applies the `bun` condition (it wins over `workerd`/`edge-light`),
|
|
287
|
+
* so conditions alone can't select the edge build. This shim pins `react-dom/server` to its edge build
|
|
288
|
+
* (`server.edge.js`, which exports `renderToReadableStream`). A no-op when nothing imports react-dom
|
|
289
|
+
* (e.g. a Solid worker) — the resolver only runs on a match.
|
|
290
|
+
*/
|
|
291
|
+
const reactDomEdgePlugin = (from: string): BunPlugin => ({
|
|
292
|
+
name: "nifra-react-dom-edge",
|
|
293
|
+
setup(build) {
|
|
294
|
+
build.onResolve({ filter: /^react-dom\/server$/ }, () => ({
|
|
295
|
+
path: Bun.resolveSync("react-dom/server.edge", from),
|
|
296
|
+
}))
|
|
297
|
+
},
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* `solid-js/web` selects its **server** runtime (`renderToStream`) via the `worker` condition, but
|
|
302
|
+
* `Bun.build` 1.3.14 **segfaults** when the `worker` condition is active (https://bun.report). And
|
|
303
|
+
* without `worker`, `browser` (which precedes the other server conditions in solid's exports map)
|
|
304
|
+
* wins → the *dom* runtime, which can't SSR. So this shim pins `solid-js/web` straight to its server
|
|
305
|
+
* build, sidestepping the crashing condition. Lazy (resolved on match) + a no-op when nothing imports
|
|
306
|
+
* `solid-js/web` (e.g. a React worker). Drop it (and use the `worker` condition) once Bun is fixed.
|
|
307
|
+
*/
|
|
308
|
+
const solidWebServerPlugin = (from: string): BunPlugin => ({
|
|
309
|
+
name: "nifra-solid-web-server",
|
|
310
|
+
setup(build) {
|
|
311
|
+
build.onResolve({ filter: /^solid-js\/web$/ }, () => {
|
|
312
|
+
// solid's `worker`/`node`/`deno` conditions all map to ./web/dist/server.js (its server build).
|
|
313
|
+
const pkg = Bun.resolveSync("solid-js/package.json", from)
|
|
314
|
+
return { path: pkg.replace(/package\.json$/, "web/dist/server.js") }
|
|
315
|
+
})
|
|
316
|
+
},
|
|
317
|
+
})
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Build a self-contained **worker bundle** for a file-routed app on a disk-less edge (Cloudflare
|
|
321
|
+
* Workers / workerd). Discovers routes (build-time fs), codegens the static-import server manifest
|
|
322
|
+
* (`generateServerManifest`, written next to `serverEntry`), then bundles `serverEntry` with
|
|
323
|
+
* `Bun.build` using **edge conditions** + the adapter's SSR plugins. The output imports no `node:fs`
|
|
324
|
+
* and does no dynamic-path import, so it runs on workerd: point `wrangler.toml`'s `main` at it and
|
|
325
|
+
* serve the client assets via Workers Assets. Throws (with the bundler logs) on failure — never
|
|
326
|
+
* silently ships a broken worker.
|
|
327
|
+
*/
|
|
328
|
+
export async function buildServer(options: BuildServerOptions): Promise<ServerBuild> {
|
|
329
|
+
const { routesDir, serverEntry, outDir, clientEntry } = options
|
|
330
|
+
const entryDir = dirname(serverEntry)
|
|
331
|
+
const manifestFile = options.manifestFile ?? "server-manifest.ts"
|
|
332
|
+
// Default: import routes relative from the generated manifest (next to serverEntry) to routesDir.
|
|
333
|
+
const rel = relative(entryDir, routesDir).replaceAll("\\", "/")
|
|
334
|
+
const resolve = options.resolve ?? ((file: string) => `./${rel}/${file}`)
|
|
335
|
+
mkdirSync(outDir, { recursive: true })
|
|
336
|
+
|
|
337
|
+
const lazy = options.lazy ?? false
|
|
338
|
+
const target = options.target ?? "browser"
|
|
339
|
+
// Edge (browser) target: Bun's `bun` condition contaminates react-dom's server build + the `worker`
|
|
340
|
+
// condition segfaults Bun.build on solid, so force the edge/server builds via shims. The `node`/`bun`
|
|
341
|
+
// targets resolve those correctly via their own condition, so the shims (and edge conditions) don't
|
|
342
|
+
// apply — defaults become `[target]` (e.g. react-dom → server.node.js under `node`).
|
|
343
|
+
const edge = target === "browser"
|
|
344
|
+
const conditions = options.conditions ?? (edge ? ["workerd", "edge-light", "browser"] : [target])
|
|
345
|
+
const manifest = discoverRoutes(routesDir)
|
|
346
|
+
writeFileSync(
|
|
347
|
+
`${entryDir}/${manifestFile}`,
|
|
348
|
+
generateServerManifest(manifest, { resolve, clientEntry, lazy }),
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
const result = await Bun.build({
|
|
352
|
+
entrypoints: [serverEntry],
|
|
353
|
+
outdir: outDir,
|
|
354
|
+
target,
|
|
355
|
+
conditions: [...conditions],
|
|
356
|
+
define: { ...(options.define ?? { "process.env.NODE_ENV": '"production"' }) },
|
|
357
|
+
minify: options.minify ?? true,
|
|
358
|
+
// Lazy → one chunk per route (loaded on first request); eager → a single self-contained file.
|
|
359
|
+
splitting: lazy,
|
|
360
|
+
plugins: [
|
|
361
|
+
...(edge ? [reactDomEdgePlugin(entryDir), solidWebServerPlugin(entryDir)] : []),
|
|
362
|
+
...(options.plugins ?? []),
|
|
363
|
+
],
|
|
364
|
+
})
|
|
365
|
+
if (!result.success) {
|
|
366
|
+
throw new Error(
|
|
367
|
+
`[nifra/web] server build failed:\n${result.logs.map((l) => String(l)).join("\n")}`,
|
|
368
|
+
)
|
|
369
|
+
}
|
|
370
|
+
const entryOutput = result.outputs.find((o) => o.kind === "entry-point")
|
|
371
|
+
if (entryOutput === undefined) {
|
|
372
|
+
throw new Error("[nifra/web] server build produced no entry-point output")
|
|
373
|
+
}
|
|
374
|
+
return { worker: entryOutput.path, outputs: result.outputs.map((o) => o.path) }
|
|
375
|
+
}
|