evolit 0.2.1 → 0.4.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/README.md +108 -0
- package/package.json +8 -2
- package/src/build.js +88 -11
- package/src/client-assets.js +1 -0
- package/src/compiler.js +301 -34
- package/src/deployment-runtime.js +77 -8
- package/src/index.js +3 -0
- package/src/litsx-pipeline.d.ts +137 -0
- package/src/litsx-pipeline.js +705 -0
- package/src/render.js +1 -0
- package/src/server-setup.js +65 -0
- package/src/server.js +41 -1
- package/src/urql-ssr.js +67 -2
package/src/render.js
CHANGED
|
@@ -147,6 +147,7 @@ function createRouteModuleOptions(projectRoot, mode, options = {}) {
|
|
|
147
147
|
staticAssetPublicUrls: resolvedStaticAssetPublicUrls,
|
|
148
148
|
onDevelopmentEvent: options.onDevelopmentEvent,
|
|
149
149
|
managedSourceRoots: options.managedSourceRoots,
|
|
150
|
+
litsxPipeline: options.litsxPipeline,
|
|
150
151
|
};
|
|
151
152
|
}
|
|
152
153
|
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { importCompiledModule, resolveProjectModuleSpecifier } from "./compiler.js";
|
|
3
|
+
|
|
4
|
+
function resolveSetupExport(moduleRecord, setupPath) {
|
|
5
|
+
const setup = moduleRecord.setup ?? moduleRecord.default;
|
|
6
|
+
if (typeof setup !== "function") {
|
|
7
|
+
throw new TypeError(
|
|
8
|
+
`Expected server setup module ${JSON.stringify(setupPath)} to export a setup function or a default function.`,
|
|
9
|
+
);
|
|
10
|
+
}
|
|
11
|
+
return setup;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function normalizeCleanup(result) {
|
|
15
|
+
if (result == null) return null;
|
|
16
|
+
if (typeof result === "function") return result;
|
|
17
|
+
if (typeof result === "object" && typeof result.dispose === "function") {
|
|
18
|
+
return () => result.dispose();
|
|
19
|
+
}
|
|
20
|
+
throw new TypeError("Evolit server setup must return nothing, a cleanup function, or an object with dispose().");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Runs the application server bootstrap once for a build or runtime instance. */
|
|
24
|
+
export async function runServerSetup({ projectRoot, mode, evolitConfig = {}, litsxPipeline = null }) {
|
|
25
|
+
const serverConfig = evolitConfig.server;
|
|
26
|
+
if (serverConfig == null) return null;
|
|
27
|
+
if (!serverConfig || typeof serverConfig !== "object" || Array.isArray(serverConfig)) {
|
|
28
|
+
throw new TypeError("Expected server in evolit.config.js to be an object.");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const configuredSetup = serverConfig.setup;
|
|
32
|
+
if (configuredSetup == null) return null;
|
|
33
|
+
|
|
34
|
+
let setup = configuredSetup;
|
|
35
|
+
if (typeof configuredSetup === "string") {
|
|
36
|
+
if (configuredSetup.length === 0) {
|
|
37
|
+
throw new TypeError("Expected server.setup to be a non-empty module specifier.");
|
|
38
|
+
}
|
|
39
|
+
const setupPath = await resolveProjectModuleSpecifier(
|
|
40
|
+
projectRoot,
|
|
41
|
+
path.join(projectRoot, "evolit.config.js"),
|
|
42
|
+
configuredSetup,
|
|
43
|
+
);
|
|
44
|
+
if (!setupPath) {
|
|
45
|
+
throw new Error(`Unable to resolve configured server setup ${JSON.stringify(configuredSetup)}.`);
|
|
46
|
+
}
|
|
47
|
+
const moduleRecord = await importCompiledModule(setupPath, {
|
|
48
|
+
projectRoot,
|
|
49
|
+
mode,
|
|
50
|
+
sourceMaps: mode === "development",
|
|
51
|
+
target: "server",
|
|
52
|
+
litsxPipeline,
|
|
53
|
+
});
|
|
54
|
+
setup = resolveSetupExport(moduleRecord, configuredSetup);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (typeof setup !== "function") {
|
|
58
|
+
throw new TypeError("Expected server.setup in evolit.config.js to be a function or module specifier.");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return normalizeCleanup(await setup({
|
|
62
|
+
projectRoot,
|
|
63
|
+
mode,
|
|
64
|
+
}));
|
|
65
|
+
}
|
package/src/server.js
CHANGED
|
@@ -214,6 +214,7 @@ async function createServer(projectRoot, mode, explicitPort, options = {}) {
|
|
|
214
214
|
responseCacheRuntime,
|
|
215
215
|
onDevelopmentEvent: reportDevelopmentEvent,
|
|
216
216
|
managedSourceRoots,
|
|
217
|
+
evolitConfig,
|
|
217
218
|
});
|
|
218
219
|
const port = getPort(explicitPort);
|
|
219
220
|
const liveReloadServer = mode === "development" ? new WebSocketServer({ noServer: true }) : null;
|
|
@@ -267,6 +268,7 @@ async function createServer(projectRoot, mode, explicitPort, options = {}) {
|
|
|
267
268
|
const response = await deploymentRuntime.handle(
|
|
268
269
|
createDevelopmentNavigationRequest(subscription, origin),
|
|
269
270
|
);
|
|
271
|
+
syncLitsxDependencyWatchers();
|
|
270
272
|
const navigationResponse = createNavigationResponseFromDocument(response);
|
|
271
273
|
if (!getResponseHeader(navigationResponse.headers, "content-type")
|
|
272
274
|
?.includes("application/vnd.evolit.navigation+json")) return null;
|
|
@@ -341,6 +343,39 @@ async function createServer(projectRoot, mode, explicitPort, options = {}) {
|
|
|
341
343
|
{ shouldIgnorePath: createSourceWatchIgnorePath(sourceRoot) },
|
|
342
344
|
)))
|
|
343
345
|
: [];
|
|
346
|
+
const litsxDependencyWatchers = new Map();
|
|
347
|
+
|
|
348
|
+
function syncLitsxDependencyWatchers() {
|
|
349
|
+
if (mode !== "development") return;
|
|
350
|
+
const activeDependencies = new Set(
|
|
351
|
+
(deploymentRuntime.litsxDependencies ?? []).map((dependency) => path.resolve(dependency)),
|
|
352
|
+
);
|
|
353
|
+
for (const [dependency, watcher] of litsxDependencyWatchers) {
|
|
354
|
+
if (activeDependencies.has(dependency)) continue;
|
|
355
|
+
watcher.close();
|
|
356
|
+
litsxDependencyWatchers.delete(dependency);
|
|
357
|
+
}
|
|
358
|
+
for (const resolvedDependency of activeDependencies) {
|
|
359
|
+
const relativePath = path.relative(projectRoot, resolvedDependency);
|
|
360
|
+
if (relativePath.split(path.sep).includes(INTERNAL_DIRECTORY)) continue;
|
|
361
|
+
const isAlreadyManaged = managedSourceRoots.some((sourceRoot) => {
|
|
362
|
+
const managedRelativePath = path.relative(sourceRoot, resolvedDependency);
|
|
363
|
+
return managedRelativePath === ""
|
|
364
|
+
|| (managedRelativePath !== ".."
|
|
365
|
+
&& !managedRelativePath.startsWith(`..${path.sep}`)
|
|
366
|
+
&& !path.isAbsolute(managedRelativePath));
|
|
367
|
+
});
|
|
368
|
+
if (isAlreadyManaged) continue;
|
|
369
|
+
if (litsxDependencyWatchers.has(resolvedDependency)) continue;
|
|
370
|
+
try {
|
|
371
|
+
const watcher = watch(resolvedDependency, () => scheduleDevelopmentInvalidation(resolvedDependency));
|
|
372
|
+
watcher.on("error", () => {});
|
|
373
|
+
litsxDependencyWatchers.set(resolvedDependency, watcher);
|
|
374
|
+
} catch (error) {
|
|
375
|
+
if (error?.code !== "ENOENT") throw error;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
344
379
|
|
|
345
380
|
const server = http.createServer(async (req, res) => {
|
|
346
381
|
try {
|
|
@@ -356,6 +391,7 @@ async function createServer(projectRoot, mode, explicitPort, options = {}) {
|
|
|
356
391
|
});
|
|
357
392
|
const requestStartedAt = performance.now();
|
|
358
393
|
const response = await deploymentRuntime.handle(request);
|
|
394
|
+
syncLitsxDependencyWatchers();
|
|
359
395
|
const navigationResponse = String(req.headers.accept ?? "").includes("application/vnd.evolit.navigation+json")
|
|
360
396
|
? createNavigationResponseFromDocument(response)
|
|
361
397
|
: response;
|
|
@@ -384,7 +420,9 @@ async function createServer(projectRoot, mode, explicitPort, options = {}) {
|
|
|
384
420
|
error,
|
|
385
421
|
});
|
|
386
422
|
res.writeHead(500, { "content-type": "text/plain; charset=utf-8" });
|
|
387
|
-
res.end(
|
|
423
|
+
res.end(mode === "development"
|
|
424
|
+
? error instanceof Error ? error.stack ?? error.message : String(error)
|
|
425
|
+
: "Internal Server Error");
|
|
388
426
|
}
|
|
389
427
|
});
|
|
390
428
|
|
|
@@ -416,6 +454,8 @@ async function createServer(projectRoot, mode, explicitPort, options = {}) {
|
|
|
416
454
|
clearTimeout(invalidationTimer);
|
|
417
455
|
}
|
|
418
456
|
for (const watcher of watchers) watcher.close();
|
|
457
|
+
for (const watcher of litsxDependencyWatchers.values()) watcher.close();
|
|
458
|
+
litsxDependencyWatchers.clear();
|
|
419
459
|
for (const socket of liveReloadSockets.keys()) {
|
|
420
460
|
socket.close();
|
|
421
461
|
}
|
package/src/urql-ssr.js
CHANGED
|
@@ -26,7 +26,18 @@ export async function getSsrUrqlAdapter() {
|
|
|
26
26
|
* Evolit owns only lifecycle here; client creation and URQL configuration stay
|
|
27
27
|
* entirely in the application and @litsx/urql.
|
|
28
28
|
*/
|
|
29
|
-
export async function runWithOptionalSsrUrqlScope(
|
|
29
|
+
export async function runWithOptionalSsrUrqlScope(
|
|
30
|
+
contextOrCallback,
|
|
31
|
+
callbackOrOptions = {},
|
|
32
|
+
maybeOptions = {},
|
|
33
|
+
) {
|
|
34
|
+
const hasRequestContext = typeof contextOrCallback !== "function";
|
|
35
|
+
const requestContext = hasRequestContext ? contextOrCallback : null;
|
|
36
|
+
const callback = hasRequestContext ? callbackOrOptions : contextOrCallback;
|
|
37
|
+
const options = hasRequestContext ? maybeOptions : callbackOrOptions;
|
|
38
|
+
if (typeof callback !== "function") {
|
|
39
|
+
throw new TypeError("runWithOptionalSsrUrqlScope() expects a render callback.");
|
|
40
|
+
}
|
|
30
41
|
const adapter = Object.hasOwn(options, "adapter")
|
|
31
42
|
? options.adapter
|
|
32
43
|
: await getSsrUrqlAdapter();
|
|
@@ -34,7 +45,61 @@ export async function runWithOptionalSsrUrqlScope(callback, options = {}) {
|
|
|
34
45
|
return callback(null);
|
|
35
46
|
}
|
|
36
47
|
|
|
37
|
-
return
|
|
48
|
+
return hasRequestContext
|
|
49
|
+
? adapter.runWithUrqlScope(requestContext, () => callback(adapter))
|
|
50
|
+
: adapter.runWithUrqlScope(() => callback(adapter));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Creates the HTTP context passed to a request-scoped URQL resource factory. */
|
|
54
|
+
export function createSsrUrqlRequestContext(request) {
|
|
55
|
+
let didUseRequestData = false;
|
|
56
|
+
const trackedRequest = new Proxy(request, {
|
|
57
|
+
get(target, property) {
|
|
58
|
+
if (property !== "constructor" && property !== Symbol.toStringTag) {
|
|
59
|
+
didUseRequestData = true;
|
|
60
|
+
}
|
|
61
|
+
const value = Reflect.get(target, property, target);
|
|
62
|
+
return typeof value === "function" ? value.bind(target) : value;
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
const responseHeaders = new Headers();
|
|
66
|
+
return {
|
|
67
|
+
request: trackedRequest,
|
|
68
|
+
responseHeaders,
|
|
69
|
+
didUseDynamicRequestData() {
|
|
70
|
+
return didUseRequestData || !responseHeaders.entries().next().done;
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function responseHeadersToObject(headers) {
|
|
76
|
+
const values = Object.fromEntries(headers.entries());
|
|
77
|
+
if (typeof headers.getSetCookie === "function") {
|
|
78
|
+
const cookies = headers.getSetCookie();
|
|
79
|
+
if (cookies.length > 0) values["set-cookie"] = cookies;
|
|
80
|
+
}
|
|
81
|
+
return values;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Applies request-resource response state before Evolit caches the render. */
|
|
85
|
+
export function applySsrUrqlRequestContext(routeResult, response, context) {
|
|
86
|
+
if (!context) return response;
|
|
87
|
+
const urqlHeaders = responseHeadersToObject(context.responseHeaders);
|
|
88
|
+
if (context.didUseDynamicRequestData()) {
|
|
89
|
+
routeResult.cachePolicy = { mode: "dynamic" };
|
|
90
|
+
}
|
|
91
|
+
if (Object.keys(urqlHeaders).length === 0) return response;
|
|
92
|
+
routeResult.responseHeaders = {
|
|
93
|
+
...(routeResult.responseHeaders ?? {}),
|
|
94
|
+
...urqlHeaders,
|
|
95
|
+
};
|
|
96
|
+
return {
|
|
97
|
+
...response,
|
|
98
|
+
headers: {
|
|
99
|
+
...(response.headers ?? {}),
|
|
100
|
+
...urqlHeaders,
|
|
101
|
+
},
|
|
102
|
+
};
|
|
38
103
|
}
|
|
39
104
|
|
|
40
105
|
function escapeJsonForHtml(value) {
|