@jk2908/solas 0.3.8 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/README.md +66 -6
- package/dist/index.d.ts +1 -1
- package/dist/index.js +16 -2
- 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/codegen/environments.js +5 -4
- package/dist/internal/env/rsc.d.ts +2 -2
- package/dist/internal/env/rsc.js +159 -62
- 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 +4 -4
- package/dist/internal/navigation/http-exception.js +4 -5
- package/dist/internal/navigation/redirect-boundary.js +2 -11
- package/dist/internal/navigation/redirect.d.ts +3 -0
- package/dist/internal/navigation/redirect.js +51 -0
- 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/resolver.d.ts +23 -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/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 +8 -3
- package/dist/utils/base-path.d.ts +14 -0
- package/dist/utils/base-path.js +85 -0
- package/dist/utils/export-reader.js +10 -4
- package/package.json +4 -7
- 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
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import path from 'node:path';
|
|
2
1
|
import { match as createMatch } from 'path-to-regexp';
|
|
2
|
+
import { BasePath } from '../../utils/base-path.js';
|
|
3
3
|
import { Solas } from '../../solas.js';
|
|
4
4
|
import { HttpException } from '../navigation/http-exception.js';
|
|
5
5
|
import { maybeAction } from '../server/actions.js';
|
|
6
|
+
import { enforce } from '../server/csrf.js';
|
|
6
7
|
import { getAlternatePathname, normalisePathname, toPathPattern } from './utils.js';
|
|
8
|
+
const BASE_PATH = BasePath.normalise(import.meta.env.BASE_URL);
|
|
7
9
|
/**
|
|
8
10
|
* Handle routing and matching for server requests
|
|
9
11
|
*/
|
|
@@ -121,7 +123,7 @@ export class HttpRouter {
|
|
|
121
123
|
/**
|
|
122
124
|
* Match a path and method, returning params and route
|
|
123
125
|
*/
|
|
124
|
-
match(path, method) {
|
|
126
|
+
#match(path, method) {
|
|
125
127
|
for (const candidate of HttpRouter.#candidates(path)) {
|
|
126
128
|
const direct = this.#routes.static.get(`${method}:${candidate}`);
|
|
127
129
|
if (direct)
|
|
@@ -163,51 +165,53 @@ export class HttpRouter {
|
|
|
163
165
|
async fetch(req) {
|
|
164
166
|
const url = new URL(req.url);
|
|
165
167
|
const trailingSlash = this.opts.trailingSlash ?? 'never';
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
|
|
168
|
+
const routedPath = BasePath.strip(url.pathname, BASE_PATH);
|
|
169
|
+
const path = routedPath == null
|
|
170
|
+
? null
|
|
171
|
+
: trailingSlash === 'ignore'
|
|
172
|
+
? routedPath
|
|
173
|
+
: normalisePathname(routedPath, trailingSlash);
|
|
169
174
|
let match = null;
|
|
170
175
|
let action = false;
|
|
171
176
|
try {
|
|
172
177
|
const method = req.method.toUpperCase();
|
|
173
|
-
|
|
174
|
-
|
|
178
|
+
const canonicalPathname = path == null ? null : BasePath.apply(path, BASE_PATH);
|
|
179
|
+
if (canonicalPathname != null &&
|
|
180
|
+
(method === 'GET' || method === 'HEAD') &&
|
|
181
|
+
canonicalPathname !== url.pathname) {
|
|
182
|
+
url.pathname = canonicalPathname;
|
|
175
183
|
return Response.redirect(url.toString(), 308);
|
|
176
184
|
}
|
|
177
|
-
if (
|
|
178
|
-
// rebuild the request
|
|
179
|
-
|
|
180
|
-
url.pathname = path;
|
|
185
|
+
if (canonicalPathname != null && canonicalPathname !== url.pathname) {
|
|
186
|
+
// rebuild the request so the rest of the app sees the same path the router used
|
|
187
|
+
url.pathname = canonicalPathname;
|
|
181
188
|
req = new Request(url.toString(), req);
|
|
182
189
|
}
|
|
183
190
|
const { action: isAction, formData: parsedFormData } = await maybeAction(req);
|
|
184
191
|
action = isAction;
|
|
185
|
-
// action requests stay on the same
|
|
186
|
-
//
|
|
187
|
-
|
|
188
|
-
// may redirect()
|
|
189
|
-
match = this.match(path, action ? 'GET' : method);
|
|
192
|
+
// action requests stay on the same path, but we match them like GET so
|
|
193
|
+
// the normal page tree can rerender around the action result
|
|
194
|
+
match = path == null ? null : this.#match(path, action ? 'GET' : method);
|
|
190
195
|
if (!match) {
|
|
191
196
|
const error = new HttpException(404, 'Not found');
|
|
192
|
-
// unmatched requests still
|
|
193
|
-
// same request metadata shape as matched requests
|
|
197
|
+
// unmatched requests still go through the same error hook shape as matched ones
|
|
194
198
|
return (this.#onError?.(error, Object.assign(req, {
|
|
195
199
|
[Solas.Config.REQUEST_META_KEY]: { match: null, error, action },
|
|
196
200
|
})) ?? new Response(error.message, { status: error.status }));
|
|
197
201
|
}
|
|
198
202
|
const matched = match;
|
|
199
|
-
// attach
|
|
200
|
-
// read the same per-request metadata
|
|
203
|
+
// attach route state once so middleware and handlers read the same request data
|
|
201
204
|
const request = Object.assign(req, {
|
|
202
205
|
[Solas.Config.REQUEST_META_KEY]: { match: matched, action, parsedFormData },
|
|
203
206
|
});
|
|
204
|
-
//
|
|
205
|
-
|
|
207
|
+
// check csrf before any middleware or handler runs
|
|
208
|
+
enforce(request, this.opts.csrf);
|
|
209
|
+
// keep global middleware outside route middleware and preserve registration order
|
|
206
210
|
const stack = [...this.#middleware.global, ...matched.route.middleware];
|
|
207
211
|
return this.#run(stack, request, () => matched.route.handler?.(request) ?? new Response('Not found', { status: 404 }));
|
|
208
212
|
}
|
|
209
213
|
catch (err) {
|
|
210
|
-
//
|
|
214
|
+
// turn unknown throws into Error objects so the error hook sees one shape
|
|
211
215
|
const error = err instanceof Error ? err : new Error(String(err), { cause: err });
|
|
212
216
|
const request = Object.assign(req, {
|
|
213
217
|
[Solas.Config.REQUEST_META_KEY]: { match, error, action },
|
|
@@ -224,17 +228,16 @@ export class HttpRouter {
|
|
|
224
228
|
* Run middleware stack
|
|
225
229
|
*/
|
|
226
230
|
#run(stack, req, next) {
|
|
227
|
-
//
|
|
231
|
+
// build the middleware chain from the inside out
|
|
228
232
|
let run = () => Promise.resolve(next());
|
|
229
|
-
//
|
|
233
|
+
// wrap each middleware around the current chain
|
|
230
234
|
for (let i = stack.length - 1; i >= 0; i -= 1) {
|
|
231
235
|
const handler = stack[i];
|
|
232
236
|
const prev = run;
|
|
233
237
|
run = () => {
|
|
234
238
|
let called = false;
|
|
235
239
|
return Promise.resolve(handler(req, () => {
|
|
236
|
-
//
|
|
237
|
-
// only execute once per request
|
|
240
|
+
// next() can only be used once per middleware call
|
|
238
241
|
if (called)
|
|
239
242
|
throw new Error('next() called more than once');
|
|
240
243
|
called = true;
|
|
@@ -242,43 +245,13 @@ export class HttpRouter {
|
|
|
242
245
|
}));
|
|
243
246
|
};
|
|
244
247
|
}
|
|
245
|
-
//
|
|
248
|
+
// start the middleware chain
|
|
246
249
|
return run();
|
|
247
250
|
}
|
|
248
|
-
/**
|
|
249
|
-
* Serve static assets from the output directory
|
|
250
|
-
* @note generated /assets/* handlers bypass +middleware conventions
|
|
251
|
-
*/
|
|
252
|
-
static static(config) {
|
|
253
|
-
return async (req) => {
|
|
254
|
-
const pathname = new URL(req.url).pathname;
|
|
255
|
-
const outDir = path.resolve(Solas.Config.OUT_DIR);
|
|
256
|
-
const staticRoot = path.resolve(outDir, 'client');
|
|
257
|
-
let decodedPathname = pathname;
|
|
258
|
-
try {
|
|
259
|
-
// validate any percent-encoding before resolving the asset path
|
|
260
|
-
decodedPathname = decodeURIComponent(pathname);
|
|
261
|
-
}
|
|
262
|
-
catch {
|
|
263
|
-
return new Response('Bad Request', { status: 400 });
|
|
264
|
-
}
|
|
265
|
-
const relativePath = decodedPathname.replace(/^\/+/, '');
|
|
266
|
-
const filePath = path.resolve(staticRoot, relativePath);
|
|
267
|
-
// keep asset requests pinned under the client output root even if the
|
|
268
|
-
// incoming path contains traversal segments
|
|
269
|
-
if (filePath !== staticRoot && !filePath.startsWith(`${staticRoot}${path.sep}`)) {
|
|
270
|
-
return new Response('Forbidden', { status: 403 });
|
|
271
|
-
}
|
|
272
|
-
// emitted assets are fingerprinted so they can be cached aggressively
|
|
273
|
-
return HttpRouter.serve(filePath, req, config.precompress, {
|
|
274
|
-
'Cache-Control': 'public, immutable, max-age=31536000',
|
|
275
|
-
});
|
|
276
|
-
};
|
|
277
|
-
}
|
|
278
251
|
/**
|
|
279
252
|
* Serve a file with optional compression content negotiation
|
|
280
253
|
*/
|
|
281
|
-
static async
|
|
254
|
+
static async serveStatic(filePath, req, precompress = false, headers = {}) {
|
|
282
255
|
const accept = req.headers.get('accept-encoding') ?? '';
|
|
283
256
|
let file = Bun.file(filePath);
|
|
284
257
|
let encoding = null;
|
|
@@ -8,8 +8,9 @@ export declare namespace HttpException {
|
|
|
8
8
|
}
|
|
9
9
|
export declare const HTTP_EXCEPTION_NAME_MAP: Record<HttpException.StatusCode, string>;
|
|
10
10
|
export type HttpExceptionLike = Pick<Error, 'name' | 'message' | 'stack'> & Partial<Pick<HttpException, 'digest' | 'payload' | 'status'>>;
|
|
11
|
+
export declare const HTTP_EXCEPTION_DIGEST_PREFIX = "HTTP_EXCEPTION";
|
|
11
12
|
/**
|
|
12
|
-
* An exception representing an
|
|
13
|
+
* An exception representing an HttpException error, with an optional payload
|
|
13
14
|
* and cause
|
|
14
15
|
*/
|
|
15
16
|
export declare class HttpException extends Error {
|
|
@@ -19,9 +20,8 @@ export declare class HttpException extends Error {
|
|
|
19
20
|
digest?: string;
|
|
20
21
|
constructor(status: HttpException.StatusCode, message: string, opts?: HttpException.Options);
|
|
21
22
|
}
|
|
22
|
-
export declare const HTTP_EXCEPTION_DIGEST_PREFIX = "HTTP_EXCEPTION";
|
|
23
23
|
/**
|
|
24
|
-
* Check if an error is an
|
|
24
|
+
* Check if an error is an HttpException
|
|
25
25
|
*/
|
|
26
26
|
export declare function isHttpException(err: unknown): err is HttpException;
|
|
27
27
|
/**
|
|
@@ -34,7 +34,7 @@ export declare function toHttpException(err: unknown): HttpException;
|
|
|
34
34
|
*/
|
|
35
35
|
export declare function toHttpExceptionLike(error: HttpException | Error): HttpExceptionLike;
|
|
36
36
|
/**
|
|
37
|
-
* Throw an
|
|
37
|
+
* Throw an HttpException
|
|
38
38
|
*/
|
|
39
39
|
export declare function abort(status: HttpException.StatusCode, message: string, opts?: {
|
|
40
40
|
payload?: HttpException.Payload;
|
|
@@ -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,7 +23,6 @@ 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
28
|
*/
|
|
@@ -30,7 +30,7 @@ function isStatusCode(value) {
|
|
|
30
30
|
return value === 401 || value === 403 || value === 404 || value === 500;
|
|
31
31
|
}
|
|
32
32
|
/**
|
|
33
|
-
* Check if an error is an
|
|
33
|
+
* Check if an error is an HttpException
|
|
34
34
|
*/
|
|
35
35
|
export function isHttpException(err) {
|
|
36
36
|
return (typeof err === 'object' &&
|
|
@@ -82,7 +82,6 @@ export function toHttpExceptionLike(error) {
|
|
|
82
82
|
return {
|
|
83
83
|
name: error.name,
|
|
84
84
|
message: error.message,
|
|
85
|
-
stack: error.stack,
|
|
86
85
|
...('digest' in error && typeof error.digest === 'string'
|
|
87
86
|
? { digest: error.digest }
|
|
88
87
|
: {}),
|
|
@@ -93,7 +92,7 @@ export function toHttpExceptionLike(error) {
|
|
|
93
92
|
};
|
|
94
93
|
}
|
|
95
94
|
/**
|
|
96
|
-
* Throw an
|
|
95
|
+
* Throw an HttpException
|
|
97
96
|
*/
|
|
98
97
|
export function abort(status, message, opts) {
|
|
99
98
|
throw new HttpException(status, message, {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
3
|
import { Component } from 'react';
|
|
4
|
-
import { isRedirect,
|
|
4
|
+
import { isRedirect, toRedirect } from './redirect.js';
|
|
5
5
|
class Boundary extends Component {
|
|
6
6
|
constructor(props) {
|
|
7
7
|
super(props);
|
|
@@ -27,15 +27,6 @@ export function RedirectBoundary({ children }) {
|
|
|
27
27
|
return (_jsx(Boundary, { fallback: err => {
|
|
28
28
|
if (!isRedirect(err))
|
|
29
29
|
throw err;
|
|
30
|
-
|
|
31
|
-
// rejoin after status so urls with colons (https://...) stay intact
|
|
32
|
-
const [type, , ...parts] = err.digest.split(':');
|
|
33
|
-
if (type === REDIRECT_DIGEST_PREFIX) {
|
|
34
|
-
const url = parts.join(':');
|
|
35
|
-
if (url)
|
|
36
|
-
return _jsx("meta", { httpEquiv: "refresh", content: `0;url=${url}` });
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
return null;
|
|
30
|
+
return _jsx("meta", { httpEquiv: "refresh", content: `0;url=${toRedirect(err).url}` });
|
|
40
31
|
}, children: children }));
|
|
41
32
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export type RedirectStatusCode = 301 | 302 | 307 | 308;
|
|
2
|
+
export type RedirectLike = Pick<Error, 'name' | 'message' | 'stack'> & Partial<Pick<Redirect, 'digest' | 'status' | 'url'>>;
|
|
2
3
|
/**
|
|
3
4
|
* Redirect exception class to signal a redirect
|
|
4
5
|
*/
|
|
@@ -13,6 +14,8 @@ export declare const REDIRECT_DIGEST_PREFIX = "REDIRECT";
|
|
|
13
14
|
* Check if an error is a Redirect error
|
|
14
15
|
*/
|
|
15
16
|
export declare function isRedirect(err: unknown): err is Redirect;
|
|
17
|
+
export declare function toRedirect(err: unknown): Redirect;
|
|
18
|
+
export declare function toRedirectLike(error: Redirect | Error): RedirectLike;
|
|
16
19
|
/**
|
|
17
20
|
* Throws a Redirect exc`eption to signal a redirect
|
|
18
21
|
* @param url - the application-relative URL or absolute http/https URL to redirect to
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { Solas } from '../../solas.js';
|
|
2
|
+
function isRedirectStatusCode(value) {
|
|
3
|
+
return value === 301 || value === 302 || value === 307 || value === 308;
|
|
4
|
+
}
|
|
2
5
|
/**
|
|
3
6
|
* Redirect exception class to signal a redirect
|
|
4
7
|
*/
|
|
@@ -53,6 +56,54 @@ export function isRedirect(err) {
|
|
|
53
56
|
typeof err.digest === 'string' &&
|
|
54
57
|
err.digest.startsWith(REDIRECT_DIGEST_PREFIX));
|
|
55
58
|
}
|
|
59
|
+
export function toRedirect(err) {
|
|
60
|
+
if (err instanceof Redirect)
|
|
61
|
+
return err;
|
|
62
|
+
let digestStatus;
|
|
63
|
+
let digestUrl;
|
|
64
|
+
if (typeof err === 'object' &&
|
|
65
|
+
err !== null &&
|
|
66
|
+
'digest' in err &&
|
|
67
|
+
typeof err.digest === 'string') {
|
|
68
|
+
const [type, rawStatus, ...rawUrlParts] = err.digest.split(':');
|
|
69
|
+
const status = Number(rawStatus);
|
|
70
|
+
if (type === REDIRECT_DIGEST_PREFIX && isRedirectStatusCode(status)) {
|
|
71
|
+
digestStatus = status;
|
|
72
|
+
digestUrl = rawUrlParts.join(':');
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const status = digestStatus ??
|
|
76
|
+
(typeof err === 'object' &&
|
|
77
|
+
err !== null &&
|
|
78
|
+
'status' in err &&
|
|
79
|
+
isRedirectStatusCode(err.status)
|
|
80
|
+
? err.status
|
|
81
|
+
: 307);
|
|
82
|
+
const url = digestUrl ||
|
|
83
|
+
(typeof err === 'object' &&
|
|
84
|
+
err !== null &&
|
|
85
|
+
'url' in err &&
|
|
86
|
+
typeof err.url === 'string'
|
|
87
|
+
? err.url
|
|
88
|
+
: undefined);
|
|
89
|
+
if (!url) {
|
|
90
|
+
throw new TypeError(`[${Solas.Config.NAME}] failed to reconstruct redirect`);
|
|
91
|
+
}
|
|
92
|
+
return new Redirect(url, status);
|
|
93
|
+
}
|
|
94
|
+
export function toRedirectLike(error) {
|
|
95
|
+
return {
|
|
96
|
+
name: error.name,
|
|
97
|
+
message: error.message,
|
|
98
|
+
...('digest' in error && typeof error.digest === 'string'
|
|
99
|
+
? { digest: error.digest }
|
|
100
|
+
: {}),
|
|
101
|
+
...('url' in error && typeof error.url === 'string' ? { url: error.url } : {}),
|
|
102
|
+
...('status' in error && isRedirectStatusCode(error.status)
|
|
103
|
+
? { status: error.status }
|
|
104
|
+
: {}),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
56
107
|
/**
|
|
57
108
|
* Throws a Redirect exc`eption to signal a redirect
|
|
58
109
|
* @param url - the application-relative URL or absolute http/https URL to redirect to
|
|
@@ -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
|
}
|