@jk2908/solas 0.2.1 → 0.2.3

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/README.md CHANGED
@@ -315,9 +315,9 @@ export default defineConfig(({ mode }) => ({
315
315
  solas({
316
316
  url: mode === 'production' ? 'https://example.com' : 'http://localhost:8787',
317
317
  sitemap: {
318
- async routes(discovered) {
319
- const posts = await fetchPostSlugs()
320
- return [...discovered, ...posts.map(s => `/blog/${s}`)]
318
+ async routes(existing) {
319
+ const posts = await getPosts()
320
+ return [...existing, ...posts.map(p => `/blog/${p.slug}`)]
321
321
  },
322
322
  },
323
323
  }),
package/dist/cli.js CHANGED
@@ -36,8 +36,8 @@ async function build() {
36
36
  const raw = await fs.readFile(manifestPath, 'utf-8');
37
37
  manifest = JSON.parse(raw);
38
38
  }
39
- catch {
40
- logger.error('[build] failed to read build manifest');
39
+ catch (err) {
40
+ logger.error('[build] failed to read build manifest', err);
41
41
  process.exit(1);
42
42
  }
43
43
  const outDir = path.resolve(cwd, Solas.Config.OUT_DIR);
@@ -223,8 +223,8 @@ async function preview() {
223
223
  try {
224
224
  await fs.access(rscEntry);
225
225
  }
226
- catch {
227
- logger.error(`[preview] missing ${path.relative(cwd, rscEntry)} - run \`${Solas.Config.SLUG} build\` first`);
226
+ catch (err) {
227
+ logger.error(`[preview] missing ${path.relative(cwd, rscEntry)} - run \`${Solas.Config.SLUG} build\` first`, err);
228
228
  process.exit(1);
229
229
  }
230
230
  const { default: app } = await import(/* @vite-ignore */ rscEntry);
package/dist/index.js CHANGED
@@ -104,8 +104,8 @@ function solas(c) {
104
104
  // early return if nothing has changed
105
105
  if (!changed.length)
106
106
  return;
107
- await Promise.all(changed.map(filePath => Format.run(filePath).catch(() => {
108
- logger.error(`[build] Failed to format file: ${filePath}`);
107
+ await Promise.all(changed.map(filePath => Format.run(filePath).catch(err => {
108
+ logger.error(`[build] Failed to format file: ${filePath}`, err);
109
109
  })));
110
110
  return changed;
111
111
  }
@@ -214,7 +214,9 @@ function solas(c) {
214
214
  // resolve sitemap routes
215
215
  let sitemapRoutes = [];
216
216
  if (config.sitemap && config.url) {
217
- const auto = [...new Set([...buildContext.knownRoutes, ...buildContext.prerenderRoutes])];
217
+ const auto = [
218
+ ...new Set([...buildContext.knownRoutes, ...buildContext.prerenderRoutes]),
219
+ ];
218
220
  if (typeof config.sitemap === 'object' && config.sitemap.routes) {
219
221
  sitemapRoutes = await config.sitemap.routes(auto);
220
222
  }
@@ -1,5 +1,15 @@
1
1
  import type { BuildContext, Endpoint, PluginConfig, Segment } from '../types';
2
+ /**
3
+ * Types, constants, and the Finder class for route discovery and manifest generation.
4
+ * The Finder walks the app directory, builds inheritance chains, and transforms
5
+ * the scan result into codegen artifacts consumed by generated entry files
6
+ */
2
7
  export declare namespace Build {
8
+ /**
9
+ * Raw output of the filesystem scan before any processing. Segments
10
+ * are renderable routes (pages and layout wrappers), endpoints are
11
+ * API routes with HTTP verb handlers
12
+ */
3
13
  type ScanResult = {
4
14
  segments: {
5
15
  dir: string;
@@ -18,6 +28,10 @@ export declare namespace Build {
18
28
  middlewares: (string | null)[];
19
29
  }[];
20
30
  };
31
+ /**
32
+ * Collected import paths keyed by entry id. Static imports are eagerly
33
+ * loaded, dynamic imports are lazy-loaded via React.lazy
34
+ */
21
35
  type Imports = {
22
36
  endpoints: {
23
37
  static: Map<string, string>;
@@ -30,6 +44,11 @@ export declare namespace Build {
30
44
  static: Map<string, string>;
31
45
  };
32
46
  };
47
+ /**
48
+ * Maps each entry id to the ids of its shell, layouts, page, error boundaries,
49
+ * loaders, and middleware. Used by codegen to produce the import map
50
+ * that the resolver reads at runtime
51
+ */
33
52
  type Modules = Record<string, {
34
53
  shellId?: string;
35
54
  layoutIds?: (string | null)[];
@@ -55,7 +74,8 @@ export declare namespace Build {
55
74
  readonly ENDPOINT: "$E";
56
75
  };
57
76
  /**
58
- * Finder class to process application routes
77
+ * Encapsulates the logic for scanning the app directory and processing the
78
+ * result into the manifest, import map, and module map
59
79
  */
60
80
  class Finder {
61
81
  #private;
@@ -75,15 +95,13 @@ export declare namespace Build {
75
95
  */
76
96
  static toCanonicalRoute(file: string): string;
77
97
  /**
78
- * Get the import path for a file
79
- * This finds the relative path from the generated
80
- * directory to the file, removes the extension and
81
- * replaces backslashes with forward slashes.
98
+ * Get the import path for a file relative to the generated directory,
99
+ * with the extension stripped and backslashes normalised
82
100
  */
83
101
  static getImportPath(file: string): string;
84
102
  /**
85
- * Run the Finder to get the app route and associated data
86
- * needed for codegen
103
+ * Entry point: scan the app directory then process the result
104
+ * into the manifest, import map, and module map
87
105
  */
88
106
  run(): Promise<{
89
107
  manifest: Record<string, (Endpoint | Segment)[] | Endpoint | Segment>;
@@ -93,7 +111,9 @@ export declare namespace Build {
93
111
  knownRoutes: Set<string>;
94
112
  }>;
95
113
  /**
96
- * Process the scanned route data
114
+ * Transform the raw scan result into the route manifest, import map,
115
+ * and module map for codegen, along with route sets for
116
+ * prerendering and sitemap generation
97
117
  */
98
118
  process(res: ScanResult): Promise<{
99
119
  manifest: Record<string, (Endpoint | Segment)[] | Endpoint | Segment>;
@@ -5,9 +5,16 @@ import { Logger } from '../utils/logger';
5
5
  import { normalisePathname } from './router/utils';
6
6
  import { Prerender } from './prerender';
7
7
  export { Build };
8
+ /**
9
+ * Types, constants, and the Finder class for route discovery and manifest generation.
10
+ * The Finder walks the app directory, builds inheritance chains, and transforms
11
+ * the scan result into codegen artifacts consumed by generated entry files
12
+ */
8
13
  var Build;
9
14
  (function (Build) {
10
15
  const HTTP_VERBS = ['GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'];
16
+ // short prefixes used to namespace entry ids by kind.
17
+ // these appear in the generated manifest and import map
11
18
  Build.EntryKind = {
12
19
  SHELL: '$S',
13
20
  LAYOUT: '$L',
@@ -21,8 +28,85 @@ var Build;
21
28
  ENDPOINT: '$E',
22
29
  };
23
30
  const logger = new Logger();
31
+ // file extensions recognised for page components vs api endpoints
32
+ const EXTENSIONS = {
33
+ page: ['tsx', 'jsx'],
34
+ api: ['ts', 'js'],
35
+ };
36
+ // canonical file names for each route file type
37
+ const TYPES = {
38
+ page: '+page',
39
+ '401': '+401',
40
+ '403': '+403',
41
+ '404': '+404',
42
+ '500': '+500',
43
+ layout: '+layout',
44
+ loading: '+loading',
45
+ middleware: '+middleware',
46
+ endpoint: '+endpoint',
47
+ };
48
+ // pre-computed sets of valid filenames for each type so we can
49
+ // use O(1) lookups during the recursive scan
50
+ const validFiles = {
51
+ [TYPES.page]: new Set(EXTENSIONS.page.map(ext => `${TYPES.page}.${ext}`)),
52
+ [TYPES['401']]: new Set(EXTENSIONS.page.map(ext => `${TYPES['401']}.${ext}`)),
53
+ [TYPES['403']]: new Set(EXTENSIONS.page.map(ext => `${TYPES['403']}.${ext}`)),
54
+ [TYPES['404']]: new Set(EXTENSIONS.page.map(ext => `${TYPES['404']}.${ext}`)),
55
+ [TYPES['500']]: new Set(EXTENSIONS.page.map(ext => `${TYPES['500']}.${ext}`)),
56
+ [TYPES.loading]: new Set(EXTENSIONS.page.map(ext => `${TYPES.loading}.${ext}`)),
57
+ [TYPES.layout]: new Set(EXTENSIONS.page.map(ext => `${TYPES.layout}.${ext}`)),
58
+ [TYPES.middleware]: new Set(EXTENSIONS.api.map(ext => `${TYPES.middleware}.${ext}`)),
59
+ [TYPES.endpoint]: new Set(EXTENSIONS.api.map(ext => `${TYPES.endpoint}.${ext}`)),
60
+ };
61
+ const EMPTY_CHAINS = {
62
+ layouts: [],
63
+ '401s': [],
64
+ '403s': [],
65
+ '404s': [],
66
+ '500s': [],
67
+ loaders: [],
68
+ middlewares: [],
69
+ };
70
+ /**
71
+ * Append the current directory's files to each inherited chain, inserting
72
+ * null when the directory does not declare that file type
73
+ */
74
+ function extendChains(prev, state) {
75
+ return {
76
+ layouts: [...prev.layouts, state.layout ?? null],
77
+ '401s': [...prev['401s'], state['401'] ?? null],
78
+ '403s': [...prev['403s'], state['403'] ?? null],
79
+ '404s': [...prev['404s'], state['404'] ?? null],
80
+ '500s': [...prev['500s'], state['500'] ?? null],
81
+ loaders: [...prev.loaders, state.loader ?? null],
82
+ middlewares: [...prev.middlewares, state.middleware ?? null],
83
+ };
84
+ }
85
+ /**
86
+ * Create a segment entry from the accumulated chains. Returns null when no
87
+ * shell (root layout) exists, which means this path cannot render.
88
+ * The shell is always layouts[0]; remaining are nested
89
+ */
90
+ function buildSegment(dir, page, chains) {
91
+ const shell = chains.layouts[0];
92
+ if (!shell)
93
+ return null;
94
+ return {
95
+ dir,
96
+ page,
97
+ '401s': chains['401s'],
98
+ '403s': chains['403s'],
99
+ '404s': chains['404s'],
100
+ '500s': chains['500s'],
101
+ loaders: chains.loaders,
102
+ middlewares: chains.middlewares,
103
+ layouts: chains.layouts.length > 1 ? chains.layouts.slice(1) : [],
104
+ shell,
105
+ };
106
+ }
24
107
  /**
25
- * Finder class to process application routes
108
+ * Encapsulates the logic for scanning the app directory and processing the
109
+ * result into the manifest, import map, and module map
26
110
  */
27
111
  class Finder {
28
112
  buildContext;
@@ -61,10 +145,8 @@ var Build;
61
145
  return route.startsWith('/') ? route : `/${route}`;
62
146
  }
63
147
  /**
64
- * Get the import path for a file
65
- * This finds the relative path from the generated
66
- * directory to the file, removes the extension and
67
- * replaces backslashes with forward slashes.
148
+ * Get the import path for a file relative to the generated directory,
149
+ * with the extension stripped and backslashes normalised
68
150
  */
69
151
  static getImportPath(file) {
70
152
  const cwd = process.cwd();
@@ -75,8 +157,8 @@ var Build;
75
157
  .replace(/\.(t|j)sx?$/, '');
76
158
  }
77
159
  /**
78
- * Run the Finder to get the app route and associated data
79
- * needed for codegen
160
+ * Entry point: scan the app directory then process the result
161
+ * into the manifest, import map, and module map
80
162
  */
81
163
  async run() {
82
164
  try {
@@ -88,51 +170,16 @@ var Build;
88
170
  }
89
171
  }
90
172
  /**
91
- * Scan the filesystem to get all routes for processing
173
+ * Recursively walk the filesystem starting at `dir`, collecting route files
174
+ * into segments and endpoints. Inheritable files (layouts, error boundaries,
175
+ * loaders, middleware) propagate down through `prev` so child
176
+ * directories inherit parent chains.
92
177
  */
93
- async #scan(dir, res = { segments: [], endpoints: [] }, prev = {
94
- layouts: [],
95
- '401s': [],
96
- '403s': [],
97
- '404s': [],
98
- '500s': [],
99
- loaders: [],
100
- middlewares: [],
101
- }) {
178
+ async #scan(dir, res = { segments: [], endpoints: [] }, prev = EMPTY_CHAINS) {
102
179
  try {
103
- // define valid route files
104
- const EXTENSIONS = {
105
- page: ['tsx', 'jsx'],
106
- api: ['ts', 'js'],
107
- };
108
- // define route file types
109
- const TYPES = {
110
- page: '+page',
111
- '401': '+401',
112
- '403': '+403',
113
- '404': '+404',
114
- '500': '+500',
115
- layout: '+layout',
116
- loading: '+loading',
117
- middleware: '+middleware',
118
- endpoint: '+endpoint',
119
- };
120
- // map of valid files for each type
121
- const validFiles = {
122
- [TYPES.page]: new Set(EXTENSIONS.page.map(ext => `${TYPES.page}.${ext}`)),
123
- [TYPES['401']]: new Set(EXTENSIONS.page.map(ext => `${TYPES['401']}.${ext}`)),
124
- [TYPES['403']]: new Set(EXTENSIONS.page.map(ext => `${TYPES['403']}.${ext}`)),
125
- [TYPES['404']]: new Set(EXTENSIONS.page.map(ext => `${TYPES['404']}.${ext}`)),
126
- [TYPES['500']]: new Set(EXTENSIONS.page.map(ext => `${TYPES['500']}.${ext}`)),
127
- [TYPES.loading]: new Set(EXTENSIONS.page.map(ext => `${TYPES.loading}.${ext}`)),
128
- [TYPES.layout]: new Set(EXTENSIONS.page.map(ext => `${TYPES.layout}.${ext}`)),
129
- [TYPES.middleware]: new Set(EXTENSIONS.api.map(ext => `${TYPES.middleware}.${ext}`)),
130
- [TYPES.endpoint]: new Set(EXTENSIONS.api.map(ext => `${TYPES.endpoint}.${ext}`)),
131
- };
132
180
  const files = await fs.readdir(dir, { withFileTypes: true });
133
- // keep a predictable order so layout/loading are picked
134
- // up before page. Avoids OS dir ordering causing pages
135
- // to steal layout/loaders first and drop alignment
181
+ // keep a predictable order so layout/loading are picked up before page.
182
+ // Avoids OS dir ordering causing alignment issues
136
183
  files.sort((a, b) => {
137
184
  if (a.isFile() && b.isDirectory())
138
185
  return -1;
@@ -165,143 +212,71 @@ var Build;
165
212
  }
166
213
  return 0;
167
214
  });
168
- // current layout, status boundaries, loader, middleware, and page files for this segment
169
- let currentLayout;
170
- let current401;
171
- let current403;
172
- let current404;
173
- let current500;
174
- let currentLoader;
175
- let currentMiddleware;
215
+ const state = {};
176
216
  let currentPage;
177
217
  for (const file of files) {
178
218
  const route = path.join(dir, file.name);
179
219
  if (file.isDirectory()) {
180
- // before recursing, create segment for current dir if it
181
- // has a layout (defines a wrapper for child routes)
182
- if (!currentPage && currentLayout) {
183
- const layouts = [...prev.layouts, currentLayout];
184
- const unauthorized = [...prev['401s'], current401 ?? null];
185
- const forbidden = [...prev['403s'], current403 ?? null];
186
- const notFounds = [...prev['404s'], current404 ?? null];
187
- const serverErrors = [...prev['500s'], current500 ?? null];
188
- const loaders = [...prev.loaders, currentLoader ?? null];
189
- const middlewares = [...prev.middlewares, currentMiddleware ?? null];
190
- const shell = layouts[0];
191
- if (shell) {
192
- res.segments.push({
193
- dir,
194
- page: undefined,
195
- '401s': unauthorized,
196
- '403s': forbidden,
197
- '404s': notFounds,
198
- '500s': serverErrors,
199
- loaders,
200
- middlewares,
201
- layouts: layouts.length > 1 ? layouts.slice(1) : [],
202
- shell,
203
- });
204
- }
220
+ // before recursing, create segment for current dir
221
+ // if it has a layout (wraps child routes)
222
+ if (!currentPage && state.layout) {
223
+ const segment = buildSegment(dir, undefined, extendChains(prev, state));
224
+ if (segment)
225
+ res.segments.push(segment);
205
226
  }
206
- const next = {
207
- layouts: [...prev.layouts, currentLayout ?? null],
208
- '401s': [...prev['401s'], current401 ?? null],
209
- '403s': [...prev['403s'], current403 ?? null],
210
- '404s': [...prev['404s'], current404 ?? null],
211
- '500s': [...prev['500s'], current500 ?? null],
212
- loaders: [...prev.loaders, currentLoader ?? null],
213
- middlewares: [...prev.middlewares, currentMiddleware ?? null],
214
- };
215
- await this.#scan(route, res, next);
227
+ await this.#scan(route, res, extendChains(prev, state));
216
228
  }
217
229
  else {
218
230
  const base = path.basename(file.name);
219
231
  const relative = path.relative(process.cwd(), route).replace(/\\/g, '/');
220
232
  if (validFiles[TYPES.layout].has(base)) {
221
- currentLayout = relative;
233
+ state.layout = relative;
222
234
  }
223
235
  else if (validFiles[TYPES['401']].has(base)) {
224
- current401 = relative;
236
+ state['401'] = relative;
225
237
  }
226
238
  else if (validFiles[TYPES['403']].has(base)) {
227
- current403 = relative;
239
+ state['403'] = relative;
228
240
  }
229
241
  else if (validFiles[TYPES['404']].has(base)) {
230
- current404 = relative;
242
+ state['404'] = relative;
231
243
  }
232
244
  else if (validFiles[TYPES['500']].has(base)) {
233
- current500 = relative;
245
+ state['500'] = relative;
234
246
  }
235
247
  else if (validFiles[TYPES.loading].has(base)) {
236
- currentLoader = relative;
248
+ state.loader = relative;
237
249
  }
238
250
  else if (validFiles[TYPES.middleware].has(base)) {
239
- currentMiddleware = relative;
251
+ state.middleware = relative;
240
252
  }
241
253
  else if (validFiles[TYPES.endpoint].has(base)) {
242
254
  res.endpoints.push({
243
255
  file: relative,
244
- middlewares: [...prev.middlewares, currentMiddleware ?? null],
256
+ middlewares: [...prev.middlewares, state.middleware ?? null],
245
257
  });
246
258
  }
247
259
  else if (validFiles[TYPES.page].has(base)) {
248
260
  currentPage = relative;
249
- const layouts = [...prev.layouts, currentLayout ?? null];
250
- const unauthorized = [...prev['401s'], current401 ?? null];
251
- const forbidden = [...prev['403s'], current403 ?? null];
252
- const notFounds = [...prev['404s'], current404 ?? null];
253
- const serverErrors = [...prev['500s'], current500 ?? null];
254
- const loaders = [...prev.loaders, currentLoader ?? null];
255
- const middlewares = [...prev.middlewares, currentMiddleware ?? null];
256
- const shell = layouts?.[0];
257
- if (!shell)
261
+ const segment = buildSegment(dir, relative, extendChains(prev, state));
262
+ if (!segment)
258
263
  throw new Error('Missing app shell');
259
- res.segments.push({
260
- dir,
261
- page: relative,
262
- '401s': unauthorized,
263
- '403s': forbidden,
264
- '404s': notFounds,
265
- '500s': serverErrors,
266
- loaders,
267
- middlewares,
268
- layouts: layouts.length > 1 ? layouts.slice(1) : [],
269
- shell,
270
- });
264
+ res.segments.push(segment);
271
265
  }
272
266
  }
273
267
  }
274
268
  // warn if segment has status boundaries/loading but no page or layout
275
269
  if (!currentPage &&
276
- !currentLayout &&
277
- (current401 || current403 || current404 || current500 || currentLoader)) {
270
+ !state.layout &&
271
+ (state['401'] || state['403'] || state['404'] || state['500'] || state.loader)) {
278
272
  logger.warn(`[Build:Finder:#scan]: ${dir} has status route files or +loading but no +page or +layout. This path will not be routable (404), but these files will still be inherited by child routes`);
279
273
  }
280
- // create segment if we have a layout but no page and
281
- // haven't created one yet (no subdirectories triggered it)
282
- if (!currentPage && currentLayout && !res.segments.some(s => s.dir === dir)) {
283
- const layouts = [...prev.layouts, currentLayout];
284
- const unauthorized = [...prev['401s'], current401 ?? null];
285
- const forbidden = [...prev['403s'], current403 ?? null];
286
- const notFounds = [...prev['404s'], current404 ?? null];
287
- const serverErrors = [...prev['500s'], current500 ?? null];
288
- const loaders = [...prev.loaders, currentLoader ?? null];
289
- const middlewares = [...prev.middlewares, currentMiddleware ?? null];
290
- const shell = layouts[0];
291
- if (shell) {
292
- res.segments.push({
293
- dir,
294
- page: undefined,
295
- '401s': unauthorized,
296
- '403s': forbidden,
297
- '404s': notFounds,
298
- '500s': serverErrors,
299
- loaders,
300
- middlewares,
301
- layouts: layouts.length > 1 ? layouts.slice(1) : [],
302
- shell,
303
- });
304
- }
274
+ // create segment if we have a layout but no page
275
+ // and haven't created one yet
276
+ if (!currentPage && state.layout && !res.segments.some(s => s.dir === dir)) {
277
+ const segment = buildSegment(dir, undefined, extendChains(prev, state));
278
+ if (segment)
279
+ res.segments.push(segment);
305
280
  }
306
281
  return res;
307
282
  }
@@ -314,35 +289,52 @@ var Build;
314
289
  }
315
290
  }
316
291
  /**
317
- * Process the scanned route data
292
+ * Transform the raw scan result into the route manifest, import map,
293
+ * and module map for codegen, along with route sets for
294
+ * prerendering and sitemap generation
318
295
  */
319
296
  async process(res) {
320
297
  const processed = new Set();
298
+ // concrete routes to prerender at build time
299
+ // (static, or dynamic with static params)
321
300
  const prerenderRoutes = new Set();
301
+ // all concrete (non-dynamic, non-wildcard) page
302
+ // routes for sitemap generation
322
303
  const knownRoutes = new Set();
323
304
  const trailingSlash = this.config?.trailingSlash ?? 'never';
305
+ // route path → segment/endpoint entries, used at runtime
306
+ // to look up the component tree for a matched route
324
307
  const manifest = {};
325
- // imports for endpoints and components
308
+ // entry id import path. Split into static (shell, middleware,
309
+ // endpoints) and dynamic (layouts, pages, boundaries)
310
+ // so the bundler can code-split
326
311
  const imports = {
327
312
  endpoints: { static: new Map() },
328
313
  components: { static: new Map(), dynamic: new Map() },
329
314
  middlewares: { static: new Map() },
330
315
  };
316
+ // entry id → related entry ids. Wires up which shell,
317
+ // layouts, loaders, and middleware belong
318
+ // to each page
331
319
  const modules = {};
320
+ // cache prerender flags per file path so shared layouts
321
+ // only trigger one export read across routes
332
322
  const prerenderCache = new Map();
333
323
  for (const segment of res.segments) {
334
324
  try {
335
325
  if (!this.buildContext || !this.config)
336
326
  continue;
337
327
  const { shell: shellPath, layouts: layoutPaths, '401s': unauthorizedPaths, '403s': forbiddenPaths, page: pagePath, '404s': notFoundPaths, '500s': serverErrorPaths, loaders: loaderPaths, middlewares: middlewarePaths, dir, } = segment;
338
- // route is derived from dir path, not page
328
+ // derive the route pattern from the directory path, falling
329
+ // back to a synthetic +page.tsx when this is
330
+ // a layout-only segment
339
331
  const route = Finder.toCanonicalRoute(pagePath ?? `${dir.replace(/\\/g, '/')}/+page.tsx`);
340
332
  const params = Finder.getParams(dir);
341
333
  const depth = Finder.getDepth(route);
342
334
  const isDynamic = route.includes(':');
343
335
  const isWildcard = route.includes('*');
344
- // effective mode for this segment; start from global config then
345
- // apply shell/layout/page overrides
336
+ // effective mode for this segment; start from global
337
+ // config then apply shell/layout/page overrides
346
338
  let currentPrerenderMode = this.config?.prerender ?? false;
347
339
  /**
348
340
  * Apply explicit prerender mode overrides in inheritance order
@@ -463,8 +455,8 @@ var Build;
463
455
  const middlewareId = `${Build.EntryKind.MIDDLEWARE}${Bun.hash(middlewareImport)}`;
464
456
  middlewareIds.push(middlewareId);
465
457
  if (!processed.has(middlewarePath)) {
466
- // route scanning only tells us this is a +middleware file path so
467
- // we still validate that the module actually exports middleware
458
+ // route scanning only tells us this is a +middleware file path
459
+ // so we still validate the module exports middleware
468
460
  if (!(await this.buildContext.exportReader.has(middlewarePath, 'middleware'))) {
469
461
  throw new Error(`Missing middleware export in ${middlewarePath}`);
470
462
  }
@@ -482,10 +474,13 @@ var Build;
482
474
  imports.components.dynamic.set(entryId, Finder.getImportPath(pagePath));
483
475
  processed.add(pagePath);
484
476
  }
477
+ // resolve final prerender mode after shell → layout → page overrides
485
478
  const shouldPrerender = currentPrerenderMode !== false;
486
479
  const prerenderMode = shouldPrerender
487
480
  ? currentPrerenderMode
488
481
  : false;
482
+ // collect concrete prerender routes; for dynamic routes,
483
+ // expand using the page's exported static params
489
484
  if (shouldPrerender) {
490
485
  if (!isDynamic && !isWildcard) {
491
486
  prerenderRoutes.add(normalisePathname(route, trailingSlash));
@@ -497,6 +492,8 @@ var Build;
497
492
  }
498
493
  }
499
494
  }
495
+ // track all concrete routes regardless of prerender mode
496
+ // so the sitemap can include ssr-only pages too
500
497
  if (!isDynamic && !isWildcard) {
501
498
  knownRoutes.add(normalisePathname(route, trailingSlash));
502
499
  }
@@ -552,6 +549,9 @@ var Build;
552
549
  logger.error('[Build:Finder:process]: failed to process segment', err);
553
550
  }
554
551
  }
552
+ // process api endpoints — each file can export multiple HTTP
553
+ // verbs which become separate manifest entries grouped
554
+ // under the same route
555
555
  for (const endpoint of res.endpoints) {
556
556
  try {
557
557
  const endpointFilePath = endpoint.file;
@@ -575,8 +575,8 @@ var Build;
575
575
  const middlewareImport = Finder.getImportPath(middlewarePath);
576
576
  const middlewareId = `${Build.EntryKind.MIDDLEWARE}${Bun.hash(middlewareImport)}`;
577
577
  if (!processed.has(middlewarePath)) {
578
- // endpoint middleware discovery gives us file paths, not proof of the export
579
- // so check the module shape before we register the import
578
+ // endpoint middleware discovery gives us file paths, not proof
579
+ // of the export so check the module shape first
580
580
  if (!(await this.buildContext.exportReader.has(middlewarePath, 'middleware'))) {
581
581
  throw new Error(`Missing middleware export in ${middlewarePath}`);
582
582
  }
@@ -597,6 +597,9 @@ var Build;
597
597
  modules[endpointId] = { endpointId, middlewareIds };
598
598
  processed.add(endpointFilePath);
599
599
  }
600
+ // single-verb endpoints store a plain entry, multi-verb
601
+ // endpoints store an array so the router can
602
+ // match by method at request time
600
603
  const entry = group.length === 1 ? group[0] : group;
601
604
  if (endpointMiddlewarePaths.length) {
602
605
  modules[route] = {
@@ -22,7 +22,7 @@ export declare function action(req: SolasRequest): Promise<{
22
22
  * Check if a request is an action request and reuse parsed FormData
23
23
  * when multipart action detection already had to inspect the body
24
24
  */
25
- export declare function maybeActionWithParsedFormData(req: Request): Promise<{
25
+ export declare function maybeAction(req: Request): Promise<{
26
26
  action: boolean;
27
27
  formData: null;
28
28
  } | {
@@ -186,7 +186,7 @@ export async function action(req) {
186
186
  * Check if a request is an action request and reuse parsed FormData
187
187
  * when multipart action detection already had to inspect the body
188
188
  */
189
- export async function maybeActionWithParsedFormData(req) {
189
+ export async function maybeAction(req) {
190
190
  if (req.method !== 'POST')
191
191
  return { action: false, formData: null };
192
192
  if (req.headers.has('x-rsc-action-id'))
@@ -2,7 +2,7 @@ import path from 'node:path';
2
2
  import { match as createMatch } from 'path-to-regexp';
3
3
  import { Solas } from '../../solas';
4
4
  import { getAlternatePathname, normalisePathname, toPathPattern } from './utils';
5
- import { maybeActionWithParsedFormData } from '../env/rsc';
5
+ import { maybeAction } from '../env/rsc';
6
6
  import { HttpException } from '../navigation/http-exception';
7
7
  /**
8
8
  * Handle routing and matching for server requests
@@ -180,7 +180,7 @@ export class Router {
180
180
  url.pathname = path;
181
181
  req = new Request(url.toString(), req);
182
182
  }
183
- const { action: isAction, formData: parsedFormData } = await maybeActionWithParsedFormData(req);
183
+ const { action: isAction, formData: parsedFormData } = await maybeAction(req);
184
184
  action = isAction;
185
185
  // action requests stay on the same pathname only the method is
186
186
  // normalised to GET this lets page/layout routes match for
@@ -2,5 +2,5 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
2
2
  export default function Err({ error }) {
3
3
  const title = 'status' in error ? `${error.status} - ${error.message}` : error.message;
4
4
  return (_jsxs(_Fragment, { children: [
5
- _jsx("meta", { name: "robots", content: "noindex,nofollow" }), _jsx("title", { children: title }), _jsx("h1", { children: title }), _jsx("p", { children: error.message }), process.env.NODE_ENV === 'development' && error?.stack && (_jsx("pre", { children: error.stack }))] }));
5
+ _jsx("meta", { name: "robots", content: "noindex,nofollow" }), _jsx("title", { children: title }), _jsx("h1", { children: title }), _jsx("p", { children: error.message }), process.env.NODE_ENV === 'development' && error?.stack && _jsx("pre", { children: error.stack })] }));
6
6
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jk2908/solas",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "A React Server Components meta-framework powered by Vite",
5
5
  "keywords": [
6
6
  "framework",