@powerhousedao/builder-tools 6.2.3-dev.44 → 6.2.3-dev.46

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/dist/index.mjs CHANGED
@@ -1,10 +1,10 @@
1
1
  import { createRequire } from "node:module";
2
2
  import { createHash, randomUUID } from "node:crypto";
3
- import fs, { chmodSync, createReadStream, existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
3
+ import fs, { chmodSync, createReadStream, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
4
4
  import path, { dirname, extname, isAbsolute, join, resolve } from "node:path";
5
5
  import { exec, execSync, spawn } from "node:child_process";
6
6
  import { fileURLToPath } from "node:url";
7
- import { DYNAMIC_BASE_PLACEHOLDER, PWA_FILE_HANDLER_ACTION, buildRuntimeConfig, deepMerge, loadConnectEnv, mergeManifest, mergePwaConfig, normalizeBasePath, phConnectRuntimeConfigSchema, powerhousePackageSchema, resolveSourceConnect, setConnectEnv, unionStrings, withInferredCategory, workflowsSettingConflict } from "@powerhousedao/shared/connect";
7
+ import { DYNAMIC_BASE_PLACEHOLDER, PWA_FILE_HANDLER_ACTION, buildRuntimeConfig, deepMerge, loadConnectEnv, mergeManifest, mergePwaConfig, normalizeBasePath, phConnectRuntimeConfigSchema, powerhousePackageSchema, resolveSourceConnect, setConnectEnv, unionStrings, withInferredCategory, workerPackageFileName, workflowsSettingConflict } from "@powerhousedao/shared/connect";
8
8
  import MagicString from "magic-string";
9
9
  import { readFile, writeFile } from "node:fs/promises";
10
10
  import { cwd } from "node:process";
@@ -294,15 +294,15 @@ function readCacheHit(importMapPath, include, external, versionDigest) {
294
294
  * Unresolvable specs contribute a "missing" sentinel, unknown versions
295
295
  * "unknown".
296
296
  */
297
- function resolveDepVersions(dirname$2, specs) {
298
- const req = createRequire(join(dirname$2, "noop.js"));
297
+ function resolveDepVersions(dirname$3, specs) {
298
+ const req = createRequire(join(dirname$3, "noop.js"));
299
299
  const seen = {};
300
300
  let selfMeta = null;
301
301
  for (const spec of specs) {
302
302
  const { pkg } = parsePkg(spec);
303
303
  if (pkg in seen) continue;
304
304
  if (selfMeta === null) try {
305
- selfMeta = JSON.parse(readFileSync(join(dirname$2, "package.json"), "utf8"));
305
+ selfMeta = JSON.parse(readFileSync(join(dirname$3, "package.json"), "utf8"));
306
306
  } catch {
307
307
  selfMeta = {};
308
308
  }
@@ -311,7 +311,7 @@ function resolveDepVersions(dirname$2, specs) {
311
311
  else {
312
312
  version = "missing";
313
313
  try {
314
- const pkgRoot = realpathSync(join(dirname$2, "node_modules", pkg));
314
+ const pkgRoot = realpathSync(join(dirname$3, "node_modules", pkg));
315
315
  const meta = JSON.parse(readFileSync(join(pkgRoot, "package.json"), "utf8"));
316
316
  version = String(meta.version ?? "unknown");
317
317
  } catch {
@@ -520,12 +520,12 @@ function withVendorBase(imports, base) {
520
520
  * table, writes the runtime shared-deps module, and does the swap.
521
521
  * Returns the published import map + versions, or null on failure.
522
522
  */
523
- async function buildVendorAtomic(dirname$3, vendorDir, include, external, versionDigest, versions, base, nodeEnv) {
523
+ async function buildVendorAtomic(dirname$4, vendorDir, include, external, versionDigest, versions, base, nodeEnv) {
524
524
  const parent = dirname(vendorDir);
525
525
  mkdirSync(parent, { recursive: true });
526
526
  const tmpDir = mkdtempSync(join(parent, ".ph-vendor.tmp-"));
527
527
  try {
528
- const { ok, stderr } = await runBuildWorker(dirname$3, tmpDir, include, external, base, nodeEnv);
528
+ const { ok, stderr } = await runBuildWorker(dirname$4, tmpDir, include, external, base, nodeEnv);
529
529
  const tmpMap = join(tmpDir, "import-map.json");
530
530
  if (!ok || !existsSync(tmpMap)) {
531
531
  const detail = stderr.trim();
@@ -744,6 +744,724 @@ for (const spec of include) imports[spec] = urlPrefix + entryName(spec) + '.js';
744
744
  writeFileSync(join(vendorDir, 'import-map.json'), JSON.stringify({ include, external, imports }, null, 2));
745
745
  `;
746
746
  //#endregion
747
+ //#region connect-utils/reactor-worker-build.ts
748
+ /**
749
+ * Prebuild Connect's reactor SharedWorker into a browser-loadable bundle.
750
+ *
751
+ * The published `@powerhousedao/connect` dist ships `reactor.worker.js` as a
752
+ * library artifact: its dependencies are externalized onto bare specifiers
753
+ * (`@powerhousedao/reactor`, `kysely`, ...). The page resolves those through
754
+ * its `<script type="importmap">`, but import maps are a document feature —
755
+ * a worker never inherits one and no Worker/SharedWorker constructor option
756
+ * can supply one. A bare specifier inside a worker is unresolvable by the
757
+ * platform, so the shipped file can never run as a worker script.
758
+ *
759
+ * This module builds the worker's module graph into a self-contained bundle
760
+ * (one `vite build` in a throwaway subprocess, same pattern as the vendor
761
+ * prebuild). Two modes:
762
+ *
763
+ * - With `vendorImports` (production builds that also ship the `__vendor__/`
764
+ * bundle): specifiers present in the vendor import map are externalized and
765
+ * rewritten to `../__vendor__/<entry>.js`. The worker bundle is served as a
766
+ * sibling of `__vendor__/`, so those relative imports hit the exact URLs
767
+ * the page's import map uses — one fetch and one HTTP-cache entry per
768
+ * shared dep for the page and the worker together.
769
+ * - Without (dev servers, `PH_CONNECT_VENDOR=0` builds): everything is
770
+ * bundled; the worker is self-contained.
771
+ *
772
+ * The build uses Vite's relative base (`"./"`), so emitted chunk and asset
773
+ * URLs resolve against the worker script's own URL. That sidesteps the
774
+ * dynamic-base machinery entirely: the bundle works under any deploy base,
775
+ * concrete or proxy-substituted, as long as it is served under
776
+ * `<app-base>/__reactor_worker__/`.
777
+ */
778
+ /** URL prefix the worker bundle is served under, next to `__vendor__/`. */
779
+ const REACTOR_WORKER_URL_PREFIX = "/__reactor_worker__/";
780
+ /** Entry filename of the bundle; the tab constructs its SharedWorker from it. */
781
+ const REACTOR_WORKER_ENTRY = "reactor.worker.js";
782
+ /** Metadata file the cache check reads; lives inside the bundle dir. */
783
+ const META_FILE = "worker-meta.json";
784
+ /** Content-Type for the extensions the worker build emits; JS is the default. */
785
+ const REACTOR_WORKER_MIME = {
786
+ ".map": "application/json",
787
+ ".json": "application/json",
788
+ ".wasm": "application/wasm",
789
+ ".data": "application/octet-stream"
790
+ };
791
+ /**
792
+ * The installed Connect dist's worker entry, or null when the installed
793
+ * version predates the worker (the feature is then unavailable, not broken).
794
+ */
795
+ function resolveReactorWorkerSource(dirname) {
796
+ try {
797
+ const candidate = join(realpathSync(join(dirname, "node_modules", "@powerhousedao/connect")), "dist", REACTOR_WORKER_ENTRY);
798
+ return existsSync(candidate) ? candidate : null;
799
+ } catch {
800
+ return null;
801
+ }
802
+ }
803
+ /** `/__vendor__/<entry>.js` -> `../__vendor__/<entry>.js` (sibling dirs). */
804
+ function vendorRelativePath(vendorUrl) {
805
+ return `../__vendor__/${vendorUrl.slice(vendorUrl.lastIndexOf("/") + 1)}`;
806
+ }
807
+ const SPECIFIER_PATTERNS = [
808
+ /(?<![.\w$])import\s*\(\s*["']([^"']+)["']/g,
809
+ /(?<![.\w$])import\s*[^"'()]*?from\s*["']([^"']+)["']/g,
810
+ /(?<![.\w$])export\s*[^"'()]*?from\s*["']([^"']+)["']/g,
811
+ /(?<![.\w$])import\s*["']([^"']+)["']/g
812
+ ];
813
+ /**
814
+ * Module specifiers a worker cannot resolve: anything that is not relative,
815
+ * root-relative, or an http(s)/data/blob URL. Used as a post-build guard so a
816
+ * leftover bare (or `node:`) import fails the build with a name instead of
817
+ * shipping a worker that dies on its first import statement.
818
+ *
819
+ * Expects minified (comment-free) output: a raw-text scan cannot tell a real
820
+ * import from a JSDoc code sample, and unminified kysely/viem are full of the
821
+ * latter. The build below always minifies for exactly this reason.
822
+ */
823
+ function findDisallowedSpecifiers(code) {
824
+ const found = /* @__PURE__ */ new Set();
825
+ for (const pattern of SPECIFIER_PATTERNS) for (const match of code.matchAll(pattern)) {
826
+ const spec = match[1];
827
+ if (!spec.startsWith("./") && !spec.startsWith("../") && !spec.startsWith("/") && !/^(https?:|data:|blob:)/.test(spec)) found.add(spec);
828
+ }
829
+ return [...found].sort();
830
+ }
831
+ /**
832
+ * Fallback list of packages whose BUILT dist shapes the worker bundle's output,
833
+ * used only when the installed Connect's package.json cannot be read (see
834
+ * {@link upstreamWorkerPackages}).
835
+ */
836
+ const FALLBACK_UPSTREAM_WORKER_PACKAGES = ["@powerhousedao/reactor", "@powerhousedao/reactor-browser"];
837
+ /** Dependency scopes whose installed dist can change the bundle's output. */
838
+ const UPSTREAM_SCOPES = ["@powerhousedao/", "@renown/"];
839
+ /**
840
+ * Packages whose BUILT dist shapes the worker bundle's output even when
841
+ * connect's own dist/reactor.worker.js entry file did not change.
842
+ *
843
+ * That entry is a library artifact whose bare imports this build resolves
844
+ * through node_modules at BUILD time, so a rebuilt dist anywhere in its
845
+ * closure changes the bundle output without touching the entry file's own
846
+ * size/mtime. The list is DERIVED from the installed Connect's own
847
+ * dependencies rather than hardcoded: the bundle pulls `@renown/sdk`,
848
+ * `@powerhousedao/powerhouse-vetra-packages` and `@powerhousedao/shared` out
849
+ * of node_modules too, and a hardcoded pair left every one of those able to be
850
+ * rebuilt behind a cache hit. Connect's dependency list is the cheap stand-in
851
+ * for the resolved bundle graph: every workspace package the worker can reach
852
+ * is reachable through it, and a package not in the bundle only costs a readdir.
853
+ */
854
+ function upstreamWorkerPackages(dirname) {
855
+ const connectDir = resolveWorkspacePackageDir(dirname, "@powerhousedao/connect");
856
+ if (!connectDir) return [...FALLBACK_UPSTREAM_WORKER_PACKAGES];
857
+ let meta;
858
+ try {
859
+ meta = JSON.parse(readFileSync(join(connectDir, "package.json"), "utf8"));
860
+ } catch {
861
+ return [...FALLBACK_UPSTREAM_WORKER_PACKAGES];
862
+ }
863
+ const names = Object.keys(meta.dependencies ?? {}).filter((name) => UPSTREAM_SCOPES.some((scope) => name.startsWith(scope)));
864
+ for (const name of FALLBACK_UPSTREAM_WORKER_PACKAGES) if (!names.includes(name)) names.push(name);
865
+ return names.sort();
866
+ }
867
+ /**
868
+ * This module's own installed version, read by walking up from its file to
869
+ * the nearest `package.json` named "@powerhousedao/builder-tools" (same
870
+ * technique as `build-hash.ts`'s `readConnectVersion`: builder-tools'
871
+ * `exports` map has no `./package.json` subpath, so node resolution can't
872
+ * get there). Works whether this runs from source (tests, this monorepo) or
873
+ * the built dist.
874
+ */
875
+ function resolveOwnPackageVersion() {
876
+ try {
877
+ let dir = dirname(fileURLToPath(import.meta.url));
878
+ for (;;) {
879
+ try {
880
+ const pkg = JSON.parse(readFileSync(join(dir, "package.json"), "utf8"));
881
+ if (pkg.name === "@powerhousedao/builder-tools") return pkg.version ?? "unknown";
882
+ } catch {}
883
+ const parent = dirname(dir);
884
+ if (parent === dir) break;
885
+ dir = parent;
886
+ }
887
+ } catch {}
888
+ return "unknown";
889
+ }
890
+ /**
891
+ * This package's own version, computed once. Folded into the worker bundle
892
+ * cache key so a builder-tools release — which can change how the bundle is
893
+ * built (anything in this module beyond {@link WORKER_BUILD_WORKER} itself,
894
+ * e.g. `workerSafeVendorImports`, the resolve plugin) — invalidates bundles
895
+ * a prior builder-tools version cached.
896
+ */
897
+ const BUILDER_TOOLS_VERSION = resolveOwnPackageVersion();
898
+ /**
899
+ * A workspace package's real directory, resolved the way Node's resolver
900
+ * would (through `node_modules`), or null when it is not installed (e.g. a
901
+ * non-monorepo consumer project — graceful, not an error). pnpm's
902
+ * node_modules symlinks resolve to the real package directory, so this
903
+ * reflects the monorepo's actual dist state, not a stale symlink target.
904
+ */
905
+ function resolveWorkspacePackageDir(dirname, pkgName) {
906
+ try {
907
+ return realpathSync(join(dirname, "node_modules", ...pkgName.split("/")));
908
+ } catch {
909
+ return null;
910
+ }
911
+ }
912
+ /**
913
+ * Cheap content fingerprint of a directory's immediate files: no file bytes
914
+ * are read, just each entry's name/size/mtime, so it stays fast even for a
915
+ * large dist. Sensitive to a rebuild in the common cases: Vite's
916
+ * content-hashed chunk filenames change the name itself; a stable-named file
917
+ * (an entry point) changes size or mtime. Bounded (`maxFiles`) so a
918
+ * pathological directory cannot make this slow — deliberately not a
919
+ * full-tree hash.
920
+ *
921
+ * The names are sorted BEFORE the bound is applied, so the subset the bound
922
+ * keeps is the same subset on every run: readdir order is unspecified, and
923
+ * truncating it first made the fingerprint of a directory past the bound a
924
+ * function of whatever order the filesystem happened to report.
925
+ */
926
+ function distDirFingerprint(dir, maxFiles = 1e3) {
927
+ try {
928
+ return readdirSync(dir, { withFileTypes: true }).filter((e) => e.isFile()).map((e) => e.name).sort().slice(0, maxFiles).map((name) => {
929
+ const stat = statSync(join(dir, name));
930
+ return `${name}:${stat.size}:${Math.round(stat.mtimeMs)}`;
931
+ }).join("|");
932
+ } catch {
933
+ return "unreadable";
934
+ }
935
+ }
936
+ function computeSourceDigest(dirname, entryPath, vendorDir, builderToolsVersion = BUILDER_TOOLS_VERSION) {
937
+ const h = createHash("sha256");
938
+ h.update(`builder:${WORKER_BUILD_WORKER_HASH}\n`);
939
+ h.update(`builder-tools:${builderToolsVersion}\n`);
940
+ try {
941
+ const pkgRoot = resolveWorkspacePackageDir(dirname, "@powerhousedao/connect");
942
+ if (!pkgRoot) throw new Error("unresolved");
943
+ const meta = JSON.parse(readFileSync(join(pkgRoot, "package.json"), "utf8"));
944
+ h.update(`connect:${meta.version ?? "unknown"}\n`);
945
+ } catch {
946
+ h.update("connect:unresolved\n");
947
+ }
948
+ try {
949
+ const stat = statSync(entryPath);
950
+ h.update(`entry:${stat.size}:${Math.round(stat.mtimeMs)}\n`);
951
+ } catch {
952
+ h.update("entry:unstatable\n");
953
+ }
954
+ for (const pkg of upstreamWorkerPackages(dirname)) {
955
+ const pkgDir = resolveWorkspacePackageDir(dirname, pkg);
956
+ h.update(`${pkg}:${pkgDir ? distDirFingerprint(join(pkgDir, "dist")) : "unresolved"}\n`);
957
+ }
958
+ if (vendorDir) try {
959
+ h.update(`vendor:${createHash("sha256").update(readFileSync(join(vendorDir, "import-map.json"))).digest("hex")}\n`);
960
+ } catch {
961
+ h.update("vendor:unreadable\n");
962
+ }
963
+ return h.digest("hex").slice(0, 16);
964
+ }
965
+ function vendorKeyOf(vendorImports) {
966
+ const entries = Object.entries(vendorImports).sort(([a], [b]) => a.localeCompare(b));
967
+ return createHash("sha256").update(JSON.stringify(entries)).digest("hex").slice(0, 16);
968
+ }
969
+ function readWorkerCacheHit(metaPath, sourceDigest, vendorKey, nodeEnv) {
970
+ if (!existsSync(metaPath)) return false;
971
+ try {
972
+ const cached = JSON.parse(readFileSync(metaPath, "utf8"));
973
+ return cached.sourceDigest === sourceDigest && cached.vendorKey === vendorKey && cached.nodeEnv === nodeEnv && existsSync(join(dirname(metaPath), "reactor.worker.js"));
974
+ } catch {
975
+ return false;
976
+ }
977
+ }
978
+ async function waitForWorkerCacheHit(metaPath, sourceDigest, vendorKey, nodeEnv, lockDir) {
979
+ const deadline = Date.now() + LOCK_STALE_MS;
980
+ while (Date.now() < deadline) {
981
+ if (readWorkerCacheHit(metaPath, sourceDigest, vendorKey, nodeEnv)) return true;
982
+ if (!existsSync(lockDir)) return readWorkerCacheHit(metaPath, sourceDigest, vendorKey, nodeEnv);
983
+ await new Promise((r) => setTimeout(r, 250));
984
+ }
985
+ return false;
986
+ }
987
+ /**
988
+ * Build the worker bundle (once, in a throwaway subprocess) into `outDir`.
989
+ * Idempotent: reuses a cached bundle built from the same connect dist, vendor
990
+ * mapping, and builder logic. Returns null on failure (`errorRef` carries the
991
+ * cause) or when the installed Connect has no worker entry.
992
+ */
993
+ async function prebuildReactorWorker(options) {
994
+ const entryPath = options.entryPath ?? resolveReactorWorkerSource(options.dirname);
995
+ if (!entryPath) {
996
+ if (options.errorRef) options.errorRef.message = "the installed @powerhousedao/connect ships no dist/reactor.worker.js";
997
+ return null;
998
+ }
999
+ const vendorImports = options.vendor ? workerSafeVendorImports(options.vendor.dir, options.vendor.imports) : {};
1000
+ const nodeEnv = options.nodeEnv ?? "development";
1001
+ const outDir = options.outDir;
1002
+ const metaPath = join(outDir, META_FILE);
1003
+ const sourceDigest = computeSourceDigest(options.dirname, entryPath, options.vendor?.dir);
1004
+ const vendorKey = vendorKeyOf(vendorImports);
1005
+ const result = {
1006
+ outDir,
1007
+ entry: REACTOR_WORKER_ENTRY,
1008
+ sourceDigest,
1009
+ vendorImports
1010
+ };
1011
+ try {
1012
+ if (readWorkerCacheHit(metaPath, sourceDigest, vendorKey, nodeEnv)) return result;
1013
+ const lockDir = `${outDir}.lock`;
1014
+ const lock = acquireLock(lockDir);
1015
+ if (!lock) return await waitForWorkerCacheHit(metaPath, sourceDigest, vendorKey, nodeEnv, lockDir) ? result : null;
1016
+ try {
1017
+ if (readWorkerCacheHit(metaPath, sourceDigest, vendorKey, nodeEnv)) return result;
1018
+ await buildWorkerAtomic(options.dirname, outDir, entryPath, vendorImports, nodeEnv, {
1019
+ sourceDigest,
1020
+ vendorKey,
1021
+ nodeEnv
1022
+ });
1023
+ return result;
1024
+ } finally {
1025
+ releaseLock(lock);
1026
+ }
1027
+ } catch (err) {
1028
+ if (options.errorRef) options.errorRef.message = err instanceof Error ? err.message : String(err);
1029
+ return null;
1030
+ }
1031
+ }
1032
+ async function buildWorkerAtomic(dirname$2, outDir, entryPath, vendorImports, nodeEnv, meta) {
1033
+ const parent = dirname(outDir);
1034
+ mkdirSync(parent, { recursive: true });
1035
+ const tmpDir = mkdtempSync(join(parent, ".ph-reactor-worker.tmp-"));
1036
+ try {
1037
+ const { ok, stderr } = await runWorkerBuild(dirname$2, tmpDir, { "reactor.worker": entryPath }, vendorImports, nodeEnv);
1038
+ if (!ok || !existsSync(join(tmpDir, "reactor.worker.js"))) {
1039
+ const detail = stderr.trim();
1040
+ throw new Error(`reactor worker build failed${detail ? `:\n${detail}` : " (no output captured)"}`);
1041
+ }
1042
+ assertWorkerResolvable(tmpDir);
1043
+ writeFileSync(join(tmpDir, META_FILE), JSON.stringify(meta, null, 2));
1044
+ publishDirAtomic(tmpDir, outDir);
1045
+ } catch (err) {
1046
+ rmSync(tmpDir, {
1047
+ recursive: true,
1048
+ force: true
1049
+ });
1050
+ throw err;
1051
+ }
1052
+ }
1053
+ /** Replace `outDir` with the finished `tmpDir`, old contents removed last. */
1054
+ function publishDirAtomic(tmpDir, outDir) {
1055
+ const oldDir = `${outDir}.old-${process.pid}-${Date.now()}`;
1056
+ if (existsSync(outDir)) renameSync(outDir, oldDir);
1057
+ renameSync(tmpDir, outDir);
1058
+ chmodSync(outDir, 493);
1059
+ rmSync(oldDir, {
1060
+ recursive: true,
1061
+ force: true
1062
+ });
1063
+ }
1064
+ const RELATIVE_SPECIFIER_PATTERNS = SPECIFIER_PATTERNS;
1065
+ function findRelativeSpecifiers(code) {
1066
+ const found = /* @__PURE__ */ new Set();
1067
+ for (const pattern of RELATIVE_SPECIFIER_PATTERNS) for (const match of code.matchAll(pattern)) {
1068
+ const spec = match[1];
1069
+ if (spec.startsWith("./") || spec.startsWith("../")) found.add(spec);
1070
+ }
1071
+ return [...found];
1072
+ }
1073
+ function normalizeBundlePath(fromFile, spec) {
1074
+ const segments = fromFile.split("/").slice(0, -1);
1075
+ for (const part of spec.split("/")) {
1076
+ if (part === "" || part === ".") continue;
1077
+ if (part === "..") {
1078
+ if (segments.length === 0) return "";
1079
+ segments.pop();
1080
+ continue;
1081
+ }
1082
+ segments.push(part);
1083
+ }
1084
+ return segments.join("/");
1085
+ }
1086
+ /**
1087
+ * Scan the emitted JS files REACHABLE from the entry via import statements
1088
+ * for specifiers a worker cannot resolve. Vendor externals were rewritten to
1089
+ * ../__vendor__/ relative paths, so anything bare that survives in the graph
1090
+ * is a real defect.
1091
+ *
1092
+ * Deliberately a graph walk, not a directory walk: the bundled reactor code
1093
+ * carries `new URL(..., import.meta.url)` references to its node
1094
+ * worker-thread entries (executor/projection workers), which Vite emits as
1095
+ * stray assets full of node-only imports. They are unreachable dead weight in
1096
+ * a browser bundle — the production vendor output tolerates the same files —
1097
+ * and must not fail the build.
1098
+ */
1099
+ function findBundleSpecifierOffenders(bundleDir, entry = REACTOR_WORKER_ENTRY) {
1100
+ const offenders = [];
1101
+ walkBundleGraph(bundleDir, entry, (file, code) => {
1102
+ const specs = findDisallowedSpecifiers(code);
1103
+ if (specs.length > 0) offenders.push({
1104
+ file,
1105
+ specs
1106
+ });
1107
+ return true;
1108
+ });
1109
+ return offenders;
1110
+ }
1111
+ function walkBundleGraph(bundleDir, entry, visit) {
1112
+ const seen = /* @__PURE__ */ new Set();
1113
+ const queue = [entry];
1114
+ while (queue.length > 0) {
1115
+ const rel = queue.shift();
1116
+ if (seen.has(rel)) continue;
1117
+ seen.add(rel);
1118
+ const full = join(bundleDir, rel);
1119
+ if (!existsSync(full) || statSync(full).isDirectory()) continue;
1120
+ if (!rel.endsWith(".js") && !rel.endsWith(".mjs")) continue;
1121
+ const code = readFileSync(full, "utf8");
1122
+ if (!visit(rel, code)) return false;
1123
+ for (const spec of findRelativeSpecifiers(code)) {
1124
+ const next = normalizeBundlePath(rel, spec);
1125
+ if (next) queue.push(next);
1126
+ }
1127
+ }
1128
+ return true;
1129
+ }
1130
+ function assertWorkerResolvable(bundleDir) {
1131
+ const offenders = findBundleSpecifierOffenders(bundleDir);
1132
+ if (offenders.length > 0) throw new Error("reactor worker bundle contains module specifiers a worker cannot resolve:\n" + offenders.map((o) => ` ${o.file}: ${o.specs.join(", ")}`).join("\n"));
1133
+ }
1134
+ const WORKER_UNSAFE_MARKERS = ["__PH_DYNAMIC_BASE__", "vite:preloadError"];
1135
+ /** The page-only markers present in `code`, in a fixed order. */
1136
+ function findWorkerUnsafeMarkers(code) {
1137
+ return WORKER_UNSAFE_MARKERS.filter((marker) => code.includes(marker));
1138
+ }
1139
+ /**
1140
+ * The subset of the vendor's entries a worker may import.
1141
+ *
1142
+ * The vendor build keeps the React family external: its chunks carry bare
1143
+ * `import "react"` statements that the PAGE resolves through its import map.
1144
+ * A worker resolves no import map, so externalizing a vendor spec whose chunk
1145
+ * closure reaches such an import would kill the worker on its first load —
1146
+ * and the chunk graph is shared across vendor entries, so even a React-free
1147
+ * module (e.g. an rpc subpath) can be entangled with React through a shared
1148
+ * chunk. The vendor is also built for the page: a dynamic-base build rewrites
1149
+ * its asset URLs onto a global only the page sets, and its dynamic imports go
1150
+ * through Vite's preload helper, which needs `document`. Each entry's own
1151
+ * closure inside the vendor dir decides: clean closure, worker-safe; anything
1152
+ * bare or page-only in it (see {@link findWorkerUnsafeMarkers}), or a missing
1153
+ * entry file, bundled into the worker instead.
1154
+ */
1155
+ function workerSafeVendorImports(vendorDir, vendorImports) {
1156
+ const safe = {};
1157
+ for (const [spec, url] of Object.entries(vendorImports)) {
1158
+ const entryFile = url.slice(url.lastIndexOf("/") + 1);
1159
+ if (!existsSync(join(vendorDir, entryFile))) continue;
1160
+ if (walkBundleGraph(vendorDir, entryFile, (_file, code) => findDisallowedSpecifiers(code).length === 0 && findWorkerUnsafeMarkers(code).length === 0)) safe[spec] = url;
1161
+ }
1162
+ return safe;
1163
+ }
1164
+ /**
1165
+ * Builds arbitrary worker-loadable entries with the worker build config.
1166
+ * Exported for the local-package bundles, which need the same self-contained
1167
+ * output and vendor externalization at a different depth.
1168
+ */
1169
+ function runWorkerBuildEntries(dirname, outDir, entries, vendorImports, nodeEnv, vendorPrefix = "../__vendor__/") {
1170
+ return runWorkerBuild(dirname, outDir, entries, vendorImports, nodeEnv, vendorPrefix);
1171
+ }
1172
+ function runWorkerBuild(dirname, outDir, entries, vendorImports, nodeEnv, vendorPrefix = "../__vendor__/") {
1173
+ const workerPath = join(outDir, "build-worker.mjs");
1174
+ writeFileSync(workerPath, WORKER_BUILD_WORKER);
1175
+ return new Promise((resolve) => {
1176
+ const child = spawn(process.execPath, [
1177
+ workerPath,
1178
+ dirname,
1179
+ outDir,
1180
+ JSON.stringify(entries),
1181
+ JSON.stringify(vendorImports),
1182
+ nodeEnv,
1183
+ resolveBuilderVite() ?? "",
1184
+ vendorPrefix
1185
+ ], {
1186
+ cwd: dirname,
1187
+ stdio: [
1188
+ "ignore",
1189
+ "pipe",
1190
+ "pipe"
1191
+ ]
1192
+ });
1193
+ let stderr = "";
1194
+ child.stdout.on("data", (d) => {
1195
+ stderr += String(d);
1196
+ });
1197
+ child.stderr.on("data", (d) => {
1198
+ stderr += String(d);
1199
+ });
1200
+ child.on("exit", (code) => {
1201
+ rmSync(workerPath, { force: true });
1202
+ resolve({
1203
+ ok: code === 0,
1204
+ stderr
1205
+ });
1206
+ });
1207
+ child.on("error", (e) => resolve({
1208
+ ok: false,
1209
+ stderr: String(e)
1210
+ }));
1211
+ });
1212
+ }
1213
+ /**
1214
+ * The vite installed next to this module. The subprocess must run the vite
1215
+ * this build config was written against: a consumer project can pin an older
1216
+ * vite/rolldown (overrides) whose bundler leaves unresolvable specifiers bare
1217
+ * instead of erroring and shims node builtins differently — the output guard
1218
+ * then correctly rejects the bundle. builder-tools declares vite as a direct
1219
+ * dependency, so this resolution works wherever builder-tools is installed.
1220
+ */
1221
+ function resolveBuilderVite() {
1222
+ try {
1223
+ return createRequire(import.meta.url).resolve("vite");
1224
+ } catch {
1225
+ return null;
1226
+ }
1227
+ }
1228
+ /**
1229
+ * The build subprocess (same pattern as the vendor's). Loads builder-tools'
1230
+ * own vite (argv; project fallback) and builds the given entries with
1231
+ * relative base, so chunk/asset URLs resolve against each script's own URL
1232
+ * under any deploy base. argv: dirname, outDir, entriesJSON (name ->
1233
+ * absolute entry path), vendorImportsJSON, nodeEnv, vitePath, vendorPrefix
1234
+ * (relative path from the out dir to __vendor__/, e.g. "../__vendor__/").
1235
+ */
1236
+ const WORKER_BUILD_WORKER = `
1237
+ import { createRequire } from 'node:module';
1238
+ import { isAbsolute } from 'node:path';
1239
+ import { fileURLToPath, pathToFileURL } from 'node:url';
1240
+ const [dirname, outDir, entriesJSON, vendorImportsJSON, nodeEnvArg, vitePathArg, vendorPrefixArg] = process.argv.slice(2);
1241
+ const nodeEnv = nodeEnvArg ?? 'development';
1242
+ const entries = JSON.parse(entriesJSON);
1243
+ const vendorImports = JSON.parse(vendorImportsJSON ?? '{}');
1244
+ const vendorPrefix = vendorPrefixArg || '../__vendor__/';
1245
+ const externalSet = new Set(Object.keys(vendorImports));
1246
+ const vendorPath = (spec) => {
1247
+ const url = vendorImports[spec];
1248
+ return vendorPrefix + url.slice(url.lastIndexOf('/') + 1);
1249
+ };
1250
+ const reqProj = createRequire(dirname + '/noop.js');
1251
+ // pathToFileURL: on Windows import('D:\\\\...') parses "D:" as a URL scheme.
1252
+ const vitePath = vitePathArg || reqProj.resolve('vite');
1253
+ const { build } = await import(pathToFileURL(vitePath).href);
1254
+ // Resolve bare specifiers from the project (same plugin as the vendor build):
1255
+ // prefer the bundler's browser-condition-aware resolution, fall back to the
1256
+ // worker's own resolution for Rolldown's realpath-anchoring bug.
1257
+ const phResolveCache = new Map();
1258
+ const phWorkerResolve = {
1259
+ name: 'ph-reactor-worker-resolve', enforce: 'pre',
1260
+ async resolveId(source, importer, options) {
1261
+ if (externalSet.has(source)) return null;
1262
+ if (source[0] === '\\0' || source[0] === '.' || isAbsolute(source)) return null;
1263
+ if (source.startsWith('node:') || source.startsWith('data:')) return null;
1264
+ let viaBundler = null;
1265
+ try { viaBundler = await this.resolve(source, importer, { ...options, skipSelf: true }); } catch {}
1266
+ if (viaBundler) return viaBundler;
1267
+ if (phResolveCache.has(source)) return phResolveCache.get(source);
1268
+ let resolved = null;
1269
+ try { resolved = fileURLToPath(import.meta.resolve(source)); } catch {}
1270
+ phResolveCache.set(source, resolved);
1271
+ return resolved;
1272
+ },
1273
+ };
1274
+ // A worker has no window: on a failed dynamic import, the preload helper's
1275
+ // window.dispatchEvent would throw a ReferenceError in place of the real error.
1276
+ const PRELOAD_HELPER_ID = '\\0vite/preload-helper.js';
1277
+ const PRELOAD_DISPATCH = 'window.dispatchEvent(';
1278
+ const phWorkerPreloadHelper = {
1279
+ name: 'ph-reactor-worker-preload-helper',
1280
+ transform(code, id) {
1281
+ if (id !== PRELOAD_HELPER_ID) return null;
1282
+ if (code.split(PRELOAD_DISPATCH).length !== 2) {
1283
+ this.error('expected exactly one ' + PRELOAD_DISPATCH + ' in ' + PRELOAD_HELPER_ID);
1284
+ }
1285
+ return { code: code.replace(PRELOAD_DISPATCH, 'globalThis.dispatchEvent?.('), map: null };
1286
+ },
1287
+ };
1288
+ await build({
1289
+ root: dirname, configFile: false, logLevel: 'error',
1290
+ // The project's public/ dir belongs to the app build, not this bundle.
1291
+ publicDir: false,
1292
+ // Relative base: emitted chunk/asset URLs resolve against the worker
1293
+ // script's own URL, so the bundle works under any deploy base.
1294
+ base: './',
1295
+ define: {
1296
+ 'process.env.NODE_ENV': JSON.stringify(nodeEnv),
1297
+ 'import.meta.env.BASE_URL': JSON.stringify('./'),
1298
+ },
1299
+ plugins: [phWorkerResolve, phWorkerPreloadHelper],
1300
+ // pglite ships nested web workers as ES-module chunks.
1301
+ worker: { format: 'es' },
1302
+ build: {
1303
+ // Always minified, dev included: the bundle is browser-served output
1304
+ // (sourcemaps carry the debugging story), and minification strips the
1305
+ // JSDoc comments whose embedded code samples (kysely's, viem's) would
1306
+ // otherwise trip the bare-specifier guard below.
1307
+ outDir, emptyOutDir: false, minify: true, target: 'esnext', sourcemap: true,
1308
+ // No document in a worker: the module-preload polyfill and preload helper
1309
+ // touch the DOM, so dynamic imports must stay plain import().
1310
+ modulePreload: false,
1311
+ rollupOptions: {
1312
+ input: entries,
1313
+ // A models entry is pure re-exports; without this the bundler treats
1314
+ // its exports as unused and tree-shakes the whole file to nothing.
1315
+ // Harmless for the worker entry, which exports nothing to preserve.
1316
+ preserveEntrySignatures: 'strict',
1317
+ external: (id) => externalSet.has(id),
1318
+ output: {
1319
+ format: 'es', entryFileNames: '[name].js',
1320
+ // Chunks live at the bundle root, not a chunks/ subdir: the vendor
1321
+ // paths below are emitted VERBATIM into every chunk, so all modules
1322
+ // must sit at the same depth for ../__vendor__/ to resolve.
1323
+ chunkFileNames: '[name]-[hash].js', assetFileNames: 'assets/[name]-[hash][extname]',
1324
+ paths: (id) => (externalSet.has(id) ? vendorPath(id) : id),
1325
+ },
1326
+ },
1327
+ },
1328
+ });
1329
+ `;
1330
+ const WORKER_BUILD_WORKER_HASH = createHash("sha256").update(WORKER_BUILD_WORKER).digest("hex").slice(0, 16);
1331
+ //#endregion
1332
+ //#region connect-utils/worker-packages-build.ts
1333
+ /**
1334
+ * Prebuild a project's local packages into worker-loadable model bundles.
1335
+ *
1336
+ * The reactor worker gets document models from the registry CDN or from what
1337
+ * Connect statically bundled. A project's own (`provider: "local"`) package is
1338
+ * in neither, so its document types have no reducer in the worker and their
1339
+ * load jobs fail. Dev solves this by pointing the worker at the dev server's
1340
+ * transformed models entry; a production deployment has no dev server, so the
1341
+ * entry is prebuilt here, next to the worker bundle it is loaded beside.
1342
+ *
1343
+ * Each package's `browser/document-models/index.js` (reducers only: no React,
1344
+ * no CSS, no editors) is built self-contained, with the worker-safe subset of
1345
+ * the vendor externalized onto `../../__vendor__/` - two levels up, since
1346
+ * these land in `__reactor_worker__/packages/`. A `manifest.json` names what
1347
+ * was built so the tab can turn it into package sources without guessing.
1348
+ */
1349
+ /** Served under `<base>__reactor_worker__/packages/`. */
1350
+ const WORKER_PACKAGES_MANIFEST = "manifest.json";
1351
+ /** Vendor is two levels up from `__reactor_worker__/packages/`. */
1352
+ const VENDOR_PREFIX = "../../__vendor__/";
1353
+ function readVersion(root) {
1354
+ try {
1355
+ const meta = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
1356
+ return typeof meta.version === "string" ? meta.version : void 0;
1357
+ } catch {
1358
+ return;
1359
+ }
1360
+ }
1361
+ /**
1362
+ * A package root's models entry, or null when it ships none.
1363
+ *
1364
+ * Two roots are possible and both are normal: an installed dependency under
1365
+ * `node_modules/<name>`, and the project itself - the vetra case, where the
1366
+ * project *is* the local package and has no self-link in node_modules.
1367
+ */
1368
+ function resolvePackageModels(dirname, name) {
1369
+ const roots = [];
1370
+ try {
1371
+ roots.push(realpathSync(join(dirname, "node_modules", name)));
1372
+ } catch {}
1373
+ if (readProjectName(dirname) === name) roots.push(dirname);
1374
+ for (const root of roots) {
1375
+ const entryPath = join(root, "dist", "browser", "document-models", "index.js");
1376
+ if (existsSync(entryPath)) return {
1377
+ entryPath,
1378
+ version: readVersion(root)
1379
+ };
1380
+ }
1381
+ return null;
1382
+ }
1383
+ function readProjectName(dirname) {
1384
+ try {
1385
+ const meta = JSON.parse(readFileSync(join(dirname, "package.json"), "utf8"));
1386
+ return typeof meta.name === "string" ? meta.name : void 0;
1387
+ } catch {
1388
+ return;
1389
+ }
1390
+ }
1391
+ /**
1392
+ * The project's own package name when it ships a models bundle, so a caller
1393
+ * can include it beside the configured `provider: "local"` packages. A vetra
1394
+ * project declares no package entry for itself, yet its models are exactly
1395
+ * the ones the worker is missing.
1396
+ */
1397
+ function ownProjectPackage(dirname) {
1398
+ const name = readProjectName(dirname);
1399
+ if (!name) return void 0;
1400
+ return resolvePackageModels(dirname, name) ? name : void 0;
1401
+ }
1402
+ /**
1403
+ * Build each local package's models entry into `outDir` and write the
1404
+ * manifest the tab reads. Returns the manifest entries (empty when no local
1405
+ * package ships a models bundle), or null when the build itself failed -
1406
+ * `errorRef` then carries the cause. Both of those remove `outDir`, so a
1407
+ * previous build's bundles never outlive the packages they came from.
1408
+ */
1409
+ async function prebuildWorkerPackages(options) {
1410
+ const nodeEnv = options.nodeEnv ?? "production";
1411
+ const resolved = [];
1412
+ for (const name of options.packages) {
1413
+ const models = resolvePackageModels(options.dirname, name);
1414
+ if (!models) continue;
1415
+ resolved.push({
1416
+ entry: {
1417
+ name,
1418
+ version: models.version,
1419
+ file: workerPackageFileName(name)
1420
+ },
1421
+ entryPath: models.entryPath
1422
+ });
1423
+ }
1424
+ if (resolved.length === 0) {
1425
+ rmSync(options.outDir, {
1426
+ recursive: true,
1427
+ force: true
1428
+ });
1429
+ return [];
1430
+ }
1431
+ const vendorImports = options.safeVendorImports ?? (options.vendor ? workerSafeVendorImports(options.vendor.dir, options.vendor.imports) : {});
1432
+ const entries = Object.fromEntries(resolved.map((item) => [item.entry.file.replace(/\.js$/, ""), item.entryPath]));
1433
+ const parent = dirname(options.outDir);
1434
+ mkdirSync(parent, { recursive: true });
1435
+ const tmpDir = mkdtempSync(join(parent, ".ph-worker-packages.tmp-"));
1436
+ try {
1437
+ const { ok, stderr } = await runWorkerBuildEntries(options.dirname, tmpDir, entries, vendorImports, nodeEnv, VENDOR_PREFIX);
1438
+ const missing = resolved.filter((item) => !existsSync(join(tmpDir, item.entry.file)));
1439
+ if (!ok || missing.length > 0) {
1440
+ const detail = stderr.trim();
1441
+ throw new Error(`worker package bundles failed to build${missing.length > 0 ? ` (missing: ${missing.map((m) => m.entry.name).join(", ")})` : ""}${detail ? `:\n${detail}` : ""}`);
1442
+ }
1443
+ for (const item of resolved) {
1444
+ const offenders = findBundleSpecifierOffenders(tmpDir, item.entry.file);
1445
+ if (offenders.length > 0) throw new Error(`worker package bundle for ${item.entry.name} contains module specifiers a worker cannot resolve:\n` + offenders.map((o) => ` ${o.file}: ${o.specs.join(", ")}`).join("\n"));
1446
+ }
1447
+ const manifest = resolved.map((item) => item.entry);
1448
+ writeFileSync(join(tmpDir, WORKER_PACKAGES_MANIFEST), JSON.stringify(manifest, null, 2));
1449
+ publishDirAtomic(tmpDir, options.outDir);
1450
+ return manifest;
1451
+ } catch (err) {
1452
+ rmSync(tmpDir, {
1453
+ recursive: true,
1454
+ force: true
1455
+ });
1456
+ rmSync(options.outDir, {
1457
+ recursive: true,
1458
+ force: true
1459
+ });
1460
+ if (options.errorRef) options.errorRef.message = err instanceof Error ? err.message : String(err);
1461
+ return null;
1462
+ }
1463
+ }
1464
+ //#endregion
747
1465
  //#region connect-utils/helpers.ts
748
1466
  const DEFAULT_CONNECT_OUTDIR = ".ph/connect-build/dist/";
749
1467
  function resolveViteConfigPath(options) {
@@ -1053,7 +1771,7 @@ const VENDOR_MIME = {
1053
1771
  const VENDOR_MODE = process.env.PH_CONNECT_EXTERNALIZE_VENDOR === "1";
1054
1772
  const VENDOR_EXTRA = (process.env.PH_CONNECT_VENDOR_EXTRA ?? "").split(",").map((s) => s.trim()).filter(Boolean);
1055
1773
  const HEAVY_LIBS = [...DEFAULT_VENDOR_INCLUDE, ...VENDOR_EXTRA];
1056
- function withBase(base, p) {
1774
+ function withBase$1(base, p) {
1057
1775
  return `${base}${p}`.replace(/\/{2,}/g, "/");
1058
1776
  }
1059
1777
  function externalizePlugin(externals) {
@@ -1135,7 +1853,7 @@ async function devReactImportmapPlugin(projectRoot = process.cwd()) {
1135
1853
  }));
1136
1854
  if (vendorActive) {
1137
1855
  const vendorDir = vendor.vendorDir;
1138
- const vendorPrefixes = [withBase(base, VENDOR_URL_PREFIX), VENDOR_URL_PREFIX];
1856
+ const vendorPrefixes = [withBase$1(base, VENDOR_URL_PREFIX), VENDOR_URL_PREFIX];
1139
1857
  server.middlewares.use((req, res, next) => {
1140
1858
  const url = (req.url ?? "").split("?")[0];
1141
1859
  const prefix = vendorPrefixes.find((p) => url.startsWith(p));
@@ -1157,7 +1875,7 @@ async function devReactImportmapPlugin(projectRoot = process.cwd()) {
1157
1875
  });
1158
1876
  });
1159
1877
  }
1160
- const shimPrefixes = [withBase(base, SHIM_PATH), `/${SHIM_PATH}`];
1878
+ const shimPrefixes = [withBase$1(base, SHIM_PATH), `/${SHIM_PATH}`];
1161
1879
  server.middlewares.use((req, res, next) => {
1162
1880
  const prefix = shimPrefixes.find((p) => req.url?.startsWith(p));
1163
1881
  if (!prefix) return next();
@@ -1171,7 +1889,7 @@ async function devReactImportmapPlugin(projectRoot = process.cwd()) {
1171
1889
  return;
1172
1890
  }
1173
1891
  const browserHash = info.browserHash ?? optimizer.metadata.browserHash;
1174
- const depUrl = `${withBase(base, VITE_DEPS_PATH)}/${path.basename(info.file)}?v=${browserHash}`;
1892
+ const depUrl = `${withBase$1(base, VITE_DEPS_PATH)}/${path.basename(info.file)}?v=${browserHash}`;
1175
1893
  const names = namedExports.get(id) ?? [];
1176
1894
  res.setHeader("Content-Type", "application/javascript");
1177
1895
  res.end(`import * as M from ${JSON.stringify(depUrl)};\nconst ns = M.default ?? M;\nexport default ns;\n` + (names.length ? `export const { ${names.join(", ")} } = ns;\n` : ""));
@@ -1182,12 +1900,12 @@ async function devReactImportmapPlugin(projectRoot = process.cwd()) {
1182
1900
  handler(html, ctx) {
1183
1901
  const browserHash = ctx.server?.environments.client.depsOptimizer?.metadata.browserHash;
1184
1902
  if (!browserHash) return;
1185
- const shimPrefix = withBase(base, SHIM_PATH);
1903
+ const shimPrefix = withBase$1(base, SHIM_PATH);
1186
1904
  const imports = Object.fromEntries(REACT_DEPS.map((id) => [id, `${shimPrefix}${id}.js?v=${browserHash}`]));
1187
1905
  let dynamicBaseScript = "";
1188
1906
  if (vendorActive) {
1189
- for (const [spec, url] of Object.entries(vendor.imports)) imports[spec] = withBase(base, url);
1190
- if (vendor.imports["@powerhousedao/connect"]) imports["ph-bundled-packages-virtual"] = withBase(base, BUNDLED_PACKAGES_DEV_URL);
1907
+ for (const [spec, url] of Object.entries(vendor.imports)) imports[spec] = withBase$1(base, url);
1908
+ if (vendor.imports["@powerhousedao/connect"]) imports["ph-bundled-packages-virtual"] = withBase$1(base, BUNDLED_PACKAGES_DEV_URL);
1191
1909
  dynamicBaseScript = `<script>globalThis.__PH_DYNAMIC_BASE__=${JSON.stringify(base)};<\/script>\n`;
1192
1910
  }
1193
1911
  return html.replace(/<script type="importmap">[\s\S]*?<\/script>/, `${dynamicBaseScript}<script type="importmap">${JSON.stringify({ imports }, null, 2)}<\/script>`);
@@ -1596,7 +2314,7 @@ const BASE_MANIFEST = {
1596
2314
  */
1597
2315
  const BASE_PRECACHE = {
1598
2316
  maximumFileSizeToCacheInBytes: 16777216,
1599
- globPatterns: ["**/*.{js,css,html,wasm,data,ico,png,svg,webp,woff,woff2}"],
2317
+ globPatterns: ["**/*.{js,css,html,wasm,data,ico,png,svg,webp,woff,woff2}", "__reactor_worker__/**/*.json"],
1600
2318
  globIgnores: [
1601
2319
  "**/powerhouse.config.json",
1602
2320
  "**/*.map",
@@ -1862,6 +2580,91 @@ try {
1862
2580
  }
1863
2581
  `;
1864
2582
  //#endregion
2583
+ //#region connect-utils/vite-plugins/reactor-worker-dev.ts
2584
+ const DEV_BUNDLE_DIR = "node_modules/.ph-reactor-worker";
2585
+ function withBase(base, p) {
2586
+ return `${base}${p}`.replace(/\/{2,}/g, "/");
2587
+ }
2588
+ /**
2589
+ * Dev-only: serve Connect's reactor SharedWorker bundle under
2590
+ * `<base>__reactor_worker__/`, building it lazily on first request.
2591
+ *
2592
+ * The dev server serves Connect from its prebuilt dist in node_modules, where
2593
+ * `dist/reactor.worker.js` is a library artifact no worker can resolve. The
2594
+ * tab GETs `worker-meta.json` here (JSON, no-cache, written by
2595
+ * `prebuildReactorWorker`) to find the bundle and fold its `sourceDigest`
2596
+ * into its version fingerprint; without this plugin that GET fails and the
2597
+ * worker feature reports itself unavailable instead of dying opaquely.
2598
+ *
2599
+ * Lazy because it is a full `vite build` of the reactor + PGlite graph. The
2600
+ * result is cached in node_modules/.ph-reactor-worker until the installed
2601
+ * Connect, its upstream dists, or builder-tools change (`computeSourceDigest`).
2602
+ */
2603
+ function reactorWorkerDevPlugin(projectRoot) {
2604
+ let base = "/";
2605
+ let building = null;
2606
+ const ensureBuilt = () => {
2607
+ building ??= (async () => {
2608
+ const errorRef = {};
2609
+ const built = await prebuildReactorWorker({
2610
+ dirname: projectRoot,
2611
+ outDir: path.join(projectRoot, DEV_BUNDLE_DIR),
2612
+ nodeEnv: "development",
2613
+ errorRef
2614
+ });
2615
+ if (!built) {
2616
+ console.warn(`[connect] reactor worker bundle build failed${errorRef.message ? `: ${errorRef.message}` : ""}`);
2617
+ building = null;
2618
+ }
2619
+ return built;
2620
+ })();
2621
+ return building;
2622
+ };
2623
+ return {
2624
+ name: "ph-reactor-worker-dev",
2625
+ apply: "serve",
2626
+ configResolved(config) {
2627
+ base = config.base;
2628
+ },
2629
+ configureServer(server) {
2630
+ const prefixes = [withBase(base, REACTOR_WORKER_URL_PREFIX), REACTOR_WORKER_URL_PREFIX];
2631
+ server.middlewares.use((req, res, next) => {
2632
+ const url = (req.url ?? "").split("?")[0];
2633
+ const prefix = prefixes.find((p) => url.startsWith(p));
2634
+ if (!prefix) return next();
2635
+ const name = url.slice(prefix.length).replace(/^\/+/, "");
2636
+ if (!name) return next();
2637
+ (async () => {
2638
+ const built = await ensureBuilt();
2639
+ if (!built) {
2640
+ res.statusCode = 503;
2641
+ res.setHeader("Content-Type", "text/plain");
2642
+ res.end("reactor worker bundle unavailable (build failed; see server log)");
2643
+ return;
2644
+ }
2645
+ const file = path.join(built.outDir, name);
2646
+ const rel = path.relative(built.outDir, file);
2647
+ if (rel.startsWith("..") || path.isAbsolute(rel)) {
2648
+ next();
2649
+ return;
2650
+ }
2651
+ const stream = createReadStream(file);
2652
+ stream.on("error", () => {
2653
+ if (!res.headersSent) next();
2654
+ });
2655
+ stream.once("open", () => {
2656
+ const ext = file.slice(file.lastIndexOf("."));
2657
+ res.setHeader("Content-Type", REACTOR_WORKER_MIME[ext] ?? "text/javascript");
2658
+ const hashed = !name.startsWith("reactor.worker.js") && !name.endsWith(".json");
2659
+ res.setHeader("Cache-Control", hashed ? "public, max-age=31536000, immutable" : "no-cache");
2660
+ stream.pipe(res);
2661
+ });
2662
+ })();
2663
+ });
2664
+ }
2665
+ };
2666
+ }
2667
+ //#endregion
1865
2668
  //#region connect-utils/vite-plugins/theme-boot.ts
1866
2669
  /**
1867
2670
  * Marker attribute on the injected script. Serve-time injectors (e.g. the
@@ -2200,6 +3003,7 @@ function getConnectBaseViteConfig(options) {
2200
3003
  projectRoot: options.dirname
2201
3004
  }),
2202
3005
  devReactImportmapPlugin(options.dirname),
3006
+ reactorWorkerDevPlugin(options.dirname),
2203
3007
  ...plugins,
2204
3008
  esmExternalRequirePlugin({ external: [...reactExternal, ...options.vendor && mode === "production" ? Object.keys(options.vendor.imports) : []] }),
2205
3009
  reactSelfHostPlugin({
@@ -2224,6 +3028,6 @@ function getConnectBaseViteConfig(options) {
2224
3028
  };
2225
3029
  }
2226
3030
  //#endregion
2227
- export { BUILD_HASH_FILE, DEFAULT_CONNECT_OUTDIR, DEFAULT_VENDOR_INCLUDE, DYNAMIC_BASE_PLACEHOLDER, EXTERNAL_PACKAGES_IMPORT, IMPORT_SCRIPT_FILE, LOCAL_PACKAGE_ID, PH_DIR_NAME, RUNTIME_CONFIG_SCHEMA_ID, RUNTIME_CONFIG_SCHEMA_URL, THEME_BOOT_MARKER, VENDOR_EXTERNAL, VENDOR_URL_PREFIX, appendToHtmlHead, backupIndexHtml, buildHashFromBuildOptions, computeBuildHash, connectBuildHashPlugin, connectDynamicBasePlugin, connectThemeBootPlugin, copyConnect, ensureNodeVersion, escapeForRegExp, getConnectBaseViteConfig, getConnectHtmlTags, makeImportScriptFromPackages, missingVendorEntries, phConfigPlugin, prebuildConnectVendor, prependToHtmlHead, readJsonFile, removeBase64EnvValues, resolveConnectBundle, resolveConnectPackageJson, resolveConnectPublicDir, resolvePackage, resolveViteConfigPath, runShellScriptPlugin, runTsc, runtimeConfigSchema, stripVersionFromPackage, vendorImportMapPlugin, withVendorBase };
3031
+ export { BUILDER_TOOLS_VERSION, BUILD_HASH_FILE, DEFAULT_CONNECT_OUTDIR, DEFAULT_VENDOR_INCLUDE, DYNAMIC_BASE_PLACEHOLDER, EXTERNAL_PACKAGES_IMPORT, IMPORT_SCRIPT_FILE, LOCAL_PACKAGE_ID, LOCK_STALE_MS, PH_DIR_NAME, REACTOR_WORKER_ENTRY, REACTOR_WORKER_MIME, REACTOR_WORKER_URL_PREFIX, RUNTIME_CONFIG_SCHEMA_ID, RUNTIME_CONFIG_SCHEMA_URL, THEME_BOOT_MARKER, VENDOR_EXTERNAL, VENDOR_URL_PREFIX, WORKER_PACKAGES_MANIFEST, acquireLock, appendToHtmlHead, backupIndexHtml, buildHashFromBuildOptions, computeBuildHash, computeSourceDigest, connectBuildHashPlugin, connectDynamicBasePlugin, connectThemeBootPlugin, copyConnect, distDirFingerprint, ensureNodeVersion, escapeForRegExp, findBundleSpecifierOffenders, findDisallowedSpecifiers, findWorkerUnsafeMarkers, getConnectBaseViteConfig, getConnectHtmlTags, makeImportScriptFromPackages, missingVendorEntries, ownProjectPackage, phConfigPlugin, prebuildConnectVendor, prebuildReactorWorker, prebuildWorkerPackages, prependToHtmlHead, publishDirAtomic, readJsonFile, releaseLock, removeBase64EnvValues, resolveConnectBundle, resolveConnectPackageJson, resolveConnectPublicDir, resolveOwnPackageVersion, resolvePackage, resolveReactorWorkerSource, resolveViteConfigPath, resolveWorkspacePackageDir, runShellScriptPlugin, runTsc, runWorkerBuildEntries, runtimeConfigSchema, stripVersionFromPackage, upstreamWorkerPackages, vendorImportMapPlugin, vendorRelativePath, withVendorBase, workerSafeVendorImports };
2228
3032
 
2229
3033
  //# sourceMappingURL=index.mjs.map