@jk2908/solas 0.3.8 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +66 -6
  3. package/dist/index.d.ts +1 -1
  4. package/dist/index.js +16 -2
  5. package/dist/internal/browser-router/link.d.ts +1 -1
  6. package/dist/internal/browser-router/link.js +1 -1
  7. package/dist/internal/browser-router/router.d.ts +2 -165
  8. package/dist/internal/browser-router/router.js +3 -99
  9. package/dist/internal/browser-router/shared.d.ts +169 -0
  10. package/dist/internal/browser-router/shared.js +71 -0
  11. package/dist/internal/browser-router/use-router.d.ts +1 -1
  12. package/dist/internal/codegen/environments.js +5 -4
  13. package/dist/internal/env/rsc.d.ts +2 -2
  14. package/dist/internal/env/rsc.js +82 -22
  15. package/dist/internal/http-router/create-http-router.d.ts +1 -1
  16. package/dist/internal/http-router/create-http-router.js +4 -2
  17. package/dist/internal/http-router/router.d.ts +4 -14
  18. package/dist/internal/http-router/router.js +32 -59
  19. package/dist/internal/navigation/http-exception.d.ts +4 -4
  20. package/dist/internal/navigation/http-exception.js +4 -5
  21. package/dist/internal/postbuild.d.ts +1 -0
  22. package/dist/{cli/build.js → internal/postbuild.js} +13 -48
  23. package/dist/internal/prerender.d.ts +4 -19
  24. package/dist/internal/prerender.js +8 -98
  25. package/dist/internal/public-files.d.ts +18 -0
  26. package/dist/internal/public-files.js +63 -0
  27. package/dist/internal/resolver.d.ts +23 -23
  28. package/dist/internal/server/actions.d.ts +2 -5
  29. package/dist/internal/server/actions.js +4 -35
  30. package/dist/internal/server/csrf.d.ts +14 -0
  31. package/dist/internal/server/csrf.js +98 -0
  32. package/dist/router.d.ts +1 -0
  33. package/dist/router.js +1 -0
  34. package/dist/solas.d.ts +12 -1
  35. package/dist/solas.js +116 -1
  36. package/dist/types.d.ts +8 -3
  37. package/dist/utils/base-path.d.ts +14 -0
  38. package/dist/utils/base-path.js +85 -0
  39. package/package.json +3 -6
  40. package/dist/cli/build.d.ts +0 -7
  41. package/dist/cli/dev.d.ts +0 -4
  42. package/dist/cli/dev.js +0 -13
  43. package/dist/cli/preview.d.ts +0 -1
  44. package/dist/cli/preview.js +0 -47
  45. package/dist/cli.d.ts +0 -2
  46. 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 path = trailingSlash === 'ignore'
167
- ? url.pathname
168
- : normalisePathname(url.pathname, trailingSlash);
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
- if ((method === 'GET' || method === 'HEAD') && path !== url.pathname) {
174
- url.pathname = path;
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 (path !== url.pathname) {
178
- // rebuild the request with the canonical pathname so downstream code
179
- // sees the same url the router matched against
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 pathname only the method is
186
- // normalised to GET this lets page/layout routes match for
187
- // rerender action execution still reads POST body and
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 pass through the shared error hook with the
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 routing state to the request once so middleware and handlers can
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
- // global middleware stays outside route middleware by preserving
205
- // registration order here before composition in #run
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
- // normalise unknown throwables so the error hook always receives an Error
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
- // compose middleware stack
231
+ // build the middleware chain from the inside out
228
232
  let run = () => Promise.resolve(next());
229
- // unwind stack
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
- // guard against double invocation so handlers/inner middleware
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
- // run composed middleware stack
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 serve(filePath, req, precompress = false, headers = {}) {
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 HTTP error, with an optional payload
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 HTTPException
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 HTTPException
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 HTTP error, with an optional payload
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 HTTPException
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 HTTPException
95
+ * Throw an HttpException
97
96
  */
98
97
  export function abort(status, message, opts) {
99
98
  throw new HttpException(status, message, {
@@ -0,0 +1 @@
1
+ export declare function postbuild(cwd?: string): Promise<void>;
@@ -2,33 +2,11 @@ import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import { Compress } from '../utils/compress.js';
4
4
  import { Logger } from '../utils/logger.js';
5
- import { Prerender } from '../internal/prerender.js';
6
5
  import { Solas } from '../solas.js';
6
+ import { Prerender } from './prerender.js';
7
7
  const logger = new Logger();
8
- /**
9
- * The build command does more than just run vite build - it also handles prerendering and
10
- * precompressing assets. This is because prerendering needs to run against the built
11
- * server entry to ensure the same code paths as preview, and precompressing needs
12
- * to include the prerendered html and json files
13
- */
14
- export async function build() {
15
- // build and prerender should both run in production mode
16
- process.env.NODE_ENV = 'production';
17
- const cwd = process.cwd();
8
+ export async function postbuild(cwd = process.cwd()) {
18
9
  const manifestPath = path.join(cwd, Solas.Config.GENERATED_DIR, 'build.json');
19
- // run vite build
20
- logger.info('[build]', 'running vite build...');
21
- const vite = Bun.spawnSync(['bunx', '--bun', 'vite', 'build', '--mode', 'production'], {
22
- cwd,
23
- stdout: 'inherit',
24
- stderr: 'inherit',
25
- env: { ...process.env, NODE_ENV: 'production' },
26
- });
27
- if (vite.exitCode !== 0) {
28
- logger.error('[build] vite build failed');
29
- process.exit(1);
30
- }
31
- // read build manifest
32
10
  let manifest;
33
11
  try {
34
12
  const raw = await fs.readFile(manifestPath, 'utf-8');
@@ -36,7 +14,7 @@ export async function build() {
36
14
  }
37
15
  catch (err) {
38
16
  logger.error('[build] failed to read build manifest', err);
39
- process.exit(1);
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
- // prerender routes
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 (timeout: ${timeout}ms, concurrency: ${concurrency})...`);
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
- timeout,
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, fullPrerenderFilename), artifact.html),
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
- // sitemap
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, with a specified concurrency limit and timeout for
144
- * each route, by calling the 'get' function for each route and yielding the results as they
145
- * become available
129
+ * Run the prerendering process for a list of routes with a specified concurrency limit,
130
+ * by calling the 'get' function for each route and yielding the results as they become available
146
131
  */
147
132
  function run(app: {
148
133
  fetch: (req: Request) => Promise<Response>;
149
134
  }, routes: string[], opts: {
150
- timeout: number;
151
135
  concurrency?: number;
152
136
  origin?: string;
137
+ base?: string;
153
138
  }): AsyncGenerator<Result, void, unknown>;
154
139
  }
155
140
  }
@@ -1,18 +1,16 @@
1
1
  import path from 'node:path';
2
2
  import { compile } from 'path-to-regexp';
3
+ import { BasePath } from '../utils/base-path.js';
3
4
  import { Logger } from '../utils/logger.js';
4
- import { Time } from '../utils/time.js';
5
5
  import { Solas } from '../solas.js';
6
6
  import { toPathPattern } from './http-router/utils.js';
7
7
  const logger = new Logger();
8
8
  export { Prerender };
9
9
  var Prerender;
10
10
  (function (Prerender) {
11
- const DEFAULT_TIMEOUT_MS = 15_000;
12
11
  const DEFAULT_CONCURRENCY = 4;
13
12
  let Artifact;
14
13
  (function (Artifact) {
15
- const manifestCache = new Map();
16
14
  /**
17
15
  * Check whether a file name is safe to join under an artifact directory
18
16
  */
@@ -30,14 +28,6 @@ var Prerender;
30
28
  return path.join(outDir, Solas.Config.GENERATED_DIR, 'ppr');
31
29
  }
32
30
  Artifact.getRootPath = getRootPath;
33
- /**
34
- * Get the file system path for the prerender artifact manifest, which
35
- * contains metadata about all prerendered routes and their artifacts
36
- */
37
- function getManifestPath(outDir) {
38
- return path.join(getRootPath(outDir), 'manifest.json');
39
- }
40
- Artifact.getManifestPath = getManifestPath;
41
31
  /**
42
32
  * Get the file system path for storing prerender artifacts for a given route
43
33
  */
@@ -66,73 +56,6 @@ var Prerender;
66
56
  * File name used for saved full-prerender html inside each route artifact directory
67
57
  */
68
58
  Artifact.FULL_PRERENDER_FILENAME = 'prerendered.html';
69
- /**
70
- * Load the prerender artifact manifest for faster runtime route mode checks
71
- */
72
- async function loadManifest(outDir) {
73
- // if we already loaded this outDir, return cached result
74
- // (either a valid manifest or null when it was missing/invalid)
75
- if (manifestCache.has(outDir)) {
76
- return manifestCache.get(outDir) ?? null;
77
- }
78
- const file = Bun.file(getManifestPath(outDir));
79
- // no manifest means no prerender metadata to use
80
- if (!(await file.exists())) {
81
- manifestCache.set(outDir, null);
82
- return null;
83
- }
84
- try {
85
- // parse once, then validate the shape before trusting any fields
86
- const value = JSON.parse(await file.text());
87
- if (!value || typeof value !== 'object') {
88
- manifestCache.set(outDir, null);
89
- return null;
90
- }
91
- const routes = value.routes;
92
- if (!routes || typeof routes !== 'object') {
93
- manifestCache.set(outDir, null);
94
- return null;
95
- }
96
- // verify each route entry so runtime can rely on mode/files safely
97
- for (const entry of Object.values(routes)) {
98
- if (!entry || typeof entry !== 'object') {
99
- manifestCache.set(outDir, null);
100
- return null;
101
- }
102
- const { mode, files } = entry;
103
- // only allow known modes
104
- if (mode !== 'full' && mode !== 'ppr') {
105
- manifestCache.set(outDir, null);
106
- return null;
107
- }
108
- if (files !== undefined) {
109
- if (!Array.isArray(files)) {
110
- manifestCache.set(outDir, null);
111
- return null;
112
- }
113
- // only allow known artifact file labels
114
- for (const f of files) {
115
- if (f !== 'html' &&
116
- f !== 'prelude' &&
117
- f !== 'postponed' &&
118
- f !== 'metadata') {
119
- manifestCache.set(outDir, null);
120
- return null;
121
- }
122
- }
123
- }
124
- }
125
- const manifest = routes;
126
- // cache validated manifest to avoid reparsing on every request
127
- manifestCache.set(outDir, manifest);
128
- return manifest;
129
- }
130
- catch {
131
- manifestCache.set(outDir, null);
132
- return null;
133
- }
134
- }
135
- Artifact.loadManifest = loadManifest;
136
59
  /**
137
60
  * Load the postponed state for a given route from the file system, if it exists
138
61
  */
@@ -319,18 +242,6 @@ var Prerender;
319
242
  })(Runtime = Prerender.Runtime || (Prerender.Runtime = {}));
320
243
  let Build;
321
244
  (function (Build) {
322
- /**
323
- * Get the prerender timeout value from the environment variable, or return the default
324
- * if it's not set or invalid
325
- */
326
- function getTimeout() {
327
- const v = Number(process.env.SOLAS_PRERENDER_TIMEOUT_MS);
328
- if (!Number.isFinite(v) || v <= 0) {
329
- return DEFAULT_TIMEOUT_MS;
330
- }
331
- return v;
332
- }
333
- Build.getTimeout = getTimeout;
334
245
  /**
335
246
  * Get the prerender concurrency value from the environment variable, or return the default
336
247
  * if it's not set or invalid
@@ -359,7 +270,7 @@ var Prerender;
359
270
  const params = await buildContext.exportReader.value(filePath, 'params', (v) => typeof v === 'function');
360
271
  if (!params)
361
272
  return [];
362
- const resolved = await Time.timeout(Promise.try(() => params()), getTimeout(), `static params for ${filePath}`);
273
+ const resolved = await Promise.try(() => params());
363
274
  if (!Array.isArray(resolved))
364
275
  return [];
365
276
  return resolved;
@@ -402,14 +313,14 @@ var Prerender;
402
313
  * postponed to request-time
403
314
  */
404
315
  async function get(app, route, opts) {
405
- const url = `${opts.origin ?? `http://${Solas.Config.SLUG}.local`}${route}`;
406
- const res = await Time.timeout(app.fetch(new Request(url, {
316
+ const url = `${opts.origin ?? `http://${Solas.Config.SLUG}.local`}${BasePath.apply(route, opts.base)}`;
317
+ const res = await app.fetch(new Request(url, {
407
318
  headers: {
408
319
  Accept: 'text/html',
409
320
  [`x-${Solas.Config.SLUG}-prerender`]: '1',
410
321
  [`x-${Solas.Config.SLUG}-prerender-artifact`]: '1',
411
322
  },
412
- })), opts.timeout, `route ${route}`);
323
+ }));
413
324
  if (!(res instanceof Response)) {
414
325
  const error = new TypeError(`Invalid response for ${route}`);
415
326
  logger.error(`[prerender:get] ${error.message}`, error);
@@ -421,9 +332,8 @@ var Prerender;
421
332
  }
422
333
  Build.get = get;
423
334
  /**
424
- * Run the prerendering process for a list of routes, with a specified concurrency limit and timeout for
425
- * each route, by calling the 'get' function for each route and yielding the results as they
426
- * become available
335
+ * Run the prerendering process for a list of routes with a specified concurrency limit,
336
+ * by calling the 'get' function for each route and yielding the results as they become available
427
337
  */
428
338
  async function* run(app, routes, opts) {
429
339
  const limit = Math.max(1, Math.min(opts.concurrency ?? 4, routes.length || 1));
@@ -434,7 +344,7 @@ var Prerender;
434
344
  const i = index++;
435
345
  const value = routes[i];
436
346
  pending.set(i, get(app, value, {
437
- timeout: opts.timeout,
347
+ base: opts.base,
438
348
  origin: opts.origin,
439
349
  })
440
350
  .then(result => ({ index: i, result }))
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Collect the root request paths for files that originate in Vite's public dir.
3
+ * Vite copies these files into the built client output unchanged. Solas stores
4
+ * the request paths here so runtime serving can whitelist them
5
+ *
6
+ * @example
7
+ * ```ts
8
+ * // public/robots.txt
9
+ * // becomes '/robots.txt'
10
+ * ```
11
+ *
12
+ * @example
13
+ * ```ts
14
+ * // public/images/logo 1.png
15
+ * // becomes '/images/logo%201.png'
16
+ * ```
17
+ */
18
+ export declare function collect(root: string | false | null | undefined): Promise<string[]>;