@jk2908/solas 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +333 -0
  3. package/dist/cli.d.ts +2 -0
  4. package/dist/cli.js +219 -0
  5. package/dist/error-boundary.d.ts +1 -0
  6. package/dist/error-boundary.js +1 -0
  7. package/dist/index.d.ts +7 -0
  8. package/dist/index.js +235 -0
  9. package/dist/internal/build.d.ts +104 -0
  10. package/dist/internal/build.js +633 -0
  11. package/dist/internal/codegen/config.d.ts +5 -0
  12. package/dist/internal/codegen/config.js +19 -0
  13. package/dist/internal/codegen/environments.d.ts +12 -0
  14. package/dist/internal/codegen/environments.js +42 -0
  15. package/dist/internal/codegen/manifest.d.ts +5 -0
  16. package/dist/internal/codegen/manifest.js +15 -0
  17. package/dist/internal/codegen/maps.d.ts +5 -0
  18. package/dist/internal/codegen/maps.js +75 -0
  19. package/dist/internal/codegen/utils.d.ts +1 -0
  20. package/dist/internal/codegen/utils.js +2 -0
  21. package/dist/internal/env/browser.d.ts +4 -0
  22. package/dist/internal/env/browser.js +58 -0
  23. package/dist/internal/env/request-context.d.ts +19 -0
  24. package/dist/internal/env/request-context.js +2 -0
  25. package/dist/internal/env/rsc.d.ts +39 -0
  26. package/dist/internal/env/rsc.js +368 -0
  27. package/dist/internal/env/ssr.d.ts +42 -0
  28. package/dist/internal/env/ssr.js +149 -0
  29. package/dist/internal/env/utils.d.ts +2 -0
  30. package/dist/internal/env/utils.js +28 -0
  31. package/dist/internal/metadata.d.ts +81 -0
  32. package/dist/internal/metadata.js +185 -0
  33. package/dist/internal/navigation/http-exception-boundary.d.ts +12 -0
  34. package/dist/internal/navigation/http-exception-boundary.js +48 -0
  35. package/dist/internal/navigation/http-exception.d.ts +33 -0
  36. package/dist/internal/navigation/http-exception.js +45 -0
  37. package/dist/internal/navigation/link.d.ts +13 -0
  38. package/dist/internal/navigation/link.js +63 -0
  39. package/dist/internal/navigation/redirect-boundary.d.ts +12 -0
  40. package/dist/internal/navigation/redirect-boundary.js +39 -0
  41. package/dist/internal/navigation/redirect.d.ts +21 -0
  42. package/dist/internal/navigation/redirect.js +63 -0
  43. package/dist/internal/navigation/use-search-params.d.ts +1 -0
  44. package/dist/internal/navigation/use-search-params.js +13 -0
  45. package/dist/internal/prerender.d.ts +151 -0
  46. package/dist/internal/prerender.js +422 -0
  47. package/dist/internal/render/head.d.ts +4 -0
  48. package/dist/internal/render/head.js +38 -0
  49. package/dist/internal/render/tree.d.ts +47 -0
  50. package/dist/internal/render/tree.js +108 -0
  51. package/dist/internal/router/create-router.d.ts +6 -0
  52. package/dist/internal/router/create-router.js +95 -0
  53. package/dist/internal/router/pattern.d.ts +8 -0
  54. package/dist/internal/router/pattern.js +31 -0
  55. package/dist/internal/router/prefetcher.d.ts +47 -0
  56. package/dist/internal/router/prefetcher.js +90 -0
  57. package/dist/internal/router/resolver.d.ts +174 -0
  58. package/dist/internal/router/resolver.js +356 -0
  59. package/dist/internal/router/router-context.d.ts +11 -0
  60. package/dist/internal/router/router-context.js +7 -0
  61. package/dist/internal/router/router-provider.d.ts +6 -0
  62. package/dist/internal/router/router-provider.js +131 -0
  63. package/dist/internal/router/router.d.ts +79 -0
  64. package/dist/internal/router/router.js +417 -0
  65. package/dist/internal/router/use-router.d.ts +5 -0
  66. package/dist/internal/router/use-router.js +5 -0
  67. package/dist/internal/server/cookies.d.ts +6 -0
  68. package/dist/internal/server/cookies.js +17 -0
  69. package/dist/internal/server/dynamic.d.ts +9 -0
  70. package/dist/internal/server/dynamic.js +22 -0
  71. package/dist/internal/server/headers.d.ts +5 -0
  72. package/dist/internal/server/headers.js +19 -0
  73. package/dist/internal/server/url.d.ts +5 -0
  74. package/dist/internal/server/url.js +16 -0
  75. package/dist/internal/ui/defaults/error.d.ts +4 -0
  76. package/dist/internal/ui/defaults/error.js +6 -0
  77. package/dist/internal/ui/error-boundary.d.ts +26 -0
  78. package/dist/internal/ui/error-boundary.js +41 -0
  79. package/dist/navigation.d.ts +6 -0
  80. package/dist/navigation.js +6 -0
  81. package/dist/prerender.d.ts +1 -0
  82. package/dist/prerender.js +1 -0
  83. package/dist/router.d.ts +4 -0
  84. package/dist/router.js +4 -0
  85. package/dist/server.d.ts +4 -0
  86. package/dist/server.js +4 -0
  87. package/dist/solas.d.ts +32 -0
  88. package/dist/solas.js +125 -0
  89. package/dist/types.d.ts +93 -0
  90. package/dist/types.js +1 -0
  91. package/dist/utils/compress.d.ts +11 -0
  92. package/dist/utils/compress.js +76 -0
  93. package/dist/utils/context.d.ts +6 -0
  94. package/dist/utils/context.js +25 -0
  95. package/dist/utils/cookies.d.ts +3 -0
  96. package/dist/utils/cookies.js +35 -0
  97. package/dist/utils/export-reader.d.ts +29 -0
  98. package/dist/utils/export-reader.js +117 -0
  99. package/dist/utils/format.d.ts +6 -0
  100. package/dist/utils/format.js +72 -0
  101. package/dist/utils/logger.d.ts +52 -0
  102. package/dist/utils/logger.js +105 -0
  103. package/dist/utils/time.d.ts +4 -0
  104. package/dist/utils/time.js +29 -0
  105. package/package.json +111 -0
@@ -0,0 +1,633 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { Solas } from '../solas';
4
+ import { Logger } from '../utils/logger';
5
+ import { Prerender } from './prerender';
6
+ export { Build };
7
+ var Build;
8
+ (function (Build) {
9
+ const HTTP_VERBS = ['GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'];
10
+ Build.EntryKind = {
11
+ SHELL: '$S',
12
+ LAYOUT: '$L',
13
+ PAGE: '$P',
14
+ 401: '$401',
15
+ 403: '$403',
16
+ 404: '$404',
17
+ 500: '$500',
18
+ LOADING: '$LOAD',
19
+ MIDDLEWARE: '$MW',
20
+ ENDPOINT: '$E',
21
+ };
22
+ const logger = new Logger();
23
+ /**
24
+ * Finder class to process application routes
25
+ */
26
+ class Finder {
27
+ buildContext;
28
+ config;
29
+ constructor(buildContext, config) {
30
+ this.buildContext = buildContext;
31
+ this.config = config;
32
+ }
33
+ /**
34
+ * Extracts dynamic parameter names from a file path
35
+ */
36
+ static getParams(file) {
37
+ return Array.from(file.matchAll(/\[(?:\.\.\.)?([^\]]+)\]/g), m => m[1]);
38
+ }
39
+ /**
40
+ * Get the depth of a route based on slashes
41
+ */
42
+ static getDepth(route) {
43
+ if (route === '/')
44
+ return 0;
45
+ // count slashes to determine depth
46
+ return route.split('/').length - 1;
47
+ }
48
+ /**
49
+ * Convert a file path to a canonical route.
50
+ */
51
+ static toCanonicalRoute(file) {
52
+ const route = file
53
+ .replace(new RegExp(`^${Solas.Config.APP_DIR}`), '')
54
+ .replace(/\/\+page\.(j|t)sx?$/, '')
55
+ .replace(/\/\+endpoint\.(j|t)sx?$/, '')
56
+ .replace(/\[\.\.\..+?\]/g, '*') // wildcard routes
57
+ .replace(/\[(.+?)\]/g, ':$1'); // dynamic routes
58
+ if (!route || route === '')
59
+ return '/';
60
+ return route.startsWith('/') ? route : `/${route}`;
61
+ }
62
+ /**
63
+ * Get the import path for a file
64
+ * This finds the relative path from the generated
65
+ * directory to the file, removes the extension and
66
+ * replaces backslashes with forward slashes.
67
+ */
68
+ static getImportPath(file) {
69
+ const cwd = process.cwd();
70
+ const generatedDir = path.join(cwd, Solas.Config.GENERATED_DIR);
71
+ return path
72
+ .relative(generatedDir, path.resolve(cwd, file))
73
+ .replace(/\\/g, '/')
74
+ .replace(/\.(t|j)sx?$/, '');
75
+ }
76
+ /**
77
+ * Run the Finder to get the app route and associated data
78
+ * needed for codegen
79
+ */
80
+ async run() {
81
+ try {
82
+ return await this.process(await this.#scan(Solas.Config.APP_DIR));
83
+ }
84
+ catch (err) {
85
+ logger.error('[Build:Finder:run]: failed to build manifest', err);
86
+ throw err;
87
+ }
88
+ }
89
+ /**
90
+ * Scan the filesystem to get all routes for processing
91
+ */
92
+ async #scan(dir, res = { segments: [], endpoints: [] }, prev = {
93
+ layouts: [],
94
+ '401s': [],
95
+ '403s': [],
96
+ '404s': [],
97
+ '500s': [],
98
+ loaders: [],
99
+ middlewares: [],
100
+ }) {
101
+ try {
102
+ // define valid route files
103
+ const EXTENSIONS = {
104
+ page: ['tsx', 'jsx'],
105
+ api: ['ts', 'js'],
106
+ };
107
+ // define route file types
108
+ const TYPES = {
109
+ page: '+page',
110
+ '401': '+401',
111
+ '403': '+403',
112
+ '404': '+404',
113
+ '500': '+500',
114
+ layout: '+layout',
115
+ loading: '+loading',
116
+ middleware: '+middleware',
117
+ endpoint: '+endpoint',
118
+ };
119
+ // map of valid files for each type
120
+ const validFiles = {
121
+ [TYPES.page]: new Set(EXTENSIONS.page.map(ext => `${TYPES.page}.${ext}`)),
122
+ [TYPES['401']]: new Set(EXTENSIONS.page.map(ext => `${TYPES['401']}.${ext}`)),
123
+ [TYPES['403']]: new Set(EXTENSIONS.page.map(ext => `${TYPES['403']}.${ext}`)),
124
+ [TYPES['404']]: new Set(EXTENSIONS.page.map(ext => `${TYPES['404']}.${ext}`)),
125
+ [TYPES['500']]: new Set(EXTENSIONS.page.map(ext => `${TYPES['500']}.${ext}`)),
126
+ [TYPES.loading]: new Set(EXTENSIONS.page.map(ext => `${TYPES.loading}.${ext}`)),
127
+ [TYPES.layout]: new Set(EXTENSIONS.page.map(ext => `${TYPES.layout}.${ext}`)),
128
+ [TYPES.middleware]: new Set(EXTENSIONS.api.map(ext => `${TYPES.middleware}.${ext}`)),
129
+ [TYPES.endpoint]: new Set(EXTENSIONS.api.map(ext => `${TYPES.endpoint}.${ext}`)),
130
+ };
131
+ const files = await fs.readdir(dir, { withFileTypes: true });
132
+ // keep a predictable order so layout/loading are picked
133
+ // up before page. Avoids OS dir ordering causing pages
134
+ // to steal layout/loaders first and drop alignment
135
+ files.sort((a, b) => {
136
+ if (a.isFile() && b.isDirectory())
137
+ return -1;
138
+ if (a.isDirectory() && b.isFile())
139
+ return 1;
140
+ if (a.isFile() && b.isFile()) {
141
+ const priority = (d) => {
142
+ const base = path.basename(d.name);
143
+ if (validFiles[TYPES.layout].has(base))
144
+ return 0;
145
+ if (validFiles[TYPES['401']].has(base))
146
+ return 1;
147
+ if (validFiles[TYPES['403']].has(base))
148
+ return 2;
149
+ if (validFiles[TYPES['404']].has(base))
150
+ return 3;
151
+ if (validFiles[TYPES['500']].has(base))
152
+ return 4;
153
+ if (validFiles[TYPES.loading].has(base))
154
+ return 5;
155
+ if (validFiles[TYPES.middleware].has(base))
156
+ return 6;
157
+ if (validFiles[TYPES.page].has(base))
158
+ return 7;
159
+ if (validFiles[TYPES.endpoint].has(base))
160
+ return 8;
161
+ return 8;
162
+ };
163
+ return priority(a) - priority(b);
164
+ }
165
+ return 0;
166
+ });
167
+ // current layout, status boundaries, loader, middleware, and page files for this segment
168
+ let currentLayout;
169
+ let current401;
170
+ let current403;
171
+ let current404;
172
+ let current500;
173
+ let currentLoader;
174
+ let currentMiddleware;
175
+ let currentPage;
176
+ for (const file of files) {
177
+ const route = path.join(dir, file.name);
178
+ if (file.isDirectory()) {
179
+ // before recursing, create segment for current dir if it
180
+ // has a layout (defines a wrapper for child routes)
181
+ if (!currentPage && currentLayout) {
182
+ const layouts = [...prev.layouts, currentLayout];
183
+ const unauthorized = [...prev['401s'], current401 ?? null];
184
+ const forbidden = [...prev['403s'], current403 ?? null];
185
+ const notFounds = [...prev['404s'], current404 ?? null];
186
+ const serverErrors = [...prev['500s'], current500 ?? null];
187
+ const loaders = [...prev.loaders, currentLoader ?? null];
188
+ const middlewares = [...prev.middlewares, currentMiddleware ?? null];
189
+ const shell = layouts[0];
190
+ if (shell) {
191
+ res.segments.push({
192
+ dir,
193
+ page: undefined,
194
+ '401s': unauthorized,
195
+ '403s': forbidden,
196
+ '404s': notFounds,
197
+ '500s': serverErrors,
198
+ loaders,
199
+ middlewares,
200
+ layouts: layouts.length > 1 ? layouts.slice(1) : [],
201
+ shell,
202
+ });
203
+ }
204
+ }
205
+ const next = {
206
+ layouts: [...prev.layouts, currentLayout ?? null],
207
+ '401s': [...prev['401s'], current401 ?? null],
208
+ '403s': [...prev['403s'], current403 ?? null],
209
+ '404s': [...prev['404s'], current404 ?? null],
210
+ '500s': [...prev['500s'], current500 ?? null],
211
+ loaders: [...prev.loaders, currentLoader ?? null],
212
+ middlewares: [...prev.middlewares, currentMiddleware ?? null],
213
+ };
214
+ await this.#scan(route, res, next);
215
+ }
216
+ else {
217
+ const base = path.basename(file.name);
218
+ const relative = path.relative(process.cwd(), route).replace(/\\/g, '/');
219
+ if (validFiles[TYPES.layout].has(base)) {
220
+ currentLayout = relative;
221
+ }
222
+ else if (validFiles[TYPES['401']].has(base)) {
223
+ current401 = relative;
224
+ }
225
+ else if (validFiles[TYPES['403']].has(base)) {
226
+ current403 = relative;
227
+ }
228
+ else if (validFiles[TYPES['404']].has(base)) {
229
+ current404 = relative;
230
+ }
231
+ else if (validFiles[TYPES['500']].has(base)) {
232
+ current500 = relative;
233
+ }
234
+ else if (validFiles[TYPES.loading].has(base)) {
235
+ currentLoader = relative;
236
+ }
237
+ else if (validFiles[TYPES.middleware].has(base)) {
238
+ currentMiddleware = relative;
239
+ }
240
+ else if (validFiles[TYPES.endpoint].has(base)) {
241
+ res.endpoints.push({
242
+ file: relative,
243
+ middlewares: [...prev.middlewares, currentMiddleware ?? null],
244
+ });
245
+ }
246
+ else if (validFiles[TYPES.page].has(base)) {
247
+ currentPage = relative;
248
+ const layouts = [...prev.layouts, currentLayout ?? null];
249
+ const unauthorized = [...prev['401s'], current401 ?? null];
250
+ const forbidden = [...prev['403s'], current403 ?? null];
251
+ const notFounds = [...prev['404s'], current404 ?? null];
252
+ const serverErrors = [...prev['500s'], current500 ?? null];
253
+ const loaders = [...prev.loaders, currentLoader ?? null];
254
+ const middlewares = [...prev.middlewares, currentMiddleware ?? null];
255
+ const shell = layouts?.[0];
256
+ if (!shell)
257
+ throw new Error('Missing app shell');
258
+ res.segments.push({
259
+ dir,
260
+ page: relative,
261
+ '401s': unauthorized,
262
+ '403s': forbidden,
263
+ '404s': notFounds,
264
+ '500s': serverErrors,
265
+ loaders,
266
+ middlewares,
267
+ layouts: layouts.length > 1 ? layouts.slice(1) : [],
268
+ shell,
269
+ });
270
+ }
271
+ }
272
+ }
273
+ // warn if segment has status boundaries/loading but no page or layout
274
+ if (!currentPage &&
275
+ !currentLayout &&
276
+ (current401 || current403 || current404 || current500 || currentLoader)) {
277
+ 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`);
278
+ }
279
+ // create segment if we have a layout but no page and
280
+ // haven't created one yet (no subdirectories triggered it)
281
+ if (!currentPage && currentLayout && !res.segments.some(s => s.dir === dir)) {
282
+ const layouts = [...prev.layouts, currentLayout];
283
+ const unauthorized = [...prev['401s'], current401 ?? null];
284
+ const forbidden = [...prev['403s'], current403 ?? null];
285
+ const notFounds = [...prev['404s'], current404 ?? null];
286
+ const serverErrors = [...prev['500s'], current500 ?? null];
287
+ const loaders = [...prev.loaders, currentLoader ?? null];
288
+ const middlewares = [...prev.middlewares, currentMiddleware ?? null];
289
+ const shell = layouts[0];
290
+ if (shell) {
291
+ res.segments.push({
292
+ dir,
293
+ page: undefined,
294
+ '401s': unauthorized,
295
+ '403s': forbidden,
296
+ '404s': notFounds,
297
+ '500s': serverErrors,
298
+ loaders,
299
+ middlewares,
300
+ layouts: layouts.length > 1 ? layouts.slice(1) : [],
301
+ shell,
302
+ });
303
+ }
304
+ }
305
+ return res;
306
+ }
307
+ catch (err) {
308
+ logger.error(`[Build:Finder:#scan]: Failed to compose manifest from ${dir}`, err);
309
+ return {
310
+ segments: [],
311
+ endpoints: [],
312
+ };
313
+ }
314
+ }
315
+ /**
316
+ * Process the scanned route data
317
+ */
318
+ async process(res) {
319
+ const processed = new Set();
320
+ const prerenderedRoutes = new Set();
321
+ const manifest = {};
322
+ // imports for endpoints and components
323
+ const imports = {
324
+ endpoints: { static: new Map() },
325
+ components: { static: new Map(), dynamic: new Map() },
326
+ middlewares: { static: new Map() },
327
+ };
328
+ const modules = {};
329
+ const prerenderCache = new Map();
330
+ for (const segment of res.segments) {
331
+ try {
332
+ if (!this.buildContext || !this.config)
333
+ continue;
334
+ const { shell: shellPath, layouts: layoutPaths, '401s': unauthorizedPaths, '403s': forbiddenPaths, page: pagePath, '404s': notFoundPaths, '500s': serverErrorPaths, loaders: loaderPaths, middlewares: middlewarePaths, dir, } = segment;
335
+ // route is derived from dir path, not page
336
+ const route = Finder.toCanonicalRoute(pagePath ?? `${dir.replace(/\\/g, '/')}/+page.tsx`);
337
+ const params = Finder.getParams(dir);
338
+ const depth = Finder.getDepth(route);
339
+ const isDynamic = route.includes(':');
340
+ const isWildcard = route.includes('*');
341
+ // effective mode for this segment; start from global config then
342
+ // apply shell/layout/page overrides
343
+ let currentPrerenderMode = this.config?.prerender ?? false;
344
+ /**
345
+ * Apply explicit prerender mode overrides in inheritance order
346
+ */
347
+ function applyPrerenderMode(flag) {
348
+ if (flag === undefined)
349
+ return;
350
+ currentPrerenderMode = flag;
351
+ }
352
+ const shellImport = Finder.getImportPath(shellPath);
353
+ const shellId = `${Build.EntryKind.SHELL}${Bun.hash(shellImport)}`;
354
+ const layoutIds = [];
355
+ const unauthorizedIds = [];
356
+ const forbiddenIds = [];
357
+ const notFoundIds = [];
358
+ const serverErrorIds = [];
359
+ const loadingIds = [];
360
+ const middlewareIds = [];
361
+ // check shell prerender
362
+ if (!processed.has(shellPath)) {
363
+ prerenderCache.set(shellPath, await Prerender.Build.getStaticFlag(shellPath, this.buildContext));
364
+ imports.components.static.set(shellId, shellImport);
365
+ processed.add(shellPath);
366
+ }
367
+ applyPrerenderMode(prerenderCache.get(shellPath));
368
+ for (const layoutPath of layoutPaths) {
369
+ if (!layoutPath) {
370
+ layoutIds.push(null);
371
+ continue;
372
+ }
373
+ const layoutImport = Finder.getImportPath(layoutPath);
374
+ const layoutId = `${Build.EntryKind.LAYOUT}${Bun.hash(layoutImport)}`;
375
+ if (!processed.has(layoutPath)) {
376
+ prerenderCache.set(layoutPath, await Prerender.Build.getStaticFlag(layoutPath, this.buildContext));
377
+ imports.components.dynamic.set(layoutId, layoutImport);
378
+ processed.add(layoutPath);
379
+ }
380
+ applyPrerenderMode(prerenderCache.get(layoutPath));
381
+ layoutIds.push(layoutId);
382
+ }
383
+ for (const unauthorizedPath of unauthorizedPaths) {
384
+ if (!unauthorizedPath) {
385
+ unauthorizedIds.push(null);
386
+ continue;
387
+ }
388
+ const unauthorizedImport = Finder.getImportPath(unauthorizedPath);
389
+ const unauthorizedId = `${Build.EntryKind['401']}${Bun.hash(unauthorizedImport)}`;
390
+ unauthorizedIds.push(unauthorizedId);
391
+ if (!processed.has(unauthorizedPath)) {
392
+ imports.components.dynamic.set(unauthorizedId, unauthorizedImport);
393
+ processed.add(unauthorizedPath);
394
+ }
395
+ }
396
+ for (const forbiddenPath of forbiddenPaths) {
397
+ if (!forbiddenPath) {
398
+ forbiddenIds.push(null);
399
+ continue;
400
+ }
401
+ const forbiddenImport = Finder.getImportPath(forbiddenPath);
402
+ const forbiddenId = `${Build.EntryKind['403']}${Bun.hash(forbiddenImport)}`;
403
+ forbiddenIds.push(forbiddenId);
404
+ if (!processed.has(forbiddenPath)) {
405
+ imports.components.dynamic.set(forbiddenId, forbiddenImport);
406
+ processed.add(forbiddenPath);
407
+ }
408
+ }
409
+ for (const notFoundPath of notFoundPaths) {
410
+ // hole if level does not declare a 404 boundary.
411
+ // Keep slot so indices match layouts
412
+ if (!notFoundPath) {
413
+ notFoundIds.push(null);
414
+ continue;
415
+ }
416
+ const notFoundImport = Finder.getImportPath(notFoundPath);
417
+ const notFoundId = `${Build.EntryKind['404']}${Bun.hash(notFoundImport)}`;
418
+ notFoundIds.push(notFoundId);
419
+ // dedupe imports but still assign the slot for this route
420
+ if (!processed.has(notFoundPath)) {
421
+ imports.components.dynamic.set(notFoundId, notFoundImport);
422
+ processed.add(notFoundPath);
423
+ }
424
+ }
425
+ for (const serverErrorPath of serverErrorPaths) {
426
+ if (!serverErrorPath) {
427
+ serverErrorIds.push(null);
428
+ continue;
429
+ }
430
+ const serverErrorImport = Finder.getImportPath(serverErrorPath);
431
+ const serverErrorId = `${Build.EntryKind['500']}${Bun.hash(serverErrorImport)}`;
432
+ serverErrorIds.push(serverErrorId);
433
+ if (!processed.has(serverErrorPath)) {
434
+ imports.components.dynamic.set(serverErrorId, serverErrorImport);
435
+ processed.add(serverErrorPath);
436
+ }
437
+ }
438
+ for (const loaderPath of loaderPaths) {
439
+ // hole if level does not declare a loader.
440
+ // Keep slot so indices match layouts
441
+ if (!loaderPath) {
442
+ loadingIds.push(null);
443
+ continue;
444
+ }
445
+ const loaderImport = Finder.getImportPath(loaderPath);
446
+ const loaderId = `${Build.EntryKind.LOADING}${Bun.hash(loaderImport)}`;
447
+ loadingIds.push(loaderId);
448
+ // dedupe imports but still assign the slot for this route
449
+ if (!processed.has(loaderPath)) {
450
+ imports.components.dynamic.set(loaderId, loaderImport);
451
+ processed.add(loaderPath);
452
+ }
453
+ }
454
+ for (const middlewarePath of middlewarePaths) {
455
+ if (!middlewarePath) {
456
+ middlewareIds.push(null);
457
+ continue;
458
+ }
459
+ const middlewareImport = Finder.getImportPath(middlewarePath);
460
+ const middlewareId = `${Build.EntryKind.MIDDLEWARE}${Bun.hash(middlewareImport)}`;
461
+ middlewareIds.push(middlewareId);
462
+ if (!processed.has(middlewarePath)) {
463
+ // route scanning only tells us this is a +middleware file path so
464
+ // we still validate that the module actually exports middleware
465
+ if (!(await this.buildContext.exportReader.has(middlewarePath, 'middleware'))) {
466
+ throw new Error(`Missing middleware export in ${middlewarePath}`);
467
+ }
468
+ imports.middlewares.static.set(middlewareId, middlewareImport);
469
+ processed.add(middlewarePath);
470
+ }
471
+ }
472
+ // generate entry id based on page if exists, otherwise dir
473
+ const entryId = pagePath
474
+ ? `${Build.EntryKind.PAGE}${Bun.hash(Finder.getImportPath(pagePath))}`
475
+ : `${Build.EntryKind.PAGE}${Bun.hash(route)}`;
476
+ if (pagePath) {
477
+ const pagePrerender = await Prerender.Build.getStaticFlag(pagePath, this.buildContext);
478
+ applyPrerenderMode(pagePrerender);
479
+ imports.components.dynamic.set(entryId, Finder.getImportPath(pagePath));
480
+ processed.add(pagePath);
481
+ }
482
+ const shouldPrerender = currentPrerenderMode !== false;
483
+ const prerenderMode = shouldPrerender
484
+ ? currentPrerenderMode
485
+ : false;
486
+ if (shouldPrerender) {
487
+ if (!isDynamic && !isWildcard) {
488
+ prerenderedRoutes.add(route);
489
+ }
490
+ else if (pagePath) {
491
+ const staticParams = await Prerender.Build.getStaticParams(pagePath, this.buildContext);
492
+ for (const r of Prerender.Build.getDynamicRouteList(route, params, staticParams)) {
493
+ prerenderedRoutes.add(r);
494
+ }
495
+ }
496
+ }
497
+ const entry = {
498
+ __id: entryId,
499
+ __path: route,
500
+ __params: params,
501
+ __kind: Build.EntryKind.PAGE,
502
+ __depth: depth,
503
+ method: 'get',
504
+ paths: {
505
+ layouts: [shellPath, ...layoutPaths].map(layout => layout ? Finder.getImportPath(layout) : null),
506
+ '401s': unauthorizedPaths.map(unauthorized => unauthorized ? Finder.getImportPath(unauthorized) : null),
507
+ '403s': forbiddenPaths.map(forbidden => forbidden ? Finder.getImportPath(forbidden) : null),
508
+ '404s': notFoundPaths.map(notFound => notFound ? Finder.getImportPath(notFound) : null),
509
+ '500s': serverErrorPaths.map(serverError => serverError ? Finder.getImportPath(serverError) : null),
510
+ loaders: loaderPaths.map(loader => loader ? Finder.getImportPath(loader) : null),
511
+ middlewares: middlewarePaths.map(middleware => middleware ? Finder.getImportPath(middleware) : null),
512
+ page: pagePath ? Finder.getImportPath(pagePath) : null,
513
+ },
514
+ prerender: prerenderMode,
515
+ dynamic: isDynamic,
516
+ wildcard: isWildcard,
517
+ };
518
+ if (manifest[route]) {
519
+ if (Array.isArray(manifest[route])) {
520
+ manifest[route].push(entry);
521
+ }
522
+ else {
523
+ manifest[route] = [manifest[route], entry];
524
+ }
525
+ }
526
+ else {
527
+ manifest[route] = entry;
528
+ }
529
+ modules[entryId] = {
530
+ shellId,
531
+ layoutIds,
532
+ pageId: pagePath ? entryId : undefined,
533
+ '401Ids': unauthorizedIds,
534
+ '403Ids': forbiddenIds,
535
+ '404Ids': notFoundIds,
536
+ '500Ids': serverErrorIds,
537
+ loadingIds,
538
+ middlewareIds,
539
+ };
540
+ }
541
+ catch (err) {
542
+ if (err instanceof Error &&
543
+ err.message.startsWith('Missing middleware export')) {
544
+ throw err;
545
+ }
546
+ logger.error('[Build:Finder:process]: failed to process segment', err);
547
+ }
548
+ }
549
+ for (const endpoint of res.endpoints) {
550
+ try {
551
+ const endpointFilePath = endpoint.file;
552
+ const endpointMiddlewarePaths = endpoint.middlewares;
553
+ if (!this.buildContext || processed.has(endpointFilePath))
554
+ continue;
555
+ const route = Finder.toCanonicalRoute(endpointFilePath);
556
+ const params = Finder.getParams(endpointFilePath);
557
+ const endpointExports = await this.buildContext.exportReader.exports(endpointFilePath);
558
+ const group = [];
559
+ for (const method of endpointExports) {
560
+ if (!HTTP_VERBS.includes(method)) {
561
+ logger.warn('[Build:Finder:process]', `Ignoring unsupported HTTP verb: ${method} in ${endpointFilePath}`);
562
+ continue;
563
+ }
564
+ const m = method.toLowerCase();
565
+ const endpointId = `${Build.EntryKind.ENDPOINT}${Bun.hash(Finder.getImportPath(endpointFilePath))}_${m}`;
566
+ const middlewareIds = await Promise.all(endpointMiddlewarePaths.map(async middlewarePath => {
567
+ if (!middlewarePath)
568
+ return null;
569
+ const middlewareImport = Finder.getImportPath(middlewarePath);
570
+ const middlewareId = `${Build.EntryKind.MIDDLEWARE}${Bun.hash(middlewareImport)}`;
571
+ if (!processed.has(middlewarePath)) {
572
+ // endpoint middleware discovery gives us file paths, not proof of the export
573
+ // so check the module shape before we register the import
574
+ if (!(await this.buildContext.exportReader.has(middlewarePath, 'middleware'))) {
575
+ throw new Error(`Missing middleware export in ${middlewarePath}`);
576
+ }
577
+ imports.middlewares.static.set(middlewareId, middlewareImport);
578
+ processed.add(middlewarePath);
579
+ }
580
+ return middlewareId;
581
+ }));
582
+ group.push({
583
+ __id: endpointId,
584
+ __path: route,
585
+ __params: params,
586
+ __kind: Build.EntryKind.ENDPOINT,
587
+ method: m,
588
+ middlewares: endpointMiddlewarePaths.map(middlewarePath => middlewarePath ? Finder.getImportPath(middlewarePath) : null),
589
+ });
590
+ imports.endpoints.static.set(endpointId, Finder.getImportPath(endpointFilePath));
591
+ modules[endpointId] = { endpointId, middlewareIds };
592
+ processed.add(endpointFilePath);
593
+ }
594
+ const entry = group.length === 1 ? group[0] : group;
595
+ if (endpointMiddlewarePaths.length) {
596
+ modules[route] = {
597
+ ...(modules[route] ?? {}),
598
+ middlewareIds: endpointMiddlewarePaths.map(middlewarePath => middlewarePath
599
+ ? `${Build.EntryKind.MIDDLEWARE}${Bun.hash(Finder.getImportPath(middlewarePath))}`
600
+ : null),
601
+ };
602
+ }
603
+ if (manifest[route]) {
604
+ if (Array.isArray(manifest[route])) {
605
+ manifest[route] = [
606
+ ...manifest[route],
607
+ ...(Array.isArray(entry) ? entry : [entry]),
608
+ ];
609
+ }
610
+ else {
611
+ manifest[route] = [
612
+ manifest[route],
613
+ ...(Array.isArray(entry) ? entry : [entry]),
614
+ ];
615
+ }
616
+ }
617
+ else {
618
+ manifest[route] = entry;
619
+ }
620
+ }
621
+ catch (err) {
622
+ if (err instanceof Error &&
623
+ err.message.startsWith('Missing middleware export')) {
624
+ throw err;
625
+ }
626
+ logger.error('[Build:Finder:process]: failed to process route', err);
627
+ }
628
+ }
629
+ return { manifest, imports, modules, prerenderedRoutes };
630
+ }
631
+ }
632
+ Build.Finder = Finder;
633
+ })(Build || (Build = {}));
@@ -0,0 +1,5 @@
1
+ import type { PluginConfig } from '../../types';
2
+ /**
3
+ * Generates the code to create an exported config object
4
+ */
5
+ export declare function writeConfig(config: PluginConfig): string;
@@ -0,0 +1,19 @@
1
+ import { Solas } from '../../solas';
2
+ import { AUTOGEN_MSG } from './utils';
3
+ /**
4
+ * Generates the code to create an exported config object
5
+ */
6
+ export function writeConfig(config) {
7
+ return `
8
+ ${AUTOGEN_MSG}
9
+
10
+ import type { PluginConfig } from '${Solas.Config.PKG_NAME}'
11
+ import { Logger } from '${Solas.Config.PKG_NAME}/utils/logger'
12
+
13
+ const config = ${JSON.stringify(config, null, 2)} as const satisfies PluginConfig
14
+
15
+ if (config.logger?.level) Logger.defaultLevel = config.logger.level
16
+
17
+ export { config }
18
+ `.trim();
19
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Generates the RSC entry code
3
+ */
4
+ export declare function writeRSCEntry(): string;
5
+ /**
6
+ * Generates the SSR entry code
7
+ */
8
+ export declare function writeSSREntry(): string;
9
+ /**
10
+ * Generates the browser entry code
11
+ */
12
+ export declare function writeBrowserEntry(): string;