evolit 0.1.0-alpha.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/LICENSE +201 -0
- package/README.md +352 -0
- package/package.json +63 -0
- package/src/app-discovery.js +312 -0
- package/src/build.js +417 -0
- package/src/cli.js +83 -0
- package/src/client-assets.js +1881 -0
- package/src/compiler.js +353 -0
- package/src/config.js +20 -0
- package/src/constants.js +49 -0
- package/src/deployment-runtime.js +463 -0
- package/src/fs-utils.js +71 -0
- package/src/index.js +167 -0
- package/src/render.js +516 -0
- package/src/request-context-browser.js +11 -0
- package/src/request-context.js +230 -0
- package/src/response-cache.js +214 -0
- package/src/route-config.js +60 -0
- package/src/scaffold.js +107 -0
- package/src/server-api.js +51 -0
- package/src/server.js +230 -0
- package/src/ssr-adapter.js +189 -0
- package/templates/default/app/about/page.litsx +20 -0
- package/templates/default/app/assets.d.ts +1 -0
- package/templates/default/app/components/feature-card.litsx +35 -0
- package/templates/default/app/global.css +27 -0
- package/templates/default/app/layout.litsx +18 -0
- package/templates/default/app/page.litsx +36 -0
- package/templates/default/jsconfig.json +22 -0
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { APP_DIRECTORY, MODULE_EXTENSIONS, ROUTE_HANDLER_EXTENSIONS } from "./constants.js";
|
|
3
|
+
import { pathExists, walkFiles } from "./fs-utils.js";
|
|
4
|
+
|
|
5
|
+
function stripExtension(filename) {
|
|
6
|
+
for (const extension of MODULE_EXTENSIONS) {
|
|
7
|
+
if (filename.endsWith(extension)) {
|
|
8
|
+
return filename.slice(0, -extension.length);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
return filename;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function normalizeSegment(segment) {
|
|
16
|
+
if (segment.startsWith("(") && segment.endsWith(")")) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return segment;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function routePathFromSegments(segments) {
|
|
24
|
+
if (segments.length === 0) {
|
|
25
|
+
return "/";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const pathname = segments
|
|
29
|
+
.map((segment) => normalizeSegment(segment))
|
|
30
|
+
.filter(Boolean)
|
|
31
|
+
.map((segment) => {
|
|
32
|
+
if (segment.startsWith("[[...") && segment.endsWith("]]")) {
|
|
33
|
+
return `**${segment.slice(5, -2)}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (segment.startsWith("[...") && segment.endsWith("]")) {
|
|
37
|
+
return `*${segment.slice(4, -1)}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (segment.startsWith("[") && segment.endsWith("]")) {
|
|
41
|
+
return `:${segment.slice(1, -1)}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return segment;
|
|
45
|
+
})
|
|
46
|
+
.join("/");
|
|
47
|
+
|
|
48
|
+
return pathname ? `/${pathname}` : "/";
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function routeHasDynamicSegments(pathname) {
|
|
52
|
+
return pathname.includes(":") || pathname.includes("*");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function encodeRouteParamSegment(value, pathname, paramName) {
|
|
56
|
+
if (typeof value !== "string" && typeof value !== "number") {
|
|
57
|
+
throw new Error(
|
|
58
|
+
`Expected generateStaticParams() to provide a string or number for "${paramName}" in route ${pathname}.`,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return encodeURIComponent(String(value));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function resolveRoutePathname(pathname, params = {}) {
|
|
66
|
+
if (pathname === "/") {
|
|
67
|
+
return "/";
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const resolvedSegments = [];
|
|
71
|
+
const routeSegments = pathname.split("/").filter(Boolean);
|
|
72
|
+
|
|
73
|
+
for (const segment of routeSegments) {
|
|
74
|
+
if (segment.startsWith("**")) {
|
|
75
|
+
const paramName = segment.slice(2);
|
|
76
|
+
const value = params[paramName];
|
|
77
|
+
if (value == null) {
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (!Array.isArray(value)) {
|
|
82
|
+
throw new Error(
|
|
83
|
+
`Expected generateStaticParams() to provide an array for optional catch-all "${paramName}" in route ${pathname}.`,
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
resolvedSegments.push(
|
|
88
|
+
...value.map((entry) => encodeRouteParamSegment(entry, pathname, paramName)),
|
|
89
|
+
);
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (segment.startsWith("*")) {
|
|
94
|
+
const paramName = segment.slice(1);
|
|
95
|
+
const value = params[paramName];
|
|
96
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
97
|
+
throw new Error(
|
|
98
|
+
`Expected generateStaticParams() to provide a non-empty array for catch-all "${paramName}" in route ${pathname}.`,
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
resolvedSegments.push(
|
|
103
|
+
...value.map((entry) => encodeRouteParamSegment(entry, pathname, paramName)),
|
|
104
|
+
);
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (segment.startsWith(":")) {
|
|
109
|
+
const paramName = segment.slice(1);
|
|
110
|
+
if (!Object.hasOwn(params, paramName)) {
|
|
111
|
+
throw new Error(
|
|
112
|
+
`Expected generateStaticParams() to provide a value for "${paramName}" in route ${pathname}.`,
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
resolvedSegments.push(
|
|
117
|
+
encodeRouteParamSegment(params[paramName], pathname, paramName),
|
|
118
|
+
);
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
resolvedSegments.push(segment);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return `/${resolvedSegments.join("/")}`;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function collectLayoutCandidates(routeDirectory, appRoot) {
|
|
129
|
+
const layouts = [];
|
|
130
|
+
let currentDirectory = routeDirectory;
|
|
131
|
+
|
|
132
|
+
while (currentDirectory.startsWith(appRoot)) {
|
|
133
|
+
layouts.push(currentDirectory);
|
|
134
|
+
if (currentDirectory === appRoot) {
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
currentDirectory = path.dirname(currentDirectory);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return layouts.reverse();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function collectConventionCandidates(routeDirectory, appRoot, filesByStem, stem) {
|
|
144
|
+
return collectLayoutCandidates(routeDirectory, appRoot)
|
|
145
|
+
.map((directory) => ({
|
|
146
|
+
directory,
|
|
147
|
+
module: findModuleByStem(filesByStem, directory, stem),
|
|
148
|
+
}))
|
|
149
|
+
.filter((entry) => entry.module);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function findModuleByStem(filesByStem, directory, stem) {
|
|
153
|
+
for (const extension of MODULE_EXTENSIONS) {
|
|
154
|
+
const candidate = path.join(directory, `${stem}${extension}`);
|
|
155
|
+
if (filesByStem.has(candidate)) {
|
|
156
|
+
return candidate;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function findRouteHandlerByStem(filesByStem, directory) {
|
|
164
|
+
for (const extension of ROUTE_HANDLER_EXTENSIONS) {
|
|
165
|
+
const candidate = path.join(directory, `route${extension}`);
|
|
166
|
+
if (filesByStem.has(candidate)) {
|
|
167
|
+
return candidate;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export async function discoverAppRoutes(projectRoot) {
|
|
175
|
+
const appRoot = path.join(projectRoot, APP_DIRECTORY);
|
|
176
|
+
if (!(await pathExists(appRoot))) {
|
|
177
|
+
return [];
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const allFiles = await walkFiles(appRoot);
|
|
181
|
+
const filesByStem = new Set(allFiles);
|
|
182
|
+
|
|
183
|
+
const pageFiles = allFiles.filter((filePath) => {
|
|
184
|
+
const baseName = path.basename(stripExtension(filePath));
|
|
185
|
+
return baseName === "page";
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
return pageFiles.map((pagePath) => {
|
|
189
|
+
const routeDirectory = path.dirname(pagePath);
|
|
190
|
+
const relativeDirectory = path.relative(appRoot, routeDirectory);
|
|
191
|
+
const routeSegments =
|
|
192
|
+
relativeDirectory === "" ? [] : relativeDirectory.split(path.sep);
|
|
193
|
+
const layoutFiles = collectLayoutCandidates(routeDirectory, appRoot)
|
|
194
|
+
.map((directory) => findModuleByStem(filesByStem, directory, "layout"))
|
|
195
|
+
.filter(Boolean);
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
pathname: routePathFromSegments(routeSegments),
|
|
199
|
+
page: pagePath,
|
|
200
|
+
layouts: layoutFiles,
|
|
201
|
+
notFoundBoundaries: collectConventionCandidates(
|
|
202
|
+
routeDirectory,
|
|
203
|
+
appRoot,
|
|
204
|
+
filesByStem,
|
|
205
|
+
"not-found",
|
|
206
|
+
),
|
|
207
|
+
errorBoundaries: collectConventionCandidates(
|
|
208
|
+
routeDirectory,
|
|
209
|
+
appRoot,
|
|
210
|
+
filesByStem,
|
|
211
|
+
"error",
|
|
212
|
+
),
|
|
213
|
+
};
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export async function discoverAppRouteHandlers(projectRoot) {
|
|
218
|
+
const appRoot = path.join(projectRoot, APP_DIRECTORY);
|
|
219
|
+
if (!(await pathExists(appRoot))) {
|
|
220
|
+
return [];
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const allFiles = await walkFiles(appRoot);
|
|
224
|
+
const filesByStem = new Set(allFiles);
|
|
225
|
+
const routeDirectories = new Set(
|
|
226
|
+
allFiles
|
|
227
|
+
.filter((filePath) => path.basename(stripExtension(filePath)) === "route")
|
|
228
|
+
.map((filePath) => path.dirname(filePath)),
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
return [...routeDirectories].map((routeDirectory) => {
|
|
232
|
+
const handler = findRouteHandlerByStem(filesByStem, routeDirectory);
|
|
233
|
+
if (!handler) {
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const page = findModuleByStem(filesByStem, routeDirectory, "page");
|
|
238
|
+
if (page) {
|
|
239
|
+
throw new Error(
|
|
240
|
+
`Route handler ${handler} cannot coexist with page module ${page}.`,
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const relativeDirectory = path.relative(appRoot, routeDirectory);
|
|
245
|
+
const routeSegments = relativeDirectory === "" ? [] : relativeDirectory.split(path.sep);
|
|
246
|
+
return {
|
|
247
|
+
pathname: routePathFromSegments(routeSegments),
|
|
248
|
+
handler,
|
|
249
|
+
};
|
|
250
|
+
}).filter(Boolean);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export function matchRoute(pathname, routes) {
|
|
254
|
+
const requestSegments = pathname
|
|
255
|
+
.split("/")
|
|
256
|
+
.filter(Boolean)
|
|
257
|
+
.map((segment) => decodeURIComponent(segment));
|
|
258
|
+
|
|
259
|
+
for (const route of routes) {
|
|
260
|
+
const routeSegments = route.pathname.split("/").filter(Boolean);
|
|
261
|
+
const params = {};
|
|
262
|
+
let matched = true;
|
|
263
|
+
|
|
264
|
+
for (let index = 0; index < routeSegments.length; index += 1) {
|
|
265
|
+
const routeSegment = routeSegments[index];
|
|
266
|
+
const requestSegment = requestSegments[index];
|
|
267
|
+
|
|
268
|
+
if (routeSegment?.startsWith("**")) {
|
|
269
|
+
const paramName = routeSegment.slice(2);
|
|
270
|
+
const remainingSegments = requestSegments.slice(index);
|
|
271
|
+
params[paramName] = remainingSegments.length > 0 ? remainingSegments : undefined;
|
|
272
|
+
break;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (routeSegment?.startsWith("*")) {
|
|
276
|
+
params[routeSegment.slice(1)] = requestSegments.slice(index);
|
|
277
|
+
break;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (requestSegment == null) {
|
|
281
|
+
matched = false;
|
|
282
|
+
break;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (routeSegment?.startsWith(":")) {
|
|
286
|
+
params[routeSegment.slice(1)] = requestSegment;
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (routeSegment !== requestSegment) {
|
|
291
|
+
matched = false;
|
|
292
|
+
break;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (!matched) {
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const lastSegment = routeSegments[routeSegments.length - 1] ?? null;
|
|
301
|
+
const hasCatchAll = Boolean(
|
|
302
|
+
lastSegment?.startsWith("*") || lastSegment?.startsWith("**"),
|
|
303
|
+
);
|
|
304
|
+
if (!hasCatchAll && routeSegments.length !== requestSegments.length) {
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return { route, params };
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return null;
|
|
312
|
+
}
|