@jk2908/solas 0.5.4 → 0.6.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 +81 -85
- package/dist/config.d.ts +25 -0
- package/dist/config.js +152 -0
- package/dist/events.d.ts +4 -0
- package/dist/events.js +5 -0
- package/dist/index.d.ts +1 -2
- package/dist/index.js +59 -37
- package/dist/internal/boundary-error.d.ts +4 -0
- package/dist/internal/boundary-error.js +8 -0
- package/dist/internal/browser-router/history.d.ts +1 -1
- 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 -2
- package/dist/internal/browser-router/router.js +5 -5
- package/dist/internal/browser-router/shared.d.ts +182 -184
- package/dist/internal/browser-router/shared.js +49 -56
- package/dist/internal/browser-router/use-router.d.ts +1 -1
- package/dist/internal/browser-router/use-search-params.js +3 -3
- package/dist/internal/build.d.ts +108 -110
- package/dist/internal/build.js +569 -581
- package/dist/internal/codegen/config.js +3 -3
- package/dist/internal/codegen/environments.js +5 -6
- package/dist/internal/codegen/format.d.ts +6 -0
- package/dist/internal/codegen/format.js +24 -0
- package/dist/internal/codegen/manifest.js +2 -2
- package/dist/internal/codegen/maps.d.ts +1 -1
- package/dist/internal/codegen/maps.js +2 -2
- package/dist/internal/codegen/types.js +4 -4
- package/dist/internal/codegen/utils.js +2 -2
- package/dist/internal/env/request-context.js +1 -1
- package/dist/internal/env/rsc.d.ts +3 -2
- package/dist/internal/env/rsc.js +26 -25
- package/dist/internal/env/ssr.js +4 -4
- package/dist/internal/http-router/router.js +8 -8
- package/dist/internal/metadata.d.ts +1 -1
- package/dist/internal/metadata.js +1 -1
- package/dist/internal/navigation/http-exception-boundary.d.ts +1 -1
- package/dist/internal/navigation/redirect-boundary.d.ts +1 -1
- package/dist/internal/navigation/redirect.js +6 -6
- package/dist/internal/postbuild.js +18 -17
- package/dist/internal/prerender.d.ts +130 -138
- package/dist/internal/prerender.js +326 -354
- package/dist/internal/public-files.js +2 -4
- package/dist/internal/render/head.js +2 -2
- package/dist/internal/resolver.js +1 -1
- package/dist/internal/server/actions.js +2 -2
- package/dist/internal/ui/error-boundary.d.ts +1 -1
- package/dist/manifest.d.ts +7 -0
- package/dist/manifest.js +85 -0
- package/dist/prerender.d.ts +1 -1
- package/dist/prerender.js +1 -1
- package/dist/solas.d.ts +2 -46
- package/dist/solas.js +6 -266
- package/dist/types.d.ts +14 -21
- package/dist/types.js +1 -1
- package/dist/utils/base-path.d.ts +12 -14
- package/dist/utils/base-path.js +75 -82
- package/dist/utils/compress.d.ts +9 -11
- package/dist/utils/compress.js +59 -64
- package/dist/utils/context.d.ts +4 -6
- package/dist/utils/context.js +17 -22
- package/dist/utils/logger.js +2 -2
- package/dist/utils/time.d.ts +2 -4
- package/dist/utils/time.js +23 -29
- package/package.json +18 -8
|
@@ -1,368 +1,340 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { compile } from 'path-to-regexp';
|
|
3
|
-
import {
|
|
3
|
+
import { applyBasePath } from '../utils/base-path.js';
|
|
4
4
|
import { Logger } from '../utils/logger.js';
|
|
5
|
-
import
|
|
5
|
+
import * as Config from '../config.js';
|
|
6
|
+
import * as Solas from '../solas.js';
|
|
6
7
|
import { toPathPattern } from './http-router/utils.js';
|
|
7
8
|
import { Runtime as AdapterRuntime } from './runtimes/runtime.js';
|
|
8
9
|
const logger = new Logger();
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
10
|
+
const DEFAULT_CONCURRENCY = 4;
|
|
11
|
+
/**
|
|
12
|
+
* Check whether a file name is safe to join under an artifact directory
|
|
13
|
+
*/
|
|
14
|
+
function isArtifactFileName(value) {
|
|
15
|
+
return (typeof value === 'string' &&
|
|
16
|
+
value.length > 0 &&
|
|
17
|
+
path.basename(value) === value &&
|
|
18
|
+
!value.includes(path.sep));
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Get the root directory path where prerender artifacts are stored,
|
|
22
|
+
* based on the output directory specified in the configuration
|
|
23
|
+
*/
|
|
24
|
+
export function getArtifactRootPath(outDir) {
|
|
25
|
+
return path.join(outDir, Config.GENERATED_DIR, 'ppr');
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Get the file system path for storing prerender artifacts for a given route
|
|
29
|
+
*/
|
|
30
|
+
export function getArtifactPath(outDir, pathname) {
|
|
31
|
+
const root = path.resolve(getArtifactRootPath(outDir));
|
|
32
|
+
const dir = pathname === '/' ? 'index' : pathname.replace(/^\//, '');
|
|
33
|
+
const artifactPath = path.resolve(root, dir);
|
|
34
|
+
// this also runs at request time, so make sure the pathname cannot escape the artifact folder
|
|
35
|
+
if (artifactPath !== root && !artifactPath.startsWith(`${root}${path.sep}`)) {
|
|
36
|
+
throw new Error('[prerender] invalid artifact path');
|
|
37
|
+
}
|
|
38
|
+
return artifactPath;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Get the file system path for a single prerender artifact file under a route directory
|
|
42
|
+
*/
|
|
43
|
+
export function getArtifactFilePath(outDir, pathname, fileName) {
|
|
44
|
+
if (!isArtifactFileName(fileName)) {
|
|
45
|
+
throw new Error('[prerender] invalid artifact file name');
|
|
46
|
+
}
|
|
47
|
+
return path.join(getArtifactPath(outDir, pathname), fileName);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* File name used for saved full-prerender html inside each route artifact directory
|
|
51
|
+
*/
|
|
52
|
+
export const FULL_PRERENDER_FILENAME = 'prerendered.html';
|
|
53
|
+
/**
|
|
54
|
+
* Load the postponed state for a given route from the file system, if it exists
|
|
55
|
+
*/
|
|
56
|
+
export async function loadPostponedState(outDir, pathname) {
|
|
57
|
+
let filePath;
|
|
58
|
+
try {
|
|
59
|
+
filePath = path.join(getArtifactPath(outDir, pathname), 'postponed.json');
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
logger.warn(`[prerender:artifacts] rejected postponed state path for ${pathname}`, Logger.print(err));
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
if (!(await AdapterRuntime.exists(filePath)))
|
|
66
|
+
return null;
|
|
67
|
+
try {
|
|
68
|
+
return JSON.parse(await AdapterRuntime.readText(filePath));
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Load the prelude HTML for a given route from the file system, if it exists
|
|
76
|
+
*/
|
|
77
|
+
export async function loadPrelude(outDir, pathname) {
|
|
78
|
+
let filePath;
|
|
79
|
+
try {
|
|
80
|
+
filePath = path.join(getArtifactPath(outDir, pathname), 'prelude.html');
|
|
81
|
+
}
|
|
82
|
+
catch (err) {
|
|
83
|
+
logger.warn(`[prerender:artifacts] rejected prelude path for ${pathname}`, Logger.print(err));
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
if (!(await AdapterRuntime.exists(filePath)))
|
|
87
|
+
return null;
|
|
88
|
+
try {
|
|
89
|
+
return await AdapterRuntime.readText(filePath);
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Load the prerender artifact metadata for a given route from the file system, if it exists and is valid
|
|
97
|
+
*/
|
|
98
|
+
export async function loadMetadata(outDir, pathname) {
|
|
99
|
+
let filePath;
|
|
100
|
+
try {
|
|
101
|
+
filePath = path.join(getArtifactPath(outDir, pathname), 'metadata.json');
|
|
102
|
+
}
|
|
103
|
+
catch (err) {
|
|
104
|
+
logger.warn(`[prerender:artifacts] rejected metadata path for ${pathname}`, Logger.print(err));
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
if (!(await AdapterRuntime.exists(filePath)))
|
|
108
|
+
return null;
|
|
109
|
+
try {
|
|
110
|
+
const value = JSON.parse(await AdapterRuntime.readText(filePath));
|
|
111
|
+
if (!value || typeof value !== 'object')
|
|
112
|
+
return null;
|
|
113
|
+
const schema = value.schema;
|
|
114
|
+
const route = value.route;
|
|
115
|
+
const createdAt = value.createdAt;
|
|
116
|
+
const mode = value.mode;
|
|
117
|
+
if (typeof schema !== 'string')
|
|
118
|
+
return null;
|
|
119
|
+
if (typeof route !== 'string')
|
|
120
|
+
return null;
|
|
121
|
+
if (typeof createdAt !== 'number')
|
|
122
|
+
return null;
|
|
123
|
+
if (mode !== 'full' && mode !== 'ppr')
|
|
124
|
+
return null;
|
|
125
|
+
return {
|
|
126
|
+
schema,
|
|
127
|
+
route,
|
|
128
|
+
createdAt,
|
|
129
|
+
mode,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Check if a prerender artifact is compatible with the current application version and route,
|
|
138
|
+
* based on its metadata
|
|
139
|
+
*/
|
|
140
|
+
export function isCompatible(artifactMetadata, pathname, mode) {
|
|
141
|
+
const schema = Solas.getVersion();
|
|
142
|
+
return (artifactMetadata.schema === schema &&
|
|
143
|
+
artifactMetadata.route === pathname &&
|
|
144
|
+
artifactMetadata.mode === mode);
|
|
145
|
+
}
|
|
146
|
+
// shared instances, both are stateless so one per module is fine
|
|
147
|
+
const encoder = new TextEncoder();
|
|
148
|
+
const decoder = new TextDecoder();
|
|
149
|
+
/**
|
|
150
|
+
* Compose the prelude HTML and the resume stream into a single HTML stream, by injecting the resume stream
|
|
151
|
+
* into the prelude at the appropriate location (before </body> or </html>)
|
|
152
|
+
*/
|
|
153
|
+
export function composePreludeAndResume(prelude, resumeStream) {
|
|
154
|
+
// `prelude` is the static shell html as one complete string, usually shaped like
|
|
155
|
+
// `<html>...<body>static shell...</body></html>` or an html fragment with no close tags
|
|
156
|
+
// search both cases to avoid duplicating the full string with toLowerCase
|
|
157
|
+
const bodyClose = Math.max(prelude.lastIndexOf('</body>'), prelude.lastIndexOf('</BODY>'));
|
|
158
|
+
const htmlClose = Math.max(prelude.lastIndexOf('</html>'), prelude.lastIndexOf('</HTML>'));
|
|
159
|
+
// prefer inserting before </body>, then before </html>, and fall back
|
|
160
|
+
// to appending when the prelude is only a fragment with no close tags
|
|
161
|
+
const splitAt = bodyClose >= 0 ? bodyClose : htmlClose >= 0 ? htmlClose : prelude.length;
|
|
162
|
+
return new ReadableStream({
|
|
163
|
+
async start(controller) {
|
|
164
|
+
// send everything before the closing tags so the resume stream can be injected
|
|
165
|
+
controller.enqueue(encoder.encode(prelude.slice(0, splitAt)));
|
|
166
|
+
// resumeStream is the html React emits when it resumes postponed work for this page. Its
|
|
167
|
+
// first chunk begins with an extra `</body></html>` pair, then continues with the
|
|
168
|
+
// resumed scripts and markup for the unfinished work. Strip that leading
|
|
169
|
+
// pair once, then pass the rest through unchanged
|
|
170
|
+
const reader = resumeStream.getReader();
|
|
171
|
+
let strippedLeadingClose = false;
|
|
118
172
|
try {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
schema,
|
|
136
|
-
route,
|
|
137
|
-
createdAt,
|
|
138
|
-
mode,
|
|
139
|
-
};
|
|
140
|
-
}
|
|
141
|
-
catch {
|
|
142
|
-
return null;
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
Artifact.loadMetadata = loadMetadata;
|
|
146
|
-
/**
|
|
147
|
-
* Check if a prerender artifact is compatible with the current application version and route,
|
|
148
|
-
* based on its metadata
|
|
149
|
-
*/
|
|
150
|
-
function isCompatible(artifactMetadata, pathname, mode) {
|
|
151
|
-
const schema = Solas.getVersion();
|
|
152
|
-
return (artifactMetadata.schema === schema &&
|
|
153
|
-
artifactMetadata.route === pathname &&
|
|
154
|
-
artifactMetadata.mode === mode);
|
|
155
|
-
}
|
|
156
|
-
Artifact.isCompatible = isCompatible;
|
|
157
|
-
// shared instances, both are stateless so one per module is fine
|
|
158
|
-
const encoder = new TextEncoder();
|
|
159
|
-
const decoder = new TextDecoder();
|
|
160
|
-
/**
|
|
161
|
-
* Compose the prelude HTML and the resume stream into a single HTML stream, by injecting the resume stream
|
|
162
|
-
* into the prelude at the appropriate location (before </body> or </html>)
|
|
163
|
-
*/
|
|
164
|
-
function composePreludeAndResume(prelude, resumeStream) {
|
|
165
|
-
// `prelude` is the static shell html as one complete string, usually shaped like
|
|
166
|
-
// `<html>...<body>static shell...</body></html>` or an html fragment with no close tags
|
|
167
|
-
// search both cases to avoid duplicating the full string with toLowerCase
|
|
168
|
-
const bodyClose = Math.max(prelude.lastIndexOf('</body>'), prelude.lastIndexOf('</BODY>'));
|
|
169
|
-
const htmlClose = Math.max(prelude.lastIndexOf('</html>'), prelude.lastIndexOf('</HTML>'));
|
|
170
|
-
// prefer inserting before </body>, then before </html>, and fall back
|
|
171
|
-
// to appending when the prelude is only a fragment with no close tags
|
|
172
|
-
const splitAt = bodyClose >= 0 ? bodyClose : htmlClose >= 0 ? htmlClose : prelude.length;
|
|
173
|
-
return new ReadableStream({
|
|
174
|
-
async start(controller) {
|
|
175
|
-
// send everything before the closing tags so the resume stream can be injected
|
|
176
|
-
controller.enqueue(encoder.encode(prelude.slice(0, splitAt)));
|
|
177
|
-
// resumeStream is the html React emits when it resumes postponed work for this page. Its
|
|
178
|
-
// first chunk begins with an extra `</body></html>` pair, then continues with the
|
|
179
|
-
// resumed scripts and markup for the unfinished work. Strip that leading
|
|
180
|
-
// pair once, then pass the rest through unchanged
|
|
181
|
-
const reader = resumeStream.getReader();
|
|
182
|
-
let strippedLeadingClose = false;
|
|
183
|
-
try {
|
|
184
|
-
while (true) {
|
|
185
|
-
const { value, done } = await reader.read();
|
|
186
|
-
if (done)
|
|
187
|
-
break;
|
|
188
|
-
if (!value)
|
|
189
|
-
continue;
|
|
190
|
-
if (!strippedLeadingClose) {
|
|
191
|
-
strippedLeadingClose = true;
|
|
192
|
-
const text = decoder.decode(value);
|
|
193
|
-
// we already wrote the prelude up to the insertion point before its closing tags.
|
|
194
|
-
// React's first resumed chunk starts with an extra `</body></html>` pair,
|
|
195
|
-
// so strip that prefix and keep the rest of the chunk
|
|
196
|
-
const trimmed = text.replace(/^\s*<\/body>\s*<\/html>/i, '');
|
|
197
|
-
if (trimmed.length > 0)
|
|
198
|
-
controller.enqueue(encoder.encode(trimmed));
|
|
199
|
-
continue;
|
|
200
|
-
}
|
|
201
|
-
// once the duplicated `</body></html>` prefix is removed, stop trying to
|
|
202
|
-
// interpret the stream and forward each remaining chunk exactly as React
|
|
203
|
-
// emitted it
|
|
204
|
-
controller.enqueue(value);
|
|
205
|
-
}
|
|
173
|
+
while (true) {
|
|
174
|
+
const { value, done } = await reader.read();
|
|
175
|
+
if (done)
|
|
176
|
+
break;
|
|
177
|
+
if (!value)
|
|
178
|
+
continue;
|
|
179
|
+
if (!strippedLeadingClose) {
|
|
180
|
+
strippedLeadingClose = true;
|
|
181
|
+
const text = decoder.decode(value);
|
|
182
|
+
// we already wrote the prelude up to the insertion point before its closing tags.
|
|
183
|
+
// React's first resumed chunk starts with an extra `</body></html>` pair,
|
|
184
|
+
// so strip that prefix and keep the rest of the chunk
|
|
185
|
+
const trimmed = text.replace(/^\s*<\/body>\s*<\/html>/i, '');
|
|
186
|
+
if (trimmed.length > 0)
|
|
187
|
+
controller.enqueue(encoder.encode(trimmed));
|
|
188
|
+
continue;
|
|
206
189
|
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
}
|
|
212
|
-
});
|
|
213
|
-
}
|
|
214
|
-
Artifact.composePreludeAndResume = composePreludeAndResume;
|
|
215
|
-
})(Artifact = Prerender.Artifact || (Prerender.Artifact = {}));
|
|
216
|
-
let Runtime;
|
|
217
|
-
(function (Runtime) {
|
|
218
|
-
/**
|
|
219
|
-
* Custom error class to indicate that prerendering has been postponed to request-time
|
|
220
|
-
*/
|
|
221
|
-
class Postponed extends Error {
|
|
222
|
-
constructor(message = 'postponed') {
|
|
223
|
-
super(message);
|
|
224
|
-
this.name = 'Postponed';
|
|
190
|
+
// once the duplicated `</body></html>` prefix is removed, stop trying to
|
|
191
|
+
// interpret the stream and forward each remaining chunk exactly as React
|
|
192
|
+
// emitted it
|
|
193
|
+
controller.enqueue(value);
|
|
194
|
+
}
|
|
225
195
|
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
* Type guard to check if an error is a Postponed error, including wrapped errors like
|
|
230
|
-
* AbortError or TimeoutError
|
|
231
|
-
*/
|
|
232
|
-
function isPostponed(error) {
|
|
233
|
-
if (error instanceof Postponed)
|
|
234
|
-
return true;
|
|
235
|
-
if (error instanceof Error &&
|
|
236
|
-
(error.name === 'AbortError' || error.name === 'TimeoutError')) {
|
|
237
|
-
if (error.cause instanceof Postponed)
|
|
238
|
-
return true;
|
|
196
|
+
finally {
|
|
197
|
+
reader.releaseLock();
|
|
198
|
+
controller.close();
|
|
239
199
|
}
|
|
240
|
-
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Custom error class to indicate that prerendering has been postponed to request-time
|
|
205
|
+
*/
|
|
206
|
+
export class Postponed extends Error {
|
|
207
|
+
constructor(message = 'postponed') {
|
|
208
|
+
super(message);
|
|
209
|
+
this.name = 'Postponed';
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Type guard to check if an error is a Postponed error, including wrapped errors like
|
|
214
|
+
* AbortError or TimeoutError
|
|
215
|
+
*/
|
|
216
|
+
export function isPostponed(error) {
|
|
217
|
+
if (error instanceof Postponed)
|
|
218
|
+
return true;
|
|
219
|
+
if (error instanceof Error &&
|
|
220
|
+
(error.name === 'AbortError' || error.name === 'TimeoutError')) {
|
|
221
|
+
if (error.cause instanceof Postponed)
|
|
222
|
+
return true;
|
|
223
|
+
}
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Get the prerender concurrency value from the environment variable, or return the default
|
|
228
|
+
* if it's not set or invalid
|
|
229
|
+
*/
|
|
230
|
+
export function getConcurrency() {
|
|
231
|
+
const v = Number(process.env.SOLAS_PRERENDER_CONCURRENCY);
|
|
232
|
+
if (!Number.isInteger(v) || v <= 0) {
|
|
233
|
+
return DEFAULT_CONCURRENCY;
|
|
234
|
+
}
|
|
235
|
+
return v;
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Extract the prerendering mode ('full', 'ppr', or false) from the source code of a route module, by
|
|
239
|
+
* looking for an exported `prerender` binding and validating its value
|
|
240
|
+
*/
|
|
241
|
+
export async function getStaticFlag(filePath, buildContext) {
|
|
242
|
+
return buildContext.exportReader.literal(filePath, 'prerender', (v) => v === 'full' || v === 'ppr' || v === false);
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Get the list of static parameters for a dynamic route, by looking for an exported `params` function
|
|
246
|
+
* in the route module and calling it to get the list of parameter objects
|
|
247
|
+
*/
|
|
248
|
+
export async function getStaticParams(filePath, buildContext) {
|
|
249
|
+
const params = await buildContext.exportReader.value(filePath, 'params', (v) => typeof v === 'function');
|
|
250
|
+
if (!params)
|
|
251
|
+
return [];
|
|
252
|
+
const resolved = await Promise.resolve().then(() => params());
|
|
253
|
+
if (!Array.isArray(resolved))
|
|
254
|
+
return [];
|
|
255
|
+
return resolved;
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Generate the list of prerenderable routes for a dynamic route, by combining the static parameters obtained from
|
|
259
|
+
* the route module with the route pattern, and filtering out any routes that still contain dynamic segments
|
|
260
|
+
*/
|
|
261
|
+
export function getDynamicRouteList(route, paramNames, staticParams) {
|
|
262
|
+
if (!staticParams.length)
|
|
263
|
+
return [];
|
|
264
|
+
const { path: compilePath, wildcardNames } = toPathPattern(route, paramNames);
|
|
265
|
+
const toPath = compile(compilePath);
|
|
266
|
+
return staticParams
|
|
267
|
+
.map(value => {
|
|
268
|
+
try {
|
|
269
|
+
return toPath(Object.fromEntries(Object.entries(value).map(([key, entry]) => [
|
|
270
|
+
key,
|
|
271
|
+
wildcardNames.has(key)
|
|
272
|
+
? Array.isArray(entry)
|
|
273
|
+
? entry.map(part => String(part))
|
|
274
|
+
: [String(entry)]
|
|
275
|
+
: Array.isArray(entry)
|
|
276
|
+
? entry.map(part => String(part)).join('/')
|
|
277
|
+
: String(entry),
|
|
278
|
+
])));
|
|
241
279
|
}
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
let Build;
|
|
245
|
-
(function (Build) {
|
|
246
|
-
/**
|
|
247
|
-
* Get the prerender concurrency value from the environment variable, or return the default
|
|
248
|
-
* if it's not set or invalid
|
|
249
|
-
*/
|
|
250
|
-
function getConcurrency() {
|
|
251
|
-
const v = Number(process.env.SOLAS_PRERENDER_CONCURRENCY);
|
|
252
|
-
if (!Number.isInteger(v) || v <= 0) {
|
|
253
|
-
return DEFAULT_CONCURRENCY;
|
|
254
|
-
}
|
|
255
|
-
return v;
|
|
280
|
+
catch {
|
|
281
|
+
return null;
|
|
256
282
|
}
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
? entry.map(part => String(part)).join('/')
|
|
300
|
-
: String(entry),
|
|
301
|
-
])));
|
|
302
|
-
}
|
|
303
|
-
catch {
|
|
304
|
-
return null;
|
|
305
|
-
}
|
|
283
|
+
})
|
|
284
|
+
.filter((value) => value !== null)
|
|
285
|
+
.filter(r => !r.includes(':') && !r.includes('*'));
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Function to prerender a single route by making a request to the route with special headers, and returning the
|
|
289
|
+
* result which includes either the prerender artifact or an error/status code if the prerendering failed or was
|
|
290
|
+
* postponed to request-time
|
|
291
|
+
*/
|
|
292
|
+
export async function get(app, route, opts) {
|
|
293
|
+
const url = `${opts.origin ?? `http://${Config.SLUG}.local`}${applyBasePath(route, opts.base)}`;
|
|
294
|
+
const res = await app.fetch(new Request(url, {
|
|
295
|
+
headers: {
|
|
296
|
+
Accept: 'text/html',
|
|
297
|
+
[`x-${Config.SLUG}-prerender`]: '1',
|
|
298
|
+
[`x-${Config.SLUG}-prerender-artifact`]: '1',
|
|
299
|
+
},
|
|
300
|
+
}));
|
|
301
|
+
if (!(res instanceof Response)) {
|
|
302
|
+
const error = new TypeError(`Invalid response for ${route}`);
|
|
303
|
+
logger.error(`[prerender:get] ${error.message}`, error);
|
|
304
|
+
throw error;
|
|
305
|
+
}
|
|
306
|
+
if (!res.ok)
|
|
307
|
+
return { route, status: res.status };
|
|
308
|
+
return { route, artifact: await res.json() };
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Run the prerendering process for a list of routes with a specified concurrency limit,
|
|
312
|
+
* by calling the 'get' function for each route and yielding the results as they become available
|
|
313
|
+
*/
|
|
314
|
+
export async function* run(app, routes, opts) {
|
|
315
|
+
const limit = Math.max(1, Math.min(opts.concurrency ?? 4, routes.length || 1));
|
|
316
|
+
let index = 0;
|
|
317
|
+
const pending = new Map();
|
|
318
|
+
function enqueue() {
|
|
319
|
+
while (index < routes.length && pending.size < limit) {
|
|
320
|
+
const i = index++;
|
|
321
|
+
const value = routes[i];
|
|
322
|
+
pending.set(i, get(app, value, {
|
|
323
|
+
base: opts.base,
|
|
324
|
+
origin: opts.origin,
|
|
306
325
|
})
|
|
307
|
-
.
|
|
308
|
-
.
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
* Function to prerender a single route by making a request to the route with special headers, and returning the
|
|
313
|
-
* result which includes either the prerender artifact or an error/status code if the prerendering failed or was
|
|
314
|
-
* postponed to request-time
|
|
315
|
-
*/
|
|
316
|
-
async function get(app, route, opts) {
|
|
317
|
-
const url = `${opts.origin ?? `http://${Solas.Config.SLUG}.local`}${BasePath.apply(route, opts.base)}`;
|
|
318
|
-
const res = await app.fetch(new Request(url, {
|
|
319
|
-
headers: {
|
|
320
|
-
Accept: 'text/html',
|
|
321
|
-
[`x-${Solas.Config.SLUG}-prerender`]: '1',
|
|
322
|
-
[`x-${Solas.Config.SLUG}-prerender-artifact`]: '1',
|
|
323
|
-
},
|
|
324
|
-
}));
|
|
325
|
-
if (!(res instanceof Response)) {
|
|
326
|
-
const error = new TypeError(`Invalid response for ${route}`);
|
|
327
|
-
logger.error(`[prerender:get] ${error.message}`, error);
|
|
328
|
-
throw error;
|
|
329
|
-
}
|
|
330
|
-
if (!res.ok)
|
|
331
|
-
return { route, status: res.status };
|
|
332
|
-
return { route, artifact: await res.json() };
|
|
333
|
-
}
|
|
334
|
-
Build.get = get;
|
|
335
|
-
/**
|
|
336
|
-
* Run the prerendering process for a list of routes with a specified concurrency limit,
|
|
337
|
-
* by calling the 'get' function for each route and yielding the results as they become available
|
|
338
|
-
*/
|
|
339
|
-
async function* run(app, routes, opts) {
|
|
340
|
-
const limit = Math.max(1, Math.min(opts.concurrency ?? 4, routes.length || 1));
|
|
341
|
-
let index = 0;
|
|
342
|
-
const pending = new Map();
|
|
343
|
-
function enqueue() {
|
|
344
|
-
while (index < routes.length && pending.size < limit) {
|
|
345
|
-
const i = index++;
|
|
346
|
-
const value = routes[i];
|
|
347
|
-
pending.set(i, get(app, value, {
|
|
348
|
-
base: opts.base,
|
|
349
|
-
origin: opts.origin,
|
|
350
|
-
})
|
|
351
|
-
.then(result => ({ index: i, result }))
|
|
352
|
-
.catch(err => ({
|
|
353
|
-
index: i,
|
|
354
|
-
result: { route: value, error: err },
|
|
355
|
-
})));
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
enqueue();
|
|
359
|
-
while (pending.size > 0) {
|
|
360
|
-
const settled = await Promise.race(pending.values());
|
|
361
|
-
pending.delete(settled.index);
|
|
362
|
-
yield settled.result;
|
|
363
|
-
enqueue();
|
|
364
|
-
}
|
|
326
|
+
.then(result => ({ index: i, result }))
|
|
327
|
+
.catch(err => ({
|
|
328
|
+
index: i,
|
|
329
|
+
result: { route: value, error: err },
|
|
330
|
+
})));
|
|
365
331
|
}
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
332
|
+
}
|
|
333
|
+
enqueue();
|
|
334
|
+
while (pending.size > 0) {
|
|
335
|
+
const settled = await Promise.race(pending.values());
|
|
336
|
+
pending.delete(settled.index);
|
|
337
|
+
yield settled.result;
|
|
338
|
+
enqueue();
|
|
339
|
+
}
|
|
340
|
+
}
|