evolit 0.4.3 → 0.4.5

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
@@ -444,6 +444,17 @@ configuration therefore stays isolated per render, extracted SSR data is read
444
444
  before the scope closes, and integration response headers are included before
445
445
  the response is cached.
446
446
 
447
+ The supported SSR combination is `evolit@0.4.5` or newer,
448
+ `@litsx/core@1.0.0-next.8` or newer, and `@litsx/urql@0.4.2` or newer. Evolit
449
+ resolves the server-conditioned URQL adapter from the application project and
450
+ uses that same module instance for setup, request resources, route rendering,
451
+ SSR extraction, and cleanup.
452
+
453
+ The concurrent UnoCSS + URQL regression gate is validated with
454
+ `evolit@0.4.5`, `@litsx/compiler@1.0.0-next.13`,
455
+ `@litsx/ssr@1.0.0-next.5`, `@litsx/unocss@1.0.0-next.8`,
456
+ `@litsx/urql@0.4.2`, and `unocss@66.8.1`.
457
+
447
458
  ## Extensions
448
459
 
449
460
  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.4.3",
3
+ "version": "0.4.5",
4
4
  "description": "A convention-driven application framework for LitSX and web components.",
5
5
  "type": "module",
6
6
  "packageManager": "yarn@4.10.3",
@@ -75,7 +75,7 @@
75
75
  "ws": "^8.18.3"
76
76
  },
77
77
  "peerDependencies": {
78
- "@litsx/urql": "^0.4.0"
78
+ "@litsx/urql": "^0.4.2"
79
79
  },
80
80
  "peerDependenciesMeta": {
81
81
  "@litsx/urql": {
@@ -85,6 +85,7 @@
85
85
  "devDependencies": {
86
86
  "@litsx/tailwind": "1.0.0-next.8",
87
87
  "@litsx/unocss": "1.0.0-next.8",
88
+ "@litsx/urql": "^0.4.2",
88
89
  "@playwright/test": "^1.62.0",
89
90
  "@webcomponents/scoped-custom-element-registry": "^0.0.10",
90
91
  "unocss": "66.8.1",
package/src/build.js CHANGED
@@ -606,7 +606,7 @@ export async function buildProject(projectRoot, options = {}) {
606
606
  urqlRequestContext,
607
607
  ),
608
608
  };
609
- });
609
+ }, { projectRoot });
610
610
  if (!response) {
611
611
  continue;
612
612
  }
@@ -581,7 +581,22 @@ function createSharedChunkGroup(id) {
581
581
  return "vendor-litsx";
582
582
  }
583
583
 
584
- return "vendor-misc";
584
+ return undefined;
585
+ }
586
+
587
+ function createBrowserEnvironmentPlugin(mode) {
588
+ const nodeEnvironment = mode === "production" ? "production" : "development";
589
+ const replacement = JSON.stringify(nodeEnvironment);
590
+ return {
591
+ name: "evolit-browser-environment",
592
+ transform(code) {
593
+ if (!code.includes("process.env.NODE_ENV")) return null;
594
+ return {
595
+ code: code.replaceAll("process.env.NODE_ENV", replacement),
596
+ map: null,
597
+ };
598
+ },
599
+ };
585
600
  }
586
601
 
587
602
  function isClientAssetStubModule(relativePath) {
@@ -746,6 +761,7 @@ export async function buildSharedVendorRuntime(
746
761
  .map((group) => buildSharedVendorRuntime(projectRoot, mode, {
747
762
  ...options,
748
763
  additionalEntrySpecifiers: group.specifiers,
764
+ externalSpecifiers: developmentGroups.baseSpecifiers,
749
765
  vendorGroup: group.vendorGroup,
750
766
  includeBase: false,
751
767
  })),
@@ -790,9 +806,16 @@ export async function buildSharedVendorRuntime(
790
806
  const specifierByEntryName = new Map(
791
807
  specifiers.map((specifier) => [createSharedEntryName(specifier), specifier]),
792
808
  );
809
+ const externalSpecifiers = new Set(
810
+ Array.isArray(options.externalSpecifiers) ? options.externalSpecifiers : [],
811
+ );
793
812
  const bundle = await rollup({
794
813
  input: inputEntries,
814
+ external(source) {
815
+ return externalSpecifiers.has(source);
816
+ },
795
817
  plugins: [
818
+ createBrowserEnvironmentPlugin(mode),
796
819
  {
797
820
  name: "evolit-browser-exports",
798
821
  resolveId(source) {
@@ -814,10 +837,6 @@ export async function buildSharedVendorRuntime(
814
837
  ],
815
838
  preserveEntrySignatures: "strict",
816
839
  onwarn(warning, warn) {
817
- if (warning.code === "CIRCULAR_DEPENDENCY") {
818
- return;
819
- }
820
-
821
840
  warn(warning);
822
841
  },
823
842
  });
@@ -836,6 +855,10 @@ export async function buildSharedVendorRuntime(
836
855
  manualChunks(id) {
837
856
  return createSharedChunkGroup(id);
838
857
  },
858
+ paths(id) {
859
+ if (!externalSpecifiers.has(id)) return id;
860
+ return `/_evolit/shared/base/${createSharedEntryName(id)}.mjs`;
861
+ },
839
862
  });
840
863
 
841
864
  const imports = {};
package/src/compiler.js CHANGED
@@ -28,6 +28,7 @@ const RESOLVABLE_IMPORT_EXTENSIONS = [
28
28
  const developmentGraphCache = new Map();
29
29
  const developmentGraphDependencies = new Map();
30
30
  const developmentModuleNamespaceCache = new Map();
31
+ const productionModuleNamespaceCache = new Map();
31
32
  const developmentGraphVersions = new Map();
32
33
  const developmentUnmanagedImportWarnings = new Set();
33
34
  const projectPathAliasesCache = new Map();
@@ -1785,34 +1786,34 @@ export async function emitClientStaticAssets(assetPaths, options = {}) {
1785
1786
 
1786
1787
  export async function importCompiledModule(entryPath, options = {}) {
1787
1788
  const mode = options.mode ?? "development";
1788
- const normalizedOptions = mode === "development"
1789
- ? { ...options, projectRoot: path.resolve(options.projectRoot ?? process.cwd()) }
1790
- : options;
1791
- const cacheKey = mode === "development"
1792
- ? getDevelopmentGraphCacheKey(path.resolve(entryPath), normalizedOptions)
1793
- : null;
1794
- const cachedModule = cacheKey ? developmentModuleNamespaceCache.get(cacheKey) : null;
1789
+ const normalizedOptions = {
1790
+ ...options,
1791
+ projectRoot: path.resolve(options.projectRoot ?? process.cwd()),
1792
+ };
1793
+ const cacheKey = getDevelopmentGraphCacheKey(path.resolve(entryPath), normalizedOptions);
1794
+ const moduleCache = mode === "development"
1795
+ ? developmentModuleNamespaceCache
1796
+ : productionModuleNamespaceCache;
1797
+ const cachedModule = moduleCache.get(cacheKey);
1795
1798
  if (cachedModule) {
1796
1799
  return cachedModule;
1797
1800
  }
1798
1801
 
1799
- const { entrypoint } = await compileModuleGraph(entryPath, normalizedOptions);
1800
- const moduleUrl = new URL(pathToFileURL(entrypoint).href);
1801
-
1802
- if (mode === "development") {
1803
- moduleUrl.searchParams.set("t", String(developmentGraphVersions.get(cacheKey) ?? 0));
1804
- }
1805
-
1806
- const pendingModule = import(moduleUrl.href);
1807
- if (cacheKey) {
1808
- developmentModuleNamespaceCache.set(cacheKey, pendingModule);
1809
- }
1802
+ const pendingModule = (async () => {
1803
+ const { entrypoint } = await compileModuleGraph(entryPath, normalizedOptions);
1804
+ const moduleUrl = new URL(pathToFileURL(entrypoint).href);
1805
+ if (mode === "development") {
1806
+ moduleUrl.searchParams.set("t", String(developmentGraphVersions.get(cacheKey) ?? 0));
1807
+ }
1808
+ return import(moduleUrl.href);
1809
+ })();
1810
+ moduleCache.set(cacheKey, pendingModule);
1810
1811
 
1811
1812
  try {
1812
1813
  return await pendingModule;
1813
1814
  } catch (error) {
1814
- if (developmentModuleNamespaceCache.get(cacheKey) === pendingModule) {
1815
- developmentModuleNamespaceCache.delete(cacheKey);
1815
+ if (moduleCache.get(cacheKey) === pendingModule) {
1816
+ moduleCache.delete(cacheKey);
1816
1817
  }
1817
1818
  throw error;
1818
1819
  }
@@ -930,7 +930,7 @@ export async function createRequestRenderer({
930
930
  routeResult,
931
931
  response,
932
932
  };
933
- });
933
+ }, { projectRoot });
934
934
  },
935
935
  get assetManifest() {
936
936
  return currentAssetManifest;
@@ -206,8 +206,13 @@ export async function createLitsxPipeline({ projectRoot, mode, config } = {}) {
206
206
  const instances = [];
207
207
  const names = new Set();
208
208
  let disposed = false;
209
+ let closing = false;
209
210
  let generation = 0;
210
211
  let finalizedGeneration = -1;
212
+ let finalizationFlight = null;
213
+ let invalidationFlight = null;
214
+ let queuedFinalization = null;
215
+ let disposalPromise = null;
211
216
  const dependencies = new Set();
212
217
  const processedModules = new Set();
213
218
  const moduleDependencies = new Map();
@@ -288,7 +293,7 @@ export async function createLitsxPipeline({ projectRoot, mode, config } = {}) {
288
293
  }
289
294
 
290
295
  function assertActive() {
291
- if (disposed) throw new Error("This LitSX pipeline has already been disposed.");
296
+ if (closing || disposed) throw new Error("This LitSX pipeline has already been disposed.");
292
297
  }
293
298
 
294
299
  function recordContribution(integrationName, moduleId, contribution) {
@@ -496,16 +501,43 @@ export async function createLitsxPipeline({ projectRoot, mode, config } = {}) {
496
501
  evolitDependencies: [...(moduleDependencies.get(moduleId) ?? [])].sort(),
497
502
  };
498
503
  },
499
- async finalize(context = {}) {
504
+ finalize(context = {}) {
500
505
  assertActive();
501
- if (finalizedGeneration === generation) return null;
506
+ if (invalidationFlight) {
507
+ if (queuedFinalization) return queuedFinalization.promise;
508
+ const awaitedInvalidation = invalidationFlight.promise;
509
+ const promise = awaitedInvalidation.then(() => {
510
+ if (queuedFinalization?.promise === promise) queuedFinalization = null;
511
+ return pipeline.finalize(context);
512
+ });
513
+ queuedFinalization = { promise };
514
+ promise.then(
515
+ () => {
516
+ if (queuedFinalization?.promise === promise) queuedFinalization = null;
517
+ },
518
+ () => {
519
+ if (queuedFinalization?.promise === promise) queuedFinalization = null;
520
+ },
521
+ );
522
+ return promise;
523
+ }
524
+ if (finalizedGeneration === generation) return Promise.resolve(null);
525
+ if (finalizationFlight?.generation === generation) return finalizationFlight.promise;
526
+ if (finalizationFlight) {
527
+ return finalizationFlight.promise.then(
528
+ () => pipeline.finalize(context),
529
+ () => pipeline.finalize(context),
530
+ );
531
+ }
532
+ const targetGeneration = generation;
533
+ const publishGeneration = async () => {
502
534
  if (moduleDependencies.delete("<graph>")) rebuildDependencies();
503
535
  removeOwnedContributions("<graph>");
504
536
  for (const { descriptor, instance } of instances) {
505
537
  if (typeof instance.finalize !== "function") continue;
506
538
  try {
507
539
  const contribution = normalizeContribution(
508
- await instance.finalize(Object.freeze({ ...context, generation, mode })),
540
+ await instance.finalize(Object.freeze({ ...context, generation: targetGeneration, mode })),
509
541
  descriptor.name,
510
542
  resolvedProjectRoot,
511
543
  );
@@ -539,6 +571,7 @@ export async function createLitsxPipeline({ projectRoot, mode, config } = {}) {
539
571
  hadPreviousRoot: false,
540
572
  installed: false,
541
573
  };
574
+ rootOperations.push(operation);
542
575
  if (outputs.length > 0) {
543
576
  operation.stagingRoot = `${root}.staging-${randomUUID()}`;
544
577
  await ensureDirectory(operation.stagingRoot);
@@ -558,7 +591,6 @@ export async function createLitsxPipeline({ projectRoot, mode, config } = {}) {
558
591
  if (output.kind === "style" && output.document) manifest.documentStyles.push(record);
559
592
  }
560
593
  }
561
- rootOperations.push(operation);
562
594
  }
563
595
 
564
596
  for (const { output, integrationName } of pendingOutputs.values()) {
@@ -634,10 +666,22 @@ export async function createLitsxPipeline({ projectRoot, mode, config } = {}) {
634
666
  if (operation.stagingRoot) publishedRoots.set(operation.integrationName, operation.root);
635
667
  else publishedRoots.delete(operation.integrationName);
636
668
  }
637
- finalizedGeneration = generation;
669
+ finalizedGeneration = targetGeneration;
638
670
  return manifest;
671
+ };
672
+ const promise = publishGeneration();
673
+ finalizationFlight = { generation: targetGeneration, promise };
674
+ promise.then(
675
+ () => {
676
+ if (finalizationFlight?.promise === promise) finalizationFlight = null;
677
+ },
678
+ () => {
679
+ if (finalizationFlight?.promise === promise) finalizationFlight = null;
680
+ },
681
+ );
682
+ return promise;
639
683
  },
640
- async invalidate(changedPaths = null) {
684
+ invalidate(changedPaths = null) {
641
685
  assertActive();
642
686
  const paths = Array.isArray(changedPaths)
643
687
  ? changedPaths.map((entry) => canonicalDependency(resolvedProjectRoot, entry))
@@ -645,6 +689,26 @@ export async function createLitsxPipeline({ projectRoot, mode, config } = {}) {
645
689
  const affected = paths == null || paths.some((entry) => (
646
690
  dependencies.has(entry) || processedModules.has(entry)
647
691
  ));
692
+ const previousInvalidation = invalidationFlight?.promise ?? null;
693
+ const activeFinalization = finalizationFlight?.promise ?? null;
694
+ const promise = (async () => {
695
+ if (previousInvalidation) {
696
+ try {
697
+ await previousInvalidation;
698
+ } catch {
699
+ // A later invalidation is allowed to recover from an earlier failure.
700
+ }
701
+ }
702
+ if (activeFinalization) {
703
+ try {
704
+ await activeFinalization;
705
+ } catch {
706
+ // Failed finalization leaves the generation retryable before invalidation.
707
+ }
708
+ }
709
+ if (closing || disposed) {
710
+ throw new Error("This LitSX pipeline has already been disposed.");
711
+ }
648
712
  for (const { descriptor, instance } of instances) {
649
713
  if (typeof instance.invalidate !== "function") continue;
650
714
  try {
@@ -661,6 +725,17 @@ export async function createLitsxPipeline({ projectRoot, mode, config } = {}) {
661
725
  }
662
726
  }
663
727
  return affected;
728
+ })();
729
+ invalidationFlight = { promise };
730
+ promise.then(
731
+ () => {
732
+ if (invalidationFlight?.promise === promise) invalidationFlight = null;
733
+ },
734
+ () => {
735
+ if (invalidationFlight?.promise === promise) invalidationFlight = null;
736
+ },
737
+ );
738
+ return promise;
664
739
  },
665
740
  async forget(moduleId) {
666
741
  assertActive();
@@ -683,8 +758,19 @@ export async function createLitsxPipeline({ projectRoot, mode, config } = {}) {
683
758
  await removePath(virtualModule);
684
759
  }
685
760
  },
686
- async dispose() {
687
- if (disposed) return;
761
+ dispose() {
762
+ if (disposalPromise) return disposalPromise;
763
+ if (disposed) return Promise.resolve();
764
+ closing = true;
765
+ disposalPromise = (async () => {
766
+ for (const flight of [queuedFinalization, invalidationFlight, finalizationFlight]) {
767
+ if (!flight) continue;
768
+ try {
769
+ await flight.promise;
770
+ } catch {
771
+ // The original caller owns a lifecycle error; disposal still completes.
772
+ }
773
+ }
688
774
  disposed = true;
689
775
  let firstError = null;
690
776
  for (const { descriptor, instance } of [...instances].reverse()) {
@@ -696,6 +782,8 @@ export async function createLitsxPipeline({ projectRoot, mode, config } = {}) {
696
782
  }
697
783
  await removePath(virtualRoot);
698
784
  if (firstError) throw firstError;
785
+ })();
786
+ return disposalPromise;
699
787
  },
700
788
  };
701
789
 
package/src/urql-ssr.js CHANGED
@@ -1,13 +1,18 @@
1
- let adapterPromise = null;
1
+ import { createRequire } from "node:module";
2
+ import path from "node:path";
3
+ import { pathToFileURL } from "node:url";
4
+
5
+ const adapterPromisesByModuleUrl = new Map();
2
6
 
3
7
  function isMissingUrqlAdapter(error) {
4
- return error?.code === "ERR_MODULE_NOT_FOUND"
8
+ return (error?.code === "ERR_MODULE_NOT_FOUND" || error?.code === "MODULE_NOT_FOUND")
5
9
  && String(error.message).includes("@litsx/urql");
6
10
  }
7
11
 
8
- async function loadSsrUrqlAdapter() {
12
+ function resolveSsrUrqlAdapterUrl(projectRoot) {
9
13
  try {
10
- return await import("@litsx/urql");
14
+ const requireFromProject = createRequire(path.join(path.resolve(projectRoot), "package.json"));
15
+ return pathToFileURL(requireFromProject.resolve("@litsx/urql")).href;
11
16
  } catch (error) {
12
17
  if (isMissingUrqlAdapter(error)) {
13
18
  return null;
@@ -16,9 +21,22 @@ async function loadSsrUrqlAdapter() {
16
21
  }
17
22
  }
18
23
 
19
- export async function getSsrUrqlAdapter() {
20
- adapterPromise ??= loadSsrUrqlAdapter();
21
- return adapterPromise;
24
+ export async function getSsrUrqlAdapter(projectRoot = process.cwd()) {
25
+ const moduleUrl = resolveSsrUrqlAdapterUrl(projectRoot);
26
+ if (!moduleUrl) return null;
27
+ let adapterPromise = adapterPromisesByModuleUrl.get(moduleUrl);
28
+ if (!adapterPromise) {
29
+ adapterPromise = import(moduleUrl);
30
+ adapterPromisesByModuleUrl.set(moduleUrl, adapterPromise);
31
+ }
32
+ try {
33
+ return await adapterPromise;
34
+ } catch (error) {
35
+ if (adapterPromisesByModuleUrl.get(moduleUrl) === adapterPromise) {
36
+ adapterPromisesByModuleUrl.delete(moduleUrl);
37
+ }
38
+ throw error;
39
+ }
22
40
  }
23
41
 
24
42
  /**
@@ -40,7 +58,7 @@ export async function runWithOptionalSsrUrqlScope(
40
58
  }
41
59
  const adapter = Object.hasOwn(options, "adapter")
42
60
  ? options.adapter
43
- : await getSsrUrqlAdapter();
61
+ : await getSsrUrqlAdapter(options.projectRoot);
44
62
  if (!adapter) {
45
63
  return callback(null);
46
64
  }