@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,643 +1,631 @@
1
1
  import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import { Logger } from '../utils/logger.js';
4
- import { Solas } from '../solas.js';
4
+ import * as Config from '../config.js';
5
5
  import { normalisePathname } from './http-router/utils.js';
6
- import { Prerender } from './prerender.js';
6
+ import * as Prerender from './prerender.js';
7
7
  import { Runtime } from './runtimes/runtime.js';
8
- export { Build };
8
+ const HTTP_VERBS = ['GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'];
9
+ // short prefixes used to namespace entry ids by kind.
10
+ // these appear in the generated manifest and import map
11
+ export const EntryKind = {
12
+ SHELL: '$S',
13
+ LAYOUT: '$L',
14
+ PAGE: '$P',
15
+ 401: '$401',
16
+ 403: '$403',
17
+ 404: '$404',
18
+ 500: '$500',
19
+ LOADING: '$LOAD',
20
+ MIDDLEWARE: '$MW',
21
+ ENDPOINT: '$E',
22
+ };
23
+ const logger = new Logger();
24
+ // file extensions recognised for page components vs api endpoints
25
+ const EXTENSIONS = {
26
+ page: ['tsx', 'jsx'],
27
+ api: ['ts', 'js'],
28
+ };
29
+ // canonical file names for each route file type
30
+ const TYPES = {
31
+ page: '+page',
32
+ '401': '+401',
33
+ '403': '+403',
34
+ '404': '+404',
35
+ '500': '+500',
36
+ layout: '+layout',
37
+ loading: '+loading',
38
+ middleware: '+middleware',
39
+ endpoint: '+endpoint',
40
+ };
41
+ // pre-computed sets of valid filenames for each type so we can
42
+ // use O(1) lookups during the recursive scan
43
+ const validFiles = {
44
+ [TYPES.page]: new Set(EXTENSIONS.page.map(ext => `${TYPES.page}.${ext}`)),
45
+ [TYPES['401']]: new Set(EXTENSIONS.page.map(ext => `${TYPES['401']}.${ext}`)),
46
+ [TYPES['403']]: new Set(EXTENSIONS.page.map(ext => `${TYPES['403']}.${ext}`)),
47
+ [TYPES['404']]: new Set(EXTENSIONS.page.map(ext => `${TYPES['404']}.${ext}`)),
48
+ [TYPES['500']]: new Set(EXTENSIONS.page.map(ext => `${TYPES['500']}.${ext}`)),
49
+ [TYPES.loading]: new Set(EXTENSIONS.page.map(ext => `${TYPES.loading}.${ext}`)),
50
+ [TYPES.layout]: new Set(EXTENSIONS.page.map(ext => `${TYPES.layout}.${ext}`)),
51
+ [TYPES.middleware]: new Set(EXTENSIONS.api.map(ext => `${TYPES.middleware}.${ext}`)),
52
+ [TYPES.endpoint]: new Set(EXTENSIONS.api.map(ext => `${TYPES.endpoint}.${ext}`)),
53
+ };
54
+ const EMPTY_CHAINS = {
55
+ layouts: [],
56
+ '401s': [],
57
+ '403s': [],
58
+ '404s': [],
59
+ '500s': [],
60
+ loaders: [],
61
+ middlewares: [],
62
+ };
9
63
  /**
10
- * Types, constants, and the Finder class for route discovery and manifest generation.
11
- * The Finder walks the app directory, builds inheritance chains, and transforms
12
- * the scan result into codegen artifacts consumed by generated entry files
64
+ * Append the current directory's files to each inherited chain, inserting
65
+ * null when the directory does not declare that file type
13
66
  */
14
- var Build;
15
- (function (Build) {
16
- const HTTP_VERBS = ['GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'];
17
- // short prefixes used to namespace entry ids by kind.
18
- // these appear in the generated manifest and import map
19
- Build.EntryKind = {
20
- SHELL: '$S',
21
- LAYOUT: '$L',
22
- PAGE: '$P',
23
- 401: '$401',
24
- 403: '$403',
25
- 404: '$404',
26
- 500: '$500',
27
- LOADING: '$LOAD',
28
- MIDDLEWARE: '$MW',
29
- ENDPOINT: '$E',
67
+ function extendChains(prev, state) {
68
+ return {
69
+ layouts: [...prev.layouts, state.layout ?? null],
70
+ '401s': [...prev['401s'], state['401'] ?? null],
71
+ '403s': [...prev['403s'], state['403'] ?? null],
72
+ '404s': [...prev['404s'], state['404'] ?? null],
73
+ '500s': [...prev['500s'], state['500'] ?? null],
74
+ loaders: [...prev.loaders, state.loader ?? null],
75
+ middlewares: [...prev.middlewares, state.middleware ?? null],
30
76
  };
31
- const logger = new Logger();
32
- // file extensions recognised for page components vs api endpoints
33
- const EXTENSIONS = {
34
- page: ['tsx', 'jsx'],
35
- api: ['ts', 'js'],
36
- };
37
- // canonical file names for each route file type
38
- const TYPES = {
39
- page: '+page',
40
- '401': '+401',
41
- '403': '+403',
42
- '404': '+404',
43
- '500': '+500',
44
- layout: '+layout',
45
- loading: '+loading',
46
- middleware: '+middleware',
47
- endpoint: '+endpoint',
48
- };
49
- // pre-computed sets of valid filenames for each type so we can
50
- // use O(1) lookups during the recursive scan
51
- const validFiles = {
52
- [TYPES.page]: new Set(EXTENSIONS.page.map(ext => `${TYPES.page}.${ext}`)),
53
- [TYPES['401']]: new Set(EXTENSIONS.page.map(ext => `${TYPES['401']}.${ext}`)),
54
- [TYPES['403']]: new Set(EXTENSIONS.page.map(ext => `${TYPES['403']}.${ext}`)),
55
- [TYPES['404']]: new Set(EXTENSIONS.page.map(ext => `${TYPES['404']}.${ext}`)),
56
- [TYPES['500']]: new Set(EXTENSIONS.page.map(ext => `${TYPES['500']}.${ext}`)),
57
- [TYPES.loading]: new Set(EXTENSIONS.page.map(ext => `${TYPES.loading}.${ext}`)),
58
- [TYPES.layout]: new Set(EXTENSIONS.page.map(ext => `${TYPES.layout}.${ext}`)),
59
- [TYPES.middleware]: new Set(EXTENSIONS.api.map(ext => `${TYPES.middleware}.${ext}`)),
60
- [TYPES.endpoint]: new Set(EXTENSIONS.api.map(ext => `${TYPES.endpoint}.${ext}`)),
61
- };
62
- const EMPTY_CHAINS = {
63
- layouts: [],
64
- '401s': [],
65
- '403s': [],
66
- '404s': [],
67
- '500s': [],
68
- loaders: [],
69
- middlewares: [],
77
+ }
78
+ /**
79
+ * Create a segment entry from the accumulated chains. Returns null when no
80
+ * shell (root layout) exists, which means this path cannot render.
81
+ * The shell is always layouts[0]; remaining are nested
82
+ */
83
+ function buildSegment(dir, page, chains) {
84
+ const shell = chains.layouts[0];
85
+ if (!shell)
86
+ return null;
87
+ return {
88
+ dir,
89
+ page,
90
+ '401s': chains['401s'],
91
+ '403s': chains['403s'],
92
+ '404s': chains['404s'],
93
+ '500s': chains['500s'],
94
+ loaders: chains.loaders,
95
+ middlewares: chains.middlewares,
96
+ layouts: chains.layouts.length > 1 ? chains.layouts.slice(1) : [],
97
+ shell,
70
98
  };
99
+ }
100
+ /**
101
+ * Encapsulates the logic for scanning the app directory and processing the
102
+ * result into the manifest, import map, and module map
103
+ */
104
+ export class Finder {
105
+ buildContext;
106
+ config;
107
+ constructor(buildContext, config) {
108
+ this.buildContext = buildContext;
109
+ this.config = config;
110
+ }
71
111
  /**
72
- * Append the current directory's files to each inherited chain, inserting
73
- * null when the directory does not declare that file type
112
+ * Extracts dynamic parameter names from a file path
74
113
  */
75
- function extendChains(prev, state) {
76
- return {
77
- layouts: [...prev.layouts, state.layout ?? null],
78
- '401s': [...prev['401s'], state['401'] ?? null],
79
- '403s': [...prev['403s'], state['403'] ?? null],
80
- '404s': [...prev['404s'], state['404'] ?? null],
81
- '500s': [...prev['500s'], state['500'] ?? null],
82
- loaders: [...prev.loaders, state.loader ?? null],
83
- middlewares: [...prev.middlewares, state.middleware ?? null],
84
- };
114
+ static getParams(file) {
115
+ return Array.from(file.matchAll(/\[(?:\.\.\.)?([^\]]+)\]/g), m => m[1]);
85
116
  }
86
117
  /**
87
- * Create a segment entry from the accumulated chains. Returns null when no
88
- * shell (root layout) exists, which means this path cannot render.
89
- * The shell is always layouts[0]; remaining are nested
118
+ * Get the depth of a route based on slashes
90
119
  */
91
- function buildSegment(dir, page, chains) {
92
- const shell = chains.layouts[0];
93
- if (!shell)
94
- return null;
95
- return {
96
- dir,
97
- page,
98
- '401s': chains['401s'],
99
- '403s': chains['403s'],
100
- '404s': chains['404s'],
101
- '500s': chains['500s'],
102
- loaders: chains.loaders,
103
- middlewares: chains.middlewares,
104
- layouts: chains.layouts.length > 1 ? chains.layouts.slice(1) : [],
105
- shell,
106
- };
120
+ static getDepth(route) {
121
+ if (route === '/')
122
+ return 0;
123
+ // count slashes to determine depth
124
+ return route.split('/').length - 1;
107
125
  }
108
126
  /**
109
- * Encapsulates the logic for scanning the app directory and processing the
110
- * result into the manifest, import map, and module map
127
+ * Convert a file path to a canonical route.
111
128
  */
112
- class Finder {
113
- buildContext;
114
- config;
115
- constructor(buildContext, config) {
116
- this.buildContext = buildContext;
117
- this.config = config;
118
- }
119
- /**
120
- * Extracts dynamic parameter names from a file path
121
- */
122
- static getParams(file) {
123
- return Array.from(file.matchAll(/\[(?:\.\.\.)?([^\]]+)\]/g), m => m[1]);
124
- }
125
- /**
126
- * Get the depth of a route based on slashes
127
- */
128
- static getDepth(route) {
129
- if (route === '/')
130
- return 0;
131
- // count slashes to determine depth
132
- return route.split('/').length - 1;
133
- }
134
- /**
135
- * Convert a file path to a canonical route.
136
- */
137
- static toCanonicalRoute(file) {
138
- const route = file
139
- .replace(new RegExp(`^${Solas.Config.APP_DIR}`), '')
140
- .replace(/\/\+page\.(j|t)sx?$/, '')
141
- .replace(/\/\+endpoint\.(j|t)sx?$/, '')
142
- .replace(/\[\.\.\..+?\]/g, '*') // wildcard routes
143
- .replace(/\[(.+?)\]/g, ':$1'); // dynamic routes
144
- if (!route || route === '')
145
- return '/';
146
- return route.startsWith('/') ? route : `/${route}`;
147
- }
148
- /**
149
- * Get the import path for a file relative to the generated directory,
150
- * with the extension stripped and backslashes normalised
151
- */
152
- static getImportPath(file) {
153
- const cwd = process.cwd();
154
- const generatedDir = path.join(cwd, Solas.Config.GENERATED_DIR);
155
- return path
156
- .relative(generatedDir, path.resolve(cwd, file))
157
- .replace(/\\/g, '/')
158
- .replace(/\.(t|j)sx?$/, '');
129
+ static toCanonicalRoute(file) {
130
+ const route = file
131
+ .replace(new RegExp(`^${Config.APP_DIR}`), '')
132
+ .replace(/\/\+page\.(j|t)sx?$/, '')
133
+ .replace(/\/\+endpoint\.(j|t)sx?$/, '')
134
+ .replace(/\[\.\.\..+?\]/g, '*') // wildcard routes
135
+ .replace(/\[(.+?)\]/g, ':$1'); // dynamic routes
136
+ if (!route || route === '')
137
+ return '/';
138
+ return route.startsWith('/') ? route : `/${route}`;
139
+ }
140
+ /**
141
+ * Get the import path for a file relative to the generated directory,
142
+ * with the extension stripped and backslashes normalised
143
+ */
144
+ static getImportPath(file) {
145
+ const cwd = process.cwd();
146
+ const generatedDir = path.join(cwd, Config.GENERATED_DIR);
147
+ return path
148
+ .relative(generatedDir, path.resolve(cwd, file))
149
+ .replace(/\\/g, '/')
150
+ .replace(/\.(t|j)sx?$/, '');
151
+ }
152
+ /**
153
+ * Entry point: scan the app directory then process the result
154
+ * into the manifest, import map, and module map
155
+ */
156
+ async run() {
157
+ try {
158
+ return await this.process(await this.#scan(Config.APP_DIR));
159
159
  }
160
- /**
161
- * Entry point: scan the app directory then process the result
162
- * into the manifest, import map, and module map
163
- */
164
- async run() {
165
- try {
166
- return await this.process(await this.#scan(Solas.Config.APP_DIR));
167
- }
168
- catch (err) {
169
- logger.error('[Build:Finder:run]: failed to build manifest', err);
170
- throw err;
171
- }
160
+ catch (err) {
161
+ logger.error('[Build:Finder:run]: failed to build manifest', err);
162
+ throw err;
172
163
  }
173
- /**
174
- * Recursively walk the filesystem starting at `dir`, collecting route files
175
- * into segments and endpoints. Inheritable files (layouts, error boundaries,
176
- * loaders, middleware) propagate down through `prev` so child
177
- * directories inherit parent chains.
178
- */
179
- async #scan(dir, res = { segments: [], endpoints: [] }, prev = EMPTY_CHAINS) {
180
- try {
181
- const files = await fs.readdir(dir, { withFileTypes: true });
182
- // keep a predictable order so layout/loading are picked up before page.
183
- // Avoids OS dir ordering causing alignment issues
184
- files.sort((a, b) => {
185
- if (a.isFile() && b.isDirectory())
186
- return -1;
187
- if (a.isDirectory() && b.isFile())
188
- return 1;
189
- if (a.isFile() && b.isFile()) {
190
- const priority = (d) => {
191
- const base = path.basename(d.name);
192
- if (validFiles[TYPES.layout].has(base))
193
- return 0;
194
- if (validFiles[TYPES['401']].has(base))
195
- return 1;
196
- if (validFiles[TYPES['403']].has(base))
197
- return 2;
198
- if (validFiles[TYPES['404']].has(base))
199
- return 3;
200
- if (validFiles[TYPES['500']].has(base))
201
- return 4;
202
- if (validFiles[TYPES.loading].has(base))
203
- return 5;
204
- if (validFiles[TYPES.middleware].has(base))
205
- return 6;
206
- if (validFiles[TYPES.page].has(base))
207
- return 7;
208
- if (validFiles[TYPES.endpoint].has(base))
209
- return 8;
164
+ }
165
+ /**
166
+ * Recursively walk the filesystem starting at `dir`, collecting route files
167
+ * into segments and endpoints. Inheritable files (layouts, error boundaries,
168
+ * loaders, middleware) propagate down through `prev` so child
169
+ * directories inherit parent chains.
170
+ */
171
+ async #scan(dir, res = { segments: [], endpoints: [] }, prev = EMPTY_CHAINS) {
172
+ try {
173
+ const files = await fs.readdir(dir, { withFileTypes: true });
174
+ // keep a predictable order so layout/loading are picked up before page.
175
+ // Avoids OS dir ordering causing alignment issues
176
+ files.sort((a, b) => {
177
+ if (a.isFile() && b.isDirectory())
178
+ return -1;
179
+ if (a.isDirectory() && b.isFile())
180
+ return 1;
181
+ if (a.isFile() && b.isFile()) {
182
+ const priority = (d) => {
183
+ const base = path.basename(d.name);
184
+ if (validFiles[TYPES.layout].has(base))
185
+ return 0;
186
+ if (validFiles[TYPES['401']].has(base))
187
+ return 1;
188
+ if (validFiles[TYPES['403']].has(base))
189
+ return 2;
190
+ if (validFiles[TYPES['404']].has(base))
191
+ return 3;
192
+ if (validFiles[TYPES['500']].has(base))
193
+ return 4;
194
+ if (validFiles[TYPES.loading].has(base))
195
+ return 5;
196
+ if (validFiles[TYPES.middleware].has(base))
197
+ return 6;
198
+ if (validFiles[TYPES.page].has(base))
199
+ return 7;
200
+ if (validFiles[TYPES.endpoint].has(base))
210
201
  return 8;
211
- };
212
- return priority(a) - priority(b);
213
- }
214
- return 0;
215
- });
216
- const state = {};
217
- let currentPage;
218
- for (const file of files) {
219
- const route = path.join(dir, file.name);
220
- if (file.isDirectory()) {
221
- // before recursing, create segment for current dir
222
- // if it has a layout (wraps child routes)
223
- if (!currentPage && state.layout) {
224
- const segment = buildSegment(dir, undefined, extendChains(prev, state));
225
- if (segment)
226
- res.segments.push(segment);
227
- }
228
- await this.#scan(route, res, extendChains(prev, state));
229
- }
230
- else {
231
- const base = path.basename(file.name);
232
- const relative = path.relative(process.cwd(), route).replace(/\\/g, '/');
233
- if (validFiles[TYPES.layout].has(base)) {
234
- state.layout = relative;
235
- }
236
- else if (validFiles[TYPES['401']].has(base)) {
237
- state['401'] = relative;
238
- }
239
- else if (validFiles[TYPES['403']].has(base)) {
240
- state['403'] = relative;
241
- }
242
- else if (validFiles[TYPES['404']].has(base)) {
243
- state['404'] = relative;
244
- }
245
- else if (validFiles[TYPES['500']].has(base)) {
246
- state['500'] = relative;
247
- }
248
- else if (validFiles[TYPES.loading].has(base)) {
249
- state.loader = relative;
250
- }
251
- else if (validFiles[TYPES.middleware].has(base)) {
252
- state.middleware = relative;
253
- }
254
- else if (validFiles[TYPES.endpoint].has(base)) {
255
- res.endpoints.push({
256
- file: relative,
257
- middlewares: [...prev.middlewares, state.middleware ?? null],
258
- });
259
- }
260
- else if (validFiles[TYPES.page].has(base)) {
261
- currentPage = relative;
262
- const segment = buildSegment(dir, relative, extendChains(prev, state));
263
- if (!segment)
264
- throw new Error('Missing app shell');
202
+ return 8;
203
+ };
204
+ return priority(a) - priority(b);
205
+ }
206
+ return 0;
207
+ });
208
+ const state = {};
209
+ let currentPage;
210
+ for (const file of files) {
211
+ const route = path.join(dir, file.name);
212
+ if (file.isDirectory()) {
213
+ // before recursing, create segment for current dir
214
+ // if it has a layout (wraps child routes)
215
+ if (!currentPage && state.layout) {
216
+ const segment = buildSegment(dir, undefined, extendChains(prev, state));
217
+ if (segment)
265
218
  res.segments.push(segment);
266
- }
267
219
  }
220
+ await this.#scan(route, res, extendChains(prev, state));
268
221
  }
269
- // warn if segment has status boundaries/loading but no page or layout
270
- if (!currentPage &&
271
- !state.layout &&
272
- (state['401'] || state['403'] || state['404'] || state['500'] || state.loader)) {
273
- 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`);
274
- }
275
- // create segment if we have a layout but no page
276
- // and haven't created one yet
277
- if (!currentPage && state.layout && !res.segments.some(s => s.dir === dir)) {
278
- const segment = buildSegment(dir, undefined, extendChains(prev, state));
279
- if (segment)
222
+ else {
223
+ const base = path.basename(file.name);
224
+ const relative = path.relative(process.cwd(), route).replace(/\\/g, '/');
225
+ if (validFiles[TYPES.layout].has(base)) {
226
+ state.layout = relative;
227
+ }
228
+ else if (validFiles[TYPES['401']].has(base)) {
229
+ state['401'] = relative;
230
+ }
231
+ else if (validFiles[TYPES['403']].has(base)) {
232
+ state['403'] = relative;
233
+ }
234
+ else if (validFiles[TYPES['404']].has(base)) {
235
+ state['404'] = relative;
236
+ }
237
+ else if (validFiles[TYPES['500']].has(base)) {
238
+ state['500'] = relative;
239
+ }
240
+ else if (validFiles[TYPES.loading].has(base)) {
241
+ state.loader = relative;
242
+ }
243
+ else if (validFiles[TYPES.middleware].has(base)) {
244
+ state.middleware = relative;
245
+ }
246
+ else if (validFiles[TYPES.endpoint].has(base)) {
247
+ res.endpoints.push({
248
+ file: relative,
249
+ middlewares: [...prev.middlewares, state.middleware ?? null],
250
+ });
251
+ }
252
+ else if (validFiles[TYPES.page].has(base)) {
253
+ currentPage = relative;
254
+ const segment = buildSegment(dir, relative, extendChains(prev, state));
255
+ if (!segment)
256
+ throw new Error('Missing app shell');
280
257
  res.segments.push(segment);
258
+ }
281
259
  }
282
- return res;
283
260
  }
284
- catch (err) {
285
- logger.error(`[Build:Finder:#scan]: Failed to compose manifest from ${dir}`, err);
286
- return {
287
- segments: [],
288
- endpoints: [],
289
- };
261
+ // warn if segment has status boundaries/loading but no page or layout
262
+ if (!currentPage &&
263
+ !state.layout &&
264
+ (state['401'] || state['403'] || state['404'] || state['500'] || state.loader)) {
265
+ 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`);
266
+ }
267
+ // create segment if we have a layout but no page
268
+ // and haven't created one yet
269
+ if (!currentPage && state.layout && !res.segments.some(s => s.dir === dir)) {
270
+ const segment = buildSegment(dir, undefined, extendChains(prev, state));
271
+ if (segment)
272
+ res.segments.push(segment);
290
273
  }
274
+ return res;
291
275
  }
292
- /**
293
- * Transform the raw scan result into the route manifest, import map,
294
- * and module map for codegen, along with route sets for
295
- * prerendering and sitemap generation
296
- */
297
- async process(res) {
298
- const processed = new Set();
299
- // concrete routes to prerender at build time
300
- // (static, or dynamic with static params)
301
- const prerenderRoutes = new Set();
302
- // all concrete (non-dynamic, non-wildcard) page
303
- // routes for sitemap generation
304
- const knownRoutes = new Set();
305
- const trailingSlash = this.config?.trailingSlash ?? 'never';
306
- // route path → segment/endpoint entries, used at runtime
307
- // to look up the component tree for a matched route
308
- const manifest = {};
309
- // entry id → import path. Split into static (shell, middleware,
310
- // endpoints) and dynamic (layouts, pages, boundaries)
311
- // so the bundler can code-split
312
- const imports = {
313
- endpoints: { static: new Map() },
314
- components: { static: new Map(), dynamic: new Map() },
315
- middlewares: { static: new Map() },
276
+ catch (err) {
277
+ logger.error(`[Build:Finder:#scan]: Failed to compose manifest from ${dir}`, err);
278
+ return {
279
+ segments: [],
280
+ endpoints: [],
316
281
  };
317
- // entry id → related entry ids. Wires up which shell,
318
- // layouts, loaders, and middleware belong
319
- // to each page
320
- const modules = {};
321
- // cache prerender flags per file path so shared layouts
322
- // only trigger one export read across routes
323
- const prerenderCache = new Map();
324
- for (const segment of res.segments) {
325
- try {
326
- if (!this.buildContext || !this.config)
282
+ }
283
+ }
284
+ /**
285
+ * Transform the raw scan result into the route manifest, import map,
286
+ * and module map for codegen, along with route sets for
287
+ * prerendering and sitemap generation
288
+ */
289
+ async process(res) {
290
+ const processed = new Set();
291
+ // concrete routes to prerender at build time
292
+ // (static, or dynamic with static params)
293
+ const prerenderRoutes = new Set();
294
+ // all concrete (non-dynamic, non-wildcard) page
295
+ // routes for sitemap generation
296
+ const knownRoutes = new Set();
297
+ const trailingSlash = this.config?.trailingSlash ?? 'never';
298
+ // route path → segment/endpoint entries, used at runtime
299
+ // to look up the component tree for a matched route
300
+ const manifest = {};
301
+ // entry id → import path. Split into static (shell, middleware,
302
+ // endpoints) and dynamic (layouts, pages, boundaries)
303
+ // so the bundler can code-split
304
+ const imports = {
305
+ endpoints: { static: new Map() },
306
+ components: { static: new Map(), dynamic: new Map() },
307
+ middlewares: { static: new Map() },
308
+ };
309
+ // entry id → related entry ids. Wires up which shell,
310
+ // layouts, loaders, and middleware belong
311
+ // to each page
312
+ const modules = {};
313
+ // cache prerender flags per file path so shared layouts
314
+ // only trigger one export read across routes
315
+ const prerenderCache = new Map();
316
+ for (const segment of res.segments) {
317
+ try {
318
+ if (!this.buildContext || !this.config)
319
+ continue;
320
+ const { shell: shellPath, layouts: layoutPaths, '401s': unauthorisedPaths, '403s': forbiddenPaths, page: pagePath, '404s': notFoundPaths, '500s': serverErrorPaths, loaders: loaderPaths, middlewares: middlewarePaths, dir, } = segment;
321
+ // derive the route pattern from the directory path, falling
322
+ // back to a synthetic +page.tsx when this is
323
+ // a layout-only segment
324
+ const route = Finder.toCanonicalRoute(pagePath ?? `${dir.replace(/\\/g, '/')}/+page.tsx`);
325
+ const params = Finder.getParams(dir);
326
+ const depth = Finder.getDepth(route);
327
+ const isDynamic = route.includes(':');
328
+ const isWildcard = route.includes('*');
329
+ // effective mode for this segment; start from global
330
+ // config then apply shell/layout/page overrides
331
+ let currentPrerenderMode = this.config?.prerender ?? false;
332
+ /**
333
+ * Apply explicit prerender mode overrides in inheritance order
334
+ */
335
+ function applyPrerenderMode(flag) {
336
+ if (flag === undefined)
337
+ return;
338
+ currentPrerenderMode = flag;
339
+ }
340
+ const shellImport = Finder.getImportPath(shellPath);
341
+ const shellId = `${EntryKind.SHELL}${Runtime.hash(shellImport)}`;
342
+ const layoutIds = [];
343
+ const unauthorisedIds = [];
344
+ const forbiddenIds = [];
345
+ const notFoundIds = [];
346
+ const serverErrorIds = [];
347
+ const loadingIds = [];
348
+ const middlewareIds = [];
349
+ // check shell prerender
350
+ if (!processed.has(shellPath)) {
351
+ prerenderCache.set(shellPath, await Prerender.getStaticFlag(shellPath, this.buildContext));
352
+ imports.components.static.set(shellId, shellImport);
353
+ processed.add(shellPath);
354
+ }
355
+ applyPrerenderMode(prerenderCache.get(shellPath));
356
+ for (const layoutPath of layoutPaths) {
357
+ if (!layoutPath) {
358
+ layoutIds.push(null);
327
359
  continue;
328
- const { shell: shellPath, layouts: layoutPaths, '401s': unauthorisedPaths, '403s': forbiddenPaths, page: pagePath, '404s': notFoundPaths, '500s': serverErrorPaths, loaders: loaderPaths, middlewares: middlewarePaths, dir, } = segment;
329
- // derive the route pattern from the directory path, falling
330
- // back to a synthetic +page.tsx when this is
331
- // a layout-only segment
332
- const route = Finder.toCanonicalRoute(pagePath ?? `${dir.replace(/\\/g, '/')}/+page.tsx`);
333
- const params = Finder.getParams(dir);
334
- const depth = Finder.getDepth(route);
335
- const isDynamic = route.includes(':');
336
- const isWildcard = route.includes('*');
337
- // effective mode for this segment; start from global
338
- // config then apply shell/layout/page overrides
339
- let currentPrerenderMode = this.config?.prerender ?? false;
340
- /**
341
- * Apply explicit prerender mode overrides in inheritance order
342
- */
343
- function applyPrerenderMode(flag) {
344
- if (flag === undefined)
345
- return;
346
- currentPrerenderMode = flag;
347
360
  }
348
- const shellImport = Finder.getImportPath(shellPath);
349
- const shellId = `${Build.EntryKind.SHELL}${Runtime.hash(shellImport)}`;
350
- const layoutIds = [];
351
- const unauthorisedIds = [];
352
- const forbiddenIds = [];
353
- const notFoundIds = [];
354
- const serverErrorIds = [];
355
- const loadingIds = [];
356
- const middlewareIds = [];
357
- // check shell prerender
358
- if (!processed.has(shellPath)) {
359
- prerenderCache.set(shellPath, await Prerender.Build.getStaticFlag(shellPath, this.buildContext));
360
- imports.components.static.set(shellId, shellImport);
361
- processed.add(shellPath);
361
+ const layoutImport = Finder.getImportPath(layoutPath);
362
+ const layoutId = `${EntryKind.LAYOUT}${Runtime.hash(layoutImport)}`;
363
+ if (!processed.has(layoutPath)) {
364
+ prerenderCache.set(layoutPath, await Prerender.getStaticFlag(layoutPath, this.buildContext));
365
+ imports.components.dynamic.set(layoutId, layoutImport);
366
+ processed.add(layoutPath);
362
367
  }
363
- applyPrerenderMode(prerenderCache.get(shellPath));
364
- for (const layoutPath of layoutPaths) {
365
- if (!layoutPath) {
366
- layoutIds.push(null);
367
- continue;
368
- }
369
- const layoutImport = Finder.getImportPath(layoutPath);
370
- const layoutId = `${Build.EntryKind.LAYOUT}${Runtime.hash(layoutImport)}`;
371
- if (!processed.has(layoutPath)) {
372
- prerenderCache.set(layoutPath, await Prerender.Build.getStaticFlag(layoutPath, this.buildContext));
373
- imports.components.dynamic.set(layoutId, layoutImport);
374
- processed.add(layoutPath);
375
- }
376
- applyPrerenderMode(prerenderCache.get(layoutPath));
377
- layoutIds.push(layoutId);
368
+ applyPrerenderMode(prerenderCache.get(layoutPath));
369
+ layoutIds.push(layoutId);
370
+ }
371
+ for (const unauthorisedPath of unauthorisedPaths) {
372
+ if (!unauthorisedPath) {
373
+ unauthorisedIds.push(null);
374
+ continue;
378
375
  }
379
- for (const unauthorisedPath of unauthorisedPaths) {
380
- if (!unauthorisedPath) {
381
- unauthorisedIds.push(null);
382
- continue;
383
- }
384
- const unauthorisedImport = Finder.getImportPath(unauthorisedPath);
385
- const unauthorisedId = `${Build.EntryKind['401']}${Runtime.hash(unauthorisedImport)}`;
386
- unauthorisedIds.push(unauthorisedId);
387
- if (!processed.has(unauthorisedPath)) {
388
- imports.components.dynamic.set(unauthorisedId, unauthorisedImport);
389
- processed.add(unauthorisedPath);
390
- }
376
+ const unauthorisedImport = Finder.getImportPath(unauthorisedPath);
377
+ const unauthorisedId = `${EntryKind['401']}${Runtime.hash(unauthorisedImport)}`;
378
+ unauthorisedIds.push(unauthorisedId);
379
+ if (!processed.has(unauthorisedPath)) {
380
+ imports.components.dynamic.set(unauthorisedId, unauthorisedImport);
381
+ processed.add(unauthorisedPath);
391
382
  }
392
- for (const forbiddenPath of forbiddenPaths) {
393
- if (!forbiddenPath) {
394
- forbiddenIds.push(null);
395
- continue;
396
- }
397
- const forbiddenImport = Finder.getImportPath(forbiddenPath);
398
- const forbiddenId = `${Build.EntryKind['403']}${Runtime.hash(forbiddenImport)}`;
399
- forbiddenIds.push(forbiddenId);
400
- if (!processed.has(forbiddenPath)) {
401
- imports.components.dynamic.set(forbiddenId, forbiddenImport);
402
- processed.add(forbiddenPath);
403
- }
383
+ }
384
+ for (const forbiddenPath of forbiddenPaths) {
385
+ if (!forbiddenPath) {
386
+ forbiddenIds.push(null);
387
+ continue;
404
388
  }
405
- for (const notFoundPath of notFoundPaths) {
406
- // hole if level does not declare a 404 boundary.
407
- // Keep slot so indices match layouts
408
- if (!notFoundPath) {
409
- notFoundIds.push(null);
410
- continue;
411
- }
412
- const notFoundImport = Finder.getImportPath(notFoundPath);
413
- const notFoundId = `${Build.EntryKind['404']}${Runtime.hash(notFoundImport)}`;
414
- notFoundIds.push(notFoundId);
415
- // dedupe imports but still assign the slot for this route
416
- if (!processed.has(notFoundPath)) {
417
- imports.components.dynamic.set(notFoundId, notFoundImport);
418
- processed.add(notFoundPath);
419
- }
389
+ const forbiddenImport = Finder.getImportPath(forbiddenPath);
390
+ const forbiddenId = `${EntryKind['403']}${Runtime.hash(forbiddenImport)}`;
391
+ forbiddenIds.push(forbiddenId);
392
+ if (!processed.has(forbiddenPath)) {
393
+ imports.components.dynamic.set(forbiddenId, forbiddenImport);
394
+ processed.add(forbiddenPath);
420
395
  }
421
- for (const serverErrorPath of serverErrorPaths) {
422
- if (!serverErrorPath) {
423
- serverErrorIds.push(null);
424
- continue;
425
- }
426
- const serverErrorImport = Finder.getImportPath(serverErrorPath);
427
- const serverErrorId = `${Build.EntryKind['500']}${Runtime.hash(serverErrorImport)}`;
428
- serverErrorIds.push(serverErrorId);
429
- if (!processed.has(serverErrorPath)) {
430
- imports.components.dynamic.set(serverErrorId, serverErrorImport);
431
- processed.add(serverErrorPath);
432
- }
396
+ }
397
+ for (const notFoundPath of notFoundPaths) {
398
+ // hole if level does not declare a 404 boundary.
399
+ // Keep slot so indices match layouts
400
+ if (!notFoundPath) {
401
+ notFoundIds.push(null);
402
+ continue;
433
403
  }
434
- for (const loaderPath of loaderPaths) {
435
- // hole if level does not declare a loader.
436
- // Keep slot so indices match layouts
437
- if (!loaderPath) {
438
- loadingIds.push(null);
439
- continue;
440
- }
441
- const loaderImport = Finder.getImportPath(loaderPath);
442
- const loaderId = `${Build.EntryKind.LOADING}${Runtime.hash(loaderImport)}`;
443
- loadingIds.push(loaderId);
444
- // dedupe imports but still assign the slot for this route
445
- if (!processed.has(loaderPath)) {
446
- imports.components.dynamic.set(loaderId, loaderImport);
447
- processed.add(loaderPath);
448
- }
404
+ const notFoundImport = Finder.getImportPath(notFoundPath);
405
+ const notFoundId = `${EntryKind['404']}${Runtime.hash(notFoundImport)}`;
406
+ notFoundIds.push(notFoundId);
407
+ // dedupe imports but still assign the slot for this route
408
+ if (!processed.has(notFoundPath)) {
409
+ imports.components.dynamic.set(notFoundId, notFoundImport);
410
+ processed.add(notFoundPath);
449
411
  }
450
- for (const middlewarePath of middlewarePaths) {
451
- if (!middlewarePath) {
452
- middlewareIds.push(null);
453
- continue;
454
- }
455
- const middlewareImport = Finder.getImportPath(middlewarePath);
456
- const middlewareId = `${Build.EntryKind.MIDDLEWARE}${Runtime.hash(middlewareImport)}`;
457
- middlewareIds.push(middlewareId);
458
- if (!processed.has(middlewarePath)) {
459
- // route scanning only tells us this is a +middleware file path
460
- // so we still validate the module exports middleware
461
- if (!(await this.buildContext.exportReader.has(middlewarePath, 'middleware'))) {
462
- throw new Error(`Missing middleware export in ${middlewarePath}`);
463
- }
464
- imports.middlewares.static.set(middlewareId, middlewareImport);
465
- processed.add(middlewarePath);
466
- }
412
+ }
413
+ for (const serverErrorPath of serverErrorPaths) {
414
+ if (!serverErrorPath) {
415
+ serverErrorIds.push(null);
416
+ continue;
467
417
  }
468
- // generate entry id based on page if exists, otherwise dir
469
- const entryId = pagePath
470
- ? `${Build.EntryKind.PAGE}${Runtime.hash(Finder.getImportPath(pagePath))}`
471
- : `${Build.EntryKind.PAGE}${Runtime.hash(route)}`;
472
- if (pagePath) {
473
- const pagePrerender = await Prerender.Build.getStaticFlag(pagePath, this.buildContext);
474
- applyPrerenderMode(pagePrerender);
475
- imports.components.dynamic.set(entryId, Finder.getImportPath(pagePath));
476
- processed.add(pagePath);
418
+ const serverErrorImport = Finder.getImportPath(serverErrorPath);
419
+ const serverErrorId = `${EntryKind['500']}${Runtime.hash(serverErrorImport)}`;
420
+ serverErrorIds.push(serverErrorId);
421
+ if (!processed.has(serverErrorPath)) {
422
+ imports.components.dynamic.set(serverErrorId, serverErrorImport);
423
+ processed.add(serverErrorPath);
477
424
  }
478
- // resolve final prerender mode after shell → layout → page overrides
479
- const shouldPrerender = currentPrerenderMode !== false && this.buildContext.command === 'build';
480
- const prerenderMode = shouldPrerender
481
- ? currentPrerenderMode
482
- : false;
483
- // collect concrete prerender routes; for dynamic routes,
484
- // expand using the page's exported static params
485
- if (shouldPrerender) {
486
- if (!isDynamic && !isWildcard) {
487
- prerenderRoutes.add(normalisePathname(route, trailingSlash));
488
- }
489
- else if (pagePath) {
490
- const staticParams = await Prerender.Build.getStaticParams(pagePath, this.buildContext);
491
- for (const r of Prerender.Build.getDynamicRouteList(route, params, staticParams)) {
492
- prerenderRoutes.add(normalisePathname(r, trailingSlash));
493
- }
425
+ }
426
+ for (const loaderPath of loaderPaths) {
427
+ // hole if level does not declare a loader.
428
+ // Keep slot so indices match layouts
429
+ if (!loaderPath) {
430
+ loadingIds.push(null);
431
+ continue;
432
+ }
433
+ const loaderImport = Finder.getImportPath(loaderPath);
434
+ const loaderId = `${EntryKind.LOADING}${Runtime.hash(loaderImport)}`;
435
+ loadingIds.push(loaderId);
436
+ // dedupe imports but still assign the slot for this route
437
+ if (!processed.has(loaderPath)) {
438
+ imports.components.dynamic.set(loaderId, loaderImport);
439
+ processed.add(loaderPath);
440
+ }
441
+ }
442
+ for (const middlewarePath of middlewarePaths) {
443
+ if (!middlewarePath) {
444
+ middlewareIds.push(null);
445
+ continue;
446
+ }
447
+ const middlewareImport = Finder.getImportPath(middlewarePath);
448
+ const middlewareId = `${EntryKind.MIDDLEWARE}${Runtime.hash(middlewareImport)}`;
449
+ middlewareIds.push(middlewareId);
450
+ if (!processed.has(middlewarePath)) {
451
+ // route scanning only tells us this is a +middleware file path
452
+ // so we still validate the module exports middleware
453
+ if (!(await this.buildContext.exportReader.has(middlewarePath, 'middleware'))) {
454
+ throw new Error(`Missing middleware export in ${middlewarePath}`);
494
455
  }
456
+ imports.middlewares.static.set(middlewareId, middlewareImport);
457
+ processed.add(middlewarePath);
495
458
  }
496
- // track all concrete routes regardless of prerender mode
497
- // so the sitemap can include ssr-only pages too
459
+ }
460
+ // generate entry id based on page if exists, otherwise dir
461
+ const entryId = pagePath
462
+ ? `${EntryKind.PAGE}${Runtime.hash(Finder.getImportPath(pagePath))}`
463
+ : `${EntryKind.PAGE}${Runtime.hash(route)}`;
464
+ if (pagePath) {
465
+ const pagePrerender = await Prerender.getStaticFlag(pagePath, this.buildContext);
466
+ applyPrerenderMode(pagePrerender);
467
+ imports.components.dynamic.set(entryId, Finder.getImportPath(pagePath));
468
+ processed.add(pagePath);
469
+ }
470
+ // resolve final prerender mode after shell → layout → page overrides
471
+ const shouldPrerender = currentPrerenderMode !== false && this.buildContext.command === 'build';
472
+ const prerenderMode = shouldPrerender
473
+ ? currentPrerenderMode
474
+ : false;
475
+ // collect concrete prerender routes; for dynamic routes,
476
+ // expand using the page's exported static params
477
+ if (shouldPrerender) {
498
478
  if (!isDynamic && !isWildcard) {
499
- knownRoutes.add(normalisePathname(route, trailingSlash));
479
+ prerenderRoutes.add(normalisePathname(route, trailingSlash));
500
480
  }
501
- const entry = {
502
- __id: entryId,
503
- __path: route,
504
- __params: params,
505
- __kind: Build.EntryKind.PAGE,
506
- __depth: depth,
507
- method: 'get',
508
- paths: {
509
- layouts: [shellPath, ...layoutPaths].map(layout => layout ? Finder.getImportPath(layout) : null),
510
- '401s': unauthorisedPaths.map(unauthorised => unauthorised ? Finder.getImportPath(unauthorised) : null),
511
- '403s': forbiddenPaths.map(forbidden => forbidden ? Finder.getImportPath(forbidden) : null),
512
- '404s': notFoundPaths.map(notFound => notFound ? Finder.getImportPath(notFound) : null),
513
- '500s': serverErrorPaths.map(serverError => serverError ? Finder.getImportPath(serverError) : null),
514
- loaders: loaderPaths.map(loader => loader ? Finder.getImportPath(loader) : null),
515
- middlewares: middlewarePaths.map(middleware => middleware ? Finder.getImportPath(middleware) : null),
516
- page: pagePath ? Finder.getImportPath(pagePath) : null,
517
- },
518
- prerender: prerenderMode,
519
- dynamic: isDynamic,
520
- wildcard: isWildcard,
521
- };
522
- if (manifest[route]) {
523
- if (Array.isArray(manifest[route])) {
524
- manifest[route].push(entry);
525
- }
526
- else {
527
- manifest[route] = [manifest[route], entry];
481
+ else if (pagePath) {
482
+ const staticParams = await Prerender.getStaticParams(pagePath, this.buildContext);
483
+ for (const r of Prerender.getDynamicRouteList(route, params, staticParams)) {
484
+ prerenderRoutes.add(normalisePathname(r, trailingSlash));
528
485
  }
529
486
  }
487
+ }
488
+ // track all concrete routes regardless of prerender mode
489
+ // so the sitemap can include ssr-only pages too
490
+ if (!isDynamic && !isWildcard) {
491
+ knownRoutes.add(normalisePathname(route, trailingSlash));
492
+ }
493
+ const entry = {
494
+ __id: entryId,
495
+ __path: route,
496
+ __params: params,
497
+ __kind: EntryKind.PAGE,
498
+ __depth: depth,
499
+ method: 'get',
500
+ paths: {
501
+ layouts: [shellPath, ...layoutPaths].map(layout => layout ? Finder.getImportPath(layout) : null),
502
+ '401s': unauthorisedPaths.map(unauthorised => unauthorised ? Finder.getImportPath(unauthorised) : null),
503
+ '403s': forbiddenPaths.map(forbidden => forbidden ? Finder.getImportPath(forbidden) : null),
504
+ '404s': notFoundPaths.map(notFound => notFound ? Finder.getImportPath(notFound) : null),
505
+ '500s': serverErrorPaths.map(serverError => serverError ? Finder.getImportPath(serverError) : null),
506
+ loaders: loaderPaths.map(loader => loader ? Finder.getImportPath(loader) : null),
507
+ middlewares: middlewarePaths.map(middleware => middleware ? Finder.getImportPath(middleware) : null),
508
+ page: pagePath ? Finder.getImportPath(pagePath) : null,
509
+ },
510
+ prerender: prerenderMode,
511
+ dynamic: isDynamic,
512
+ wildcard: isWildcard,
513
+ };
514
+ if (manifest[route]) {
515
+ if (Array.isArray(manifest[route])) {
516
+ manifest[route].push(entry);
517
+ }
530
518
  else {
531
- manifest[route] = entry;
519
+ manifest[route] = [manifest[route], entry];
532
520
  }
533
- modules[entryId] = {
534
- shellId,
535
- layoutIds,
536
- pageId: pagePath ? entryId : undefined,
537
- '401Ids': unauthorisedIds,
538
- '403Ids': forbiddenIds,
539
- '404Ids': notFoundIds,
540
- '500Ids': serverErrorIds,
541
- loadingIds,
542
- middlewareIds,
543
- };
544
521
  }
545
- catch (err) {
546
- if (err instanceof Error &&
547
- err.message.startsWith('Missing middleware export')) {
548
- throw err;
549
- }
550
- logger.error('[Build:Finder:process]: failed to process segment', err);
522
+ else {
523
+ manifest[route] = entry;
551
524
  }
525
+ modules[entryId] = {
526
+ shellId,
527
+ layoutIds,
528
+ pageId: pagePath ? entryId : undefined,
529
+ '401Ids': unauthorisedIds,
530
+ '403Ids': forbiddenIds,
531
+ '404Ids': notFoundIds,
532
+ '500Ids': serverErrorIds,
533
+ loadingIds,
534
+ middlewareIds,
535
+ };
552
536
  }
553
- // process api endpoints — each file can export multiple HTTP
554
- // verbs which become separate manifest entries grouped
555
- // under the same route
556
- for (const endpoint of res.endpoints) {
557
- try {
558
- const endpointFilePath = endpoint.file;
559
- const endpointMiddlewarePaths = endpoint.middlewares;
560
- if (!this.buildContext || processed.has(endpointFilePath))
537
+ catch (err) {
538
+ if (err instanceof Error && err.message.startsWith('Missing middleware export')) {
539
+ throw err;
540
+ }
541
+ logger.error('[Build:Finder:process]: failed to process segment', err);
542
+ }
543
+ }
544
+ // process api endpoints — each file can export multiple HTTP
545
+ // verbs which become separate manifest entries grouped
546
+ // under the same route
547
+ for (const endpoint of res.endpoints) {
548
+ try {
549
+ const endpointFilePath = endpoint.file;
550
+ const endpointMiddlewarePaths = endpoint.middlewares;
551
+ if (!this.buildContext || processed.has(endpointFilePath))
552
+ continue;
553
+ const route = Finder.toCanonicalRoute(endpointFilePath);
554
+ const params = Finder.getParams(endpointFilePath);
555
+ const endpointExports = await this.buildContext.exportReader.exports(endpointFilePath);
556
+ const group = [];
557
+ for (const method of endpointExports) {
558
+ if (!HTTP_VERBS.includes(method)) {
559
+ logger.warn('[Build:Finder:process]', `Ignoring unsupported HTTP verb: ${method} in ${endpointFilePath}`);
561
560
  continue;
562
- const route = Finder.toCanonicalRoute(endpointFilePath);
563
- const params = Finder.getParams(endpointFilePath);
564
- const endpointExports = await this.buildContext.exportReader.exports(endpointFilePath);
565
- const group = [];
566
- for (const method of endpointExports) {
567
- if (!HTTP_VERBS.includes(method)) {
568
- logger.warn('[Build:Finder:process]', `Ignoring unsupported HTTP verb: ${method} in ${endpointFilePath}`);
569
- continue;
570
- }
571
- const m = method.toLowerCase();
572
- const endpointId = `${Build.EntryKind.ENDPOINT}${Runtime.hash(Finder.getImportPath(endpointFilePath))}_${m}`;
573
- const middlewareIds = await Promise.all(endpointMiddlewarePaths.map(async middlewarePath => {
574
- if (!middlewarePath)
575
- return null;
576
- const middlewareImport = Finder.getImportPath(middlewarePath);
577
- const middlewareId = `${Build.EntryKind.MIDDLEWARE}${Runtime.hash(middlewareImport)}`;
578
- if (!processed.has(middlewarePath)) {
579
- // endpoint middleware discovery gives us file paths, not proof
580
- // of the export so check the module shape first
581
- if (!(await this.buildContext.exportReader.has(middlewarePath, 'middleware'))) {
582
- throw new Error(`Missing middleware export in ${middlewarePath}`);
583
- }
584
- imports.middlewares.static.set(middlewareId, middlewareImport);
585
- processed.add(middlewarePath);
586
- }
587
- return middlewareId;
588
- }));
589
- group.push({
590
- __id: endpointId,
591
- __path: route,
592
- __params: params,
593
- __kind: Build.EntryKind.ENDPOINT,
594
- method: m,
595
- middlewares: endpointMiddlewarePaths.map(middlewarePath => middlewarePath ? Finder.getImportPath(middlewarePath) : null),
596
- });
597
- imports.endpoints.static.set(endpointId, Finder.getImportPath(endpointFilePath));
598
- modules[endpointId] = { endpointId, middlewareIds };
599
- processed.add(endpointFilePath);
600
561
  }
601
- // single-verb endpoints store a plain entry, multi-verb
602
- // endpoints store an array so the router can
603
- // match by method at request time
604
- const entry = group.length === 1 ? group[0] : group;
605
- if (endpointMiddlewarePaths.length) {
606
- modules[route] = {
607
- ...(modules[route] ?? {}),
608
- middlewareIds: endpointMiddlewarePaths.map(middlewarePath => middlewarePath
609
- ? `${Build.EntryKind.MIDDLEWARE}${Runtime.hash(Finder.getImportPath(middlewarePath))}`
610
- : null),
611
- };
612
- }
613
- if (manifest[route]) {
614
- if (Array.isArray(manifest[route])) {
615
- manifest[route] = [
616
- ...manifest[route],
617
- ...(Array.isArray(entry) ? entry : [entry]),
618
- ];
619
- }
620
- else {
621
- manifest[route] = [
622
- manifest[route],
623
- ...(Array.isArray(entry) ? entry : [entry]),
624
- ];
562
+ const m = method.toLowerCase();
563
+ const endpointId = `${EntryKind.ENDPOINT}${Runtime.hash(Finder.getImportPath(endpointFilePath))}_${m}`;
564
+ const middlewareIds = await Promise.all(endpointMiddlewarePaths.map(async middlewarePath => {
565
+ if (!middlewarePath)
566
+ return null;
567
+ const middlewareImport = Finder.getImportPath(middlewarePath);
568
+ const middlewareId = `${EntryKind.MIDDLEWARE}${Runtime.hash(middlewareImport)}`;
569
+ if (!processed.has(middlewarePath)) {
570
+ // endpoint middleware discovery gives us file paths, not proof
571
+ // of the export so check the module shape first
572
+ if (!(await this.buildContext.exportReader.has(middlewarePath, 'middleware'))) {
573
+ throw new Error(`Missing middleware export in ${middlewarePath}`);
574
+ }
575
+ imports.middlewares.static.set(middlewareId, middlewareImport);
576
+ processed.add(middlewarePath);
625
577
  }
578
+ return middlewareId;
579
+ }));
580
+ group.push({
581
+ __id: endpointId,
582
+ __path: route,
583
+ __params: params,
584
+ __kind: EntryKind.ENDPOINT,
585
+ method: m,
586
+ middlewares: endpointMiddlewarePaths.map(middlewarePath => middlewarePath ? Finder.getImportPath(middlewarePath) : null),
587
+ });
588
+ imports.endpoints.static.set(endpointId, Finder.getImportPath(endpointFilePath));
589
+ modules[endpointId] = { endpointId, middlewareIds };
590
+ processed.add(endpointFilePath);
591
+ }
592
+ // single-verb endpoints store a plain entry, multi-verb
593
+ // endpoints store an array so the router can
594
+ // match by method at request time
595
+ const entry = group.length === 1 ? group[0] : group;
596
+ if (endpointMiddlewarePaths.length) {
597
+ modules[route] = {
598
+ ...(modules[route] ?? {}),
599
+ middlewareIds: endpointMiddlewarePaths.map(middlewarePath => middlewarePath
600
+ ? `${EntryKind.MIDDLEWARE}${Runtime.hash(Finder.getImportPath(middlewarePath))}`
601
+ : null),
602
+ };
603
+ }
604
+ if (manifest[route]) {
605
+ if (Array.isArray(manifest[route])) {
606
+ manifest[route] = [
607
+ ...manifest[route],
608
+ ...(Array.isArray(entry) ? entry : [entry]),
609
+ ];
626
610
  }
627
611
  else {
628
- manifest[route] = entry;
612
+ manifest[route] = [
613
+ manifest[route],
614
+ ...(Array.isArray(entry) ? entry : [entry]),
615
+ ];
629
616
  }
630
617
  }
631
- catch (err) {
632
- if (err instanceof Error &&
633
- err.message.startsWith('Missing middleware export')) {
634
- throw err;
635
- }
636
- logger.error('[Build:Finder:process]: failed to process route', err);
618
+ else {
619
+ manifest[route] = entry;
620
+ }
621
+ }
622
+ catch (err) {
623
+ if (err instanceof Error && err.message.startsWith('Missing middleware export')) {
624
+ throw err;
637
625
  }
626
+ logger.error('[Build:Finder:process]: failed to process route', err);
638
627
  }
639
- return { manifest, imports, modules, prerenderRoutes, knownRoutes };
640
628
  }
629
+ return { manifest, imports, modules, prerenderRoutes, knownRoutes };
641
630
  }
642
- Build.Finder = Finder;
643
- })(Build || (Build = {}));
631
+ }