@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.
- package/CHANGELOG.md +29 -0
- package/README.md +81 -85
- package/dist/config.d.ts +25 -0
- package/dist/config.js +152 -0
- package/dist/events.d.ts +4 -0
- package/dist/events.js +5 -0
- package/dist/index.d.ts +1 -2
- package/dist/index.js +59 -37
- package/dist/internal/boundary-error.d.ts +4 -0
- package/dist/internal/boundary-error.js +8 -0
- package/dist/internal/browser-router/history.d.ts +46 -0
- package/dist/internal/browser-router/history.js +131 -0
- package/dist/internal/browser-router/link.d.ts +1 -1
- package/dist/internal/browser-router/link.js +1 -1
- package/dist/internal/browser-router/router.d.ts +6 -14
- package/dist/internal/browser-router/router.js +39 -13
- package/dist/internal/browser-router/shared.d.ts +183 -164
- package/dist/internal/browser-router/shared.js +49 -56
- package/dist/internal/browser-router/use-router.d.ts +1 -10
- package/dist/internal/browser-router/use-search-params.js +3 -3
- package/dist/internal/build.d.ts +108 -110
- package/dist/internal/build.js +569 -581
- package/dist/internal/codegen/config.js +3 -3
- package/dist/internal/codegen/environments.js +5 -6
- package/dist/internal/codegen/format.d.ts +6 -0
- package/dist/internal/codegen/format.js +24 -0
- package/dist/internal/codegen/manifest.js +2 -2
- package/dist/internal/codegen/maps.d.ts +1 -1
- package/dist/internal/codegen/maps.js +2 -2
- package/dist/internal/codegen/types.js +4 -4
- package/dist/internal/codegen/utils.js +2 -2
- package/dist/internal/env/request-context.js +1 -1
- package/dist/internal/env/rsc.d.ts +6 -4
- package/dist/internal/env/rsc.js +27 -25
- package/dist/internal/env/ssr.js +4 -4
- package/dist/internal/http-router/router.js +8 -8
- package/dist/internal/metadata.d.ts +1 -1
- package/dist/internal/metadata.js +1 -1
- package/dist/internal/navigation/http-exception-boundary.d.ts +1 -1
- package/dist/internal/navigation/redirect-boundary.d.ts +1 -1
- package/dist/internal/navigation/redirect.js +6 -6
- package/dist/internal/postbuild.js +18 -17
- package/dist/internal/prerender.d.ts +130 -138
- package/dist/internal/prerender.js +326 -354
- package/dist/internal/public-files.js +2 -4
- package/dist/internal/render/head.js +2 -2
- package/dist/internal/resolver.js +1 -1
- package/dist/internal/server/actions.js +2 -2
- package/dist/internal/ui/error-boundary.d.ts +1 -1
- package/dist/manifest.d.ts +7 -0
- package/dist/manifest.js +85 -0
- package/dist/prerender.d.ts +1 -1
- package/dist/prerender.js +1 -1
- package/dist/solas.d.ts +2 -46
- package/dist/solas.js +6 -266
- package/dist/types.d.ts +14 -21
- package/dist/types.js +1 -1
- package/dist/utils/base-path.d.ts +12 -14
- package/dist/utils/base-path.js +75 -82
- package/dist/utils/compress.d.ts +9 -11
- package/dist/utils/compress.js +59 -64
- package/dist/utils/context.d.ts +4 -6
- package/dist/utils/context.js +17 -22
- package/dist/utils/logger.js +2 -2
- package/dist/utils/time.d.ts +2 -4
- package/dist/utils/time.js +23 -29
- package/package.json +18 -8
- package/dist/runtimes/bun.d.ts +0 -13
- package/dist/runtimes/bun.js +0 -39
package/dist/internal/build.js
CHANGED
|
@@ -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
|
|
4
|
+
import * as Config from '../config.js';
|
|
5
5
|
import { normalisePathname } from './http-router/utils.js';
|
|
6
|
-
import
|
|
6
|
+
import * as Prerender from './prerender.js';
|
|
7
7
|
import { Runtime } from './runtimes/runtime.js';
|
|
8
|
-
|
|
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
|
-
*
|
|
11
|
-
*
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
*
|
|
73
|
-
* null when the directory does not declare that file type
|
|
112
|
+
* Extracts dynamic parameter names from a file path
|
|
74
113
|
*/
|
|
75
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
*
|
|
110
|
-
* result into the manifest, import map, and module map
|
|
127
|
+
* Convert a file path to a canonical route.
|
|
111
128
|
*/
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
162
|
-
|
|
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
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
(
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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
|
|
349
|
-
const
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
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(
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
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
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
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
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
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
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
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
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
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
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
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
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
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
|
-
|
|
469
|
-
const
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
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
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
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
|
-
|
|
497
|
-
|
|
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
|
-
|
|
479
|
+
prerenderRoutes.add(normalisePathname(route, trailingSlash));
|
|
500
480
|
}
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
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
|
-
|
|
546
|
-
|
|
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
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
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
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
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] =
|
|
612
|
+
manifest[route] = [
|
|
613
|
+
manifest[route],
|
|
614
|
+
...(Array.isArray(entry) ? entry : [entry]),
|
|
615
|
+
];
|
|
629
616
|
}
|
|
630
617
|
}
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
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
|
-
|
|
643
|
-
})(Build || (Build = {}));
|
|
631
|
+
}
|