@jk2908/solas 0.5.3 → 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.
Files changed (69) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +81 -85
  3. package/dist/config.d.ts +25 -0
  4. package/dist/config.js +152 -0
  5. package/dist/events.d.ts +4 -0
  6. package/dist/events.js +5 -0
  7. package/dist/index.d.ts +1 -2
  8. package/dist/index.js +59 -37
  9. package/dist/internal/boundary-error.d.ts +4 -0
  10. package/dist/internal/boundary-error.js +8 -0
  11. package/dist/internal/browser-router/history.d.ts +46 -0
  12. package/dist/internal/browser-router/history.js +131 -0
  13. package/dist/internal/browser-router/link.d.ts +1 -1
  14. package/dist/internal/browser-router/link.js +1 -1
  15. package/dist/internal/browser-router/router.d.ts +6 -14
  16. package/dist/internal/browser-router/router.js +39 -13
  17. package/dist/internal/browser-router/shared.d.ts +183 -164
  18. package/dist/internal/browser-router/shared.js +49 -56
  19. package/dist/internal/browser-router/use-router.d.ts +1 -10
  20. package/dist/internal/browser-router/use-search-params.js +3 -3
  21. package/dist/internal/build.d.ts +108 -110
  22. package/dist/internal/build.js +569 -581
  23. package/dist/internal/codegen/config.js +3 -3
  24. package/dist/internal/codegen/environments.js +5 -6
  25. package/dist/internal/codegen/format.d.ts +6 -0
  26. package/dist/internal/codegen/format.js +24 -0
  27. package/dist/internal/codegen/manifest.js +2 -2
  28. package/dist/internal/codegen/maps.d.ts +1 -1
  29. package/dist/internal/codegen/maps.js +2 -2
  30. package/dist/internal/codegen/types.js +4 -4
  31. package/dist/internal/codegen/utils.js +2 -2
  32. package/dist/internal/env/request-context.js +1 -1
  33. package/dist/internal/env/rsc.d.ts +6 -4
  34. package/dist/internal/env/rsc.js +27 -25
  35. package/dist/internal/env/ssr.js +4 -4
  36. package/dist/internal/http-router/router.js +8 -8
  37. package/dist/internal/metadata.d.ts +1 -1
  38. package/dist/internal/metadata.js +1 -1
  39. package/dist/internal/navigation/http-exception-boundary.d.ts +1 -1
  40. package/dist/internal/navigation/redirect-boundary.d.ts +1 -1
  41. package/dist/internal/navigation/redirect.js +6 -6
  42. package/dist/internal/postbuild.js +18 -17
  43. package/dist/internal/prerender.d.ts +130 -138
  44. package/dist/internal/prerender.js +326 -354
  45. package/dist/internal/public-files.js +2 -4
  46. package/dist/internal/render/head.js +2 -2
  47. package/dist/internal/resolver.js +1 -1
  48. package/dist/internal/server/actions.js +2 -2
  49. package/dist/internal/ui/error-boundary.d.ts +1 -1
  50. package/dist/manifest.d.ts +7 -0
  51. package/dist/manifest.js +85 -0
  52. package/dist/prerender.d.ts +1 -1
  53. package/dist/prerender.js +1 -1
  54. package/dist/solas.d.ts +2 -46
  55. package/dist/solas.js +6 -266
  56. package/dist/types.d.ts +14 -21
  57. package/dist/types.js +1 -1
  58. package/dist/utils/base-path.d.ts +12 -14
  59. package/dist/utils/base-path.js +75 -82
  60. package/dist/utils/compress.d.ts +9 -11
  61. package/dist/utils/compress.js +59 -64
  62. package/dist/utils/context.d.ts +4 -6
  63. package/dist/utils/context.js +17 -22
  64. package/dist/utils/logger.js +2 -2
  65. package/dist/utils/time.d.ts +2 -4
  66. package/dist/utils/time.js +23 -29
  67. package/package.json +18 -8
  68. package/dist/runtimes/bun.d.ts +0 -13
  69. package/dist/runtimes/bun.js +0 -39
@@ -1,368 +1,340 @@
1
1
  import path from 'node:path';
2
2
  import { compile } from 'path-to-regexp';
3
- import { BasePath } from '../utils/base-path.js';
3
+ import { applyBasePath } from '../utils/base-path.js';
4
4
  import { Logger } from '../utils/logger.js';
5
- import { Solas } from '../solas.js';
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
- export { Prerender };
10
- var Prerender;
11
- (function (Prerender) {
12
- const DEFAULT_CONCURRENCY = 4;
13
- let Artifact;
14
- (function (Artifact) {
15
- /**
16
- * Check whether a file name is safe to join under an artifact directory
17
- */
18
- function isArtifactFileName(value) {
19
- return (typeof value === 'string' &&
20
- value.length > 0 &&
21
- path.basename(value) === value &&
22
- !value.includes(path.sep));
23
- }
24
- /**
25
- * Get the root directory path where prerender artifacts are stored,
26
- * based on the output directory specified in the configuration
27
- */
28
- function getRootPath(outDir) {
29
- return path.join(outDir, Solas.Config.GENERATED_DIR, 'ppr');
30
- }
31
- Artifact.getRootPath = getRootPath;
32
- /**
33
- * Get the file system path for storing prerender artifacts for a given route
34
- */
35
- function getPath(outDir, pathname) {
36
- const root = path.resolve(getRootPath(outDir));
37
- const dir = pathname === '/' ? 'index' : pathname.replace(/^\//, '');
38
- const artifactPath = path.resolve(root, dir);
39
- // this also runs at request time, so make sure the pathname cannot escape the artifact folder
40
- if (artifactPath !== root && !artifactPath.startsWith(`${root}${path.sep}`)) {
41
- throw new Error('[prerender] invalid artifact path');
42
- }
43
- return artifactPath;
44
- }
45
- Artifact.getPath = getPath;
46
- /**
47
- * Get the file system path for a single prerender artifact file under a route directory
48
- */
49
- function getFilePath(outDir, pathname, fileName) {
50
- if (!isArtifactFileName(fileName)) {
51
- throw new Error('[prerender] invalid artifact file name');
52
- }
53
- return path.join(getPath(outDir, pathname), fileName);
54
- }
55
- Artifact.getFilePath = getFilePath;
56
- /**
57
- * File name used for saved full-prerender html inside each route artifact directory
58
- */
59
- Artifact.FULL_PRERENDER_FILENAME = 'prerendered.html';
60
- /**
61
- * Load the postponed state for a given route from the file system, if it exists
62
- */
63
- async function loadPostponedState(outDir, pathname) {
64
- let filePath;
65
- try {
66
- filePath = path.join(getPath(outDir, pathname), 'postponed.json');
67
- }
68
- catch (err) {
69
- logger.warn(`[prerender:artifacts] rejected postponed state path for ${pathname}`, Logger.print(err));
70
- return null;
71
- }
72
- if (!(await AdapterRuntime.exists(filePath)))
73
- return null;
74
- try {
75
- return JSON.parse(await AdapterRuntime.readText(filePath));
76
- }
77
- catch {
78
- return null;
79
- }
80
- }
81
- Artifact.loadPostponedState = loadPostponedState;
82
- /**
83
- * Load the prelude HTML for a given route from the file system, if it exists
84
- */
85
- async function loadPrelude(outDir, pathname) {
86
- let filePath;
87
- try {
88
- filePath = path.join(getPath(outDir, pathname), 'prelude.html');
89
- }
90
- catch (err) {
91
- logger.warn(`[prerender:artifacts] rejected prelude path for ${pathname}`, Logger.print(err));
92
- return null;
93
- }
94
- if (!(await AdapterRuntime.exists(filePath)))
95
- return null;
96
- try {
97
- return await AdapterRuntime.readText(filePath);
98
- }
99
- catch {
100
- return null;
101
- }
102
- }
103
- Artifact.loadPrelude = loadPrelude;
104
- /**
105
- * Load the prerender artifact metadata for a given route from the file system, if it exists and is valid
106
- */
107
- async function loadMetadata(outDir, pathname) {
108
- let filePath;
109
- try {
110
- filePath = path.join(getPath(outDir, pathname), 'metadata.json');
111
- }
112
- catch (err) {
113
- logger.warn(`[prerender:artifacts] rejected metadata path for ${pathname}`, Logger.print(err));
114
- return null;
115
- }
116
- if (!(await AdapterRuntime.exists(filePath)))
117
- return null;
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
- const value = JSON.parse(await AdapterRuntime.readText(filePath));
120
- if (!value || typeof value !== 'object')
121
- return null;
122
- const schema = value.schema;
123
- const route = value.route;
124
- const createdAt = value.createdAt;
125
- const mode = value.mode;
126
- if (typeof schema !== 'string')
127
- return null;
128
- if (typeof route !== 'string')
129
- return null;
130
- if (typeof createdAt !== 'number')
131
- return null;
132
- if (mode !== 'full' && mode !== 'ppr')
133
- return null;
134
- return {
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
- finally {
208
- reader.releaseLock();
209
- controller.close();
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
- Runtime.Postponed = Postponed;
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
- return false;
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
- Runtime.isPostponed = isPostponed;
243
- })(Runtime = Prerender.Runtime || (Prerender.Runtime = {}));
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
- Build.getConcurrency = getConcurrency;
258
- /**
259
- * Extract the prerendering mode ('full', 'ppr', or false) from the source code of a route module, by
260
- * looking for an exported `prerender` binding and validating its value
261
- */
262
- async function getStaticFlag(filePath, buildContext) {
263
- return buildContext.exportReader.literal(filePath, 'prerender', (v) => v === 'full' || v === 'ppr' || v === false);
264
- }
265
- Build.getStaticFlag = getStaticFlag;
266
- /**
267
- * Get the list of static parameters for a dynamic route, by looking for an exported `params` function
268
- * in the route module and calling it to get the list of parameter objects
269
- */
270
- async function getStaticParams(filePath, buildContext) {
271
- const params = await buildContext.exportReader.value(filePath, 'params', (v) => typeof v === 'function');
272
- if (!params)
273
- return [];
274
- const resolved = await Promise.resolve().then(() => params());
275
- if (!Array.isArray(resolved))
276
- return [];
277
- return resolved;
278
- }
279
- Build.getStaticParams = getStaticParams;
280
- /**
281
- * Generate the list of prerenderable routes for a dynamic route, by combining the static parameters obtained from
282
- * the route module with the route pattern, and filtering out any routes that still contain dynamic segments
283
- */
284
- function getDynamicRouteList(route, paramNames, staticParams) {
285
- if (!staticParams.length)
286
- return [];
287
- const { path: compilePath, wildcardNames } = toPathPattern(route, paramNames);
288
- const toPath = compile(compilePath);
289
- return staticParams
290
- .map(value => {
291
- try {
292
- return toPath(Object.fromEntries(Object.entries(value).map(([key, entry]) => [
293
- key,
294
- wildcardNames.has(key)
295
- ? Array.isArray(entry)
296
- ? entry.map(part => String(part))
297
- : [String(entry)]
298
- : Array.isArray(entry)
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
- .filter((value) => value !== null)
308
- .filter(r => !r.includes(':') && !r.includes('*'));
309
- }
310
- Build.getDynamicRouteList = getDynamicRouteList;
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
- Build.run = run;
367
- })(Build = Prerender.Build || (Prerender.Build = {}));
368
- })(Prerender || (Prerender = {}));
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
+ }