@jk2908/solas 0.1.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/LICENSE +21 -0
- package/README.md +333 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +219 -0
- package/dist/error-boundary.d.ts +1 -0
- package/dist/error-boundary.js +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +235 -0
- package/dist/internal/build.d.ts +104 -0
- package/dist/internal/build.js +633 -0
- package/dist/internal/codegen/config.d.ts +5 -0
- package/dist/internal/codegen/config.js +19 -0
- package/dist/internal/codegen/environments.d.ts +12 -0
- package/dist/internal/codegen/environments.js +42 -0
- package/dist/internal/codegen/manifest.d.ts +5 -0
- package/dist/internal/codegen/manifest.js +15 -0
- package/dist/internal/codegen/maps.d.ts +5 -0
- package/dist/internal/codegen/maps.js +75 -0
- package/dist/internal/codegen/utils.d.ts +1 -0
- package/dist/internal/codegen/utils.js +2 -0
- package/dist/internal/env/browser.d.ts +4 -0
- package/dist/internal/env/browser.js +58 -0
- package/dist/internal/env/request-context.d.ts +19 -0
- package/dist/internal/env/request-context.js +2 -0
- package/dist/internal/env/rsc.d.ts +39 -0
- package/dist/internal/env/rsc.js +368 -0
- package/dist/internal/env/ssr.d.ts +42 -0
- package/dist/internal/env/ssr.js +149 -0
- package/dist/internal/env/utils.d.ts +2 -0
- package/dist/internal/env/utils.js +28 -0
- package/dist/internal/metadata.d.ts +81 -0
- package/dist/internal/metadata.js +185 -0
- package/dist/internal/navigation/http-exception-boundary.d.ts +12 -0
- package/dist/internal/navigation/http-exception-boundary.js +48 -0
- package/dist/internal/navigation/http-exception.d.ts +33 -0
- package/dist/internal/navigation/http-exception.js +45 -0
- package/dist/internal/navigation/link.d.ts +13 -0
- package/dist/internal/navigation/link.js +63 -0
- package/dist/internal/navigation/redirect-boundary.d.ts +12 -0
- package/dist/internal/navigation/redirect-boundary.js +39 -0
- package/dist/internal/navigation/redirect.d.ts +21 -0
- package/dist/internal/navigation/redirect.js +63 -0
- package/dist/internal/navigation/use-search-params.d.ts +1 -0
- package/dist/internal/navigation/use-search-params.js +13 -0
- package/dist/internal/prerender.d.ts +151 -0
- package/dist/internal/prerender.js +422 -0
- package/dist/internal/render/head.d.ts +4 -0
- package/dist/internal/render/head.js +38 -0
- package/dist/internal/render/tree.d.ts +47 -0
- package/dist/internal/render/tree.js +108 -0
- package/dist/internal/router/create-router.d.ts +6 -0
- package/dist/internal/router/create-router.js +95 -0
- package/dist/internal/router/pattern.d.ts +8 -0
- package/dist/internal/router/pattern.js +31 -0
- package/dist/internal/router/prefetcher.d.ts +47 -0
- package/dist/internal/router/prefetcher.js +90 -0
- package/dist/internal/router/resolver.d.ts +174 -0
- package/dist/internal/router/resolver.js +356 -0
- package/dist/internal/router/router-context.d.ts +11 -0
- package/dist/internal/router/router-context.js +7 -0
- package/dist/internal/router/router-provider.d.ts +6 -0
- package/dist/internal/router/router-provider.js +131 -0
- package/dist/internal/router/router.d.ts +79 -0
- package/dist/internal/router/router.js +417 -0
- package/dist/internal/router/use-router.d.ts +5 -0
- package/dist/internal/router/use-router.js +5 -0
- package/dist/internal/server/cookies.d.ts +6 -0
- package/dist/internal/server/cookies.js +17 -0
- package/dist/internal/server/dynamic.d.ts +9 -0
- package/dist/internal/server/dynamic.js +22 -0
- package/dist/internal/server/headers.d.ts +5 -0
- package/dist/internal/server/headers.js +19 -0
- package/dist/internal/server/url.d.ts +5 -0
- package/dist/internal/server/url.js +16 -0
- package/dist/internal/ui/defaults/error.d.ts +4 -0
- package/dist/internal/ui/defaults/error.js +6 -0
- package/dist/internal/ui/error-boundary.d.ts +26 -0
- package/dist/internal/ui/error-boundary.js +41 -0
- package/dist/navigation.d.ts +6 -0
- package/dist/navigation.js +6 -0
- package/dist/prerender.d.ts +1 -0
- package/dist/prerender.js +1 -0
- package/dist/router.d.ts +4 -0
- package/dist/router.js +4 -0
- package/dist/server.d.ts +4 -0
- package/dist/server.js +4 -0
- package/dist/solas.d.ts +32 -0
- package/dist/solas.js +125 -0
- package/dist/types.d.ts +93 -0
- package/dist/types.js +1 -0
- package/dist/utils/compress.d.ts +11 -0
- package/dist/utils/compress.js +76 -0
- package/dist/utils/context.d.ts +6 -0
- package/dist/utils/context.js +25 -0
- package/dist/utils/cookies.d.ts +3 -0
- package/dist/utils/cookies.js +35 -0
- package/dist/utils/export-reader.d.ts +29 -0
- package/dist/utils/export-reader.js +117 -0
- package/dist/utils/format.d.ts +6 -0
- package/dist/utils/format.js +72 -0
- package/dist/utils/logger.d.ts +52 -0
- package/dist/utils/logger.js +105 -0
- package/dist/utils/time.d.ts +4 -0
- package/dist/utils/time.js +29 -0
- package/package.json +111 -0
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { compile } from 'path-to-regexp';
|
|
3
|
+
import { Solas } from '../solas';
|
|
4
|
+
import { Logger } from '../utils/logger';
|
|
5
|
+
import { Time } from '../utils/time';
|
|
6
|
+
import { toPathPattern } from './router/pattern';
|
|
7
|
+
const logger = new Logger();
|
|
8
|
+
export { Prerender };
|
|
9
|
+
var Prerender;
|
|
10
|
+
(function (Prerender) {
|
|
11
|
+
const DEFAULT_TIMEOUT_MS = 15_000;
|
|
12
|
+
const DEFAULT_CONCURRENCY = 4;
|
|
13
|
+
let Artifact;
|
|
14
|
+
(function (Artifact) {
|
|
15
|
+
const manifestCache = new Map();
|
|
16
|
+
/**
|
|
17
|
+
* Get the root directory path where prerender artifacts are stored,
|
|
18
|
+
* based on the output directory specified in the configuration
|
|
19
|
+
*/
|
|
20
|
+
function getRootPath(outDir) {
|
|
21
|
+
return path.join(outDir, Solas.Config.GENERATED_DIR, 'ppr');
|
|
22
|
+
}
|
|
23
|
+
Artifact.getRootPath = getRootPath;
|
|
24
|
+
/**
|
|
25
|
+
* Get the file system path for the prerender artifact manifest, which
|
|
26
|
+
* contains metadata about all prerendered routes and their artifacts
|
|
27
|
+
*/
|
|
28
|
+
function getManifestPath(outDir) {
|
|
29
|
+
return path.join(getRootPath(outDir), 'manifest.json');
|
|
30
|
+
}
|
|
31
|
+
Artifact.getManifestPath = getManifestPath;
|
|
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
|
+
* Load the prerender artifact manifest for faster runtime route mode checks
|
|
48
|
+
*/
|
|
49
|
+
async function loadManifest(outDir) {
|
|
50
|
+
// if we already loaded this outDir, return cached result
|
|
51
|
+
// (either a valid manifest or null when it was missing/invalid)
|
|
52
|
+
if (manifestCache.has(outDir)) {
|
|
53
|
+
return manifestCache.get(outDir) ?? null;
|
|
54
|
+
}
|
|
55
|
+
const file = Bun.file(getManifestPath(outDir));
|
|
56
|
+
// no manifest means no prerender metadata to use
|
|
57
|
+
if (!(await file.exists())) {
|
|
58
|
+
manifestCache.set(outDir, null);
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
// parse once, then validate the shape before trusting any fields
|
|
63
|
+
const value = JSON.parse(await file.text());
|
|
64
|
+
if (!value || typeof value !== 'object') {
|
|
65
|
+
manifestCache.set(outDir, null);
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
const generatedAt = value.generatedAt;
|
|
69
|
+
const routes = value.routes;
|
|
70
|
+
if (typeof generatedAt !== 'number') {
|
|
71
|
+
manifestCache.set(outDir, null);
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
if (!routes || typeof routes !== 'object') {
|
|
75
|
+
manifestCache.set(outDir, null);
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
// verify each route entry so runtime can rely on mode/files safely
|
|
79
|
+
for (const entry of Object.values(routes)) {
|
|
80
|
+
if (!entry || typeof entry !== 'object') {
|
|
81
|
+
manifestCache.set(outDir, null);
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
const { mode, createdAt, files } = entry;
|
|
85
|
+
// only allow known modes
|
|
86
|
+
if (mode !== 'full' && mode !== 'ppr') {
|
|
87
|
+
manifestCache.set(outDir, null);
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
if (typeof createdAt !== 'number') {
|
|
91
|
+
manifestCache.set(outDir, null);
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
if (files !== undefined) {
|
|
95
|
+
if (!Array.isArray(files)) {
|
|
96
|
+
manifestCache.set(outDir, null);
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
// only allow known artifact file labels
|
|
100
|
+
for (const f of files) {
|
|
101
|
+
if (f !== 'html' &&
|
|
102
|
+
f !== 'prelude' &&
|
|
103
|
+
f !== 'postponed' &&
|
|
104
|
+
f !== 'metadata') {
|
|
105
|
+
manifestCache.set(outDir, null);
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const manifest = { generatedAt, routes };
|
|
112
|
+
// cache validated manifest to avoid reparsing on every request
|
|
113
|
+
manifestCache.set(outDir, manifest);
|
|
114
|
+
return manifest;
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
manifestCache.set(outDir, null);
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
Artifact.loadManifest = loadManifest;
|
|
122
|
+
/**
|
|
123
|
+
* Load the postponed state for a given route from the file system, if it exists
|
|
124
|
+
*/
|
|
125
|
+
async function loadPostponedState(outDir, pathname) {
|
|
126
|
+
let file;
|
|
127
|
+
try {
|
|
128
|
+
file = Bun.file(path.join(getPath(outDir, pathname), 'postponed.json'));
|
|
129
|
+
}
|
|
130
|
+
catch (err) {
|
|
131
|
+
logger.warn(`[prerender:artifacts] rejected postponed state path for ${pathname}`, Logger.print(err));
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
if (!(await file.exists()))
|
|
135
|
+
return null;
|
|
136
|
+
try {
|
|
137
|
+
return JSON.parse(await file.text());
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
Artifact.loadPostponedState = loadPostponedState;
|
|
144
|
+
/**
|
|
145
|
+
* Load the prelude HTML for a given route from the file system, if it exists
|
|
146
|
+
*/
|
|
147
|
+
async function loadPrelude(outDir, pathname) {
|
|
148
|
+
let file;
|
|
149
|
+
try {
|
|
150
|
+
file = Bun.file(path.join(getPath(outDir, pathname), 'prelude.html'));
|
|
151
|
+
}
|
|
152
|
+
catch (err) {
|
|
153
|
+
logger.warn(`[prerender:artifacts] rejected prelude path for ${pathname}`, Logger.print(err));
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
if (!(await file.exists()))
|
|
157
|
+
return null;
|
|
158
|
+
try {
|
|
159
|
+
return await file.text();
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
Artifact.loadPrelude = loadPrelude;
|
|
166
|
+
/**
|
|
167
|
+
* Load the prerender artifact metadata for a given route from the file system, if it exists and is valid
|
|
168
|
+
*/
|
|
169
|
+
async function loadMetadata(outDir, pathname) {
|
|
170
|
+
let file;
|
|
171
|
+
try {
|
|
172
|
+
file = Bun.file(path.join(getPath(outDir, pathname), 'metadata.json'));
|
|
173
|
+
}
|
|
174
|
+
catch (err) {
|
|
175
|
+
logger.warn(`[prerender:artifacts] rejected metadata path for ${pathname}`, Logger.print(err));
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
if (!(await file.exists()))
|
|
179
|
+
return null;
|
|
180
|
+
try {
|
|
181
|
+
const value = JSON.parse(await file.text());
|
|
182
|
+
if (!value || typeof value !== 'object')
|
|
183
|
+
return null;
|
|
184
|
+
const schema = value.schema;
|
|
185
|
+
const route = value.route;
|
|
186
|
+
const createdAt = value.createdAt;
|
|
187
|
+
const mode = value.mode;
|
|
188
|
+
if (typeof schema !== 'string')
|
|
189
|
+
return null;
|
|
190
|
+
if (typeof route !== 'string')
|
|
191
|
+
return null;
|
|
192
|
+
if (typeof createdAt !== 'number')
|
|
193
|
+
return null;
|
|
194
|
+
if (mode !== 'full' && mode !== 'ppr')
|
|
195
|
+
return null;
|
|
196
|
+
return { schema, route, createdAt, mode };
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
Artifact.loadMetadata = loadMetadata;
|
|
203
|
+
/**
|
|
204
|
+
* Check if a prerender artifact is compatible with the current application version and route,
|
|
205
|
+
* based on its metadata
|
|
206
|
+
*/
|
|
207
|
+
function isCompatible(artifactMetadata, pathname, mode) {
|
|
208
|
+
const schema = Solas.getVersion();
|
|
209
|
+
return (artifactMetadata.schema === schema &&
|
|
210
|
+
artifactMetadata.route === pathname &&
|
|
211
|
+
artifactMetadata.mode === mode);
|
|
212
|
+
}
|
|
213
|
+
Artifact.isCompatible = isCompatible;
|
|
214
|
+
/**
|
|
215
|
+
* Compose the prelude HTML and the resume stream into a single HTML stream, by injecting the resume stream
|
|
216
|
+
* into the prelude at the appropriate location (before </body> or </html>)
|
|
217
|
+
*/
|
|
218
|
+
function composePreludeAndResume(prelude, resumeStream) {
|
|
219
|
+
const lower = prelude.toLowerCase();
|
|
220
|
+
const bodyClose = lower.lastIndexOf('</body>');
|
|
221
|
+
const htmlClose = lower.lastIndexOf('</html>');
|
|
222
|
+
const splitAt = bodyClose >= 0 && htmlClose > bodyClose ? bodyClose : prelude.length;
|
|
223
|
+
return new ReadableStream({
|
|
224
|
+
async start(controller) {
|
|
225
|
+
const encoder = new TextEncoder();
|
|
226
|
+
const decoder = new TextDecoder();
|
|
227
|
+
controller.enqueue(new TextEncoder().encode(prelude.slice(0, splitAt)));
|
|
228
|
+
const reader = resumeStream.getReader();
|
|
229
|
+
let strippedLeadingClose = false;
|
|
230
|
+
try {
|
|
231
|
+
while (true) {
|
|
232
|
+
const { value, done } = await reader.read();
|
|
233
|
+
if (done)
|
|
234
|
+
break;
|
|
235
|
+
if (!value)
|
|
236
|
+
continue;
|
|
237
|
+
if (!strippedLeadingClose) {
|
|
238
|
+
strippedLeadingClose = true;
|
|
239
|
+
const text = decoder.decode(value);
|
|
240
|
+
const trimmed = text.replace(/^\s*<\/body>\s*<\/html>/i, '');
|
|
241
|
+
if (trimmed.length > 0)
|
|
242
|
+
controller.enqueue(encoder.encode(trimmed));
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
controller.enqueue(value);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
finally {
|
|
249
|
+
reader.releaseLock();
|
|
250
|
+
controller.close();
|
|
251
|
+
}
|
|
252
|
+
},
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
Artifact.composePreludeAndResume = composePreludeAndResume;
|
|
256
|
+
})(Artifact = Prerender.Artifact || (Prerender.Artifact = {}));
|
|
257
|
+
let Runtime;
|
|
258
|
+
(function (Runtime) {
|
|
259
|
+
/**
|
|
260
|
+
* Custom error class to indicate that prerendering has been postponed to request-time
|
|
261
|
+
*/
|
|
262
|
+
class Postponed extends Error {
|
|
263
|
+
constructor(message = 'postponed') {
|
|
264
|
+
super(message);
|
|
265
|
+
this.name = 'Postponed';
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
Runtime.Postponed = Postponed;
|
|
269
|
+
/**
|
|
270
|
+
* Type guard to check if an error is a Postponed error, including wrapped errors like
|
|
271
|
+
* AbortError or TimeoutError
|
|
272
|
+
*/
|
|
273
|
+
function isPostponed(error) {
|
|
274
|
+
if (error instanceof Postponed)
|
|
275
|
+
return true;
|
|
276
|
+
if (error instanceof Error &&
|
|
277
|
+
(error.name === 'AbortError' || error.name === 'TimeoutError')) {
|
|
278
|
+
if (error.cause instanceof Postponed)
|
|
279
|
+
return true;
|
|
280
|
+
}
|
|
281
|
+
return false;
|
|
282
|
+
}
|
|
283
|
+
Runtime.isPostponed = isPostponed;
|
|
284
|
+
})(Runtime = Prerender.Runtime || (Prerender.Runtime = {}));
|
|
285
|
+
let Build;
|
|
286
|
+
(function (Build) {
|
|
287
|
+
/**
|
|
288
|
+
* Get the prerender timeout value from the environment variable, or return the default
|
|
289
|
+
* if it's not set or invalid
|
|
290
|
+
*/
|
|
291
|
+
function getTimeout() {
|
|
292
|
+
const v = Number(process.env.SOLAS_PRERENDER_TIMEOUT_MS);
|
|
293
|
+
if (!Number.isFinite(v) || v <= 0) {
|
|
294
|
+
return DEFAULT_TIMEOUT_MS;
|
|
295
|
+
}
|
|
296
|
+
return v;
|
|
297
|
+
}
|
|
298
|
+
Build.getTimeout = getTimeout;
|
|
299
|
+
/**
|
|
300
|
+
* Get the prerender concurrency value from the environment variable, or return the default
|
|
301
|
+
* if it's not set or invalid
|
|
302
|
+
*/
|
|
303
|
+
function getConcurrency() {
|
|
304
|
+
const v = Number(process.env.SOLAS_PRERENDER_CONCURRENCY);
|
|
305
|
+
if (!Number.isInteger(v) || v <= 0) {
|
|
306
|
+
return DEFAULT_CONCURRENCY;
|
|
307
|
+
}
|
|
308
|
+
return v;
|
|
309
|
+
}
|
|
310
|
+
Build.getConcurrency = getConcurrency;
|
|
311
|
+
/**
|
|
312
|
+
* Extract the prerendering mode ('full', 'ppr', or false) from the source code of a route module, by
|
|
313
|
+
* looking for an exported `prerender` binding and validating its value
|
|
314
|
+
*/
|
|
315
|
+
async function getStaticFlag(filePath, buildContext) {
|
|
316
|
+
return buildContext.exportReader.literal(filePath, 'prerender', (v) => v === 'full' || v === 'ppr' || v === false);
|
|
317
|
+
}
|
|
318
|
+
Build.getStaticFlag = getStaticFlag;
|
|
319
|
+
/**
|
|
320
|
+
* Get the list of static parameters for a dynamic route, by looking for an exported `params` function
|
|
321
|
+
* in the route module and calling it to get the list of parameter objects
|
|
322
|
+
*/
|
|
323
|
+
async function getStaticParams(filePath, buildContext) {
|
|
324
|
+
const params = await buildContext.exportReader.value(filePath, 'params', (v) => typeof v === 'function');
|
|
325
|
+
if (!params)
|
|
326
|
+
return [];
|
|
327
|
+
const resolved = await Time.timeout(Promise.try(() => params()), getTimeout(), `static params for ${filePath}`);
|
|
328
|
+
if (!Array.isArray(resolved))
|
|
329
|
+
return [];
|
|
330
|
+
return resolved;
|
|
331
|
+
}
|
|
332
|
+
Build.getStaticParams = getStaticParams;
|
|
333
|
+
/**
|
|
334
|
+
* Generate the list of prerenderable routes for a dynamic route, by combining the static parameters obtained from
|
|
335
|
+
* the route module with the route pattern, and filtering out any routes that still contain dynamic segments
|
|
336
|
+
*/
|
|
337
|
+
function getDynamicRouteList(route, paramNames, staticParams) {
|
|
338
|
+
if (!staticParams.length)
|
|
339
|
+
return [];
|
|
340
|
+
const { path: compilePath, wildcardNames } = toPathPattern(route, paramNames);
|
|
341
|
+
const toPath = compile(compilePath);
|
|
342
|
+
return staticParams
|
|
343
|
+
.map(value => {
|
|
344
|
+
try {
|
|
345
|
+
return toPath(Object.fromEntries(Object.entries(value).map(([key, entry]) => [
|
|
346
|
+
key,
|
|
347
|
+
wildcardNames.has(key)
|
|
348
|
+
? Array.isArray(entry)
|
|
349
|
+
? entry.map(part => String(part))
|
|
350
|
+
: [String(entry)]
|
|
351
|
+
: Array.isArray(entry)
|
|
352
|
+
? entry.map(part => String(part)).join('/')
|
|
353
|
+
: String(entry),
|
|
354
|
+
])));
|
|
355
|
+
}
|
|
356
|
+
catch {
|
|
357
|
+
return null;
|
|
358
|
+
}
|
|
359
|
+
})
|
|
360
|
+
.filter((value) => value !== null)
|
|
361
|
+
.filter(r => !r.includes(':') && !r.includes('*'));
|
|
362
|
+
}
|
|
363
|
+
Build.getDynamicRouteList = getDynamicRouteList;
|
|
364
|
+
/**
|
|
365
|
+
* Function to prerender a single route by making a request to the route with special headers, and returning the
|
|
366
|
+
* result which includes either the prerender artifact or an error/status code if the prerendering failed or was
|
|
367
|
+
* postponed to request-time
|
|
368
|
+
*/
|
|
369
|
+
async function get(app, route, opts) {
|
|
370
|
+
const url = `${opts.origin ?? `http://${Solas.Config.SLUG}.local`}${route}`;
|
|
371
|
+
const res = await Time.timeout(app.fetch(new Request(url, {
|
|
372
|
+
headers: {
|
|
373
|
+
Accept: 'text/html',
|
|
374
|
+
[`x-${Solas.Config.SLUG}-prerender`]: '1',
|
|
375
|
+
[`x-${Solas.Config.SLUG}-prerender-artifact`]: '1',
|
|
376
|
+
},
|
|
377
|
+
})), opts.timeout, `route ${route}`);
|
|
378
|
+
if (!(res instanceof Response)) {
|
|
379
|
+
const error = new TypeError(`Invalid response for ${route}`);
|
|
380
|
+
logger.error(`[prerender:get] ${error.message}`, error);
|
|
381
|
+
throw error;
|
|
382
|
+
}
|
|
383
|
+
if (!res.ok)
|
|
384
|
+
return { route, status: res.status };
|
|
385
|
+
return { route, artifact: await res.json() };
|
|
386
|
+
}
|
|
387
|
+
Build.get = get;
|
|
388
|
+
/**
|
|
389
|
+
* Run the prerendering process for a list of routes, with a specified concurrency limit and timeout for
|
|
390
|
+
* each route, by calling the 'get' function for each route and yielding the results as they
|
|
391
|
+
* become available
|
|
392
|
+
*/
|
|
393
|
+
async function* run(app, routes, opts) {
|
|
394
|
+
const limit = Math.max(1, Math.min(opts.concurrency ?? 4, routes.length || 1));
|
|
395
|
+
let index = 0;
|
|
396
|
+
const pending = new Map();
|
|
397
|
+
function enqueue() {
|
|
398
|
+
while (index < routes.length && pending.size < limit) {
|
|
399
|
+
const i = index++;
|
|
400
|
+
const value = routes[i];
|
|
401
|
+
pending.set(i, get(app, value, {
|
|
402
|
+
timeout: opts.timeout,
|
|
403
|
+
origin: opts.origin,
|
|
404
|
+
})
|
|
405
|
+
.then(result => ({ index: i, result }))
|
|
406
|
+
.catch(err => ({
|
|
407
|
+
index: i,
|
|
408
|
+
result: { route: value, error: err },
|
|
409
|
+
})));
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
enqueue();
|
|
413
|
+
while (pending.size > 0) {
|
|
414
|
+
const settled = await Promise.race(pending.values());
|
|
415
|
+
pending.delete(settled.index);
|
|
416
|
+
yield settled.result;
|
|
417
|
+
enqueue();
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
Build.run = run;
|
|
421
|
+
})(Build = Prerender.Build || (Prerender.Build = {}));
|
|
422
|
+
})(Prerender || (Prerender = {}));
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { use } from 'react';
|
|
3
|
+
import { Solas } from '../../solas';
|
|
4
|
+
import { Logger } from '../../utils/logger';
|
|
5
|
+
const logger = new Logger();
|
|
6
|
+
const cache = new WeakMap();
|
|
7
|
+
export function Head({ metadata: m, }) {
|
|
8
|
+
if (!m)
|
|
9
|
+
return null;
|
|
10
|
+
const metadata = use(toSafeUsable(m));
|
|
11
|
+
return (_jsxs(_Fragment, { children: [
|
|
12
|
+
_jsx("meta", { name: "generator", content: Solas.Config.NAME }), metadata.title && _jsx("title", { children: metadata.title.toString() }), metadata.meta?.map(meta => {
|
|
13
|
+
if ('charSet' in meta) {
|
|
14
|
+
return _jsx("meta", { charSet: meta.charSet }, meta.charSet);
|
|
15
|
+
}
|
|
16
|
+
if ('name' in meta) {
|
|
17
|
+
return (_jsx("meta", { name: meta.name, content: meta.content?.toString() }, meta.name));
|
|
18
|
+
}
|
|
19
|
+
if ('httpEquiv' in meta) {
|
|
20
|
+
return (_jsx("meta", { httpEquiv: meta.httpEquiv, content: meta.content?.toString() }, meta.httpEquiv));
|
|
21
|
+
}
|
|
22
|
+
if ('property' in meta) {
|
|
23
|
+
return (_jsx("meta", { property: meta.property, content: meta.content?.toString() }, meta.property));
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
}), metadata.link?.map(link => (_jsx("link", { ...link }, `${link.rel}${link.href ?? ''}`)))] }));
|
|
27
|
+
}
|
|
28
|
+
function toSafeUsable(metadata) {
|
|
29
|
+
const cached = cache.get(metadata);
|
|
30
|
+
if (cached)
|
|
31
|
+
return cached;
|
|
32
|
+
const safe = Promise.resolve(metadata).catch(err => {
|
|
33
|
+
logger.error('[head] failed to resolve metadata', err);
|
|
34
|
+
return {};
|
|
35
|
+
});
|
|
36
|
+
cache.set(metadata, safe);
|
|
37
|
+
return safe;
|
|
38
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { Resolver } from '../router/resolver';
|
|
2
|
+
type Match = NonNullable<Resolver.EnhancedMatch>;
|
|
3
|
+
/**
|
|
4
|
+
* Render the resolved route tree for a matched page
|
|
5
|
+
*
|
|
6
|
+
* The shell is always `layouts[0]`. Every deeper segment is then wrapped from
|
|
7
|
+
* the inside out in this order:
|
|
8
|
+
*
|
|
9
|
+
* 1. `Layout`
|
|
10
|
+
* 2. `Suspense` with that segment's loading fallback
|
|
11
|
+
* 3. `HttpExceptionBoundary` with that segment's status boundaries
|
|
12
|
+
*
|
|
13
|
+
* The shell level is applied last using the same outer wrapper order:
|
|
14
|
+
* `HttpExceptionBoundary` -> `Suspense` -> `Shell`
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```tsx
|
|
18
|
+
* <HttpExceptionBoundary shell>
|
|
19
|
+
* <Suspense fallback={<ShellLoading />}>
|
|
20
|
+
* <Shell>
|
|
21
|
+
* <HttpExceptionBoundary segmentN>
|
|
22
|
+
* <Suspense fallback={<LoadingN />}>
|
|
23
|
+
* <LayoutN>
|
|
24
|
+
* ...
|
|
25
|
+
* <HttpExceptionBoundary segment1>
|
|
26
|
+
* <Suspense fallback={<Loading1 />}>
|
|
27
|
+
* <Layout1>
|
|
28
|
+
* <Page />
|
|
29
|
+
* </Layout1>
|
|
30
|
+
* </Suspense>
|
|
31
|
+
* </HttpExceptionBoundary>
|
|
32
|
+
* ...
|
|
33
|
+
* </LayoutN>
|
|
34
|
+
* </Suspense>
|
|
35
|
+
* </HttpExceptionBoundary>
|
|
36
|
+
* </Shell>
|
|
37
|
+
* </Suspense>
|
|
38
|
+
* </HttpExceptionBoundary>
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
export declare function Tree({ depth, params, error, ui }: {
|
|
42
|
+
depth: Match['__depth'];
|
|
43
|
+
params: Match['params'];
|
|
44
|
+
error: Match['error'];
|
|
45
|
+
ui: Match['ui'];
|
|
46
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
47
|
+
export {};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Suspense } from 'react';
|
|
3
|
+
import { HttpException, isHttpException } from '../navigation/http-exception';
|
|
4
|
+
import { HttpExceptionBoundary } from '../navigation/http-exception-boundary';
|
|
5
|
+
import DefaultErr from '../ui/defaults/error';
|
|
6
|
+
/**
|
|
7
|
+
* Render the resolved route tree for a matched page
|
|
8
|
+
*
|
|
9
|
+
* The shell is always `layouts[0]`. Every deeper segment is then wrapped from
|
|
10
|
+
* the inside out in this order:
|
|
11
|
+
*
|
|
12
|
+
* 1. `Layout`
|
|
13
|
+
* 2. `Suspense` with that segment's loading fallback
|
|
14
|
+
* 3. `HttpExceptionBoundary` with that segment's status boundaries
|
|
15
|
+
*
|
|
16
|
+
* The shell level is applied last using the same outer wrapper order:
|
|
17
|
+
* `HttpExceptionBoundary` -> `Suspense` -> `Shell`
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```tsx
|
|
21
|
+
* <HttpExceptionBoundary shell>
|
|
22
|
+
* <Suspense fallback={<ShellLoading />}>
|
|
23
|
+
* <Shell>
|
|
24
|
+
* <HttpExceptionBoundary segmentN>
|
|
25
|
+
* <Suspense fallback={<LoadingN />}>
|
|
26
|
+
* <LayoutN>
|
|
27
|
+
* ...
|
|
28
|
+
* <HttpExceptionBoundary segment1>
|
|
29
|
+
* <Suspense fallback={<Loading1 />}>
|
|
30
|
+
* <Layout1>
|
|
31
|
+
* <Page />
|
|
32
|
+
* </Layout1>
|
|
33
|
+
* </Suspense>
|
|
34
|
+
* </HttpExceptionBoundary>
|
|
35
|
+
* ...
|
|
36
|
+
* </LayoutN>
|
|
37
|
+
* </Suspense>
|
|
38
|
+
* </HttpExceptionBoundary>
|
|
39
|
+
* </Shell>
|
|
40
|
+
* </Suspense>
|
|
41
|
+
* </HttpExceptionBoundary>
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
export function Tree({ depth, params, error, ui, }) {
|
|
45
|
+
const { layouts, Page, '401s': unauthorized, '403s': forbidden, '404s': notFounds, '500s': serverErrors, loaders, } = ui;
|
|
46
|
+
const Shell = layouts[0];
|
|
47
|
+
if (!Shell)
|
|
48
|
+
throw new Error('Shell layout is required in the route tree');
|
|
49
|
+
// build the inner inner (everything after shell)
|
|
50
|
+
let inner = null;
|
|
51
|
+
// map http status codes to exception components
|
|
52
|
+
const httpExceptionMap = {
|
|
53
|
+
401: unauthorized,
|
|
54
|
+
403: forbidden,
|
|
55
|
+
404: notFounds,
|
|
56
|
+
500: serverErrors,
|
|
57
|
+
};
|
|
58
|
+
if (error && isHttpException(error)) {
|
|
59
|
+
const Exception = httpExceptionMap[error.status].slice(0, depth + 1).findLast(e => e !== null) ??
|
|
60
|
+
DefaultErr;
|
|
61
|
+
inner = (_jsxs(_Fragment, { children: [
|
|
62
|
+
_jsx("meta", { name: "robots", content: "noindex,nofollow" }), _jsx(Exception, { error: error })
|
|
63
|
+
] }));
|
|
64
|
+
}
|
|
65
|
+
else if (Page) {
|
|
66
|
+
inner = _jsx(Page, { params: params });
|
|
67
|
+
}
|
|
68
|
+
// wrap from innermost to layouts[1] (skip shell)
|
|
69
|
+
for (let idx = layouts.length - 1; idx >= 1; idx--) {
|
|
70
|
+
const Layout = layouts[idx];
|
|
71
|
+
const Loading = loaders[idx];
|
|
72
|
+
const Unauthorized = unauthorized[idx];
|
|
73
|
+
const Forbidden = forbidden[idx];
|
|
74
|
+
const NotFound = notFounds[idx];
|
|
75
|
+
const ServerError = serverErrors[idx];
|
|
76
|
+
// wrap in layout
|
|
77
|
+
if (Layout) {
|
|
78
|
+
inner = (_jsx(Layout, { params: params, children: inner }, `l:${idx}`));
|
|
79
|
+
}
|
|
80
|
+
// wrap in suspense (for this segment's loading state)
|
|
81
|
+
if (Loading) {
|
|
82
|
+
inner = _jsx(Suspense, { fallback: _jsx(Loading, {}), children: inner });
|
|
83
|
+
}
|
|
84
|
+
const errorBoundaries = {
|
|
85
|
+
401: Unauthorized ? (_jsx(Unauthorized, { error: new HttpException(401, 'Unauthorized') })) : null,
|
|
86
|
+
403: Forbidden ? _jsx(Forbidden, { error: new HttpException(403, 'Forbidden') }) : null,
|
|
87
|
+
404: NotFound ? _jsx(NotFound, { error: new HttpException(404, 'Not found') }) : null,
|
|
88
|
+
500: ServerError ? (_jsx(ServerError, { error: new HttpException(500, 'Internal Server Error') })) : null,
|
|
89
|
+
};
|
|
90
|
+
// wrap in error boundaries (if supplied for this segment's http errors)
|
|
91
|
+
if (Object.values(errorBoundaries).some(c => c !== null)) {
|
|
92
|
+
inner = (_jsx(HttpExceptionBoundary, { components: errorBoundaries, children: inner }));
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
// now wrap with shell structure: shell renders immediately,
|
|
96
|
+
// inner streams inside Suspense
|
|
97
|
+
const ShellLoading = loaders[0];
|
|
98
|
+
const ShellUnauthorized = unauthorized[0];
|
|
99
|
+
const ShellForbidden = forbidden[0];
|
|
100
|
+
const ShellNotFound = notFounds[0];
|
|
101
|
+
const ShellServerError = serverErrors[0];
|
|
102
|
+
return (_jsx(HttpExceptionBoundary, { components: {
|
|
103
|
+
401: ShellUnauthorized ? _jsx(ShellUnauthorized, {}) : null,
|
|
104
|
+
403: ShellForbidden ? _jsx(ShellForbidden, {}) : null,
|
|
105
|
+
404: ShellNotFound ? _jsx(ShellNotFound, {}) : null,
|
|
106
|
+
500: ShellServerError ? _jsx(ShellServerError, {}) : null,
|
|
107
|
+
}, children: _jsx(Suspense, { fallback: ShellLoading ? _jsx(ShellLoading, {}) : null, children: _jsx(Shell, { params: params, children: inner }) }) }));
|
|
108
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { ImportMap, Manifest, PluginConfig, SolasRequest } from '../../types';
|
|
2
|
+
import { Router } from './router';
|
|
3
|
+
/**
|
|
4
|
+
* Create the application router from the generated manifest and import map
|
|
5
|
+
*/
|
|
6
|
+
export declare function createRouter(config: Pick<PluginConfig, 'precompress' | 'trailingSlash'>, manifest: Manifest, importMap: ImportMap, rsc: (req: SolasRequest) => Response | Promise<Response>): Router;
|