@jk2908/solas 0.3.7 → 0.4.0
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/CHANGELOG.md +24 -0
- package/README.md +66 -6
- package/dist/index.d.ts +2 -2
- package/dist/index.js +75 -6
- package/dist/internal/browser-router/link.d.ts +1 -1
- package/dist/internal/browser-router/link.js +1 -1
- package/dist/internal/browser-router/router.d.ts +2 -165
- package/dist/internal/browser-router/router.js +3 -99
- package/dist/internal/browser-router/shared.d.ts +169 -0
- package/dist/internal/browser-router/shared.js +71 -0
- package/dist/internal/browser-router/use-router.d.ts +1 -1
- package/dist/internal/build.js +14 -14
- package/dist/internal/codegen/environments.js +5 -4
- package/dist/internal/env/browser.js +11 -9
- package/dist/internal/env/rsc.d.ts +2 -2
- package/dist/internal/env/rsc.js +170 -86
- package/dist/internal/http-router/create-http-router.d.ts +1 -1
- package/dist/internal/http-router/create-http-router.js +4 -2
- package/dist/internal/http-router/router.d.ts +4 -14
- package/dist/internal/http-router/router.js +32 -59
- package/dist/internal/navigation/http-exception.d.ts +8 -4
- package/dist/internal/navigation/http-exception.js +46 -6
- package/dist/internal/postbuild.d.ts +1 -0
- package/dist/{cli/build.js → internal/postbuild.js} +13 -48
- package/dist/internal/prerender.d.ts +4 -19
- package/dist/internal/prerender.js +8 -98
- package/dist/internal/public-files.d.ts +18 -0
- package/dist/internal/public-files.js +63 -0
- package/dist/internal/render/tree.d.ts +0 -3
- package/dist/internal/render/tree.js +1 -6
- package/dist/internal/resolver.d.ts +31 -23
- package/dist/internal/server/actions.d.ts +2 -5
- package/dist/internal/server/actions.js +4 -35
- package/dist/internal/server/csrf.d.ts +14 -0
- package/dist/internal/server/csrf.js +98 -0
- package/dist/internal/ui/defaults/error.d.ts +2 -0
- package/dist/internal/ui/defaults/error.js +1 -1
- package/dist/navigation.d.ts +1 -1
- package/dist/router.d.ts +1 -0
- package/dist/router.js +1 -0
- package/dist/solas.d.ts +12 -1
- package/dist/solas.js +116 -1
- package/dist/types.d.ts +27 -5
- package/dist/utils/base-path.d.ts +14 -0
- package/dist/utils/base-path.js +85 -0
- package/dist/utils/export-reader.d.ts +6 -1
- package/dist/utils/export-reader.js +24 -15
- package/package.json +3 -6
- package/dist/cli/build.d.ts +0 -7
- package/dist/cli/dev.d.ts +0 -4
- package/dist/cli/dev.js +0 -13
- package/dist/cli/preview.d.ts +0 -1
- package/dist/cli/preview.js +0 -47
- package/dist/cli.d.ts +0 -2
- package/dist/cli.js +0 -28
|
@@ -4,8 +4,9 @@ export const HTTP_EXCEPTION_NAME_MAP = {
|
|
|
4
4
|
404: 'NOT_FOUND',
|
|
5
5
|
500: 'INTERNAL_SERVER_ERROR',
|
|
6
6
|
};
|
|
7
|
+
export const HTTP_EXCEPTION_DIGEST_PREFIX = 'HTTP_EXCEPTION';
|
|
7
8
|
/**
|
|
8
|
-
* An exception representing an
|
|
9
|
+
* An exception representing an HttpException error, with an optional payload
|
|
9
10
|
* and cause
|
|
10
11
|
*/
|
|
11
12
|
export class HttpException extends Error {
|
|
@@ -22,9 +23,14 @@ export class HttpException extends Error {
|
|
|
22
23
|
this.digest = `${HTTP_EXCEPTION_DIGEST_PREFIX}:${status}:${message}`;
|
|
23
24
|
}
|
|
24
25
|
}
|
|
25
|
-
export const HTTP_EXCEPTION_DIGEST_PREFIX = 'HTTP_EXCEPTION';
|
|
26
26
|
/**
|
|
27
|
-
*
|
|
27
|
+
* Status type predicate
|
|
28
|
+
*/
|
|
29
|
+
function isStatusCode(value) {
|
|
30
|
+
return value === 401 || value === 403 || value === 404 || value === 500;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Check if an error is an HttpException
|
|
28
34
|
*/
|
|
29
35
|
export function isHttpException(err) {
|
|
30
36
|
return (typeof err === 'object' &&
|
|
@@ -33,6 +39,41 @@ export function isHttpException(err) {
|
|
|
33
39
|
typeof err.digest === 'string' &&
|
|
34
40
|
err.digest.startsWith(HTTP_EXCEPTION_DIGEST_PREFIX));
|
|
35
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* Convert any error into an HttpException
|
|
44
|
+
*/
|
|
45
|
+
export function toHttpException(err) {
|
|
46
|
+
if (err instanceof HttpException)
|
|
47
|
+
return err;
|
|
48
|
+
let digestStatus;
|
|
49
|
+
let digestMessage;
|
|
50
|
+
if (typeof err === 'object' &&
|
|
51
|
+
err !== null &&
|
|
52
|
+
'digest' in err &&
|
|
53
|
+
typeof err.digest === 'string') {
|
|
54
|
+
const [type, rawStatus, ...rawMessageParts] = err.digest.split(':');
|
|
55
|
+
const status = Number(rawStatus);
|
|
56
|
+
if (type === HTTP_EXCEPTION_DIGEST_PREFIX && isStatusCode(status)) {
|
|
57
|
+
digestStatus = status;
|
|
58
|
+
digestMessage = rawMessageParts.join(':');
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const status = digestStatus ??
|
|
62
|
+
(typeof err === 'object' &&
|
|
63
|
+
err !== null &&
|
|
64
|
+
'status' in err &&
|
|
65
|
+
isStatusCode(err.status)
|
|
66
|
+
? err.status
|
|
67
|
+
: 500);
|
|
68
|
+
const message = digestMessage ||
|
|
69
|
+
(typeof err === 'object' &&
|
|
70
|
+
err !== null &&
|
|
71
|
+
'message' in err &&
|
|
72
|
+
typeof err.message === 'string'
|
|
73
|
+
? err.message
|
|
74
|
+
: 'Internal Server Error');
|
|
75
|
+
return new HttpException(status, message, { cause: err });
|
|
76
|
+
}
|
|
36
77
|
/**
|
|
37
78
|
* Convert an HttpException or any Error into a plain object that can be
|
|
38
79
|
* safely serialised
|
|
@@ -41,18 +82,17 @@ export function toHttpExceptionLike(error) {
|
|
|
41
82
|
return {
|
|
42
83
|
name: error.name,
|
|
43
84
|
message: error.message,
|
|
44
|
-
stack: error.stack,
|
|
45
85
|
...('digest' in error && typeof error.digest === 'string'
|
|
46
86
|
? { digest: error.digest }
|
|
47
87
|
: {}),
|
|
48
88
|
...('payload' in error && error.payload !== undefined
|
|
49
89
|
? { payload: error.payload }
|
|
50
90
|
: {}),
|
|
51
|
-
...('status' in error ? { status: error.status } : {}),
|
|
91
|
+
...('status' in error && isStatusCode(error.status) ? { status: error.status } : {}),
|
|
52
92
|
};
|
|
53
93
|
}
|
|
54
94
|
/**
|
|
55
|
-
* Throw an
|
|
95
|
+
* Throw an HttpException
|
|
56
96
|
*/
|
|
57
97
|
export function abort(status, message, opts) {
|
|
58
98
|
throw new HttpException(status, message, {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function postbuild(cwd?: string): Promise<void>;
|
|
@@ -2,33 +2,11 @@ import fs from 'node:fs/promises';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { Compress } from '../utils/compress.js';
|
|
4
4
|
import { Logger } from '../utils/logger.js';
|
|
5
|
-
import { Prerender } from '../internal/prerender.js';
|
|
6
5
|
import { Solas } from '../solas.js';
|
|
6
|
+
import { Prerender } from './prerender.js';
|
|
7
7
|
const logger = new Logger();
|
|
8
|
-
|
|
9
|
-
* The build command does more than just run vite build - it also handles prerendering and
|
|
10
|
-
* precompressing assets. This is because prerendering needs to run against the built
|
|
11
|
-
* server entry to ensure the same code paths as preview, and precompressing needs
|
|
12
|
-
* to include the prerendered html and json files
|
|
13
|
-
*/
|
|
14
|
-
export async function build() {
|
|
15
|
-
// build and prerender should both run in production mode
|
|
16
|
-
process.env.NODE_ENV = 'production';
|
|
17
|
-
const cwd = process.cwd();
|
|
8
|
+
export async function postbuild(cwd = process.cwd()) {
|
|
18
9
|
const manifestPath = path.join(cwd, Solas.Config.GENERATED_DIR, 'build.json');
|
|
19
|
-
// run vite build
|
|
20
|
-
logger.info('[build]', 'running vite build...');
|
|
21
|
-
const vite = Bun.spawnSync(['bunx', '--bun', 'vite', 'build', '--mode', 'production'], {
|
|
22
|
-
cwd,
|
|
23
|
-
stdout: 'inherit',
|
|
24
|
-
stderr: 'inherit',
|
|
25
|
-
env: { ...process.env, NODE_ENV: 'production' },
|
|
26
|
-
});
|
|
27
|
-
if (vite.exitCode !== 0) {
|
|
28
|
-
logger.error('[build] vite build failed');
|
|
29
|
-
process.exit(1);
|
|
30
|
-
}
|
|
31
|
-
// read build manifest
|
|
32
10
|
let manifest;
|
|
33
11
|
try {
|
|
34
12
|
const raw = await fs.readFile(manifestPath, 'utf-8');
|
|
@@ -36,7 +14,7 @@ export async function build() {
|
|
|
36
14
|
}
|
|
37
15
|
catch (err) {
|
|
38
16
|
logger.error('[build] failed to read build manifest', err);
|
|
39
|
-
|
|
17
|
+
throw err;
|
|
40
18
|
}
|
|
41
19
|
const outDir = path.resolve(cwd, Solas.Config.OUT_DIR);
|
|
42
20
|
const rscDir = path.join(outDir, 'rsc');
|
|
@@ -44,16 +22,11 @@ export async function build() {
|
|
|
44
22
|
// clear old prerender artifacts so routes that have switched modes
|
|
45
23
|
// do not keep stale metadata from a previous build
|
|
46
24
|
await fs.rm(artifactRoot, { recursive: true, force: true });
|
|
47
|
-
|
|
25
|
+
const artifactManifest = {};
|
|
48
26
|
if (manifest.prerenderRoutes.length > 0) {
|
|
49
|
-
const timeout = Prerender.Build.getTimeout();
|
|
50
27
|
const concurrency = Prerender.Build.getConcurrency();
|
|
51
|
-
// track the extra prerender files we write for preview
|
|
52
|
-
const artifactManifest = {};
|
|
53
|
-
// keep in-flight artifact writes bounded so result handling does not block on one route at a time
|
|
54
28
|
const pendingWrites = new Set();
|
|
55
|
-
logger.info('[prerender]', `prerendering ${manifest.prerenderRoutes.length} routes (
|
|
56
|
-
// load the built server entry and render each prerendered route through it
|
|
29
|
+
logger.info('[prerender]', `prerendering ${manifest.prerenderRoutes.length} routes (concurrency: ${concurrency})...`);
|
|
57
30
|
const rscEntry = path.join(rscDir, 'index.js');
|
|
58
31
|
const { default: app } = await import(/* @vite-ignore */ rscEntry);
|
|
59
32
|
async function enqueueWrite(task) {
|
|
@@ -65,9 +38,8 @@ export async function build() {
|
|
|
65
38
|
await Promise.race(pendingWrites);
|
|
66
39
|
}
|
|
67
40
|
}
|
|
68
|
-
// run prerender through the built app so build output uses the same path as preview
|
|
69
41
|
for await (const result of Prerender.Build.run(app, manifest.prerenderRoutes, {
|
|
70
|
-
|
|
42
|
+
base: manifest.base,
|
|
71
43
|
concurrency,
|
|
72
44
|
origin: manifest.url,
|
|
73
45
|
})) {
|
|
@@ -85,7 +57,6 @@ export async function build() {
|
|
|
85
57
|
await enqueueWrite(async () => {
|
|
86
58
|
try {
|
|
87
59
|
if (artifact.mode === 'ppr') {
|
|
88
|
-
// for ppr save the shell now and keep the postponed state for later
|
|
89
60
|
await fs.mkdir(artifactDir, { recursive: true });
|
|
90
61
|
const writes = [
|
|
91
62
|
Bun.write(path.join(artifactDir, 'prelude.html'), artifact.html),
|
|
@@ -120,9 +91,7 @@ export async function build() {
|
|
|
120
91
|
logger.info('[prerender]', `${route} (ppr)`);
|
|
121
92
|
return;
|
|
122
93
|
}
|
|
123
|
-
// full prerender still keeps metadata so preview knows to serve saved html
|
|
124
94
|
await fs.mkdir(artifactDir, { recursive: true });
|
|
125
|
-
const fullPrerenderFilename = Prerender.Artifact.FULL_PRERENDER_FILENAME;
|
|
126
95
|
await Promise.all([
|
|
127
96
|
Bun.write(path.join(artifactDir, 'metadata.json'), JSON.stringify({
|
|
128
97
|
schema: artifact.schema,
|
|
@@ -130,7 +99,7 @@ export async function build() {
|
|
|
130
99
|
createdAt: artifact.createdAt,
|
|
131
100
|
mode: artifact.mode,
|
|
132
101
|
})),
|
|
133
|
-
Bun.write(Prerender.Artifact.getFilePath(outDir, route,
|
|
102
|
+
Bun.write(Prerender.Artifact.getFilePath(outDir, route, Prerender.Artifact.FULL_PRERENDER_FILENAME), artifact.html),
|
|
134
103
|
]);
|
|
135
104
|
artifactManifest[route] = {
|
|
136
105
|
mode: artifact.mode,
|
|
@@ -144,13 +113,13 @@ export async function build() {
|
|
|
144
113
|
});
|
|
145
114
|
}
|
|
146
115
|
await Promise.all(pendingWrites);
|
|
147
|
-
// write one manifest for the saved prerender files after all routes finish
|
|
148
|
-
await fs.mkdir(artifactRoot, { recursive: true });
|
|
149
|
-
await Bun.write(Prerender.Artifact.getManifestPath(outDir), JSON.stringify({
|
|
150
|
-
routes: artifactManifest,
|
|
151
|
-
}));
|
|
152
116
|
}
|
|
153
|
-
|
|
117
|
+
await fs.mkdir(artifactRoot, { recursive: true });
|
|
118
|
+
const runtimeManifest = {
|
|
119
|
+
artifacts: artifactManifest,
|
|
120
|
+
publicFiles: manifest.publicFiles,
|
|
121
|
+
};
|
|
122
|
+
await Bun.write(Solas.Runtime.getManifestPath(outDir), JSON.stringify(runtimeManifest));
|
|
154
123
|
if (manifest.sitemapRoutes.length > 0 && manifest.url) {
|
|
155
124
|
const origin = manifest.url.replace(/\/$/, '');
|
|
156
125
|
const urls = manifest.sitemapRoutes
|
|
@@ -165,10 +134,8 @@ export async function build() {
|
|
|
165
134
|
await Bun.write(path.join(outDir, 'sitemap.xml'), sitemap);
|
|
166
135
|
logger.info('[sitemap]', `generated ${manifest.sitemapRoutes.length} urls`);
|
|
167
136
|
}
|
|
168
|
-
// precompress
|
|
169
137
|
if (manifest.precompress) {
|
|
170
138
|
logger.info('[precompress]', 'compressing assets...');
|
|
171
|
-
// compress after prerender so generated html and json are included too
|
|
172
139
|
for await (const { input, compressed } of Compress.run(outDir, {
|
|
173
140
|
filter: f => /\.(js|css|html|svg|json|txt)$/.test(f),
|
|
174
141
|
})) {
|
|
@@ -176,8 +143,6 @@ export async function build() {
|
|
|
176
143
|
logger.info('[precompress]', `${path.basename(input)}.br`);
|
|
177
144
|
}
|
|
178
145
|
}
|
|
179
|
-
// cleanup
|
|
180
|
-
// this file is only needed while the build command is running
|
|
181
146
|
await fs.unlink(manifestPath).catch(() => { });
|
|
182
147
|
logger.info('[build]', 'done');
|
|
183
148
|
}
|
|
@@ -22,11 +22,6 @@ export declare namespace Prerender {
|
|
|
22
22
|
* based on the output directory specified in the configuration
|
|
23
23
|
*/
|
|
24
24
|
function getRootPath(outDir: string): string;
|
|
25
|
-
/**
|
|
26
|
-
* Get the file system path for the prerender artifact manifest, which
|
|
27
|
-
* contains metadata about all prerendered routes and their artifacts
|
|
28
|
-
*/
|
|
29
|
-
function getManifestPath(outDir: string): string;
|
|
30
25
|
/**
|
|
31
26
|
* Get the file system path for storing prerender artifacts for a given route
|
|
32
27
|
*/
|
|
@@ -39,10 +34,6 @@ export declare namespace Prerender {
|
|
|
39
34
|
* File name used for saved full-prerender html inside each route artifact directory
|
|
40
35
|
*/
|
|
41
36
|
const FULL_PRERENDER_FILENAME = "prerendered.html";
|
|
42
|
-
/**
|
|
43
|
-
* Load the prerender artifact manifest for faster runtime route mode checks
|
|
44
|
-
*/
|
|
45
|
-
function loadManifest(outDir: string): Promise<Manifest | null>;
|
|
46
37
|
/**
|
|
47
38
|
* Load the postponed state for a given route from the file system, if it exists
|
|
48
39
|
*/
|
|
@@ -95,11 +86,6 @@ export declare namespace Prerender {
|
|
|
95
86
|
function isPostponed(error: unknown): boolean;
|
|
96
87
|
}
|
|
97
88
|
namespace Build {
|
|
98
|
-
/**
|
|
99
|
-
* Get the prerender timeout value from the environment variable, or return the default
|
|
100
|
-
* if it's not set or invalid
|
|
101
|
-
*/
|
|
102
|
-
function getTimeout(): number;
|
|
103
89
|
/**
|
|
104
90
|
* Get the prerender concurrency value from the environment variable, or return the default
|
|
105
91
|
* if it's not set or invalid
|
|
@@ -128,8 +114,8 @@ export declare namespace Prerender {
|
|
|
128
114
|
function get(app: {
|
|
129
115
|
fetch: (req: Request) => Promise<Response>;
|
|
130
116
|
}, route: string, opts: {
|
|
131
|
-
timeout: number;
|
|
132
117
|
origin?: string;
|
|
118
|
+
base?: string;
|
|
133
119
|
}): Promise<{
|
|
134
120
|
route: string;
|
|
135
121
|
status: number;
|
|
@@ -140,16 +126,15 @@ export declare namespace Prerender {
|
|
|
140
126
|
artifact: any;
|
|
141
127
|
}>;
|
|
142
128
|
/**
|
|
143
|
-
* Run the prerendering process for a list of routes
|
|
144
|
-
*
|
|
145
|
-
* become available
|
|
129
|
+
* Run the prerendering process for a list of routes with a specified concurrency limit,
|
|
130
|
+
* by calling the 'get' function for each route and yielding the results as they become available
|
|
146
131
|
*/
|
|
147
132
|
function run(app: {
|
|
148
133
|
fetch: (req: Request) => Promise<Response>;
|
|
149
134
|
}, routes: string[], opts: {
|
|
150
|
-
timeout: number;
|
|
151
135
|
concurrency?: number;
|
|
152
136
|
origin?: string;
|
|
137
|
+
base?: string;
|
|
153
138
|
}): AsyncGenerator<Result, void, unknown>;
|
|
154
139
|
}
|
|
155
140
|
}
|
|
@@ -1,18 +1,16 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { compile } from 'path-to-regexp';
|
|
3
|
+
import { BasePath } from '../utils/base-path.js';
|
|
3
4
|
import { Logger } from '../utils/logger.js';
|
|
4
|
-
import { Time } from '../utils/time.js';
|
|
5
5
|
import { Solas } from '../solas.js';
|
|
6
6
|
import { toPathPattern } from './http-router/utils.js';
|
|
7
7
|
const logger = new Logger();
|
|
8
8
|
export { Prerender };
|
|
9
9
|
var Prerender;
|
|
10
10
|
(function (Prerender) {
|
|
11
|
-
const DEFAULT_TIMEOUT_MS = 15_000;
|
|
12
11
|
const DEFAULT_CONCURRENCY = 4;
|
|
13
12
|
let Artifact;
|
|
14
13
|
(function (Artifact) {
|
|
15
|
-
const manifestCache = new Map();
|
|
16
14
|
/**
|
|
17
15
|
* Check whether a file name is safe to join under an artifact directory
|
|
18
16
|
*/
|
|
@@ -30,14 +28,6 @@ var Prerender;
|
|
|
30
28
|
return path.join(outDir, Solas.Config.GENERATED_DIR, 'ppr');
|
|
31
29
|
}
|
|
32
30
|
Artifact.getRootPath = getRootPath;
|
|
33
|
-
/**
|
|
34
|
-
* Get the file system path for the prerender artifact manifest, which
|
|
35
|
-
* contains metadata about all prerendered routes and their artifacts
|
|
36
|
-
*/
|
|
37
|
-
function getManifestPath(outDir) {
|
|
38
|
-
return path.join(getRootPath(outDir), 'manifest.json');
|
|
39
|
-
}
|
|
40
|
-
Artifact.getManifestPath = getManifestPath;
|
|
41
31
|
/**
|
|
42
32
|
* Get the file system path for storing prerender artifacts for a given route
|
|
43
33
|
*/
|
|
@@ -66,73 +56,6 @@ var Prerender;
|
|
|
66
56
|
* File name used for saved full-prerender html inside each route artifact directory
|
|
67
57
|
*/
|
|
68
58
|
Artifact.FULL_PRERENDER_FILENAME = 'prerendered.html';
|
|
69
|
-
/**
|
|
70
|
-
* Load the prerender artifact manifest for faster runtime route mode checks
|
|
71
|
-
*/
|
|
72
|
-
async function loadManifest(outDir) {
|
|
73
|
-
// if we already loaded this outDir, return cached result
|
|
74
|
-
// (either a valid manifest or null when it was missing/invalid)
|
|
75
|
-
if (manifestCache.has(outDir)) {
|
|
76
|
-
return manifestCache.get(outDir) ?? null;
|
|
77
|
-
}
|
|
78
|
-
const file = Bun.file(getManifestPath(outDir));
|
|
79
|
-
// no manifest means no prerender metadata to use
|
|
80
|
-
if (!(await file.exists())) {
|
|
81
|
-
manifestCache.set(outDir, null);
|
|
82
|
-
return null;
|
|
83
|
-
}
|
|
84
|
-
try {
|
|
85
|
-
// parse once, then validate the shape before trusting any fields
|
|
86
|
-
const value = JSON.parse(await file.text());
|
|
87
|
-
if (!value || typeof value !== 'object') {
|
|
88
|
-
manifestCache.set(outDir, null);
|
|
89
|
-
return null;
|
|
90
|
-
}
|
|
91
|
-
const routes = value.routes;
|
|
92
|
-
if (!routes || typeof routes !== 'object') {
|
|
93
|
-
manifestCache.set(outDir, null);
|
|
94
|
-
return null;
|
|
95
|
-
}
|
|
96
|
-
// verify each route entry so runtime can rely on mode/files safely
|
|
97
|
-
for (const entry of Object.values(routes)) {
|
|
98
|
-
if (!entry || typeof entry !== 'object') {
|
|
99
|
-
manifestCache.set(outDir, null);
|
|
100
|
-
return null;
|
|
101
|
-
}
|
|
102
|
-
const { mode, files } = entry;
|
|
103
|
-
// only allow known modes
|
|
104
|
-
if (mode !== 'full' && mode !== 'ppr') {
|
|
105
|
-
manifestCache.set(outDir, null);
|
|
106
|
-
return null;
|
|
107
|
-
}
|
|
108
|
-
if (files !== undefined) {
|
|
109
|
-
if (!Array.isArray(files)) {
|
|
110
|
-
manifestCache.set(outDir, null);
|
|
111
|
-
return null;
|
|
112
|
-
}
|
|
113
|
-
// only allow known artifact file labels
|
|
114
|
-
for (const f of files) {
|
|
115
|
-
if (f !== 'html' &&
|
|
116
|
-
f !== 'prelude' &&
|
|
117
|
-
f !== 'postponed' &&
|
|
118
|
-
f !== 'metadata') {
|
|
119
|
-
manifestCache.set(outDir, null);
|
|
120
|
-
return null;
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
const manifest = routes;
|
|
126
|
-
// cache validated manifest to avoid reparsing on every request
|
|
127
|
-
manifestCache.set(outDir, manifest);
|
|
128
|
-
return manifest;
|
|
129
|
-
}
|
|
130
|
-
catch {
|
|
131
|
-
manifestCache.set(outDir, null);
|
|
132
|
-
return null;
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
Artifact.loadManifest = loadManifest;
|
|
136
59
|
/**
|
|
137
60
|
* Load the postponed state for a given route from the file system, if it exists
|
|
138
61
|
*/
|
|
@@ -319,18 +242,6 @@ var Prerender;
|
|
|
319
242
|
})(Runtime = Prerender.Runtime || (Prerender.Runtime = {}));
|
|
320
243
|
let Build;
|
|
321
244
|
(function (Build) {
|
|
322
|
-
/**
|
|
323
|
-
* Get the prerender timeout value from the environment variable, or return the default
|
|
324
|
-
* if it's not set or invalid
|
|
325
|
-
*/
|
|
326
|
-
function getTimeout() {
|
|
327
|
-
const v = Number(process.env.SOLAS_PRERENDER_TIMEOUT_MS);
|
|
328
|
-
if (!Number.isFinite(v) || v <= 0) {
|
|
329
|
-
return DEFAULT_TIMEOUT_MS;
|
|
330
|
-
}
|
|
331
|
-
return v;
|
|
332
|
-
}
|
|
333
|
-
Build.getTimeout = getTimeout;
|
|
334
245
|
/**
|
|
335
246
|
* Get the prerender concurrency value from the environment variable, or return the default
|
|
336
247
|
* if it's not set or invalid
|
|
@@ -359,7 +270,7 @@ var Prerender;
|
|
|
359
270
|
const params = await buildContext.exportReader.value(filePath, 'params', (v) => typeof v === 'function');
|
|
360
271
|
if (!params)
|
|
361
272
|
return [];
|
|
362
|
-
const resolved = await
|
|
273
|
+
const resolved = await Promise.try(() => params());
|
|
363
274
|
if (!Array.isArray(resolved))
|
|
364
275
|
return [];
|
|
365
276
|
return resolved;
|
|
@@ -402,14 +313,14 @@ var Prerender;
|
|
|
402
313
|
* postponed to request-time
|
|
403
314
|
*/
|
|
404
315
|
async function get(app, route, opts) {
|
|
405
|
-
const url = `${opts.origin ?? `http://${Solas.Config.SLUG}.local`}${route}`;
|
|
406
|
-
const res = await
|
|
316
|
+
const url = `${opts.origin ?? `http://${Solas.Config.SLUG}.local`}${BasePath.apply(route, opts.base)}`;
|
|
317
|
+
const res = await app.fetch(new Request(url, {
|
|
407
318
|
headers: {
|
|
408
319
|
Accept: 'text/html',
|
|
409
320
|
[`x-${Solas.Config.SLUG}-prerender`]: '1',
|
|
410
321
|
[`x-${Solas.Config.SLUG}-prerender-artifact`]: '1',
|
|
411
322
|
},
|
|
412
|
-
}))
|
|
323
|
+
}));
|
|
413
324
|
if (!(res instanceof Response)) {
|
|
414
325
|
const error = new TypeError(`Invalid response for ${route}`);
|
|
415
326
|
logger.error(`[prerender:get] ${error.message}`, error);
|
|
@@ -421,9 +332,8 @@ var Prerender;
|
|
|
421
332
|
}
|
|
422
333
|
Build.get = get;
|
|
423
334
|
/**
|
|
424
|
-
* Run the prerendering process for a list of routes
|
|
425
|
-
*
|
|
426
|
-
* become available
|
|
335
|
+
* Run the prerendering process for a list of routes with a specified concurrency limit,
|
|
336
|
+
* by calling the 'get' function for each route and yielding the results as they become available
|
|
427
337
|
*/
|
|
428
338
|
async function* run(app, routes, opts) {
|
|
429
339
|
const limit = Math.max(1, Math.min(opts.concurrency ?? 4, routes.length || 1));
|
|
@@ -434,7 +344,7 @@ var Prerender;
|
|
|
434
344
|
const i = index++;
|
|
435
345
|
const value = routes[i];
|
|
436
346
|
pending.set(i, get(app, value, {
|
|
437
|
-
|
|
347
|
+
base: opts.base,
|
|
438
348
|
origin: opts.origin,
|
|
439
349
|
})
|
|
440
350
|
.then(result => ({ index: i, result }))
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collect the root request paths for files that originate in Vite's public dir.
|
|
3
|
+
* Vite copies these files into the built client output unchanged. Solas stores
|
|
4
|
+
* the request paths here so runtime serving can whitelist them
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```ts
|
|
8
|
+
* // public/robots.txt
|
|
9
|
+
* // becomes '/robots.txt'
|
|
10
|
+
* ```
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* // public/images/logo 1.png
|
|
15
|
+
* // becomes '/images/logo%201.png'
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
export declare function collect(root: string | false | null | undefined): Promise<string[]>;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { Solas } from '../solas.js';
|
|
4
|
+
/**
|
|
5
|
+
* Collect the root request paths for files that originate in Vite's public dir.
|
|
6
|
+
* Vite copies these files into the built client output unchanged. Solas stores
|
|
7
|
+
* the request paths here so runtime serving can whitelist them
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* // public/robots.txt
|
|
12
|
+
* // becomes '/robots.txt'
|
|
13
|
+
* ```
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```ts
|
|
17
|
+
* // public/images/logo 1.png
|
|
18
|
+
* // becomes '/images/logo%201.png'
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export async function collect(root) {
|
|
22
|
+
if (!root)
|
|
23
|
+
return [];
|
|
24
|
+
// normalise the configured public dir before we start walking it
|
|
25
|
+
const publicRoot = path.resolve(root);
|
|
26
|
+
try {
|
|
27
|
+
const stat = await fs.stat(publicRoot);
|
|
28
|
+
if (!stat.isDirectory())
|
|
29
|
+
return [];
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
const files = [];
|
|
35
|
+
async function walk(dir, parts = []) {
|
|
36
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
37
|
+
for (const entry of entries) {
|
|
38
|
+
// top-level /public/_solas would collide with framework assets served
|
|
39
|
+
// from /_solas, so keep that namespace reserved
|
|
40
|
+
if (parts.length === 0 &&
|
|
41
|
+
entry.isDirectory() &&
|
|
42
|
+
entry.name === Solas.Config.ASSETS_DIR) {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
const nextParts = [...parts, entry.name];
|
|
46
|
+
const nextPath = path.join(dir, entry.name);
|
|
47
|
+
if (entry.isDirectory()) {
|
|
48
|
+
await walk(nextPath, nextParts);
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (!entry.isFile())
|
|
52
|
+
continue;
|
|
53
|
+
// store the external request path, not the filesystem path
|
|
54
|
+
// eg public/favicon.ico -> /favicon.ico
|
|
55
|
+
// eg public/images/logo 1.png -> /images/logo%201.png
|
|
56
|
+
// encode each segment so manifest lookups match routed URLs
|
|
57
|
+
files.push(`/${nextParts.map(part => encodeURIComponent(part)).join('/')}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
await walk(publicRoot);
|
|
61
|
+
// keep manifest output stable regardless of directory iteration order
|
|
62
|
+
return files.sort();
|
|
63
|
+
}
|
|
@@ -11,9 +11,6 @@ type Match = NonNullable<Resolver.EnhancedMatch>;
|
|
|
11
11
|
* 2. `Suspense` with that segment's loading fallback
|
|
12
12
|
* 3. `HttpExceptionBoundary` with that segment's status boundaries
|
|
13
13
|
*
|
|
14
|
-
* The shell level is applied last using the same outer wrapper order:
|
|
15
|
-
* `HttpExceptionBoundary` -> `Suspense` -> `Shell`
|
|
16
|
-
*
|
|
17
14
|
* @example
|
|
18
15
|
* ```tsx
|
|
19
16
|
* <HttpExceptionBoundary shell>
|
|
@@ -17,9 +17,6 @@ const SERVER_ERROR = new HttpException(500, 'Internal Server Error');
|
|
|
17
17
|
* 2. `Suspense` with that segment's loading fallback
|
|
18
18
|
* 3. `HttpExceptionBoundary` with that segment's status boundaries
|
|
19
19
|
*
|
|
20
|
-
* The shell level is applied last using the same outer wrapper order:
|
|
21
|
-
* `HttpExceptionBoundary` -> `Suspense` -> `Shell`
|
|
22
|
-
*
|
|
23
20
|
* @example
|
|
24
21
|
* ```tsx
|
|
25
22
|
* <HttpExceptionBoundary shell>
|
|
@@ -63,7 +60,7 @@ export function Tree({ depth, params, error, ui, }) {
|
|
|
63
60
|
const Exception = httpExceptionMap[error.status].slice(0, depth + 1).findLast(e => e !== null) ??
|
|
64
61
|
DefaultErr;
|
|
65
62
|
inner = (_jsxs(_Fragment, { children: [
|
|
66
|
-
_jsx("meta", { name: "robots", content: "noindex,nofollow" }), _jsx(Exception, { error: error })
|
|
63
|
+
_jsx("meta", { name: "robots", content: "noindex,nofollow" }), _jsx(Exception, { error: error, params: params })
|
|
67
64
|
] }));
|
|
68
65
|
}
|
|
69
66
|
else if (Page) {
|
|
@@ -96,8 +93,6 @@ export function Tree({ depth, params, error, ui, }) {
|
|
|
96
93
|
inner = (_jsx(HttpExceptionBoundary, { components: errorBoundaries, children: inner }));
|
|
97
94
|
}
|
|
98
95
|
}
|
|
99
|
-
// now wrap with shell structure: shell renders immediately,
|
|
100
|
-
// inner streams inside Suspense
|
|
101
96
|
const ShellLoading = loaders[0];
|
|
102
97
|
const ShellUnauthorised = unauthorised[0];
|
|
103
98
|
const ShellForbidden = forbidden[0];
|