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/src/build.js ADDED
@@ -0,0 +1,417 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import {
4
+ discoverAppRouteHandlers,
5
+ discoverAppRoutes,
6
+ resolveRoutePathname,
7
+ routeHasDynamicSegments,
8
+ } from "./app-discovery.js";
9
+ import {
10
+ collectTransitiveAssetPreloads,
11
+ collectTransitiveStyleUrls,
12
+ createAssetResolver,
13
+ createHydrationBootstrap,
14
+ createStaticAssetPublicUrlMap,
15
+ emitBundledClientAssets,
16
+ emitHashedClientAssets,
17
+ normalizeHydrationDataForClient,
18
+ resolveSharedVendorModuleUrl,
19
+ rewriteHydrationDataScript,
20
+ rewriteServerAssetPlaceholders,
21
+ } from "./client-assets.js";
22
+ import { compileModuleGraph, importCompiledModule } from "./compiler.js";
23
+ import { loadEvolitConfig } from "./config.js";
24
+ import {
25
+ BUILD_DIRECTORY,
26
+ DEPLOY_ASSETS_MANIFEST_FILENAME,
27
+ DEPLOY_ROUTES_MANIFEST_FILENAME,
28
+ DEPLOY_SERVER_MANIFEST_FILENAME,
29
+ INTERNAL_DIRECTORY,
30
+ MANIFEST_FILENAME,
31
+ } from "./constants.js";
32
+ import { createRouteResolver, resolveStaticParamsForRoute } from "./render.js";
33
+ import {
34
+ createCachedRouteResponse,
35
+ getRouteCacheArtifactFileName,
36
+ resolveResponseCacheRuntime,
37
+ } from "./response-cache.js";
38
+ import { serializeRouteCachePolicy } from "./route-config.js";
39
+ import { createSsrAdapter, renderRouteTreeWithAdapter } from "./ssr-adapter.js";
40
+ import { ensureDirectory, writeJson } from "./fs-utils.js";
41
+
42
+ const CONTENT_TYPE_BY_EXTENSION = new Map([
43
+ [".css", "text/css; charset=utf-8"],
44
+ [".svg", "image/svg+xml"],
45
+ [".png", "image/png"],
46
+ [".jpg", "image/jpeg"],
47
+ [".jpeg", "image/jpeg"],
48
+ [".gif", "image/gif"],
49
+ [".webp", "image/webp"],
50
+ [".avif", "image/avif"],
51
+ [".ico", "image/x-icon"],
52
+ [".woff", "font/woff"],
53
+ [".woff2", "font/woff2"],
54
+ [".ttf", "font/ttf"],
55
+ [".otf", "font/otf"],
56
+ [".map", "application/json; charset=utf-8"],
57
+ [".mjs", "text/javascript; charset=utf-8"],
58
+ [".js", "text/javascript; charset=utf-8"],
59
+ ]);
60
+
61
+ function getContentTypeForBuildArtifact(filePath) {
62
+ return CONTENT_TYPE_BY_EXTENSION.get(path.extname(filePath)) ?? "application/octet-stream";
63
+ }
64
+
65
+ function toBuildRelativePath(projectRoot, targetPath) {
66
+ return path.relative(projectRoot, targetPath).split(path.sep).join("/");
67
+ }
68
+
69
+ async function writeDeploymentRuntimeEntry(buildRoot) {
70
+ const runtimeEntryPath = path.join(buildRoot, "runtime-entry.mjs");
71
+ const frameworkEntryUrl = new URL("./index.js", import.meta.url).href;
72
+ await fs.writeFile(
73
+ runtimeEntryPath,
74
+ [
75
+ 'import fs from "node:fs/promises";',
76
+ 'import path from "node:path";',
77
+ 'import { fileURLToPath } from "node:url";',
78
+ "",
79
+ "let createDeploymentRuntime;",
80
+ "try {",
81
+ ' ({ createDeploymentRuntime } = await import("evolit"));',
82
+ "} catch {",
83
+ ` ({ createDeploymentRuntime } = await import(${JSON.stringify(frameworkEntryUrl)}));`,
84
+ "}",
85
+ "",
86
+ "const buildRoot = path.dirname(fileURLToPath(import.meta.url));",
87
+ 'const projectRoot = path.resolve(buildRoot, "..", "..");',
88
+ "let runtimePromise = null;",
89
+ "",
90
+ "export async function createBuiltDeploymentRuntime(options = {}) {",
91
+ ' const manifest = JSON.parse(await fs.readFile(path.join(buildRoot, "manifest.json"), "utf8"));',
92
+ " return createDeploymentRuntime({",
93
+ " projectRoot: options.projectRoot ?? projectRoot,",
94
+ ' mode: "production",',
95
+ " assetManifest: manifest.clientAssets ?? null,",
96
+ " responseCacheRuntime: options.responseCacheRuntime,",
97
+ " routeResolver: options.routeResolver,",
98
+ " });",
99
+ "}",
100
+ "",
101
+ "export async function getBuiltDeploymentRuntime(options = {}) {",
102
+ " if (!runtimePromise) {",
103
+ " runtimePromise = createBuiltDeploymentRuntime(options);",
104
+ " }",
105
+ " return runtimePromise;",
106
+ "}",
107
+ "",
108
+ "export async function handleRequest(request, options = {}) {",
109
+ " const runtime = await getBuiltDeploymentRuntime(options);",
110
+ " return runtime.handle(request);",
111
+ "}",
112
+ "",
113
+ ].join("\n"),
114
+ "utf8",
115
+ );
116
+ return runtimeEntryPath;
117
+ }
118
+
119
+ export async function buildProject(projectRoot) {
120
+ const evolitConfig = await loadEvolitConfig(projectRoot);
121
+ const routes = await discoverAppRoutes(projectRoot);
122
+ const routeHandlers = await discoverAppRouteHandlers(projectRoot);
123
+ const buildRoot = path.join(projectRoot, INTERNAL_DIRECTORY, BUILD_DIRECTORY);
124
+ const entryClientModules = new Set();
125
+ const deployHandlers = [];
126
+
127
+ await ensureDirectory(buildRoot);
128
+
129
+ for (const routeHandler of routeHandlers) {
130
+ await compileModuleGraph(routeHandler.handler, {
131
+ projectRoot,
132
+ mode: "production",
133
+ sourceMaps: false,
134
+ ssr: true,
135
+ target: "server",
136
+ });
137
+
138
+ const handlerModule = await importCompiledModule(routeHandler.handler, {
139
+ projectRoot,
140
+ mode: "production",
141
+ ssr: true,
142
+ target: "server",
143
+ });
144
+ const methods = Object.keys(handlerModule)
145
+ .filter((name) => /^[A-Z]+$/.test(name) && typeof handlerModule[name] === "function")
146
+ .sort();
147
+ if (methods.length === 0) {
148
+ throw new Error(`Expected route handler ${routeHandler.handler} to export an HTTP method.`);
149
+ }
150
+
151
+ deployHandlers.push({
152
+ pathname: routeHandler.pathname,
153
+ methods,
154
+ runtime: "server",
155
+ cache: "dynamic",
156
+ });
157
+ }
158
+
159
+ for (const route of routes) {
160
+ await compileModuleGraph(route.page, {
161
+ projectRoot,
162
+ mode: "production",
163
+ sourceMaps: false,
164
+ ssr: true,
165
+ target: "server",
166
+ });
167
+
168
+ const pageClientBuild = await compileModuleGraph(route.page, {
169
+ projectRoot,
170
+ mode: "production",
171
+ sourceMaps: true,
172
+ target: "client",
173
+ });
174
+ entryClientModules.add(
175
+ path.relative(pageClientBuild.outputRoot, pageClientBuild.entrypoint).split(path.sep).join("/"),
176
+ );
177
+
178
+ for (const layoutPath of route.layouts) {
179
+ await compileModuleGraph(layoutPath, {
180
+ projectRoot,
181
+ mode: "production",
182
+ sourceMaps: false,
183
+ ssr: true,
184
+ target: "server",
185
+ });
186
+
187
+ const layoutClientBuild = await compileModuleGraph(layoutPath, {
188
+ projectRoot,
189
+ mode: "production",
190
+ sourceMaps: true,
191
+ target: "client",
192
+ });
193
+ entryClientModules.add(
194
+ path.relative(layoutClientBuild.outputRoot, layoutClientBuild.entrypoint).split(path.sep).join("/"),
195
+ );
196
+ }
197
+
198
+ const boundaryModules = [
199
+ ...(route.notFoundBoundaries ?? []).map((boundary) => boundary.module),
200
+ ...(route.errorBoundaries ?? []).map((boundary) => boundary.module),
201
+ ];
202
+ for (const boundaryPath of new Set(boundaryModules)) {
203
+ await compileModuleGraph(boundaryPath, {
204
+ projectRoot,
205
+ mode: "production",
206
+ sourceMaps: false,
207
+ ssr: true,
208
+ target: "server",
209
+ });
210
+
211
+ const boundaryClientBuild = await compileModuleGraph(boundaryPath, {
212
+ projectRoot,
213
+ mode: "production",
214
+ sourceMaps: true,
215
+ target: "client",
216
+ });
217
+ entryClientModules.add(
218
+ path.relative(boundaryClientBuild.outputRoot, boundaryClientBuild.entrypoint)
219
+ .split(path.sep)
220
+ .join("/"),
221
+ );
222
+ }
223
+ }
224
+
225
+ const clientAssets = await emitBundledClientAssets(projectRoot, {
226
+ entryClientModules,
227
+ });
228
+ const staticAssetPublicUrls = createStaticAssetPublicUrlMap(clientAssets);
229
+ await rewriteServerAssetPlaceholders(projectRoot, clientAssets);
230
+
231
+ const routeResolver = await createRouteResolver(projectRoot, "production", {
232
+ staticAssetPublicUrls,
233
+ });
234
+ const assetResolver = createAssetResolver(projectRoot, {
235
+ assetManifest: clientAssets,
236
+ });
237
+ const hydrationModuleUrl = await resolveSharedVendorModuleUrl(
238
+ projectRoot,
239
+ "production",
240
+ "@litsx/ssr/hydration",
241
+ );
242
+ const ssrAdapter = createSsrAdapter({
243
+ assetResolver,
244
+ async resolveAdditionalHead({ result }) {
245
+ const clientImports = Array.isArray(result.clientImports) ? result.clientImports : [];
246
+ const urls = collectTransitiveAssetPreloads(clientImports, clientAssets);
247
+ const styleUrls = collectTransitiveStyleUrls(clientImports, clientAssets);
248
+
249
+ return [
250
+ ...urls.map((href) => `<link rel="modulepreload" href="${href}">`),
251
+ ...styleUrls.map((href) => `<link rel="stylesheet" href="${href}">`),
252
+ ].join("\n");
253
+ },
254
+ resolveBootstrap({ result }) {
255
+ return createHydrationBootstrap({
256
+ hydrationData: normalizeHydrationDataForClient(result.hydrationData, projectRoot),
257
+ assetResolver,
258
+ hydrationModuleUrl,
259
+ });
260
+ },
261
+ transformDocument({ result, document }) {
262
+ return rewriteHydrationDataScript(
263
+ document,
264
+ normalizeHydrationDataForClient(result.hydrationData, projectRoot),
265
+ );
266
+ },
267
+ });
268
+ const responseCacheRuntime = await resolveResponseCacheRuntime(projectRoot, "production", evolitConfig);
269
+ const prerenderedRoutes = [];
270
+ const routeCache = [];
271
+ const prerenderedRouteArtifacts = [];
272
+
273
+ for (const route of routes) {
274
+ const request = new Request(`http://evolit.local${route.pathname}`);
275
+ const routePolicyResult = await routeResolver.resolveRoutePolicy(request);
276
+ if (routePolicyResult.type !== "route") {
277
+ continue;
278
+ }
279
+
280
+ routeCache.push({
281
+ pathname: route.pathname,
282
+ cache: serializeRouteCachePolicy(routePolicyResult.cachePolicy),
283
+ });
284
+
285
+ const isDynamicRoute = routeHasDynamicSegments(route.pathname);
286
+ const staticParamsResult = isDynamicRoute && routePolicyResult.cachePolicy.mode !== "dynamic"
287
+ ? await resolveStaticParamsForRoute(route, projectRoot, "production", {
288
+ staticAssetPublicUrls,
289
+ })
290
+ : { hasGenerateStaticParams: false, paramsList: [] };
291
+ const prerenderTargets = [];
292
+
293
+ if (routePolicyResult.cachePolicy.mode === "static" && !isDynamicRoute) {
294
+ prerenderTargets.push(route.pathname);
295
+ }
296
+
297
+ if (staticParamsResult.hasGenerateStaticParams) {
298
+ for (const params of staticParamsResult.paramsList) {
299
+ prerenderTargets.push(resolveRoutePathname(route.pathname, params));
300
+ }
301
+ }
302
+
303
+ const seenPrerenderTargets = new Set();
304
+
305
+ for (const targetPathname of prerenderTargets) {
306
+ if (seenPrerenderTargets.has(targetPathname)) {
307
+ continue;
308
+ }
309
+ seenPrerenderTargets.add(targetPathname);
310
+
311
+ const targetRequest = new Request(`http://evolit.local${targetPathname}`);
312
+ const routeResult = await routeResolver.resolveRequest(targetRequest);
313
+ if (routeResult.type !== "route" || routeResult.cachePolicy.mode === "dynamic") {
314
+ continue;
315
+ }
316
+
317
+ const response = await renderRouteTreeWithAdapter(routeResult, ssrAdapter);
318
+ if (response.status !== 200) {
319
+ continue;
320
+ }
321
+
322
+ const cacheKey = await responseCacheRuntime.createKey({
323
+ request: targetRequest,
324
+ routeResult,
325
+ });
326
+ await responseCacheRuntime.store.put(
327
+ cacheKey,
328
+ createCachedRouteResponse(response, routeResult.cachePolicy),
329
+ );
330
+ prerenderedRouteArtifacts.push({
331
+ routePathname: route.pathname,
332
+ pathname: targetPathname,
333
+ cacheKey,
334
+ outputPath: toBuildRelativePath(
335
+ projectRoot,
336
+ path.join(
337
+ buildRoot,
338
+ "route-cache",
339
+ getRouteCacheArtifactFileName(cacheKey),
340
+ ),
341
+ ),
342
+ contentType: "application/json; charset=utf-8",
343
+ });
344
+ prerenderedRoutes.push(targetPathname);
345
+ }
346
+ }
347
+
348
+ const sortedRouteCache = routeCache.sort((left, right) => left.pathname.localeCompare(right.pathname));
349
+ const deployRoutes = {
350
+ version: 2,
351
+ routes: sortedRouteCache.map((entry) => {
352
+ const prerenderedArtifact = prerenderedRouteArtifacts.find(
353
+ (artifact) => artifact.pathname === entry.pathname,
354
+ );
355
+ const prerenderedPaths = prerenderedRouteArtifacts
356
+ .filter((artifact) => artifact.routePathname === entry.pathname)
357
+ .map((artifact) => artifact.pathname)
358
+ .sort();
359
+
360
+ return {
361
+ pathname: entry.pathname,
362
+ cache: entry.cache,
363
+ cacheKey: entry.pathname,
364
+ prerendered: prerenderedPaths.length > 0,
365
+ prerenderedPaths,
366
+ responsePath: prerenderedArtifact?.outputPath ?? null,
367
+ };
368
+ }),
369
+ handlers: deployHandlers.sort((left, right) => left.pathname.localeCompare(right.pathname)),
370
+ };
371
+
372
+ const deployAssets = {
373
+ version: 1,
374
+ assets: [
375
+ ...clientAssets.assets.map((asset) => ({
376
+ kind: asset.type,
377
+ publicUrl: asset.publicUrl,
378
+ outputPath: toBuildRelativePath(projectRoot, asset.outputPath),
379
+ contentType: getContentTypeForBuildArtifact(asset.outputPath),
380
+ })),
381
+ ...prerenderedRouteArtifacts.map((artifact) => ({
382
+ kind: "html",
383
+ pathname: artifact.pathname,
384
+ cacheKey: artifact.cacheKey,
385
+ outputPath: artifact.outputPath,
386
+ contentType: artifact.contentType,
387
+ cache: sortedRouteCache.find((entry) => entry.pathname === artifact.routePathname)?.cache
388
+ ?? "static",
389
+ })),
390
+ ],
391
+ };
392
+ const manifestPath = path.join(buildRoot, MANIFEST_FILENAME);
393
+ const runtimeEntryPath = await writeDeploymentRuntimeEntry(buildRoot);
394
+ const deployServer = {
395
+ version: 1,
396
+ runtimeEntry: toBuildRelativePath(projectRoot, runtimeEntryPath),
397
+ exports: {
398
+ factory: "createBuiltDeploymentRuntime",
399
+ handler: "handleRequest",
400
+ },
401
+ manifestPath: toBuildRelativePath(projectRoot, manifestPath),
402
+ serverOutputRoot: toBuildRelativePath(projectRoot, path.join(buildRoot, "server")),
403
+ };
404
+
405
+ await writeJson(manifestPath, {
406
+ routes,
407
+ routeCache: sortedRouteCache,
408
+ prerenderedRoutes: prerenderedRoutes.sort(),
409
+ clientAssets,
410
+ builtAt: new Date().toISOString(),
411
+ });
412
+ await writeJson(path.join(buildRoot, DEPLOY_ROUTES_MANIFEST_FILENAME), deployRoutes);
413
+ await writeJson(path.join(buildRoot, DEPLOY_ASSETS_MANIFEST_FILENAME), deployAssets);
414
+ await writeJson(path.join(buildRoot, DEPLOY_SERVER_MANIFEST_FILENAME), deployServer);
415
+
416
+ return manifestPath;
417
+ }
package/src/cli.js ADDED
@@ -0,0 +1,83 @@
1
+ #!/usr/bin/env node
2
+
3
+ import path from "node:path";
4
+ import process from "node:process";
5
+ import { buildProject } from "./build.js";
6
+ import { scaffoldSite } from "./scaffold.js";
7
+ import { createDevServer, createStartServer } from "./server.js";
8
+
9
+ function parseArguments(argv) {
10
+ const [command, ...rest] = argv;
11
+ const options = {};
12
+ const positionals = [];
13
+
14
+ for (let index = 0; index < rest.length; index += 1) {
15
+ const value = rest[index];
16
+ if (value === "--port" || value === "-p") {
17
+ options.port = Number.parseInt(rest[index + 1] ?? "3000", 10);
18
+ index += 1;
19
+ continue;
20
+ }
21
+
22
+ positionals.push(value);
23
+ }
24
+
25
+ return { command, options, positionals };
26
+ }
27
+
28
+ function printUsage() {
29
+ console.log("Usage: evolit <directory>");
30
+ console.log(" evolit init <directory>");
31
+ console.log(" evolit <dev|build|start> [--port 3000]");
32
+ }
33
+
34
+ async function createSite(projectRoot, targetDirectory) {
35
+ if (!targetDirectory) {
36
+ throw new Error("Usage: evolit init <directory>");
37
+ }
38
+
39
+ const createdDirectory = await scaffoldSite(path.resolve(projectRoot, targetDirectory));
40
+ console.log(`Created evolit site: ${createdDirectory}`);
41
+ }
42
+
43
+ async function run() {
44
+ const { command, options, positionals } = parseArguments(process.argv.slice(2));
45
+ const projectRoot = process.cwd();
46
+
47
+ if (!command || command === "--help" || command === "-h") {
48
+ printUsage();
49
+ return;
50
+ }
51
+
52
+ if (command === "init") {
53
+ await createSite(projectRoot, positionals[0]);
54
+ return;
55
+ }
56
+
57
+ if (command === "build") {
58
+ const manifestPath = await buildProject(projectRoot);
59
+ console.log(`Built evolit app: ${path.relative(projectRoot, manifestPath)}`);
60
+ return;
61
+ }
62
+
63
+ if (command === "start") {
64
+ const server = await createStartServer(projectRoot, options);
65
+ await server.listen();
66
+ console.log(`evolit start listening on http://localhost:${server.port}`);
67
+ return;
68
+ }
69
+
70
+ if (command === "dev") {
71
+ const server = await createDevServer(projectRoot, options);
72
+ await server.listen();
73
+ console.log(`evolit dev listening on http://localhost:${server.port}`);
74
+ return;
75
+ }
76
+
77
+ await createSite(projectRoot, command);
78
+ }
79
+
80
+ run().catch((error) => {
81
+ console.error(error instanceof Error ? error.stack ?? error.message : String(error));
82
+ process.exitCode = 1;
83
+ });