@rshono/core 1.0.0-rc.2 → 1.0.0-rc.21
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 +348 -158
- package/bin/rshono.mjs +3 -4
- package/dist/builder/env-shadow-loader.cjs +123 -8
- package/dist/builder/page-entry-loader.cjs +28 -1
- package/dist/builder/page-files.d.ts +0 -1
- package/dist/builder/page-files.js +7 -3
- package/dist/builder/page-files.js.map +1 -1
- package/dist/builder/public-env.d.ts +7 -1
- package/dist/builder/public-env.js +7 -0
- package/dist/builder/public-env.js.map +1 -1
- package/dist/builder/react-versions.d.ts +12 -0
- package/dist/builder/react-versions.js +74 -0
- package/dist/builder/react-versions.js.map +1 -0
- package/dist/builder/rspack-config.d.ts +13 -4
- package/dist/builder/rspack-config.js +136 -53
- package/dist/builder/rspack-config.js.map +1 -1
- package/dist/builder/server-only-imports.d.ts +32 -0
- package/dist/builder/server-only-imports.js +51 -0
- package/dist/builder/server-only-imports.js.map +1 -0
- package/dist/cli/build.d.ts +2 -3
- package/dist/cli/build.js +51 -13
- package/dist/cli/build.js.map +1 -1
- package/dist/cli/dev.d.ts +2 -3
- package/dist/cli/dev.js +149 -45
- package/dist/cli/dev.js.map +1 -1
- package/dist/cli/exit.d.ts +12 -0
- package/dist/cli/exit.js +16 -0
- package/dist/cli/exit.js.map +1 -0
- package/dist/cli/index.d.ts +0 -1
- package/dist/cli/index.js +91 -30
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/start.d.ts +0 -1
- package/dist/cli/start.js +15 -18
- package/dist/cli/start.js.map +1 -1
- package/dist/config.d.ts +72 -78
- package/dist/config.js +17 -1
- package/dist/config.js.map +1 -1
- package/dist/deploy/aws-lambda/runtime.d.ts +4 -7
- package/dist/deploy/aws-lambda/runtime.js +5 -8
- package/dist/deploy/aws-lambda/runtime.js.map +1 -1
- package/dist/deploy/build-marker.d.ts +9 -6
- package/dist/deploy/build-marker.js +8 -4
- package/dist/deploy/build-marker.js.map +1 -1
- package/dist/deploy/cloudflare/build.d.ts +0 -1
- package/dist/deploy/cloudflare/build.js +17 -13
- package/dist/deploy/cloudflare/build.js.map +1 -1
- package/dist/deploy/cloudflare/runtime.d.ts +2 -6
- package/dist/deploy/cloudflare/runtime.js +73 -68
- package/dist/deploy/cloudflare/runtime.js.map +1 -1
- package/dist/deploy/contract.d.ts +57 -41
- package/dist/deploy/contract.js.map +1 -1
- package/dist/deploy/filesystem.d.ts +3 -6
- package/dist/deploy/filesystem.js +23 -20
- package/dist/deploy/filesystem.js.map +1 -1
- package/dist/deploy/node/runtime.d.ts +5 -3
- package/dist/deploy/node/runtime.js +23 -9
- package/dist/deploy/node/runtime.js.map +1 -1
- package/dist/deploy/presets.d.ts +34 -30
- package/dist/deploy/presets.js +56 -44
- package/dist/deploy/presets.js.map +1 -1
- package/dist/deploy/public-paths.d.ts +42 -0
- package/dist/deploy/public-paths.js +66 -0
- package/dist/deploy/public-paths.js.map +1 -0
- package/dist/deploy/vercel/build.d.ts +0 -1
- package/dist/deploy/vercel/build.js +25 -16
- package/dist/deploy/vercel/build.js.map +1 -1
- package/dist/deploy/vercel/runtime.d.ts +10 -7
- package/dist/deploy/vercel/runtime.js +48 -11
- package/dist/deploy/vercel/runtime.js.map +1 -1
- package/dist/index.d.ts +19 -17
- package/dist/index.js +20 -10
- package/dist/index.js.map +1 -1
- package/dist/router.d.ts +212 -120
- package/dist/router.js +3 -8
- package/dist/router.js.map +1 -1
- package/dist/runtime/boundaries.d.ts +62 -48
- package/dist/runtime/boundaries.js +32 -34
- package/dist/runtime/boundaries.js.map +1 -1
- package/dist/runtime/client.d.ts +20 -11
- package/dist/runtime/client.js +20 -10
- package/dist/runtime/client.js.map +1 -1
- package/dist/runtime/context.d.ts +304 -123
- package/dist/runtime/context.js +377 -149
- package/dist/runtime/context.js.map +1 -1
- package/dist/runtime/control.d.ts +15 -1
- package/dist/runtime/control.js +24 -0
- package/dist/runtime/control.js.map +1 -1
- package/dist/runtime/dev-protocol.d.ts +4 -9
- package/dist/runtime/dev-protocol.js.map +1 -1
- package/dist/runtime/empty-server-app.d.ts +0 -1
- package/dist/runtime/entry.client.d.ts +4 -1
- package/dist/runtime/entry.client.js +455 -313
- package/dist/runtime/entry.client.js.map +1 -1
- package/dist/runtime/entry.rsc.d.ts +15 -7
- package/dist/runtime/entry.rsc.js +811 -252
- package/dist/runtime/entry.rsc.js.map +1 -1
- package/dist/runtime/entry.ssr.d.ts +27 -10
- package/dist/runtime/entry.ssr.js +30 -48
- package/dist/runtime/entry.ssr.js.map +1 -1
- package/dist/runtime/failure-document.d.ts +11 -0
- package/dist/runtime/failure-document.js +35 -0
- package/dist/runtime/failure-document.js.map +1 -0
- package/dist/runtime/flight-inject.d.ts +23 -0
- package/dist/runtime/flight-inject.js +403 -0
- package/dist/runtime/flight-inject.js.map +1 -0
- package/dist/runtime/hot-update.d.ts +44 -0
- package/dist/runtime/hot-update.js +44 -0
- package/dist/runtime/hot-update.js.map +1 -0
- package/dist/runtime/navigation.d.ts +46 -54
- package/dist/runtime/navigation.js +22 -63
- package/dist/runtime/navigation.js.map +1 -1
- package/dist/runtime/request.d.ts +42 -8
- package/dist/runtime/request.js +84 -7
- package/dist/runtime/request.js.map +1 -1
- package/dist/runtime/server.d.ts +20 -18
- package/dist/runtime/server.js +22 -17
- package/dist/runtime/server.js.map +1 -1
- package/dist/runtime/validate-entries.d.ts +77 -0
- package/dist/runtime/validate-entries.js +405 -0
- package/dist/runtime/validate-entries.js.map +1 -0
- package/dist/server/headers.d.ts +41 -15
- package/dist/server/headers.js +53 -24
- package/dist/server/headers.js.map +1 -1
- package/dist/server/load-config.d.ts +4 -5
- package/dist/server/load-config.js +22 -14
- package/dist/server/load-config.js.map +1 -1
- package/dist/server/load-env.d.ts +0 -1
- package/dist/server/prerendered.d.ts +88 -34
- package/dist/server/prerendered.js +184 -24
- package/dist/server/prerendered.js.map +1 -1
- package/dist/server/server-config.d.ts +35 -50
- package/dist/server/server-config.js +27 -74
- package/dist/server/server-config.js.map +1 -1
- package/dist/server/shutdown.d.ts +5 -4
- package/dist/server/shutdown.js +5 -3
- package/dist/server/shutdown.js.map +1 -1
- package/dist/server/ssg.d.ts +9 -7
- package/dist/server/ssg.js +209 -65
- package/dist/server/ssg.js.map +1 -1
- package/dist/server/static.d.ts +0 -1
- package/dist/server/static.js +7 -1
- package/dist/server/static.js.map +1 -1
- package/package.json +21 -23
- package/dist/builder/page-files.d.ts.map +0 -1
- package/dist/builder/public-env.d.ts.map +0 -1
- package/dist/builder/rspack-config.d.ts.map +0 -1
- package/dist/cli/build.d.ts.map +0 -1
- package/dist/cli/dev.d.ts.map +0 -1
- package/dist/cli/index.d.ts.map +0 -1
- package/dist/cli/start.d.ts.map +0 -1
- package/dist/config.d.ts.map +0 -1
- package/dist/deploy/aws-lambda/runtime.d.ts.map +0 -1
- package/dist/deploy/build-marker.d.ts.map +0 -1
- package/dist/deploy/bun/runtime.d.ts +0 -11
- package/dist/deploy/bun/runtime.d.ts.map +0 -1
- package/dist/deploy/bun/runtime.js +0 -22
- package/dist/deploy/bun/runtime.js.map +0 -1
- package/dist/deploy/cloudflare/build.d.ts.map +0 -1
- package/dist/deploy/cloudflare/runtime.d.ts.map +0 -1
- package/dist/deploy/contract.d.ts.map +0 -1
- package/dist/deploy/deno/runtime.d.ts +0 -11
- package/dist/deploy/deno/runtime.d.ts.map +0 -1
- package/dist/deploy/deno/runtime.js +0 -16
- package/dist/deploy/deno/runtime.js.map +0 -1
- package/dist/deploy/filesystem.d.ts.map +0 -1
- package/dist/deploy/listen.d.ts +0 -20
- package/dist/deploy/listen.d.ts.map +0 -1
- package/dist/deploy/listen.js +0 -24
- package/dist/deploy/listen.js.map +0 -1
- package/dist/deploy/netlify/build.d.ts +0 -8
- package/dist/deploy/netlify/build.d.ts.map +0 -1
- package/dist/deploy/netlify/build.js +0 -52
- package/dist/deploy/netlify/build.js.map +0 -1
- package/dist/deploy/netlify/runtime.d.ts +0 -13
- package/dist/deploy/netlify/runtime.d.ts.map +0 -1
- package/dist/deploy/netlify/runtime.js +0 -24
- package/dist/deploy/netlify/runtime.js.map +0 -1
- package/dist/deploy/node/runtime.d.ts.map +0 -1
- package/dist/deploy/presets.d.ts.map +0 -1
- package/dist/deploy/vercel/build.d.ts.map +0 -1
- package/dist/deploy/vercel/runtime.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/router.d.ts.map +0 -1
- package/dist/runtime/boundaries.d.ts.map +0 -1
- package/dist/runtime/client.d.ts.map +0 -1
- package/dist/runtime/context.d.ts.map +0 -1
- package/dist/runtime/control.d.ts.map +0 -1
- package/dist/runtime/dev-protocol.d.ts.map +0 -1
- package/dist/runtime/empty-server-app.d.ts.map +0 -1
- package/dist/runtime/entry.client.d.ts.map +0 -1
- package/dist/runtime/entry.rsc.d.ts.map +0 -1
- package/dist/runtime/entry.ssr.d.ts.map +0 -1
- package/dist/runtime/navigation.d.ts.map +0 -1
- package/dist/runtime/request.d.ts.map +0 -1
- package/dist/runtime/server.d.ts.map +0 -1
- package/dist/server/compress.d.ts +0 -15
- package/dist/server/compress.d.ts.map +0 -1
- package/dist/server/compress.js +0 -76
- package/dist/server/compress.js.map +0 -1
- package/dist/server/headers.d.ts.map +0 -1
- package/dist/server/load-config.d.ts.map +0 -1
- package/dist/server/load-env.d.ts.map +0 -1
- package/dist/server/prerendered.d.ts.map +0 -1
- package/dist/server/server-config.d.ts.map +0 -1
- package/dist/server/shutdown.d.ts.map +0 -1
- package/dist/server/ssg.d.ts.map +0 -1
- package/dist/server/static.d.ts.map +0 -1
package/dist/server/ssg.d.ts
CHANGED
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
import { type Route } from '../router.js';
|
|
2
2
|
import { type PrerenderedPage, type PrerenderVariant } from './prerendered.js';
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* A path is rejected rather than dropped: `'https://example.com/docs'` almost certainly means the
|
|
7
|
-
* author expects a base path, and silently serving from the root would be a confusing way to find
|
|
8
|
-
* out that isn't supported.
|
|
4
|
+
* Resolves `siteUrl` to the origin prerendering renders against. A path is rejected rather than dropped:
|
|
5
|
+
* `'https://example.com/docs'` means the author expects a base path, which is not supported.
|
|
9
6
|
*/
|
|
10
7
|
export declare function resolveSiteOrigin(siteUrl: string | undefined): string;
|
|
11
8
|
export declare function readPrerendered(ssgDir: string, requestPath: string, variant?: PrerenderVariant): Promise<PrerenderedPage | null>;
|
|
@@ -13,13 +10,18 @@ interface PrerenderOptions {
|
|
|
13
10
|
routes: readonly Route[];
|
|
14
11
|
fetch: (request: Request) => Response | Promise<Response>;
|
|
15
12
|
ssgDir: string;
|
|
16
|
-
/**
|
|
13
|
+
/** The origin absolute URLs in the output are built against — `siteUrl` from `rshono.config.ts`. */
|
|
17
14
|
siteUrl?: string;
|
|
18
15
|
}
|
|
19
16
|
export interface PrerenderResult {
|
|
20
17
|
written: string[];
|
|
21
18
|
skipped: string[];
|
|
19
|
+
/**
|
|
20
|
+
* The subset of {@link written} whose *document* the deployment will not serve, because the app mints a CSP
|
|
21
|
+
* nonce on that path — see {@link PRERENDER_NONCE_HEADER}. Their flight payloads are served from disk like
|
|
22
|
+
* any other, so these are half-prerendered pages rather than skipped ones, and the caller says so.
|
|
23
|
+
*/
|
|
24
|
+
flightOnly: string[];
|
|
22
25
|
}
|
|
23
26
|
export declare function prerenderStaticRoutes(options: PrerenderOptions): Promise<PrerenderResult>;
|
|
24
27
|
export {};
|
|
25
|
-
//# sourceMappingURL=ssg.d.ts.map
|
package/dist/server/ssg.js
CHANGED
|
@@ -1,21 +1,16 @@
|
|
|
1
|
-
import { createHash } from 'node:crypto';
|
|
2
1
|
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
3
2
|
import { readFile } from 'node:fs/promises';
|
|
4
3
|
import { dirname, join, resolve, sep } from 'node:path';
|
|
5
4
|
import { isPageRoute } from '../router.js';
|
|
6
|
-
import {
|
|
5
|
+
import { createPageCache, PRERENDER_NONCE_HEADER, ssgFilePath, SSG_MANIFEST_FILE, toPrerenderedPage, VARIANTS, } from './prerendered.js';
|
|
7
6
|
/**
|
|
8
|
-
* Stand-in origin for a build that
|
|
9
|
-
*
|
|
10
|
-
* to spot, and the build warns when static routes are prerendered without a real origin.
|
|
7
|
+
* Stand-in origin for a build that declared no `siteUrl`. Obviously wrong rather than a guess, so a page that
|
|
8
|
+
* bakes it into a canonical tag is easy to spot; the build warns as well.
|
|
11
9
|
*/
|
|
12
10
|
const DEFAULT_SSG_ORIGIN = 'http://localhost';
|
|
13
11
|
/**
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* A path is rejected rather than dropped: `'https://example.com/docs'` almost certainly means the
|
|
17
|
-
* author expects a base path, and silently serving from the root would be a confusing way to find
|
|
18
|
-
* out that isn't supported.
|
|
12
|
+
* Resolves `siteUrl` to the origin prerendering renders against. A path is rejected rather than dropped:
|
|
13
|
+
* `'https://example.com/docs'` means the author expects a base path, which is not supported.
|
|
19
14
|
*/
|
|
20
15
|
export function resolveSiteOrigin(siteUrl) {
|
|
21
16
|
if (!siteUrl)
|
|
@@ -29,81 +24,134 @@ export function resolveSiteOrigin(siteUrl) {
|
|
|
29
24
|
}
|
|
30
25
|
return parsed.origin;
|
|
31
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* A route's path with its params filled in, ready to render and to store.
|
|
29
|
+
*
|
|
30
|
+
* Throws for a pattern `staticPaths` cannot fill and for a set that does not fill it. Both are warned about
|
|
31
|
+
* and skipped by the caller, which already names the route — so these messages are the reason alone, written
|
|
32
|
+
* to be read at the end of that line.
|
|
33
|
+
*/
|
|
32
34
|
function interpolatePath(pattern, params) {
|
|
33
35
|
return pattern
|
|
34
36
|
.split('/')
|
|
35
37
|
.map((segment) => {
|
|
36
38
|
if (!segment.startsWith(':')) {
|
|
37
39
|
if (segment.includes('*')) {
|
|
38
|
-
throw new Error(`
|
|
40
|
+
throw new Error(`wildcard segments are not supported by staticPaths`);
|
|
39
41
|
}
|
|
40
42
|
return segment;
|
|
41
43
|
}
|
|
42
44
|
const name = segment.slice(1);
|
|
43
45
|
if (!/^\w+$/.test(name)) {
|
|
44
|
-
throw new Error(`
|
|
46
|
+
throw new Error(`optional/regex params are not supported by staticPaths`);
|
|
45
47
|
}
|
|
46
48
|
const value = params[name];
|
|
47
49
|
if (value === undefined) {
|
|
48
|
-
throw new Error(`staticPaths
|
|
50
|
+
throw new Error(`staticPaths returned a param set without "${name}"`);
|
|
49
51
|
}
|
|
50
52
|
return encodeURIComponent(value);
|
|
51
53
|
})
|
|
52
54
|
.join('/');
|
|
53
55
|
}
|
|
56
|
+
/** Prerendered pages, keyed by the request that produced them (see {@link readPrerendered}). */
|
|
57
|
+
const pageCache = createPageCache();
|
|
58
|
+
/** One in-flight or settled read of each store's {@link SSG_MANIFEST_FILE}; the file cannot change while the server is up. */
|
|
59
|
+
const storeIndexes = new Map();
|
|
54
60
|
/**
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
* Bounded so a site with thousands of prerendered pages keeps a working set rather than the whole
|
|
58
|
-
* build in memory. Only *hits* are cached: caching misses would let anyone mint entries by
|
|
59
|
-
* requesting paths that don't exist. The files are written at build time and never change while
|
|
60
|
-
* the server is up, so an entry never needs invalidating.
|
|
61
|
+
* What `ssgDir` holds, as {@link ssgFilePath} names it — or `null` where the build left no manifest, which
|
|
62
|
+
* is every build made before there was one. A `null` gates nothing, so those keep the behaviour they had.
|
|
61
63
|
*/
|
|
62
|
-
|
|
63
|
-
|
|
64
|
+
function storeIndex(ssgDir) {
|
|
65
|
+
let pending = storeIndexes.get(ssgDir);
|
|
66
|
+
if (!pending) {
|
|
67
|
+
// One `catch` for both failures worth the same answer: no manifest, and a manifest that is not one.
|
|
68
|
+
pending = readFile(join(ssgDir, SSG_MANIFEST_FILE), 'utf8')
|
|
69
|
+
.then((text) => new Set(JSON.parse(text).files ?? []))
|
|
70
|
+
.catch(() => null);
|
|
71
|
+
storeIndexes.set(ssgDir, pending);
|
|
72
|
+
}
|
|
73
|
+
return pending;
|
|
74
|
+
}
|
|
64
75
|
export async function readPrerendered(ssgDir, requestPath, variant = 'html') {
|
|
65
|
-
// Keyed by what the request carried
|
|
66
|
-
//
|
|
67
|
-
// exists only if this exact key already passed the checks below.
|
|
76
|
+
// Keyed by what the request carried rather than by the resolved filename, so a hit costs one Map lookup. Safe
|
|
77
|
+
// because only *hits* are cached: an entry exists only if this exact key already passed the checks below.
|
|
68
78
|
const key = `${ssgDir}\0${variant}\0${requestPath}`;
|
|
69
79
|
const cached = pageCache.get(key);
|
|
70
80
|
if (cached)
|
|
71
81
|
return cached;
|
|
72
|
-
const relPath =
|
|
82
|
+
const relPath = ssgFilePath(requestPath, variant);
|
|
73
83
|
if (relPath === null)
|
|
74
84
|
return null;
|
|
75
|
-
//
|
|
76
|
-
//
|
|
85
|
+
// Before the store is touched: a `render: 'static'` route whose build wrote nothing — no `staticPaths`, a
|
|
86
|
+
// param the build never saw, a page that did not render cleanly — falls through to SSR on every request,
|
|
87
|
+
// and without this each of those pays a failed read first, forever.
|
|
88
|
+
const index = await storeIndex(ssgDir);
|
|
89
|
+
if (index && !index.has(relPath))
|
|
90
|
+
return null;
|
|
91
|
+
// Belt and braces over the shared traversal guard: this proves the resolved file is under the root.
|
|
77
92
|
const root = resolve(ssgDir);
|
|
78
93
|
const file = resolve(root, relPath);
|
|
79
94
|
if (!file.startsWith(root + sep))
|
|
80
95
|
return null;
|
|
96
|
+
// No encoding argument: the bytes are what gets served. Copied out of the Buffer rather than kept as one,
|
|
97
|
+
// because `readFile` can hand back a view into Node's shared pool and this is retained for the process.
|
|
81
98
|
let body;
|
|
82
99
|
try {
|
|
83
|
-
body = await readFile(file
|
|
100
|
+
body = new Uint8Array(await readFile(file));
|
|
84
101
|
}
|
|
85
102
|
catch {
|
|
86
103
|
return null;
|
|
87
104
|
}
|
|
88
|
-
const page =
|
|
89
|
-
body,
|
|
90
|
-
contentLength: String(Buffer.byteLength(body)),
|
|
91
|
-
etag: `W/"${createHash('sha256').update(body).digest('base64url').slice(0, 22)}"`,
|
|
92
|
-
};
|
|
105
|
+
const page = await toPrerenderedPage(body);
|
|
93
106
|
pageCache.set(key, page);
|
|
94
|
-
// Insertion-ordered, so the first key is the oldest. One entry in means at most one out.
|
|
95
|
-
if (pageCache.size > MAX_CACHED_PAGES)
|
|
96
|
-
pageCache.delete(pageCache.keys().next().value);
|
|
97
107
|
return page;
|
|
98
108
|
}
|
|
99
|
-
/**
|
|
109
|
+
/**
|
|
110
|
+
* Records what the pass wrote, beside what it wrote. Written by the same pass for the same reason
|
|
111
|
+
* {@link ssgFilePath} is one function: an index that disagrees with the store is invisible, so the two are
|
|
112
|
+
* produced together or not at all.
|
|
113
|
+
*/
|
|
114
|
+
function writeManifest(ssgDir, files) {
|
|
115
|
+
mkdirSync(ssgDir, { recursive: true });
|
|
116
|
+
writeFileSync(join(ssgDir, SSG_MANIFEST_FILE), `${JSON.stringify({ files }, null, 2)}\n`);
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* How many paths are rendered at once.
|
|
120
|
+
*
|
|
121
|
+
* A prerender pass spends its time inside the app — a page's own data fetching is what each render awaits —
|
|
122
|
+
* so a few hundred `staticPaths` entries rendered one at a time pay all of that latency in series.
|
|
123
|
+
* Unbounded is the other wrong answer: it points the whole site at the app's database in one go. Eight keeps
|
|
124
|
+
* a build's peak load recognisable while hiding most of the wait.
|
|
125
|
+
*/
|
|
126
|
+
const RENDER_CONCURRENCY = 8;
|
|
127
|
+
/**
|
|
128
|
+
* `items.map(run)` with at most `limit` running at once, results in input order.
|
|
129
|
+
*
|
|
130
|
+
* A worker pool rather than fixed batches: a batch is only as fast as its slowest member, so one slow page
|
|
131
|
+
* would idle the rest of its batch.
|
|
132
|
+
*/
|
|
133
|
+
async function mapBounded(items, limit, run) {
|
|
134
|
+
const results = new Array(items.length);
|
|
135
|
+
let next = 0;
|
|
136
|
+
const worker = async () => {
|
|
137
|
+
for (let index = next++; index < items.length; index = next++)
|
|
138
|
+
results[index] = await run(items[index]);
|
|
139
|
+
};
|
|
140
|
+
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
|
|
141
|
+
return results;
|
|
142
|
+
}
|
|
100
143
|
async function renderVariant(fetch, url, variant) {
|
|
101
|
-
const response = await fetch(new Request(url, { headers:
|
|
144
|
+
const response = await fetch(new Request(url, { headers: VARIANTS[variant].headers }));
|
|
102
145
|
if (response.status !== 200)
|
|
103
|
-
return { status: response.status };
|
|
104
|
-
if (!(response.headers.get('Content-Type') ?? '').includes(VARIANTS[variant].contentType))
|
|
105
|
-
return
|
|
106
|
-
|
|
146
|
+
return { ok: false, reason: `${response.status}`, failed: response.status >= 500 };
|
|
147
|
+
if (!(response.headers.get('Content-Type') ?? '').includes(VARIANTS[variant].contentType)) {
|
|
148
|
+
return { ok: false, reason: `a non-${VARIANTS[variant].contentType} response`, failed: false };
|
|
149
|
+
}
|
|
150
|
+
// Bytes, not `response.text()`. That is a *non-fatal* UTF-8 decode, so every byte of a binary row in a
|
|
151
|
+
// flight payload — `emitChunk` puts raw typed-array bytes on the wire — becomes U+FFFD, and writing the
|
|
152
|
+
// string back out spends three real bytes on each one. The page is then prerendered and unparseable.
|
|
153
|
+
// The header is the framework saying this render minted a CSP nonce; on a flight payload it is never set.
|
|
154
|
+
return { ok: true, body: new Uint8Array(await response.arrayBuffer()), nonced: response.headers.get(PRERENDER_NONCE_HEADER) !== null };
|
|
107
155
|
}
|
|
108
156
|
export async function prerenderStaticRoutes(options) {
|
|
109
157
|
const { routes, fetch, ssgDir } = options;
|
|
@@ -115,6 +163,14 @@ export async function prerenderStaticRoutes(options) {
|
|
|
115
163
|
}
|
|
116
164
|
const written = [];
|
|
117
165
|
const skipped = [];
|
|
166
|
+
/** Pages whose document was written and will never be read — see {@link PrerenderResult.flightOnly}. */
|
|
167
|
+
const flightOnly = [];
|
|
168
|
+
/** Every file written, as the reader names it — {@link writeManifest}. */
|
|
169
|
+
const files = [];
|
|
170
|
+
/** Every path to render, in route order, each one once. Built before anything renders — see below. */
|
|
171
|
+
const targets = [];
|
|
172
|
+
/** So a `staticPaths` entry that repeats — or that a second route also produces — is rendered once. */
|
|
173
|
+
const queued = new Set();
|
|
118
174
|
for (const route of staticRoutes) {
|
|
119
175
|
let paths;
|
|
120
176
|
if (!/[:*]/.test(route.path)) {
|
|
@@ -126,35 +182,123 @@ export async function prerenderStaticRoutes(options) {
|
|
|
126
182
|
skipped.push(route.path);
|
|
127
183
|
continue;
|
|
128
184
|
}
|
|
129
|
-
|
|
185
|
+
try {
|
|
186
|
+
paths = (await route.staticPaths()).map((params) => interpolatePath(route.path, params));
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
// A route whose paths cannot be computed — a wildcard or a regex/optional param, which `staticPaths`
|
|
190
|
+
// has no way to fill, or a set it filled wrongly — is unprerenderable, not unservable. It gets the
|
|
191
|
+
// same answer as the branch above rather than killing a build the route would have survived: the
|
|
192
|
+
// page is still there, rendered per request.
|
|
193
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
194
|
+
console.warn(` ⚠ Static route "${route.path}" will SSR per request — ${reason.replace(/\.$/, '')}.`);
|
|
195
|
+
skipped.push(route.path);
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
130
198
|
}
|
|
199
|
+
let duplicates = 0;
|
|
131
200
|
for (const path of paths) {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
const rendered = document === null ? 'a non-HTML response' : `${document.status}`;
|
|
135
|
-
console.warn(` ⚠ "${path}" rendered ${rendered} at build time — skipping, will SSR per request.`);
|
|
136
|
-
skipped.push(path);
|
|
201
|
+
if (queued.has(path)) {
|
|
202
|
+
duplicates++;
|
|
137
203
|
continue;
|
|
138
204
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
}
|
|
152
|
-
else {
|
|
153
|
-
console.warn(` ⚠ "${path}" produced no flight payload — soft navigations to it will render per request.`);
|
|
205
|
+
queued.add(path);
|
|
206
|
+
// Resolved *before* the render, so a path no file can hold fails the build naming the path, rather than
|
|
207
|
+
// after the work and as a bare `join(dir, null)` TypeError. The same call answers for the reader, which
|
|
208
|
+
// is the whole point: a page written under a name the request path never resolves to is a page the build
|
|
209
|
+
// reports as prerendered and nothing ever serves. Resolved in this pass rather than the next one for a
|
|
210
|
+
// second reason too: which of several bad paths fails the build should not depend on render order.
|
|
211
|
+
const relPath = ssgFilePath(path, 'html');
|
|
212
|
+
if (relPath === null) {
|
|
213
|
+
throw new Error(`[rshono] Cannot prerender "${path}" for route "${route.path}": a path segment is "." or "..", or cannot be ` +
|
|
214
|
+
'one portable file name. It must not hold \\ / : * ? " < > | or a control character, must not end in ' +
|
|
215
|
+
'"." or a space (Windows stores the name without it, so the page lands where nothing looks for it), ' +
|
|
216
|
+
'and must not be a reserved device name — CON, PRN, AUX, NUL, COM0-9 or LPT0-9, with or without an extension.');
|
|
154
217
|
}
|
|
155
|
-
|
|
218
|
+
targets.push({ path, relPath });
|
|
156
219
|
}
|
|
220
|
+
if (duplicates > 0) {
|
|
221
|
+
console.warn(` ⚠ Static route "${route.path}" repeated ${duplicates} path${duplicates === 1 ? '' : 's'} — each page is prerendered once.`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
const outcomes = await mapBounded(targets, RENDER_CONCURRENCY, async ({ path, relPath }) => {
|
|
225
|
+
/** Buffered, so a concurrent pass logs in the same order a serial one did. */
|
|
226
|
+
const warnings = [];
|
|
227
|
+
const document = await renderVariant(fetch, origin + path, 'html');
|
|
228
|
+
if (!document.ok) {
|
|
229
|
+
// A 5xx fails the build instead of being warned about — see {@link RenderedVariant}.
|
|
230
|
+
if (!document.failed)
|
|
231
|
+
warnings.push(` ⚠ "${path}" rendered ${document.reason} at build time — skipping, will SSR per request.`);
|
|
232
|
+
return { path, ok: false, files: [], warnings, failure: document.failed ? document.reason : undefined, nonced: false };
|
|
233
|
+
}
|
|
234
|
+
// Both representations of a page share its directory and differ only in the file name.
|
|
235
|
+
const pageDir = join(ssgDir, dirname(relPath));
|
|
236
|
+
const wrote = [];
|
|
237
|
+
const write = (variant, body) => {
|
|
238
|
+
mkdirSync(pageDir, { recursive: true });
|
|
239
|
+
writeFileSync(join(pageDir, VARIANTS[variant].file), body);
|
|
240
|
+
// `ssgFilePath` again rather than the `join` above: the manifest is read against what a *request*
|
|
241
|
+
// resolves to, which is that function's output and not a filesystem path.
|
|
242
|
+
wrote.push(ssgFilePath(path, variant));
|
|
243
|
+
};
|
|
244
|
+
write('html', document.body);
|
|
245
|
+
// The soft-navigation representation of the same page. Best-effort: the document is valid on its own, and
|
|
246
|
+
// serving falls back to rendering the flight payload per request.
|
|
247
|
+
const flight = await renderVariant(fetch, origin + path, 'flight');
|
|
248
|
+
if (flight.ok) {
|
|
249
|
+
write('flight', flight.body);
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
// Not fatal the way a failed *document* is, even for a 5xx: the page is on disk and serves, and only
|
|
253
|
+
// soft navigation to it degrades to rendering per request. The reason is named because the two cases
|
|
254
|
+
// want different things done about them.
|
|
255
|
+
warnings.push(` ⚠ "${path}" produced no flight payload (${flight.reason}) — soft navigations to it will render per request.`);
|
|
256
|
+
}
|
|
257
|
+
return { path, ok: true, files: wrote, warnings, failure: undefined, nonced: document.nonced };
|
|
258
|
+
});
|
|
259
|
+
// Folded back in target order rather than completion order, so a concurrent pass writes the same manifest
|
|
260
|
+
// and prints the same log a serial one did.
|
|
261
|
+
const failures = [];
|
|
262
|
+
for (const outcome of outcomes) {
|
|
263
|
+
for (const warning of outcome.warnings)
|
|
264
|
+
console.warn(warning);
|
|
265
|
+
if (outcome.failure !== undefined)
|
|
266
|
+
failures.push(`"${outcome.path}" rendered ${outcome.failure}`);
|
|
267
|
+
if (!outcome.ok) {
|
|
268
|
+
skipped.push(outcome.path);
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
files.push(...outcome.files);
|
|
272
|
+
written.push(outcome.path);
|
|
273
|
+
if (outcome.nonced)
|
|
274
|
+
flightOnly.push(outcome.path);
|
|
275
|
+
}
|
|
276
|
+
// Raised together, and in target order, so which of several failing pages is reported does not depend on
|
|
277
|
+
// which render finished first. The render's own error is already in the build log above: the framework's
|
|
278
|
+
// `onError` reported it when it turned the throw into the 500 counted here.
|
|
279
|
+
if (failures.length > 0) {
|
|
280
|
+
throw new Error(`[rshono] ${failures.length} page${failures.length === 1 ? '' : 's'} failed to render while prerendering:\n` +
|
|
281
|
+
failures.map((failure) => ` • ${failure}`).join('\n') +
|
|
282
|
+
'\n Prerendering renders a page the way a request does, so this is not a build-only failure — the route' +
|
|
283
|
+
'\n would answer the same status per request. The error it threw is logged above.');
|
|
284
|
+
}
|
|
285
|
+
// After the failure check, so a build that is about to stop does not spend three lines on advice. Once
|
|
286
|
+
// rather than per page: under a global nonce policy this is every static page in the app, and the same
|
|
287
|
+
// sentence repeated a hundred times says no more than one does — the caller's summary line is what names
|
|
288
|
+
// them, marked there.
|
|
289
|
+
//
|
|
290
|
+
// The documents are written all the same. Whether a nonce is minted is a request-time decision, and an
|
|
291
|
+
// app is free to switch its policy on from the environment, so a build that saw one is not proof the
|
|
292
|
+
// deployment has one — dropping the file would cost a page its prerender on the strength of a guess.
|
|
293
|
+
if (flightOnly.length > 0) {
|
|
294
|
+
console.warn(` ⚠ ${flightOnly.length} page(s) mint a CSP nonce, so the framework re-renders their document per request:\n` +
|
|
295
|
+
` only the flight payload is served from disk. The documents are written anyway, in case the policy is\n` +
|
|
296
|
+
` off where this is deployed. Marked "flight only" in the summary below.`);
|
|
157
297
|
}
|
|
158
|
-
|
|
298
|
+
// Only where the app has static routes at all: an empty manifest would be a store, and a build with
|
|
299
|
+
// nothing to prerender should leave no `ssg/` directory for a preset to carry around.
|
|
300
|
+
if (staticRoutes.length > 0)
|
|
301
|
+
writeManifest(ssgDir, files);
|
|
302
|
+
return { written, skipped, flightOnly };
|
|
159
303
|
}
|
|
160
304
|
//# sourceMappingURL=ssg.js.map
|
package/dist/server/ssg.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ssg.js","sourceRoot":"","sources":["../../src/server/ssg.ts"],"names":[],"mappings":"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","sourcesContent":["import { createHash } from 'node:crypto';\nimport { mkdirSync, writeFileSync } from 'node:fs';\nimport { readFile } from 'node:fs/promises';\nimport { dirname, join, resolve, sep } from 'node:path';\nimport { isPageRoute, type PageRoute, type Route } from '../router.js';\nimport { prerenderedRelPath, ssgFilePath, VARIANTS, type PrerenderedPage, type PrerenderVariant } from './prerendered.js';\n\n/**\n * Stand-in origin for a build that didn't declare {@link RSHonoConfig.siteUrl}. Deliberately\n * obviously-wrong rather than a guess: a page that bakes this into a canonical tag should be easy\n * to spot, and the build warns when static routes are prerendered without a real origin.\n */\nconst DEFAULT_SSG_ORIGIN = 'http://localhost';\n\n/**\n * Resolve {@link RSHonoConfig.siteUrl} to the origin prerendering should render against.\n *\n * A path is rejected rather than dropped: `'https://example.com/docs'` almost certainly means the\n * author expects a base path, and silently serving from the root would be a confusing way to find\n * out that isn't supported.\n */\nexport function resolveSiteOrigin(siteUrl: string | undefined): string {\n if (!siteUrl) return DEFAULT_SSG_ORIGIN;\n const parsed = URL.parse(siteUrl);\n if (!parsed || (parsed.protocol !== 'http:' && parsed.protocol !== 'https:')) {\n throw new Error(`[rshono] invalid siteUrl ${JSON.stringify(siteUrl)} — use a full origin, e.g. 'https://example.com'.`);\n }\n if (parsed.pathname !== '/' || parsed.search || parsed.hash) {\n throw new Error(`[rshono] siteUrl ${JSON.stringify(siteUrl)} must be a bare origin — a base path is not supported.`);\n }\n return parsed.origin;\n}\n\nfunction interpolatePath(pattern: string, params: Record<string, string>): string {\n return pattern\n .split('/')\n .map((segment) => {\n if (!segment.startsWith(':')) {\n if (segment.includes('*')) {\n throw new Error(`Cannot prerender \"${pattern}\": wildcard segments are not supported by staticPaths.`);\n }\n return segment;\n }\n const name = segment.slice(1);\n if (!/^\\w+$/.test(name)) {\n throw new Error(`Cannot prerender \"${pattern}\": optional/regex params are not supported by staticPaths.`);\n }\n const value = params[name];\n if (value === undefined) {\n throw new Error(`staticPaths for \"${pattern}\" returned a param set without \"${name}\".`);\n }\n return encodeURIComponent(value);\n })\n .join('/');\n}\n\n/**\n * Prerendered pages, keyed by the request that produced them (see {@link readPrerendered}).\n *\n * Bounded so a site with thousands of prerendered pages keeps a working set rather than the whole\n * build in memory. Only *hits* are cached: caching misses would let anyone mint entries by\n * requesting paths that don't exist. The files are written at build time and never change while\n * the server is up, so an entry never needs invalidating.\n */\nconst pageCache = new Map<string, PrerenderedPage>();\nconst MAX_CACHED_PAGES = 128;\n\nexport async function readPrerendered(ssgDir: string, requestPath: string, variant: PrerenderVariant = 'html'): Promise<PrerenderedPage | null> {\n // Keyed by what the request carried, not by the resolved filename, so a hit costs one Map lookup\n // instead of re-deriving the path every time. Safe because only *hits* are cached: an entry\n // exists only if this exact key already passed the checks below.\n const key = `${ssgDir}\\0${variant}\\0${requestPath}`;\n const cached = pageCache.get(key);\n if (cached) return cached;\n\n const relPath = prerenderedRelPath(requestPath, variant);\n if (relPath === null) return null;\n // Belt and braces: the shared guard already refused a traversal, and this proves the resolved file\n // is under the root whatever else the path contained.\n const root = resolve(ssgDir);\n const file = resolve(root, relPath);\n if (!file.startsWith(root + sep)) return null;\n\n let body: string;\n try {\n body = await readFile(file, 'utf8');\n } catch {\n return null;\n }\n\n const page: PrerenderedPage = {\n body,\n contentLength: String(Buffer.byteLength(body)),\n etag: `W/\"${createHash('sha256').update(body).digest('base64url').slice(0, 22)}\"`,\n };\n pageCache.set(key, page);\n // Insertion-ordered, so the first key is the oldest. One entry in means at most one out.\n if (pageCache.size > MAX_CACHED_PAGES) pageCache.delete(pageCache.keys().next().value!);\n return page;\n}\n\ninterface PrerenderOptions {\n routes: readonly Route[];\n fetch: (request: Request) => Response | Promise<Response>;\n ssgDir: string;\n /** {@link RSHonoConfig.siteUrl} — the origin absolute URLs in the output are built against. */\n siteUrl?: string;\n}\n\nexport interface PrerenderResult {\n written: string[];\n skipped: string[];\n}\n\n/** Renders one representation of a path, or `null` if the app didn't answer with it. */\nasync function renderVariant(\n fetch: PrerenderOptions['fetch'],\n url: string,\n variant: PrerenderVariant,\n): Promise<{ body: string } | { status: number } | null> {\n const response = await fetch(new Request(url, { headers: { Accept: VARIANTS[variant].accept } }));\n if (response.status !== 200) return { status: response.status };\n if (!(response.headers.get('Content-Type') ?? '').includes(VARIANTS[variant].contentType)) return null;\n return { body: await response.text() };\n}\n\nexport async function prerenderStaticRoutes(options: PrerenderOptions): Promise<PrerenderResult> {\n const { routes, fetch, ssgDir } = options;\n const staticRoutes = routes.filter((r): r is PageRoute => isPageRoute(r) && r.render === 'static');\n const origin = resolveSiteOrigin(options.siteUrl);\n\n if (staticRoutes.length > 0 && !options.siteUrl) {\n console.warn(\n ` ⚠ No siteUrl in rshono.config — prerendered pages are built against ${DEFAULT_SSG_ORIGIN}, so any absolute URL\\n` +\n ` they derive from a page's \\`url\\` prop (canonical tags, og:url, absolute links) will point there.`,\n );\n }\n\n const written: string[] = [];\n const skipped: string[] = [];\n\n for (const route of staticRoutes) {\n let paths: string[];\n if (!/[:*]/.test(route.path)) {\n paths = [route.path];\n } else {\n if (!route.staticPaths) {\n console.warn(` ⚠ Static route \"${route.path}\" has params but no staticPaths — will SSR per request.`);\n skipped.push(route.path);\n continue;\n }\n paths = (await route.staticPaths()).map((params) => interpolatePath(route.path, params));\n }\n\n for (const path of paths) {\n const document = await renderVariant(fetch, origin + path, 'html');\n if (document === null || !('body' in document)) {\n const rendered = document === null ? 'a non-HTML response' : `${document.status}`;\n console.warn(` ⚠ \"${path}\" rendered ${rendered} at build time — skipping, will SSR per request.`);\n skipped.push(path);\n continue;\n }\n\n const write = (variant: PrerenderVariant, body: string) => {\n const file = join(ssgDir, ssgFilePath(path, variant)!);\n mkdirSync(dirname(file), { recursive: true });\n writeFileSync(file, body);\n };\n write('html', document.body);\n\n // The soft-navigation representation of the same page. Best-effort: if it doesn't come back\n // cleanly the document is still valid on its own, and serving falls back to rendering flight\n // per request — the behaviour before this was written at all.\n const flight = await renderVariant(fetch, origin + path, 'flight');\n if (flight !== null && 'body' in flight) {\n write('flight', flight.body);\n } else {\n console.warn(` ⚠ \"${path}\" produced no flight payload — soft navigations to it will render per request.`);\n }\n\n written.push(path);\n }\n }\n\n return { written, skipped };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"ssg.js","sourceRoot":"","sources":["../../src/server/ssg.ts"],"names":[],"mappings":"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","sourcesContent":["import { mkdirSync, writeFileSync } from 'node:fs';\nimport { readFile } from 'node:fs/promises';\nimport { dirname, join, resolve, sep } from 'node:path';\nimport { isPageRoute, type PageRoute, type Route } from '../router.js';\nimport {\n createPageCache,\n PRERENDER_NONCE_HEADER,\n ssgFilePath,\n SSG_MANIFEST_FILE,\n toPrerenderedPage,\n VARIANTS,\n type PrerenderedPage,\n type PrerenderVariant,\n} from './prerendered.js';\n\n/**\n * Stand-in origin for a build that declared no `siteUrl`. Obviously wrong rather than a guess, so a page that\n * bakes it into a canonical tag is easy to spot; the build warns as well.\n */\nconst DEFAULT_SSG_ORIGIN = 'http://localhost';\n\n/**\n * Resolves `siteUrl` to the origin prerendering renders against. A path is rejected rather than dropped:\n * `'https://example.com/docs'` means the author expects a base path, which is not supported.\n */\nexport function resolveSiteOrigin(siteUrl: string | undefined): string {\n if (!siteUrl) return DEFAULT_SSG_ORIGIN;\n const parsed = URL.parse(siteUrl);\n if (!parsed || (parsed.protocol !== 'http:' && parsed.protocol !== 'https:')) {\n throw new Error(`[rshono] invalid siteUrl ${JSON.stringify(siteUrl)} — use a full origin, e.g. 'https://example.com'.`);\n }\n if (parsed.pathname !== '/' || parsed.search || parsed.hash) {\n throw new Error(`[rshono] siteUrl ${JSON.stringify(siteUrl)} must be a bare origin — a base path is not supported.`);\n }\n return parsed.origin;\n}\n\n/**\n * A route's path with its params filled in, ready to render and to store.\n *\n * Throws for a pattern `staticPaths` cannot fill and for a set that does not fill it. Both are warned about\n * and skipped by the caller, which already names the route — so these messages are the reason alone, written\n * to be read at the end of that line.\n */\nfunction interpolatePath(pattern: string, params: Record<string, string>): string {\n return pattern\n .split('/')\n .map((segment) => {\n if (!segment.startsWith(':')) {\n if (segment.includes('*')) {\n throw new Error(`wildcard segments are not supported by staticPaths`);\n }\n return segment;\n }\n const name = segment.slice(1);\n if (!/^\\w+$/.test(name)) {\n throw new Error(`optional/regex params are not supported by staticPaths`);\n }\n const value = params[name];\n if (value === undefined) {\n throw new Error(`staticPaths returned a param set without \"${name}\"`);\n }\n return encodeURIComponent(value);\n })\n .join('/');\n}\n\n/** Prerendered pages, keyed by the request that produced them (see {@link readPrerendered}). */\nconst pageCache = createPageCache();\n\n/** One in-flight or settled read of each store's {@link SSG_MANIFEST_FILE}; the file cannot change while the server is up. */\nconst storeIndexes = new Map<string, Promise<ReadonlySet<string> | null>>();\n\n/**\n * What `ssgDir` holds, as {@link ssgFilePath} names it — or `null` where the build left no manifest, which\n * is every build made before there was one. A `null` gates nothing, so those keep the behaviour they had.\n */\nfunction storeIndex(ssgDir: string): Promise<ReadonlySet<string> | null> {\n let pending = storeIndexes.get(ssgDir);\n if (!pending) {\n // One `catch` for both failures worth the same answer: no manifest, and a manifest that is not one.\n pending = readFile(join(ssgDir, SSG_MANIFEST_FILE), 'utf8')\n .then((text) => new Set((JSON.parse(text) as { files?: string[] }).files ?? []) as ReadonlySet<string>)\n .catch(() => null);\n storeIndexes.set(ssgDir, pending);\n }\n return pending;\n}\n\nexport async function readPrerendered(ssgDir: string, requestPath: string, variant: PrerenderVariant = 'html'): Promise<PrerenderedPage | null> {\n // Keyed by what the request carried rather than by the resolved filename, so a hit costs one Map lookup. Safe\n // because only *hits* are cached: an entry exists only if this exact key already passed the checks below.\n const key = `${ssgDir}\\0${variant}\\0${requestPath}`;\n const cached = pageCache.get(key);\n if (cached) return cached;\n\n const relPath = ssgFilePath(requestPath, variant);\n if (relPath === null) return null;\n\n // Before the store is touched: a `render: 'static'` route whose build wrote nothing — no `staticPaths`, a\n // param the build never saw, a page that did not render cleanly — falls through to SSR on every request,\n // and without this each of those pays a failed read first, forever.\n const index = await storeIndex(ssgDir);\n if (index && !index.has(relPath)) return null;\n // Belt and braces over the shared traversal guard: this proves the resolved file is under the root.\n const root = resolve(ssgDir);\n const file = resolve(root, relPath);\n if (!file.startsWith(root + sep)) return null;\n\n // No encoding argument: the bytes are what gets served. Copied out of the Buffer rather than kept as one,\n // because `readFile` can hand back a view into Node's shared pool and this is retained for the process.\n let body: Uint8Array<ArrayBuffer>;\n try {\n body = new Uint8Array(await readFile(file));\n } catch {\n return null;\n }\n\n const page = await toPrerenderedPage(body);\n pageCache.set(key, page);\n return page;\n}\n\ninterface PrerenderOptions {\n routes: readonly Route[];\n fetch: (request: Request) => Response | Promise<Response>;\n ssgDir: string;\n /** The origin absolute URLs in the output are built against — `siteUrl` from `rshono.config.ts`. */\n siteUrl?: string;\n}\n\nexport interface PrerenderResult {\n written: string[];\n skipped: string[];\n /**\n * The subset of {@link written} whose *document* the deployment will not serve, because the app mints a CSP\n * nonce on that path — see {@link PRERENDER_NONCE_HEADER}. Their flight payloads are served from disk like\n * any other, so these are half-prerendered pages rather than skipped ones, and the caller says so.\n */\n flightOnly: string[];\n}\n\n/**\n * Records what the pass wrote, beside what it wrote. Written by the same pass for the same reason\n * {@link ssgFilePath} is one function: an index that disagrees with the store is invisible, so the two are\n * produced together or not at all.\n */\nfunction writeManifest(ssgDir: string, files: readonly string[]): void {\n mkdirSync(ssgDir, { recursive: true });\n writeFileSync(join(ssgDir, SSG_MANIFEST_FILE), `${JSON.stringify({ files }, null, 2)}\\n`);\n}\n\n/**\n * How many paths are rendered at once.\n *\n * A prerender pass spends its time inside the app — a page's own data fetching is what each render awaits —\n * so a few hundred `staticPaths` entries rendered one at a time pay all of that latency in series.\n * Unbounded is the other wrong answer: it points the whole site at the app's database in one go. Eight keeps\n * a build's peak load recognisable while hiding most of the wait.\n */\nconst RENDER_CONCURRENCY = 8;\n\n/**\n * `items.map(run)` with at most `limit` running at once, results in input order.\n *\n * A worker pool rather than fixed batches: a batch is only as fast as its slowest member, so one slow page\n * would idle the rest of its batch.\n */\nasync function mapBounded<T, R>(items: readonly T[], limit: number, run: (item: T) => Promise<R>): Promise<R[]> {\n const results = new Array<R>(items.length);\n let next = 0;\n const worker = async (): Promise<void> => {\n for (let index = next++; index < items.length; index = next++) results[index] = await run(items[index]);\n };\n await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));\n return results;\n}\n\n/**\n * One representation of a path, as the app answered for it at build time. Discriminated on `ok` because both\n * callers have to tell \"the app rendered this\" from \"it did not\", and only one cares why.\n *\n * `failed` splits the second case in two. A 404, a 3xx or a payload of the wrong type is a page this pass\n * cannot *store*, and it still serves per request — the skip is honest. A 5xx is the app throwing, and this\n * pass renders a page exactly as a request does, so per request it will throw again: skipping it prints\n * \"will SSR per request\" over a route that will 500 forever, and exits 0.\n */\ntype RenderedVariant = { ok: true; body: Uint8Array; nonced: boolean } | { ok: false; reason: string; failed: boolean };\n\nasync function renderVariant(fetch: PrerenderOptions['fetch'], url: string, variant: PrerenderVariant): Promise<RenderedVariant> {\n const response = await fetch(new Request(url, { headers: VARIANTS[variant].headers }));\n if (response.status !== 200) return { ok: false, reason: `${response.status}`, failed: response.status >= 500 };\n if (!(response.headers.get('Content-Type') ?? '').includes(VARIANTS[variant].contentType)) {\n return { ok: false, reason: `a non-${VARIANTS[variant].contentType} response`, failed: false };\n }\n // Bytes, not `response.text()`. That is a *non-fatal* UTF-8 decode, so every byte of a binary row in a\n // flight payload — `emitChunk` puts raw typed-array bytes on the wire — becomes U+FFFD, and writing the\n // string back out spends three real bytes on each one. The page is then prerendered and unparseable.\n // The header is the framework saying this render minted a CSP nonce; on a flight payload it is never set.\n return { ok: true, body: new Uint8Array(await response.arrayBuffer()), nonced: response.headers.get(PRERENDER_NONCE_HEADER) !== null };\n}\n\n/** A path the pass will render, resolved far enough that nothing left can fail the build. */\ninterface PrerenderTarget {\n path: string;\n /** Where a request for it will look — {@link ssgFilePath}, resolved before the render rather than after. */\n relPath: string;\n}\n\nexport async function prerenderStaticRoutes(options: PrerenderOptions): Promise<PrerenderResult> {\n const { routes, fetch, ssgDir } = options;\n const staticRoutes = routes.filter((r): r is PageRoute => isPageRoute(r) && r.render === 'static');\n const origin = resolveSiteOrigin(options.siteUrl);\n\n if (staticRoutes.length > 0 && !options.siteUrl) {\n console.warn(\n ` ⚠ No siteUrl in rshono.config — prerendered pages are built against ${DEFAULT_SSG_ORIGIN}, so any absolute URL\\n` +\n ` they derive from a page's \\`url\\` prop (canonical tags, og:url, absolute links) will point there.`,\n );\n }\n\n const written: string[] = [];\n const skipped: string[] = [];\n /** Pages whose document was written and will never be read — see {@link PrerenderResult.flightOnly}. */\n const flightOnly: string[] = [];\n /** Every file written, as the reader names it — {@link writeManifest}. */\n const files: string[] = [];\n\n /** Every path to render, in route order, each one once. Built before anything renders — see below. */\n const targets: PrerenderTarget[] = [];\n /** So a `staticPaths` entry that repeats — or that a second route also produces — is rendered once. */\n const queued = new Set<string>();\n\n for (const route of staticRoutes) {\n let paths: string[];\n if (!/[:*]/.test(route.path)) {\n paths = [route.path];\n } else {\n if (!route.staticPaths) {\n console.warn(` ⚠ Static route \"${route.path}\" has params but no staticPaths — will SSR per request.`);\n skipped.push(route.path);\n continue;\n }\n try {\n paths = (await route.staticPaths()).map((params) => interpolatePath(route.path, params));\n } catch (error) {\n // A route whose paths cannot be computed — a wildcard or a regex/optional param, which `staticPaths`\n // has no way to fill, or a set it filled wrongly — is unprerenderable, not unservable. It gets the\n // same answer as the branch above rather than killing a build the route would have survived: the\n // page is still there, rendered per request.\n const reason = error instanceof Error ? error.message : String(error);\n console.warn(` ⚠ Static route \"${route.path}\" will SSR per request — ${reason.replace(/\\.$/, '')}.`);\n skipped.push(route.path);\n continue;\n }\n }\n\n let duplicates = 0;\n for (const path of paths) {\n if (queued.has(path)) {\n duplicates++;\n continue;\n }\n queued.add(path);\n // Resolved *before* the render, so a path no file can hold fails the build naming the path, rather than\n // after the work and as a bare `join(dir, null)` TypeError. The same call answers for the reader, which\n // is the whole point: a page written under a name the request path never resolves to is a page the build\n // reports as prerendered and nothing ever serves. Resolved in this pass rather than the next one for a\n // second reason too: which of several bad paths fails the build should not depend on render order.\n const relPath = ssgFilePath(path, 'html');\n if (relPath === null) {\n throw new Error(\n `[rshono] Cannot prerender \"${path}\" for route \"${route.path}\": a path segment is \".\" or \"..\", or cannot be ` +\n 'one portable file name. It must not hold \\\\ / : * ? \" < > | or a control character, must not end in ' +\n '\".\" or a space (Windows stores the name without it, so the page lands where nothing looks for it), ' +\n 'and must not be a reserved device name — CON, PRN, AUX, NUL, COM0-9 or LPT0-9, with or without an extension.',\n );\n }\n targets.push({ path, relPath });\n }\n if (duplicates > 0) {\n console.warn(` ⚠ Static route \"${route.path}\" repeated ${duplicates} path${duplicates === 1 ? '' : 's'} — each page is prerendered once.`);\n }\n }\n\n const outcomes = await mapBounded(targets, RENDER_CONCURRENCY, async ({ path, relPath }) => {\n /** Buffered, so a concurrent pass logs in the same order a serial one did. */\n const warnings: string[] = [];\n const document = await renderVariant(fetch, origin + path, 'html');\n if (!document.ok) {\n // A 5xx fails the build instead of being warned about — see {@link RenderedVariant}.\n if (!document.failed) warnings.push(` ⚠ \"${path}\" rendered ${document.reason} at build time — skipping, will SSR per request.`);\n return { path, ok: false as const, files: [], warnings, failure: document.failed ? document.reason : undefined, nonced: false };\n }\n\n // Both representations of a page share its directory and differ only in the file name.\n const pageDir = join(ssgDir, dirname(relPath));\n const wrote: string[] = [];\n const write = (variant: PrerenderVariant, body: Uint8Array) => {\n mkdirSync(pageDir, { recursive: true });\n writeFileSync(join(pageDir, VARIANTS[variant].file), body);\n // `ssgFilePath` again rather than the `join` above: the manifest is read against what a *request*\n // resolves to, which is that function's output and not a filesystem path.\n wrote.push(ssgFilePath(path, variant)!);\n };\n write('html', document.body);\n\n // The soft-navigation representation of the same page. Best-effort: the document is valid on its own, and\n // serving falls back to rendering the flight payload per request.\n const flight = await renderVariant(fetch, origin + path, 'flight');\n if (flight.ok) {\n write('flight', flight.body);\n } else {\n // Not fatal the way a failed *document* is, even for a 5xx: the page is on disk and serves, and only\n // soft navigation to it degrades to rendering per request. The reason is named because the two cases\n // want different things done about them.\n warnings.push(` ⚠ \"${path}\" produced no flight payload (${flight.reason}) — soft navigations to it will render per request.`);\n }\n\n return { path, ok: true as const, files: wrote, warnings, failure: undefined, nonced: document.nonced };\n });\n\n // Folded back in target order rather than completion order, so a concurrent pass writes the same manifest\n // and prints the same log a serial one did.\n const failures: string[] = [];\n for (const outcome of outcomes) {\n for (const warning of outcome.warnings) console.warn(warning);\n if (outcome.failure !== undefined) failures.push(`\"${outcome.path}\" rendered ${outcome.failure}`);\n if (!outcome.ok) {\n skipped.push(outcome.path);\n continue;\n }\n files.push(...outcome.files);\n written.push(outcome.path);\n if (outcome.nonced) flightOnly.push(outcome.path);\n }\n\n // Raised together, and in target order, so which of several failing pages is reported does not depend on\n // which render finished first. The render's own error is already in the build log above: the framework's\n // `onError` reported it when it turned the throw into the 500 counted here.\n if (failures.length > 0) {\n throw new Error(\n `[rshono] ${failures.length} page${failures.length === 1 ? '' : 's'} failed to render while prerendering:\\n` +\n failures.map((failure) => ` • ${failure}`).join('\\n') +\n '\\n Prerendering renders a page the way a request does, so this is not a build-only failure — the route' +\n '\\n would answer the same status per request. The error it threw is logged above.',\n );\n }\n\n // After the failure check, so a build that is about to stop does not spend three lines on advice. Once\n // rather than per page: under a global nonce policy this is every static page in the app, and the same\n // sentence repeated a hundred times says no more than one does — the caller's summary line is what names\n // them, marked there.\n //\n // The documents are written all the same. Whether a nonce is minted is a request-time decision, and an\n // app is free to switch its policy on from the environment, so a build that saw one is not proof the\n // deployment has one — dropping the file would cost a page its prerender on the strength of a guess.\n if (flightOnly.length > 0) {\n console.warn(\n ` ⚠ ${flightOnly.length} page(s) mint a CSP nonce, so the framework re-renders their document per request:\\n` +\n ` only the flight payload is served from disk. The documents are written anyway, in case the policy is\\n` +\n ` off where this is deployed. Marked \"flight only\" in the summary below.`,\n );\n }\n\n // Only where the app has static routes at all: an empty manifest would be a store, and a build with\n // nothing to prerender should leave no `ssg/` directory for a preset to carry around.\n if (staticRoutes.length > 0) writeManifest(ssgDir, files);\n\n return { written, skipped, flightOnly };\n}\n"]}
|
package/dist/server/static.d.ts
CHANGED
|
@@ -11,4 +11,3 @@ export declare function createStaticAssetsApp(options: StaticOptions): Hono;
|
|
|
11
11
|
/** Middleware serving the app's `public/` files verbatim at the web root, for paths no route claimed. */
|
|
12
12
|
export declare function createPublicFallback({ root, isDev }: StaticOptions): MiddlewareHandler;
|
|
13
13
|
export {};
|
|
14
|
-
//# sourceMappingURL=static.d.ts.map
|
package/dist/server/static.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { serveStatic } from '@hono/node-server/serve-static';
|
|
2
2
|
import { Hono } from 'hono';
|
|
3
|
+
import { ASSET_MISS_CACHE_CONTROL } from './headers.js';
|
|
3
4
|
const CONTENT_HASHED = /\.[0-9a-f]{8,}\./;
|
|
4
5
|
function cacheControl(isDev) {
|
|
5
6
|
return async (c, next) => {
|
|
@@ -13,7 +14,12 @@ function cacheControl(isDev) {
|
|
|
13
14
|
export function createStaticAssetsApp(options) {
|
|
14
15
|
const { root, isDev } = options;
|
|
15
16
|
const app = new Hono();
|
|
16
|
-
|
|
17
|
+
// `GET` alone: Hono dispatches a `HEAD` as a `GET` and strips the body, so a `HEAD` registration beside it
|
|
18
|
+
// is never reached — see `HTTPMethod`.
|
|
19
|
+
// The 404 carries a `Cache-Control` of its own because `cacheControl` above returns early for it; the
|
|
20
|
+
// policy and the reason are {@link ASSET_MISS_CACHE_CONTROL}'s, and the Workers mount answers a miss with
|
|
21
|
+
// the same pair.
|
|
22
|
+
app.get('/*', cacheControl(isDev), serveStatic({ root, rewriteRequestPath: (path) => path.replace(/^\/_static/, '') }), (c) => c.text('Not Found', 404, { 'cache-control': ASSET_MISS_CACHE_CONTROL }));
|
|
17
23
|
return app;
|
|
18
24
|
}
|
|
19
25
|
/** Middleware serving the app's `public/` files verbatim at the web root, for paths no route claimed. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"static.js","sourceRoot":"","sources":["../../src/server/static.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,gCAAgC,CAAC;AAE7D,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"static.js","sourceRoot":"","sources":["../../src/server/static.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,gCAAgC,CAAC;AAE7D,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAC5B,OAAO,EAAE,wBAAwB,EAAE,MAAM,cAAc,CAAC;AAExD,MAAM,cAAc,GAAG,kBAAkB,CAAC;AAE1C,SAAS,YAAY,CAAC,KAAc;IAClC,OAAO,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,EAAE;QACvB,MAAM,IAAI,EAAE,CAAC;QACb,IAAI,CAAC,CAAC,GAAG,CAAC,MAAM,KAAK,GAAG,IAAI,CAAC,CAAC,GAAG,CAAC,MAAM,KAAK,GAAG;YAAE,OAAO;QACzD,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,GAAG,CACf,eAAe,EACf,KAAK,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,qCAAqC,CAAC,CAAC,CAAC,qBAAqB,CACrH,CAAC;IACJ,CAAC,CAAC;AACJ,CAAC;AASD,+EAA+E;AAC/E,MAAM,UAAU,qBAAqB,CAAC,OAAsB;IAC1D,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,OAAO,CAAC;IAChC,MAAM,GAAG,GAAG,IAAI,IAAI,EAAE,CAAC;IAEvB,2GAA2G;IAC3G,uCAAuC;IACvC,sGAAsG;IACtG,0GAA0G;IAC1G,iBAAiB;IACjB,GAAG,CAAC,GAAG,CAAC,IAAI,EAAE,YAAY,CAAC,KAAK,CAAC,EAAE,WAAW,CAAC,EAAE,IAAI,EAAE,kBAAkB,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,YAAY,EAAE,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAC5H,CAAC,CAAC,IAAI,CAAC,WAAW,EAAE,GAAG,EAAE,EAAE,eAAe,EAAE,wBAAwB,EAAE,CAAC,CACxE,CAAC;IAEF,OAAO,GAAG,CAAC;AACb,CAAC;AAED,yGAAyG;AACzG,MAAM,UAAU,oBAAoB,CAAC,EAAE,IAAI,EAAE,KAAK,EAAiB;IACjE,MAAM,KAAK,GAAG,WAAW,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC;IACpC,OAAO,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,EAAE;QACvB,MAAM,MAAM,GAAG,MAAM,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC;QACpC,IAAI,MAAM,YAAY,QAAQ,IAAI,CAAC,MAAM,CAAC,MAAM,KAAK,GAAG,IAAI,MAAM,CAAC,MAAM,KAAK,GAAG,CAAC,EAAE,CAAC;YACnF,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,eAAe,EAAE,KAAK,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,qBAAqB,CAAC,CAAC;QAClF,CAAC;QACD,OAAO,MAAM,CAAC;IAChB,CAAC,CAAC;AACJ,CAAC","sourcesContent":["import { serveStatic } from '@hono/node-server/serve-static';\nimport type { MiddlewareHandler } from 'hono';\nimport { Hono } from 'hono';\nimport { ASSET_MISS_CACHE_CONTROL } from './headers.js';\n\nconst CONTENT_HASHED = /\\.[0-9a-f]{8,}\\./;\n\nfunction cacheControl(isDev: boolean): MiddlewareHandler {\n return async (c, next) => {\n await next();\n if (c.res.status !== 200 && c.res.status !== 206) return;\n c.res.headers.set(\n 'Cache-Control',\n isDev ? 'no-cache' : CONTENT_HASHED.test(c.req.path) ? 'public, max-age=31536000, immutable' : 'public, max-age=300',\n );\n };\n}\n\ninterface StaticOptions {\n /** Directory the files are read from. */\n root: string;\n /** Dev serves revalidated (`no-cache`); a build serves hashed files as immutable. */\n isDev: boolean;\n}\n\n/** A sub-app serving the hashed client bundle, to be mounted at `/_static`. */\nexport function createStaticAssetsApp(options: StaticOptions): Hono {\n const { root, isDev } = options;\n const app = new Hono();\n\n // `GET` alone: Hono dispatches a `HEAD` as a `GET` and strips the body, so a `HEAD` registration beside it\n // is never reached — see `HTTPMethod`.\n // The 404 carries a `Cache-Control` of its own because `cacheControl` above returns early for it; the\n // policy and the reason are {@link ASSET_MISS_CACHE_CONTROL}'s, and the Workers mount answers a miss with\n // the same pair.\n app.get('/*', cacheControl(isDev), serveStatic({ root, rewriteRequestPath: (path) => path.replace(/^\\/_static/, '') }), (c) =>\n c.text('Not Found', 404, { 'cache-control': ASSET_MISS_CACHE_CONTROL }),\n );\n\n return app;\n}\n\n/** Middleware serving the app's `public/` files verbatim at the web root, for paths no route claimed. */\nexport function createPublicFallback({ root, isDev }: StaticOptions): MiddlewareHandler {\n const serve = serveStatic({ root });\n return async (c, next) => {\n const result = await serve(c, next);\n if (result instanceof Response && (result.status === 200 || result.status === 206)) {\n result.headers.set('Cache-Control', isDev ? 'no-cache' : 'public, max-age=300');\n }\n return result;\n };\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rshono/core",
|
|
3
|
-
"version": "1.0.0-rc.
|
|
3
|
+
"version": "1.0.0-rc.21",
|
|
4
4
|
"description": "Minimalist web framework — Hono + Rspack + React Server Components",
|
|
5
5
|
"author": "Lasse <lasse@lassetange.com> (https://www.lassetange.com)",
|
|
6
|
-
"license": "
|
|
6
|
+
"license": "MIT",
|
|
7
7
|
"keywords": [
|
|
8
8
|
"react",
|
|
9
9
|
"react-server-components",
|
|
@@ -45,35 +45,24 @@
|
|
|
45
45
|
"bin"
|
|
46
46
|
],
|
|
47
47
|
"engines": {
|
|
48
|
-
"node": ">=22.
|
|
48
|
+
"node": ">=22.18.0"
|
|
49
49
|
},
|
|
50
50
|
"publishConfig": {
|
|
51
51
|
"access": "public"
|
|
52
52
|
},
|
|
53
|
-
"scripts": {
|
|
54
|
-
"build": "tsc -p tsconfig.build.json && node scripts/copy-dist-assets.mjs",
|
|
55
|
-
"clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"",
|
|
56
|
-
"dev": "tsc -p tsconfig.build.json --watch",
|
|
57
|
-
"typecheck": "tsc --noEmit",
|
|
58
|
-
"test": "npm run build && node --test --test-concurrency=1 \"test/*.test.mjs\"",
|
|
59
|
-
"test:browser": "npm run build && playwright test",
|
|
60
|
-
"prepack": "npm run build"
|
|
61
|
-
},
|
|
62
53
|
"dependencies": {
|
|
63
|
-
"@hono/node-server": "^2.
|
|
64
|
-
"@rspack/core": "2.
|
|
54
|
+
"@hono/node-server": "^2.1.1",
|
|
55
|
+
"@rspack/core": "2.2.6",
|
|
65
56
|
"@rspack/plugin-react-refresh": "2.0.2",
|
|
66
57
|
"react-refresh": "0.18.0",
|
|
67
|
-
"react-server-dom-rspack": "0.0
|
|
68
|
-
"rsc-html-stream": "^0.0.7",
|
|
69
|
-
"tsx": "^4.23.1"
|
|
58
|
+
"react-server-dom-rspack": "0.1.0"
|
|
70
59
|
},
|
|
71
60
|
"devDependencies": {
|
|
72
|
-
"@playwright/test": "^1.
|
|
73
|
-
"@types/node": "^26.
|
|
74
|
-
"@types/react": "^19.2.
|
|
75
|
-
"@types/react-dom": "^19.2.
|
|
76
|
-
"hono": "^4.
|
|
61
|
+
"@playwright/test": "^1.63.0",
|
|
62
|
+
"@types/node": "^26.5.1",
|
|
63
|
+
"@types/react": "^19.2.18",
|
|
64
|
+
"@types/react-dom": "^19.2.5",
|
|
65
|
+
"hono": "^4.13.7",
|
|
77
66
|
"react": "19.2.8",
|
|
78
67
|
"react-dom": "19.2.8",
|
|
79
68
|
"typescript": "^7.0.2"
|
|
@@ -82,5 +71,14 @@
|
|
|
82
71
|
"hono": "^4.8.0",
|
|
83
72
|
"react": "^19.1.0",
|
|
84
73
|
"react-dom": "^19.1.0"
|
|
74
|
+
},
|
|
75
|
+
"scripts": {
|
|
76
|
+
"build": "tsc -p tsconfig.build.json && node scripts/copy-dist-assets.mjs",
|
|
77
|
+
"clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"",
|
|
78
|
+
"dev": "tsc -p tsconfig.build.json --watch",
|
|
79
|
+
"typecheck": "tsc --noEmit",
|
|
80
|
+
"test": "npm run build && node --test --test-concurrency=1 \"test/*.test.mjs\"",
|
|
81
|
+
"test:coverage": "npm run build && node --experimental-test-coverage --test-coverage-include=\"dist/**\" --test-coverage-lines=82 --test-coverage-branches=90 --test-coverage-functions=75 --test --test-concurrency=1 \"test/*.test.mjs\"",
|
|
82
|
+
"test:browser": "npm run build && playwright test"
|
|
85
83
|
}
|
|
86
|
-
}
|
|
84
|
+
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"page-files.d.ts","sourceRoot":"","sources":["../../src/builder/page-files.ts"],"names":[],"mappings":"AAsBA,wBAAgB,aAAa,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,IAAI,CAqBzF"}
|