solid-tag-runtime 0.0.12 → 0.0.13

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/runtime.js CHANGED
@@ -6,8 +6,11 @@ import {
6
6
  } from "./errors.js";
7
7
  import { createDefaultModuleUrlBackend } from "./url-backend.js";
8
8
  import { createEventDispatcher } from "./events.js";
9
+ import { approximateByteSize, sha256, stableSerialize } from "./compile-cache.js";
9
10
 
10
11
  const HOST_REGISTRY_KEY = "__solidTagRuntimeHostModules__";
12
+ const COMPILED_ARTIFACT_ABI = "solid-tag-runtime:compiled-artifact:v1";
13
+ const COMPILED_ARTIFACT_SCHEMA = 1;
11
14
  let runtimeSequence = 0;
12
15
 
13
16
  export function createRuntime(options = {}) {
@@ -18,10 +21,11 @@ export function createRuntime(options = {}) {
18
21
  allowNativeImports = true,
19
22
  resolve: customResolve,
20
23
  moduleUrlBackend = createDefaultModuleUrlBackend(),
24
+ compileCache: compileCacheOptions,
21
25
  } = options;
22
26
 
23
- if (!compiler || typeof compiler.analyze !== "function" || typeof compiler.transform !== "function") {
24
- throw new TypeError("createRuntime() requires a compiler with analyze() and transform() methods.");
27
+ if (!compiler || (typeof compiler.compile !== "function" && (typeof compiler.analyze !== "function" || typeof compiler.transform !== "function"))) {
28
+ throw new TypeError("createRuntime() requires compile(), or analyze() + transform(), on its compiler.");
25
29
  }
26
30
 
27
31
  const runtimeId = `solid-tag-runtime-${++runtimeSequence}-${randomId()}`;
@@ -35,6 +39,10 @@ export function createRuntime(options = {}) {
35
39
  });
36
40
  let eventBufferDepth = 0;
37
41
  const eventBuffer = [];
42
+ const memoryCompileArtifacts = new Map();
43
+ const inFlightCompiles = new Map();
44
+ const compileCacheConfig = normalizeCompileCacheOptions(compileCacheOptions);
45
+ let warnedMissingCompilerFingerprint = false;
38
46
 
39
47
  const globalRegistry = getHostRegistry();
40
48
  const hostNamespaces = new Map();
@@ -61,6 +69,7 @@ export function createRuntime(options = {}) {
61
69
  getModuleInfo,
62
70
  dispose,
63
71
  resolve: resolvePublic,
72
+ compileCache: createCompileCacheManager(),
64
73
  };
65
74
 
66
75
  for (const [id, namespace] of Object.entries(modules)) defineModule(id, namespace);
@@ -141,6 +150,10 @@ export function createRuntime(options = {}) {
141
150
  dependencies: new Set(),
142
151
  dependents: existing?.dependents ?? new Set(),
143
152
  compiledCode: undefined,
153
+ compiledArtifact: undefined,
154
+ compiledArtifactKey: undefined,
155
+ compileCacheInfo: createInitialCompileCacheInfo(),
156
+ forceCompileRefresh: false,
144
157
  diagnostics: [],
145
158
  url: undefined,
146
159
  importPromise: undefined,
@@ -302,7 +315,7 @@ export function createRuntime(options = {}) {
302
315
  return record.importPromise;
303
316
  }
304
317
 
305
- async function compile(id) {
318
+ async function compile(id, compileOptions = {}) {
306
319
  assertActive();
307
320
  const resolved = resolveSpecifier(String(id), null);
308
321
  const record = records.get(resolved);
@@ -318,7 +331,14 @@ export function createRuntime(options = {}) {
318
331
  };
319
332
  }
320
333
 
321
- const url = await ensureSourceUrl(record, []);
334
+ const cacheMode = normalizeCompileCacheMode(compileOptions.cache);
335
+ if (cacheMode !== "use") {
336
+ // A fresh linked URL for the requested module makes any already-linked
337
+ // dependents stale as well. Invalidate the executable graph while
338
+ // preserving their reusable compiler artifacts.
339
+ invalidate(record.id, { dependents: true, reason: `compile-cache-${cacheMode}` });
340
+ }
341
+ const url = await ensureSourceUrl(record, [], { cacheMode });
322
342
  return {
323
343
  id: record.id,
324
344
  kind: record.kind,
@@ -365,28 +385,40 @@ export function createRuntime(options = {}) {
365
385
  const normalized = normalizeLookupId(id);
366
386
  const includeDependents = invalidateOptions.dependents ?? true;
367
387
  const reason = invalidateOptions.reason ?? "explicit";
388
+ const forceCompile = invalidateOptions.compile === true;
368
389
  const seen = new Set();
369
390
 
370
- invalidateOne(normalized);
391
+ invalidateOne(normalized, true);
371
392
  return [...seen];
372
393
 
373
- function invalidateOne(targetId) {
394
+ function invalidateOne(targetId, isRequestedModule = false) {
374
395
  if (seen.has(targetId)) return;
375
396
  seen.add(targetId);
376
397
  const record = records.get(targetId);
377
398
  if (!record) return;
378
399
 
379
400
  if (includeDependents) {
380
- for (const dependent of record.dependents) invalidateOne(dependent);
401
+ for (const dependent of record.dependents) invalidateOne(dependent, false);
381
402
  }
382
403
 
383
404
  if (record.kind === "source") {
384
405
  revokeRecordUrl(record);
385
406
  record.compiledCode = undefined;
386
- record.diagnostics = [];
387
407
  record.importPromise = undefined;
388
408
  record.namespace = undefined;
389
409
  record.state = "defined";
410
+
411
+ // Normal graph invalidation preserves the pre-link compiler artifact.
412
+ // compile:true only applies to the explicitly requested module, never
413
+ // to its transitive dependents.
414
+ if (forceCompile && isRequestedModule) {
415
+ if (record.compiledArtifactKey) memoryCompileArtifacts.delete(record.compiledArtifactKey);
416
+ record.compiledArtifact = undefined;
417
+ record.compiledArtifactKey = undefined;
418
+ record.forceCompileRefresh = true;
419
+ record.compileCacheInfo = createInitialCompileCacheInfo();
420
+ record.diagnostics = [];
421
+ }
390
422
  } else if (record.kind === "host") {
391
423
  revokeRecordUrl(record);
392
424
  record.state = "ready";
@@ -493,7 +525,539 @@ export function createRuntime(options = {}) {
493
525
  return resolveSpecifier(specifier, importer ?? null);
494
526
  }
495
527
 
496
- async function ensureSourceUrl(record, stack) {
528
+ function createInitialCompileCacheInfo() {
529
+ return {
530
+ enabled: Boolean(compileCacheConfig),
531
+ status: "none",
532
+ source: undefined,
533
+ key: undefined,
534
+ compilerFingerprint: undefined,
535
+ sourceHash: undefined,
536
+ compileContextHash: undefined,
537
+ };
538
+ }
539
+
540
+ function createCompileCacheManager() {
541
+ return {
542
+ enabled: Boolean(compileCacheConfig),
543
+ namespace: compileCacheConfig?.namespace,
544
+ version: compileCacheConfig?.version,
545
+ store: compileCacheConfig?.store?.kind ?? (compileCacheConfig ? "custom" : undefined),
546
+ async invalidate(id) {
547
+ assertActive();
548
+ if (!compileCacheConfig) return false;
549
+ const record = records.get(normalizeLookupId(id));
550
+ if (!record || record.kind !== "source") return false;
551
+
552
+ const identity = await createCompileIdentity(record);
553
+ if (!identity?.key) {
554
+ record.compiledArtifact = undefined;
555
+ record.compiledArtifactKey = undefined;
556
+ record.forceCompileRefresh = true;
557
+ return false;
558
+ }
559
+
560
+ memoryCompileArtifacts.delete(identity.key);
561
+ if (record.compiledArtifactKey === identity.key) {
562
+ record.compiledArtifact = undefined;
563
+ record.compiledArtifactKey = undefined;
564
+ }
565
+ record.forceCompileRefresh = true;
566
+ record.compileCacheInfo = createInitialCompileCacheInfo();
567
+
568
+ if (!compileCacheConfig || !identity.persistable) return false;
569
+ const physicalKey = compileCacheStorageKey(identity.key);
570
+ try {
571
+ const deleted = await compileCacheConfig.store.delete(physicalKey);
572
+ return deleted !== false;
573
+ } catch (error) {
574
+ emitCompileCacheError(record, "delete", error, identity);
575
+ throw error;
576
+ }
577
+ },
578
+ async clear(clearOptions = {}) {
579
+ assertActive();
580
+ if (!compileCacheConfig) return 0;
581
+ memoryCompileArtifacts.clear();
582
+ for (const record of records.values()) {
583
+ if (record.kind !== "source") continue;
584
+ record.compiledArtifact = undefined;
585
+ record.compiledArtifactKey = undefined;
586
+ record.forceCompileRefresh = true;
587
+ record.compileCacheInfo = createInitialCompileCacheInfo();
588
+ }
589
+
590
+ const prefix = clearOptions.allVersions === true
591
+ ? compileCacheNamespacePrefix()
592
+ : compileCacheVersionPrefix();
593
+ try {
594
+ if (typeof compileCacheConfig.store.clear === "function") {
595
+ return Number(await compileCacheConfig.store.clear({ prefix })) || 0;
596
+ }
597
+ if (typeof compileCacheConfig.store.entries === "function") {
598
+ const entries = await compileCacheConfig.store.entries({ prefix });
599
+ let count = 0;
600
+ for (const [key] of entries) {
601
+ await compileCacheConfig.store.delete(key);
602
+ count += 1;
603
+ }
604
+ return count;
605
+ }
606
+ throw new Error("Compile cache store does not support clear() or entries().");
607
+ } catch (error) {
608
+ emitCompileCacheError(undefined, "clear", error);
609
+ throw error;
610
+ }
611
+ },
612
+ };
613
+ }
614
+
615
+ async function ensureCompiledArtifact(record, requestedMode = "use") {
616
+ let mode = normalizeCompileCacheMode(requestedMode);
617
+ if (record.forceCompileRefresh && mode === "use") mode = "refresh";
618
+
619
+ if (mode === "use" && record.compiledArtifact) {
620
+ record.compileCacheInfo = {
621
+ ...record.compileCacheInfo,
622
+ status: "hit",
623
+ source: "record",
624
+ };
625
+ return record.compiledArtifact;
626
+ }
627
+
628
+ let identity;
629
+ let identityFailed = false;
630
+ if (compileCacheConfig) {
631
+ try {
632
+ identity = await createCompileIdentity(record);
633
+ } catch (error) {
634
+ identityFailed = true;
635
+ emitCompileCacheError(record, "identity", error);
636
+ }
637
+ }
638
+ const canUseSharedCache = Boolean(identity?.key && identity.persistable);
639
+
640
+ if (compileCacheConfig && identity && !identity.persistable) {
641
+ if (identity.reason === "missing-compiler-fingerprint" && !warnedMissingCompilerFingerprint) {
642
+ warnedMissingCompilerFingerprint = true;
643
+ globalThis.console?.warn?.(
644
+ "solid-tag-runtime: persistent compile caching is disabled for this compiler because it does not provide a stable fingerprint.",
645
+ );
646
+ }
647
+ emit({
648
+ type: "compile-cache-bypass",
649
+ id: record.id,
650
+ key: identity?.key,
651
+ reason: identity.reason ?? "missing-compiler-fingerprint",
652
+ source: "compiler",
653
+ store: compileCacheConfig.store.kind ?? "custom",
654
+ });
655
+ } else if (compileCacheConfig && identityFailed) {
656
+ emit({
657
+ type: "compile-cache-bypass",
658
+ id: record.id,
659
+ reason: "identity-error",
660
+ source: "compiler",
661
+ store: compileCacheConfig.store.kind ?? "custom",
662
+ });
663
+ }
664
+
665
+ if (mode === "bypass") {
666
+ emit({
667
+ type: "compile-cache-bypass",
668
+ id: record.id,
669
+ key: identity?.key,
670
+ reason: "requested",
671
+ source: "compiler",
672
+ store: compileCacheConfig?.store?.kind,
673
+ });
674
+ const artifact = await compileFreshArtifact(record);
675
+ record.compiledArtifact = artifact;
676
+ record.compiledArtifactKey = undefined;
677
+ record.forceCompileRefresh = false;
678
+ record.diagnostics = [...(artifact.diagnostics ?? [])];
679
+ record.compileCacheInfo = compileCacheInfoFrom(identity, "bypass", "compiler");
680
+ return artifact;
681
+ }
682
+
683
+ if (mode === "refresh") {
684
+ emit({
685
+ type: "compile-cache-refresh",
686
+ id: record.id,
687
+ key: identity?.key,
688
+ source: "compiler",
689
+ store: compileCacheConfig?.store?.kind,
690
+ });
691
+ } else if (canUseSharedCache) {
692
+ const memoryArtifact = memoryCompileArtifacts.get(identity.key);
693
+ if (memoryArtifact) {
694
+ record.compiledArtifact = memoryArtifact;
695
+ record.compiledArtifactKey = identity.key;
696
+ record.forceCompileRefresh = false;
697
+ record.diagnostics = [...(memoryArtifact.diagnostics ?? [])];
698
+ record.compileCacheInfo = compileCacheInfoFrom(identity, "hit", "memory");
699
+ emit({
700
+ type: "compile-cache-hit",
701
+ id: record.id,
702
+ key: identity.key,
703
+ source: "memory",
704
+ store: "memory",
705
+ byteSize: approximateByteSize(memoryArtifact),
706
+ });
707
+ return memoryArtifact;
708
+ }
709
+
710
+ const started = now();
711
+ try {
712
+ const cachedRecord = await compileCacheConfig.store.get(compileCacheStorageKey(identity.key));
713
+ const durationMs = now() - started;
714
+ if (cachedRecord !== undefined) {
715
+ const validation = validateCachedCompileRecord(cachedRecord, identity);
716
+ if (validation.valid && !isCacheRecordExpired(cachedRecord)) {
717
+ const artifact = normalizeCompiledArtifact(cachedRecord.artifact);
718
+ memoryCompileArtifacts.set(identity.key, artifact);
719
+ record.compiledArtifact = artifact;
720
+ record.compiledArtifactKey = identity.key;
721
+ record.forceCompileRefresh = false;
722
+ record.diagnostics = [...(artifact.diagnostics ?? [])];
723
+ record.compileCacheInfo = compileCacheInfoFrom(identity, "hit", compileCacheConfig.store.kind ?? "persistent");
724
+ cachedRecord.lastUsedAt = Date.now();
725
+ emit({
726
+ type: "compile-cache-hit",
727
+ id: record.id,
728
+ key: identity.key,
729
+ source: compileCacheConfig.store.kind ?? "persistent",
730
+ store: compileCacheConfig.store.kind ?? "custom",
731
+ durationMs,
732
+ byteSize: cachedRecord.byteSize,
733
+ });
734
+ return artifact;
735
+ }
736
+
737
+ try {
738
+ await compileCacheConfig.store.delete(compileCacheStorageKey(identity.key));
739
+ emit({
740
+ type: "compile-cache-evict",
741
+ id: record.id,
742
+ key: identity.key,
743
+ reason: validation.valid ? "maxAge" : "invalid",
744
+ store: compileCacheConfig.store.kind ?? "custom",
745
+ });
746
+ } catch (error) {
747
+ emitCompileCacheError(record, "delete", error, identity);
748
+ }
749
+ }
750
+
751
+ emit({
752
+ type: "compile-cache-miss",
753
+ id: record.id,
754
+ key: identity.key,
755
+ source: compileCacheConfig.store.kind ?? "persistent",
756
+ store: compileCacheConfig.store.kind ?? "custom",
757
+ durationMs,
758
+ });
759
+ } catch (error) {
760
+ emitCompileCacheError(record, "read", error, identity);
761
+ try { await compileCacheConfig.store.delete(compileCacheStorageKey(identity.key)); } catch {}
762
+ }
763
+ }
764
+
765
+ const inFlightKey = canUseSharedCache ? identity.key : undefined;
766
+ if (mode === "use" && inFlightKey && inFlightCompiles.has(inFlightKey)) {
767
+ const artifact = await inFlightCompiles.get(inFlightKey);
768
+ record.compiledArtifact = artifact;
769
+ record.compiledArtifactKey = inFlightKey;
770
+ record.forceCompileRefresh = false;
771
+ record.diagnostics = [...(artifact.diagnostics ?? [])];
772
+ record.compileCacheInfo = compileCacheInfoFrom(identity, "hit", "inflight");
773
+ return artifact;
774
+ }
775
+
776
+ const compilePromise = compileFreshArtifact(record);
777
+ if (inFlightKey) inFlightCompiles.set(inFlightKey, compilePromise);
778
+
779
+ let artifact;
780
+ try {
781
+ artifact = await compilePromise;
782
+ } finally {
783
+ if (inFlightKey && inFlightCompiles.get(inFlightKey) === compilePromise) inFlightCompiles.delete(inFlightKey);
784
+ }
785
+
786
+ record.compiledArtifact = artifact;
787
+ record.compiledArtifactKey = canUseSharedCache ? identity.key : undefined;
788
+ record.forceCompileRefresh = false;
789
+ record.diagnostics = [...(artifact.diagnostics ?? [])];
790
+ record.compileCacheInfo = compileCacheInfoFrom(identity, mode === "refresh" ? "refresh" : "miss", "compiler");
791
+
792
+ if (canUseSharedCache) {
793
+ memoryCompileArtifacts.set(identity.key, artifact);
794
+ const persistentRecord = {
795
+ schema: COMPILED_ARTIFACT_SCHEMA,
796
+ key: identity.key,
797
+ artifactAbi: COMPILED_ARTIFACT_ABI,
798
+ sourceHash: identity.sourceHash,
799
+ format: record.format,
800
+ compilerFingerprint: identity.compilerFingerprint,
801
+ compileContextHash: identity.compileContextHash,
802
+ artifact,
803
+ createdAt: Date.now(),
804
+ lastUsedAt: Date.now(),
805
+ };
806
+ persistentRecord.byteSize = approximateByteSize(persistentRecord);
807
+
808
+ await writePersistentCompileRecord(record, identity, persistentRecord);
809
+ }
810
+
811
+ return artifact;
812
+ }
813
+
814
+ async function compileFreshArtifact(record) {
815
+ try {
816
+ let artifact;
817
+ const context = { id: record.id, format: record.format };
818
+ if (typeof compiler.compile === "function") {
819
+ artifact = await compiler.compile(record.source, context);
820
+ } else {
821
+ let code = record.source;
822
+ let diagnostics = [];
823
+ let sourceMap;
824
+ if (record.format !== "js") {
825
+ const transformed = await compiler.transform(record.source, {
826
+ ...context,
827
+ htmlImportSource: "@solidjs/html",
828
+ });
829
+ code = transformed.code;
830
+ diagnostics = [...(transformed.diagnostics ?? [])];
831
+ sourceMap = transformed.sourceMap;
832
+ }
833
+ const analysis = await compiler.analyze(code, context);
834
+ artifact = {
835
+ schema: COMPILED_ARTIFACT_SCHEMA,
836
+ code,
837
+ imports: analysis.imports ?? [],
838
+ diagnostics,
839
+ ...(sourceMap == null ? {} : { sourceMap }),
840
+ };
841
+ }
842
+ return normalizeCompiledArtifact(artifact);
843
+ } catch (error) {
844
+ record.state = "error";
845
+ const wrapped = error instanceof ModuleCompileError ? error : new ModuleCompileError(record.id, error);
846
+ emitError(record, "compile", wrapped);
847
+ throw wrapped;
848
+ }
849
+ }
850
+
851
+ async function createCompileIdentity(record) {
852
+ const context = { id: record.id, format: record.format };
853
+ let compilerFingerprint;
854
+ try {
855
+ const fingerprint = typeof compiler.fingerprint === "function"
856
+ ? await compiler.fingerprint(context)
857
+ : compiler.fingerprint;
858
+ if (typeof fingerprint === "string" && fingerprint) compilerFingerprint = fingerprint;
859
+ } catch (error) {
860
+ emitCompileCacheError(record, "fingerprint", error);
861
+ return {
862
+ key: undefined,
863
+ persistable: false,
864
+ reason: "fingerprint-error",
865
+ compilerFingerprint: undefined,
866
+ format: record.format,
867
+ };
868
+ }
869
+
870
+ const sourceHash = await sha256(record.source);
871
+ const cacheContext = typeof compiler.cacheContext === "function"
872
+ ? await compiler.cacheContext(context)
873
+ : { format: record.format };
874
+ const compileContextHash = await sha256(stableSerialize(cacheContext ?? {}));
875
+
876
+ const persistable = Boolean(compilerFingerprint);
877
+ if (!persistable) {
878
+ return {
879
+ key: undefined,
880
+ persistable: false,
881
+ reason: "missing-compiler-fingerprint",
882
+ compilerFingerprint: undefined,
883
+ sourceHash,
884
+ compileContextHash,
885
+ format: record.format,
886
+ };
887
+ }
888
+
889
+ const key = await sha256(stableSerialize({
890
+ artifactAbi: COMPILED_ARTIFACT_ABI,
891
+ compilerFingerprint,
892
+ format: record.format,
893
+ compileContextHash,
894
+ sourceHash,
895
+ }));
896
+
897
+ return {
898
+ key,
899
+ persistable: true,
900
+ compilerFingerprint,
901
+ sourceHash,
902
+ compileContextHash,
903
+ format: record.format,
904
+ };
905
+ }
906
+
907
+ function compileCacheInfoFrom(identity, status, source) {
908
+ return {
909
+ enabled: Boolean(compileCacheConfig),
910
+ status,
911
+ source,
912
+ key: identity?.key,
913
+ compilerFingerprint: identity?.compilerFingerprint,
914
+ sourceHash: identity?.sourceHash,
915
+ compileContextHash: identity?.compileContextHash,
916
+ };
917
+ }
918
+
919
+ function validateCachedCompileRecord(cachedRecord, identity) {
920
+ if (!cachedRecord || typeof cachedRecord !== "object") return { valid: false, reason: "record" };
921
+ if (cachedRecord.schema !== COMPILED_ARTIFACT_SCHEMA) return { valid: false, reason: "schema" };
922
+ if (cachedRecord.artifactAbi !== COMPILED_ARTIFACT_ABI) return { valid: false, reason: "artifact-abi" };
923
+ if (cachedRecord.key !== identity.key) return { valid: false, reason: "key" };
924
+ if (cachedRecord.sourceHash !== identity.sourceHash) return { valid: false, reason: "source-hash" };
925
+ if (cachedRecord.format !== identity.format) return { valid: false, reason: "format" };
926
+ if (cachedRecord.compilerFingerprint !== identity.compilerFingerprint) return { valid: false, reason: "compiler-fingerprint" };
927
+ if (cachedRecord.compileContextHash !== identity.compileContextHash) return { valid: false, reason: "compile-context" };
928
+ try {
929
+ normalizeCompiledArtifact(cachedRecord.artifact);
930
+ } catch {
931
+ return { valid: false, reason: "artifact" };
932
+ }
933
+ return { valid: true };
934
+ }
935
+
936
+ function isCacheRecordExpired(cachedRecord) {
937
+ const maxAge = compileCacheConfig?.maxAge;
938
+ return Number.isFinite(maxAge) && maxAge >= 0 && Date.now() - Number(cachedRecord.createdAt ?? 0) > maxAge;
939
+ }
940
+
941
+ async function writePersistentCompileRecord(record, identity, persistentRecord) {
942
+ const storageKey = compileCacheStorageKey(identity.key);
943
+ try {
944
+ await compileCacheConfig.store.set(storageKey, persistentRecord);
945
+ } catch (error) {
946
+ if (!isQuotaError(error)) {
947
+ emitCompileCacheError(record, "write", error, identity);
948
+ return false;
949
+ }
950
+
951
+ await pruneCompileCache(record, { quotaRecovery: true });
952
+ try {
953
+ await compileCacheConfig.store.set(storageKey, persistentRecord);
954
+ } catch (retryError) {
955
+ emitCompileCacheError(record, "write", retryError, identity);
956
+ return false;
957
+ }
958
+ }
959
+
960
+ emit({
961
+ type: "compile-cache-write",
962
+ id: record.id,
963
+ key: identity.key,
964
+ source: "compiler",
965
+ store: compileCacheConfig.store.kind ?? "custom",
966
+ byteSize: persistentRecord.byteSize,
967
+ });
968
+ await pruneCompileCache(record);
969
+ return true;
970
+ }
971
+
972
+ async function pruneCompileCache(record, pruneOptions = {}) {
973
+ if (!compileCacheConfig || typeof compileCacheConfig.store.entries !== "function") return 0;
974
+ const { maxEntries, maxBytes, maxAge } = compileCacheConfig;
975
+ const quotaRecovery = pruneOptions.quotaRecovery === true;
976
+ if (!quotaRecovery && !Number.isFinite(maxEntries) && !Number.isFinite(maxBytes) && !Number.isFinite(maxAge)) return 0;
977
+
978
+ try {
979
+ const entries = await compileCacheConfig.store.entries({ prefix: compileCacheVersionPrefix() });
980
+ const nowValue = Date.now();
981
+ const live = [];
982
+ let evicted = 0;
983
+ for (const [key, value] of entries) {
984
+ if (Number.isFinite(maxAge) && maxAge >= 0 && nowValue - Number(value?.createdAt ?? 0) > maxAge) {
985
+ await compileCacheConfig.store.delete(key);
986
+ evicted += 1;
987
+ emit({ type: "compile-cache-evict", id: record?.id, key: value?.key, reason: "maxAge", store: compileCacheConfig.store.kind ?? "custom" });
988
+ continue;
989
+ }
990
+ live.push([key, value]);
991
+ }
992
+
993
+ live.sort((a, b) => Number(a[1]?.lastUsedAt ?? a[1]?.createdAt ?? 0) - Number(b[1]?.lastUsedAt ?? b[1]?.createdAt ?? 0));
994
+ let totalBytes = live.reduce((sum, [, value]) => sum + Number(value?.byteSize ?? 0), 0);
995
+
996
+ while (
997
+ (Number.isFinite(maxEntries) && live.length > maxEntries) ||
998
+ (Number.isFinite(maxBytes) && totalBytes > maxBytes)
999
+ ) {
1000
+ const [key, value] = live.shift();
1001
+ await compileCacheConfig.store.delete(key);
1002
+ totalBytes -= Number(value?.byteSize ?? 0);
1003
+ evicted += 1;
1004
+ emit({
1005
+ type: "compile-cache-evict",
1006
+ id: record?.id,
1007
+ key: value?.key,
1008
+ reason: Number.isFinite(maxEntries) && live.length + 1 > maxEntries ? "maxEntries" : "maxBytes",
1009
+ store: compileCacheConfig.store.kind ?? "custom",
1010
+ byteSize: value?.byteSize,
1011
+ });
1012
+ }
1013
+
1014
+ if (quotaRecovery && evicted === 0 && live.length > 0) {
1015
+ const removeCount = Math.max(1, Math.ceil(live.length * 0.1));
1016
+ for (let index = 0; index < removeCount && live.length > 0; index += 1) {
1017
+ const [key, value] = live.shift();
1018
+ await compileCacheConfig.store.delete(key);
1019
+ evicted += 1;
1020
+ emit({
1021
+ type: "compile-cache-evict",
1022
+ id: record?.id,
1023
+ key: value?.key,
1024
+ reason: "quota",
1025
+ store: compileCacheConfig.store.kind ?? "custom",
1026
+ byteSize: value?.byteSize,
1027
+ });
1028
+ }
1029
+ }
1030
+ return evicted;
1031
+ } catch (error) {
1032
+ emitCompileCacheError(record, "prune", error);
1033
+ return 0;
1034
+ }
1035
+ }
1036
+
1037
+ function emitCompileCacheError(record, operation, error, identity) {
1038
+ emit({
1039
+ type: "compile-cache-error",
1040
+ id: record?.id,
1041
+ key: identity?.key,
1042
+ operation,
1043
+ store: compileCacheConfig?.store?.kind ?? (compileCacheConfig ? "custom" : undefined),
1044
+ error,
1045
+ });
1046
+ }
1047
+
1048
+ function compileCacheNamespacePrefix() {
1049
+ return `solid-tag-runtime/compile/${encodeURIComponent(compileCacheConfig.namespace)}/`;
1050
+ }
1051
+
1052
+ function compileCacheVersionPrefix() {
1053
+ return `${compileCacheNamespacePrefix()}${encodeURIComponent(compileCacheConfig.version)}/`;
1054
+ }
1055
+
1056
+ function compileCacheStorageKey(key) {
1057
+ return `${compileCacheVersionPrefix()}${key}`;
1058
+ }
1059
+
1060
+ async function ensureSourceUrl(record, stack, linkOptions = {}) {
497
1061
  if (record.url) return record.url;
498
1062
 
499
1063
  if (stack.includes(record.id)) {
@@ -512,21 +1076,12 @@ export function createRuntime(options = {}) {
512
1076
  version: record.version,
513
1077
  });
514
1078
 
515
- let analysis;
516
- try {
517
- analysis = compiler.analyze(record.source, { id: record.id, format: record.format });
518
- } catch (error) {
519
- record.state = "error";
520
- const wrapped = new ModuleCompileError(record.id, error);
521
- emitError(record, "analyze", wrapped);
522
- throw wrapped;
523
- }
524
-
1079
+ const artifact = await ensureCompiledArtifact(record, linkOptions.cacheMode ?? "use");
525
1080
  const resolvedImports = [];
526
1081
  const linkedDependencies = [];
527
1082
  const nextDependencies = new Set();
528
1083
 
529
- for (const reference of analysis.imports ?? []) {
1084
+ for (const reference of artifact.imports ?? []) {
530
1085
  let resolved;
531
1086
  try {
532
1087
  resolved = resolveSpecifier(reference.specifier, record.id);
@@ -544,7 +1099,7 @@ export function createRuntime(options = {}) {
544
1099
  nextDependencies.add(dependencyRecord.id);
545
1100
 
546
1101
  if (dependencyRecord.kind === "source") {
547
- replacement = await ensureSourceUrl(dependencyRecord, nextStack);
1102
+ replacement = await ensureSourceUrl(dependencyRecord, nextStack, { cacheMode: "use" });
548
1103
  } else if (dependencyRecord.kind === "host") {
549
1104
  replacement = await ensureHostUrl(dependencyRecord);
550
1105
  } else {
@@ -570,35 +1125,7 @@ export function createRuntime(options = {}) {
570
1125
 
571
1126
  let linkedSource;
572
1127
  try {
573
- linkedSource = applyImportReplacements(record.source, resolvedImports);
574
-
575
- // JSX produced by solid-tag needs @solidjs/html. Resolve it through the
576
- // same module system so host applications can share their existing runtime.
577
- let htmlImportSource;
578
- const needsHtmlRuntime = record.format !== "js" && analysis.needsHtmlRuntime !== false;
579
- if (needsHtmlRuntime) {
580
- const htmlResolved = resolveSpecifier("@solidjs/html", record.id);
581
- const htmlRecord = records.get(htmlResolved);
582
-
583
- if (htmlRecord) {
584
- nextDependencies.add(htmlRecord.id);
585
- htmlImportSource = htmlRecord.kind === "source"
586
- ? await ensureSourceUrl(htmlRecord, nextStack)
587
- : htmlRecord.kind === "host"
588
- ? await ensureHostUrl(htmlRecord)
589
- : htmlRecord.externalUrl;
590
- } else if (allowNativeImports) {
591
- htmlImportSource = htmlResolved;
592
- } else {
593
- throw new ModuleResolutionError("@solidjs/html", record.id);
594
- }
595
-
596
- linkedDependencies.push({
597
- specifier: "@solidjs/html",
598
- resolvedId: htmlRecord?.id ?? htmlResolved,
599
- });
600
- }
601
-
1128
+ linkedSource = applyImportReplacements(artifact.code, resolvedImports);
602
1129
  syncDependencyEdges(record, nextDependencies);
603
1130
  emit({
604
1131
  type: "module-linked",
@@ -608,31 +1135,11 @@ export function createRuntime(options = {}) {
608
1135
  dependencies: linkedDependencies,
609
1136
  module: toModuleInfo(record),
610
1137
  });
611
-
612
- try {
613
- if (record.format === "js") {
614
- record.compiledCode = linkedSource;
615
- record.diagnostics = [];
616
- } else {
617
- const transformed = compiler.transform(linkedSource, {
618
- id: record.id,
619
- format: record.format,
620
- htmlImportSource,
621
- });
622
- record.compiledCode = transformed.code;
623
- record.diagnostics = [...(transformed.diagnostics ?? [])];
624
- }
625
- } catch (error) {
626
- record.state = "error";
627
- const wrapped = new ModuleCompileError(record.id, error);
628
- emitError(record, "compile", wrapped);
629
- throw wrapped;
630
- }
1138
+ record.compiledCode = linkedSource;
1139
+ record.diagnostics = [...(artifact.diagnostics ?? [])];
631
1140
  } catch (error) {
632
- if (record.state !== "error") {
633
- record.state = "error";
634
- emitError(record, error instanceof ModuleResolutionError ? "resolve" : "link", error);
635
- }
1141
+ record.state = "error";
1142
+ emitError(record, error instanceof ModuleResolutionError ? "resolve" : "link", error);
636
1143
  throw error;
637
1144
  }
638
1145
 
@@ -805,6 +1312,7 @@ export function createRuntime(options = {}) {
805
1312
  dependents: [...record.dependents],
806
1313
  url: record.kind === "url" ? record.externalUrl : record.url,
807
1314
  diagnostics: [...(record.diagnostics ?? [])],
1315
+ ...(record.kind === "source" ? { compileCache: { ...record.compileCacheInfo } } : {}),
808
1316
  };
809
1317
  }
810
1318
  }
@@ -911,3 +1419,77 @@ function randomId() {
911
1419
  if (globalThis.crypto?.randomUUID) return globalThis.crypto.randomUUID();
912
1420
  return Math.random().toString(36).slice(2);
913
1421
  }
1422
+
1423
+ function normalizeCompileCacheOptions(options) {
1424
+ if (options == null || options === false || options.enabled === false) return undefined;
1425
+ if (typeof options !== "object") {
1426
+ throw new TypeError("compileCache must be an options object with a store.");
1427
+ }
1428
+ const store = options.store;
1429
+ if (!store || typeof store.get !== "function" || typeof store.set !== "function" || typeof store.delete !== "function") {
1430
+ throw new TypeError("compileCache.store must provide async get(), set(), and delete() methods.");
1431
+ }
1432
+ return {
1433
+ store,
1434
+ namespace: String(options.namespace ?? "default"),
1435
+ version: String(options.version ?? "1"),
1436
+ maxEntries: normalizeLimit(options.maxEntries),
1437
+ maxBytes: normalizeLimit(options.maxBytes),
1438
+ maxAge: normalizeLimit(options.maxAge),
1439
+ };
1440
+ }
1441
+
1442
+ function normalizeCompileCacheMode(mode) {
1443
+ if (mode == null) return "use";
1444
+ if (mode === "use" || mode === "refresh" || mode === "bypass") return mode;
1445
+ throw new TypeError(`Unknown compile cache mode ${JSON.stringify(mode)}. Expected "use", "refresh", or "bypass".`);
1446
+ }
1447
+
1448
+ function normalizeCompiledArtifact(artifact) {
1449
+ if (!artifact || typeof artifact !== "object") {
1450
+ throw new TypeError("Compiler returned an invalid compiled artifact.");
1451
+ }
1452
+ const code = String(artifact.code ?? "");
1453
+ const imports = Array.isArray(artifact.imports) ? artifact.imports.map((reference, index) => {
1454
+ if (!reference || typeof reference !== "object") {
1455
+ throw new TypeError(`Compiled artifact import ${index} is invalid.`);
1456
+ }
1457
+ const start = Number(reference.start);
1458
+ const end = Number(reference.end);
1459
+ const specifier = String(reference.specifier ?? "");
1460
+ if (!Number.isInteger(start) || !Number.isInteger(end) || start < 0 || end <= start || end > code.length) {
1461
+ throw new TypeError(`Compiled artifact import ${JSON.stringify(specifier)} has invalid source offsets.`);
1462
+ }
1463
+ return {
1464
+ specifier,
1465
+ start,
1466
+ end,
1467
+ kind: reference.kind ?? "static",
1468
+ };
1469
+ }) : [];
1470
+
1471
+ return {
1472
+ schema: COMPILED_ARTIFACT_SCHEMA,
1473
+ code,
1474
+ imports,
1475
+ diagnostics: Array.isArray(artifact.diagnostics) ? [...artifact.diagnostics] : [],
1476
+ ...(artifact.sourceMap == null ? {} : { sourceMap: String(artifact.sourceMap) }),
1477
+ ...(artifact.changed == null ? {} : { changed: Boolean(artifact.changed) }),
1478
+ };
1479
+ }
1480
+
1481
+ function isQuotaError(error) {
1482
+ const name = error?.name;
1483
+ const code = error?.code;
1484
+ return name === "QuotaExceededError" || name === "NS_ERROR_DOM_QUOTA_REACHED" || code === 22 || code === 1014;
1485
+ }
1486
+
1487
+ function normalizeLimit(value) {
1488
+ if (value == null) return undefined;
1489
+ const number = Number(value);
1490
+ return Number.isFinite(number) && number >= 0 ? number : undefined;
1491
+ }
1492
+
1493
+ function now() {
1494
+ return globalThis.performance?.now?.() ?? Date.now();
1495
+ }