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 CHANGED
@@ -46,6 +46,26 @@ Generated applications declare `"@/*": ["./*"]` in `jsconfig.json`, so editors a
46
46
  use the same convention. An explicit `@/*` mapping in `jsconfig.json` or `tsconfig.json` takes
47
47
  priority when an application needs a different source root.
48
48
 
49
+ ### Interpolated dynamic imports
50
+
51
+ Relative dynamic imports may use a template literal when every possible module can be discovered
52
+ from the literal path at build time:
53
+
54
+ ```tsx
55
+ const module = await import(`./templates/${locale}/${name}.tsx`);
56
+ ```
57
+
58
+ Evolit treats each interpolation as one wildcard that matches a single path segment. The example
59
+ above discovers `./templates/*/*.tsx`, compiles every matching module, and emits a finite runtime
60
+ dispatcher whose cases preserve the original source specifiers. Interpolations never cross a `/`,
61
+ and there is no framework-imposed candidate limit.
62
+
63
+ The template must be relative, end in a supported authored module extension, and match at least one
64
+ module during compilation. A runtime value that was not among the discovered candidates rejects the
65
+ import. Development watches the matched directory tree, so adding a new candidate invalidates the
66
+ affected graph. Arbitrary computed specifiers that do not use this template-literal form remain
67
+ outside the statically discoverable graph.
68
+
49
69
  ### Package CSS and static assets
50
70
 
51
71
  Applications can import stylesheets exposed through package `exports` in the same way as local
@@ -301,6 +321,94 @@ server components and helpers that do not receive route props directly. It retur
301
321
  Reading `getRouteState().url` has the same dynamic-rendering semantics as `requestUrl()`; reading
302
322
  `params` and `searchParams` participates in the normal segment-cache key tracking.
303
323
 
324
+ ## LitSX compiler integrations
325
+
326
+ `evolit.config.js` can configure the native LitSX compiler and register build-tool-neutral LitSX
327
+ integrations. The same declaration is used by development, SSR, hydration, production builds and
328
+ the standalone runtime:
329
+
330
+ ```js
331
+ import { litsxUnoCss } from "@litsx/unocss";
332
+ import { defineEvolitConfig } from "evolit/litsx";
333
+
334
+ export default defineEvolitConfig({
335
+ litsx: {
336
+ compiler: {
337
+ sourceMaps: true,
338
+ },
339
+ integrations: [litsxUnoCss()],
340
+ },
341
+ });
342
+ ```
343
+
344
+ This is the complete Evolit + UnoCSS setup. The application does not coordinate compiler plugins,
345
+ virtual modules, generation, finalization or global CSS links, and it does not need Vite or
346
+ PostCSS. `@litsx/unocss` remains the owner of candidate extraction and CSS generation; Evolit only
347
+ executes its neutral LitSX lifecycle.
348
+
349
+ `compiler` accepts the public `TransformLitsxOptions` surface except the values Evolit must own for
350
+ correct server and browser graphs. Evolit always supplies the current `filename`, selects `ssr` per
351
+ target and forces native lowering with `reactCompat: false`; `compiler.sourceMaps` can override the
352
+ existing per-pipeline default without changing applications that omit `litsx`.
353
+ Authoring and output plugins contributed by the application and integrations are composed in
354
+ declaration order rather than replacing framework invariants.
355
+
356
+ An integration is a descriptor with a unique `name` and `create(context)` method. Each call to
357
+ `create` belongs to exactly one development server, build or standalone runtime, so mutable caches
358
+ must live on the returned instance, never on the descriptor. An instance can contribute:
359
+
360
+ - `compiler`: native compiler options, including authoring and output plugins.
361
+ - `resolveModule`: sources for integration-owned virtual modules in both server and browser graphs.
362
+ - `processModule`: post-processing, observable dependencies and preliminary outputs for a compiled
363
+ module.
364
+ - `finalize`: graph-wide assets, styles or modules after every reachable module is known.
365
+ - `invalidate` and `forget`: configuration/dependency refresh and removal of stale module state.
366
+ - `dispose`: idempotent cleanup when the owning server, build or runtime closes.
367
+
368
+ Declared outputs use stable IDs unique within the pipeline and one of `asset`, `module` or `style`. A module
369
+ may expose a safe `virtual:` specifier; a style with `document: true` is content-hashed, included in
370
+ the client asset manifest and linked once in the document. Evolit publishes a generation only after
371
+ every integration finalizes successfully, so a failed generation cannot expose a partial mix of
372
+ old and new assets.
373
+
374
+ In development, dependencies reported by compiler plugins and lifecycle hooks are watched together
375
+ with application modules. Invalidating a dependency advances the integration generation, evicts
376
+ affected compiled modules and calls `forget` for modules that leave the graph. Configuration reload
377
+ therefore removes obsolete results instead of accumulating them. Hook failures identify the
378
+ integration, lifecycle phase and relevant module in development; production errors keep the same
379
+ context while redacting project-local absolute paths.
380
+
381
+ For native Shadow DOM, `@litsx/unocss` serializes component preflight inside each Declarative Shadow
382
+ Root, preserves authored `Component.styles` before generated utilities, and emits the document
383
+ theme/custom-property layers once. Those document variables inherit through Shadow Roots and the
384
+ browser hydrates the server result without switching to `react-compat`.
385
+
386
+ ## Server Setup
387
+
388
+ Server integrations that need one-time application setup can be registered with
389
+ `server.setup`. A module specifier is compiled as part of the server graph, so the
390
+ setup may be authored in JavaScript or TypeScript:
391
+
392
+ ```js
393
+ // evolit.config.js
394
+ export default {
395
+ server: {
396
+ setup: "./src/server/setup.ts",
397
+ },
398
+ };
399
+ ```
400
+
401
+ The module exports `setup` (or a default function). Evolit invokes it once per
402
+ runtime and once before build-time prerendering with `{ projectRoot, mode }`.
403
+ It may return a cleanup function or `{ dispose() }`; runtime cleanup runs from
404
+ `runtime.close()`.
405
+
406
+ When `@litsx/urql` is installed, Evolit opens its SSR resource around the whole
407
+ route render and supplies `{ request, responseHeaders }`. Request-derived URQL
408
+ configuration therefore stays isolated per render, extracted SSR data is read
409
+ before the scope closes, and integration response headers are included before
410
+ the response is cached.
411
+
304
412
  ## Extensions
305
413
 
306
414
  Optional integrations are configured explicitly in `evolit.config.js`. Core only coordinates their
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "evolit",
3
- "version": "0.2.1",
3
+ "version": "0.4.0",
4
4
  "description": "A convention-driven application framework for LitSX and web components.",
5
5
  "type": "module",
6
6
  "packageManager": "yarn@4.10.3",
@@ -34,6 +34,10 @@
34
34
  "browser": "./src/extensions-client.js",
35
35
  "default": "./src/extensions.js"
36
36
  },
37
+ "./litsx": {
38
+ "types": "./src/litsx-pipeline.d.ts",
39
+ "default": "./src/litsx-pipeline.js"
40
+ },
37
41
  "./internal/development-hot": "./src/development-hot-client.js"
38
42
  },
39
43
  "scripts": {
@@ -71,7 +75,7 @@
71
75
  "ws": "^8.18.3"
72
76
  },
73
77
  "peerDependencies": {
74
- "@litsx/urql": "^0.3.0"
78
+ "@litsx/urql": "^0.4.0"
75
79
  },
76
80
  "peerDependenciesMeta": {
77
81
  "@litsx/urql": {
@@ -79,8 +83,10 @@
79
83
  }
80
84
  },
81
85
  "devDependencies": {
86
+ "@litsx/unocss": "1.0.0-next.8",
82
87
  "@playwright/test": "^1.62.0",
83
88
  "@webcomponents/scoped-custom-element-registry": "^0.0.10",
89
+ "unocss": "66.8.1",
84
90
  "vite": "^8.1.5"
85
91
  }
86
92
  }
package/src/build.js CHANGED
@@ -51,7 +51,14 @@ import {
51
51
  import { serializeRouteCachePolicy } from "./route-config.js";
52
52
  import { createSsrAdapter, renderRouteTreeWithAdapter } from "./ssr-adapter.js";
53
53
  import { ensureDirectory, writeJson } from "./fs-utils.js";
54
- import { appendSsrUrqlData, runWithOptionalSsrUrqlScope } from "./urql-ssr.js";
54
+ import {
55
+ appendSsrUrqlData,
56
+ applySsrUrqlRequestContext,
57
+ createSsrUrqlRequestContext,
58
+ runWithOptionalSsrUrqlScope,
59
+ } from "./urql-ssr.js";
60
+ import { runServerSetup } from "./server-setup.js";
61
+ import { createLitsxPipeline, mergeLitsxAssetsIntoManifest } from "./litsx-pipeline.js";
55
62
 
56
63
  const CONTENT_TYPE_BY_EXTENSION = new Map([
57
64
  [".css", "text/css; charset=utf-8"],
@@ -157,6 +164,20 @@ async function writeDeploymentRuntimeEntry(buildRoot) {
157
164
 
158
165
  export async function buildProject(projectRoot, options = {}) {
159
166
  const evolitConfig = await loadEvolitConfig(projectRoot);
167
+ const litsxPipeline = await createLitsxPipeline({
168
+ projectRoot,
169
+ mode: "production",
170
+ config: evolitConfig,
171
+ });
172
+ let serverSetupCleanup = null;
173
+ let buildError = null;
174
+ try {
175
+ serverSetupCleanup = await runServerSetup({
176
+ projectRoot,
177
+ mode: "production",
178
+ evolitConfig,
179
+ litsxPipeline,
180
+ });
160
181
  const extensions = resolveEvolitExtensions(evolitConfig);
161
182
  const extensionClientDescriptors = getExtensionClientDescriptors(extensions);
162
183
  const packageClientSpecifiers = new Set(
@@ -183,6 +204,7 @@ export async function buildProject(projectRoot, options = {}) {
183
204
  sourceMaps: false,
184
205
  ssr: true,
185
206
  target: "server",
207
+ litsxPipeline,
186
208
  });
187
209
  result.packageImports.forEach((specifier) => ssrPackageImports.add(specifier));
188
210
  return result;
@@ -191,7 +213,7 @@ export async function buildProject(projectRoot, options = {}) {
191
213
  function getEntryInventory(entryPath) {
192
214
  let inventory = inventoriesBySourceEntry.get(entryPath);
193
215
  if (!inventory) {
194
- inventory = collectClientGraphInventory([entryPath], { projectRoot });
216
+ inventory = collectClientGraphInventory([entryPath], { projectRoot, litsxPipeline });
195
217
  inventoriesBySourceEntry.set(entryPath, inventory);
196
218
  }
197
219
  return inventory;
@@ -205,6 +227,7 @@ export async function buildProject(projectRoot, options = {}) {
205
227
  mode: "production",
206
228
  sourceMaps: true,
207
229
  target: "client",
230
+ litsxPipeline,
208
231
  });
209
232
  clientModule = path.relative(clientBuild.outputRoot, clientBuild.entrypoint)
210
233
  .split(path.sep)
@@ -224,6 +247,7 @@ export async function buildProject(projectRoot, options = {}) {
224
247
  mode: "production",
225
248
  ssr: true,
226
249
  target: "server",
250
+ litsxPipeline,
227
251
  });
228
252
  const methods = Object.keys(handlerModule)
229
253
  .filter((name) => /^[A-Z]+$/.test(name) && typeof handlerModule[name] === "function")
@@ -334,18 +358,20 @@ export async function buildProject(projectRoot, options = {}) {
334
358
  }
335
359
 
336
360
  sharedVendorOptions.additionalEntrySpecifiers = [...packageClientSpecifiers].sort();
337
- const clientAssets = await emitBundledClientAssets(projectRoot, {
361
+ let clientAssets = await emitBundledClientAssets(projectRoot, {
338
362
  entryClientModules,
339
363
  additionalVendorSpecifiers: sharedVendorOptions.additionalEntrySpecifiers,
340
364
  serverAssetImportsByEntry,
341
365
  clientBoundariesByEntry,
342
366
  });
367
+ const integrationOutputs = await litsxPipeline?.finalize({
368
+ routes,
369
+ routeHandlers,
370
+ });
371
+ clientAssets = mergeLitsxAssetsIntoManifest(clientAssets, integrationOutputs);
343
372
  const staticAssetPublicUrls = createStaticAssetPublicUrlMap(clientAssets);
344
373
  await rewriteServerAssetPlaceholders(projectRoot, clientAssets);
345
374
 
346
- const routeResolver = await createRouteResolver(projectRoot, "production", {
347
- staticAssetPublicUrls,
348
- });
349
375
  const sharedRuntime = await buildSharedVendorRuntime(projectRoot, "production", sharedVendorOptions);
350
376
  clientAssets.sharedImports = { ...sharedRuntime.imports };
351
377
  const packageImports = sharedRuntime.imports;
@@ -353,6 +379,11 @@ export async function buildProject(projectRoot, options = {}) {
353
379
  assetManifest: clientAssets,
354
380
  packageImports,
355
381
  });
382
+ const routeResolver = await createRouteResolver(projectRoot, "production", {
383
+ staticAssetPublicUrls,
384
+ assetResolver,
385
+ litsxPipeline,
386
+ });
356
387
  const hydrationModuleUrl = await resolveSharedVendorModuleUrl(
357
388
  projectRoot,
358
389
  "production",
@@ -412,7 +443,21 @@ export async function buildProject(projectRoot, options = {}) {
412
443
  }
413
444
  continue;
414
445
  }
415
- if (assetResolver(moduleId) || clientAssets.byPublicPath?.[moduleId]) continue;
446
+ const directPublicUrl = assetResolver(moduleId)
447
+ ?? (clientAssets.byPublicPath?.[moduleId] ? moduleId : null);
448
+ if (directPublicUrl) {
449
+ if (Array.isArray(result.clientImports)) {
450
+ result.clientImports = result.clientImports.map((value) => (
451
+ value === moduleId ? directPublicUrl : value
452
+ ));
453
+ }
454
+ if (Array.isArray(result.hydrationData?.clientImports)) {
455
+ result.hydrationData.clientImports = result.hydrationData.clientImports.map((value) => (
456
+ value === moduleId ? directPublicUrl : value
457
+ ));
458
+ }
459
+ continue;
460
+ }
416
461
  const importerPath = routeResult.boundaryModule
417
462
  ?? routeResult.route?.page
418
463
  ?? path.join(projectRoot, "app", "page.jsx");
@@ -462,6 +507,7 @@ export async function buildProject(projectRoot, options = {}) {
462
507
  ])]
463
508
  : [];
464
509
  const styleUrls = [...new Set([
510
+ ...(clientAssets.documentStyles ?? []),
465
511
  ...collectTransitiveStyleUrls(clientImports, clientAssets),
466
512
  ...resolveServerStyleUrls(routeResult, projectRoot, clientAssets),
467
513
  ])];
@@ -541,23 +587,32 @@ export async function buildProject(projectRoot, options = {}) {
541
587
  seenPrerenderTargets.add(targetPathname);
542
588
 
543
589
  const targetRequest = new Request(`http://evolit.local${targetPathname}`);
544
- const { routeResult, response } = await runWithOptionalSsrUrqlScope(async (urqlAdapter) => {
590
+ const urqlRequestContext = createSsrUrqlRequestContext(targetRequest);
591
+ const { routeResult, response } = await runWithOptionalSsrUrqlScope(urqlRequestContext, async (urqlAdapter) => {
545
592
  const resolvedRouteResult = await routeResolver.resolveRequest(targetRequest);
546
593
  if (resolvedRouteResult.type !== "route" || resolvedRouteResult.cachePolicy.mode === "dynamic") {
547
594
  return { routeResult: resolvedRouteResult, response: null };
548
595
  }
549
596
 
550
597
  const renderedResponse = await renderRouteTreeWithAdapter(resolvedRouteResult, ssrAdapter);
598
+ const responseWithData = urqlAdapter
599
+ ? appendSsrUrqlData(renderedResponse, await urqlAdapter.getUrqlSsrData())
600
+ : renderedResponse;
551
601
  return {
552
602
  routeResult: resolvedRouteResult,
553
- response: urqlAdapter
554
- ? appendSsrUrqlData(renderedResponse, await urqlAdapter.getUrqlSsrData())
555
- : renderedResponse,
603
+ response: applySsrUrqlRequestContext(
604
+ resolvedRouteResult,
605
+ responseWithData,
606
+ urqlRequestContext,
607
+ ),
556
608
  };
557
609
  });
558
610
  if (!response) {
559
611
  continue;
560
612
  }
613
+ if (routeResult.cachePolicy.mode === "dynamic") {
614
+ continue;
615
+ }
561
616
  if (response.status !== 200) {
562
617
  continue;
563
618
  }
@@ -630,6 +685,11 @@ export async function buildProject(projectRoot, options = {}) {
630
685
  cache: sortedRouteCache.find((entry) => entry.pathname === artifact.routePathname)?.cache
631
686
  ?? "static",
632
687
  })),
688
+ ...(integrationOutputs?.assets ?? []).map((asset) => ({
689
+ kind: asset.kind,
690
+ outputPath: toBuildRelativePath(projectRoot, asset.outputPath),
691
+ integration: asset.integration,
692
+ })),
633
693
  ],
634
694
  };
635
695
  const manifestPath = path.join(buildRoot, MANIFEST_FILENAME);
@@ -657,4 +717,21 @@ export async function buildProject(projectRoot, options = {}) {
657
717
  await writeJson(path.join(buildRoot, DEPLOY_SERVER_MANIFEST_FILENAME), deployServer);
658
718
 
659
719
  return manifestPath;
720
+ } catch (error) {
721
+ buildError = error;
722
+ throw error;
723
+ } finally {
724
+ let cleanupError = null;
725
+ try {
726
+ await serverSetupCleanup?.();
727
+ } catch (error) {
728
+ cleanupError = error;
729
+ }
730
+ try {
731
+ await litsxPipeline?.dispose();
732
+ } catch (error) {
733
+ cleanupError ??= error;
734
+ }
735
+ if (!buildError && cleanupError) throw cleanupError;
736
+ }
660
737
  }
@@ -2483,6 +2483,7 @@ export function normalizeClientAssetManifest(manifest) {
2483
2483
  manifest.clientBoundariesByEntry && typeof manifest.clientBoundariesByEntry === "object"
2484
2484
  ? manifest.clientBoundariesByEntry
2485
2485
  : {},
2486
+ documentStyles: Array.isArray(manifest.documentStyles) ? manifest.documentStyles : [],
2486
2487
  assets: manifest.assets,
2487
2488
  };
2488
2489
  }