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/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(error instanceof Error ? error.stack ?? error.message : String(error));
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(callback, options = {}) {
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 adapter.runWithUrqlScope(() => callback(adapter));
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) {