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