@powerhousedao/builder-tools 6.2.0-dev.2 → 6.2.0-dev.21

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,8 +1,9 @@
1
1
  import { createRequire } from "node:module";
2
- import { exec, execSync } from "node:child_process";
3
- import fs, { existsSync, readFileSync, realpathSync } from "node:fs";
2
+ import { exec, execSync, spawn } from "node:child_process";
3
+ import { createHash, randomUUID } from "node:crypto";
4
+ import fs, { createReadStream, existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
5
+ import path, { dirname, join, resolve } from "node:path";
4
6
  import { readFile, writeFile } from "node:fs/promises";
5
- import path, { join, resolve } from "node:path";
6
7
  import { cwd } from "node:process";
7
8
  import { DEFAULT_CONNECT_CONFIG, buildRuntimeConfig, deepMerge, loadConnectEnv, normalizeBasePath, phConnectRuntimeConfigSchema, powerhousePackageSchema, setConnectEnv } from "@powerhousedao/shared/connect";
8
9
  import { getConfig } from "@powerhousedao/config/node";
@@ -10,12 +11,462 @@ import tailwind from "@tailwindcss/vite";
10
11
  import react from "@vitejs/plugin-react";
11
12
  import { createLogger, esmExternalRequirePlugin, loadEnv, searchForWorkspaceRoot } from "vite";
12
13
  import { createHtmlPlugin } from "vite-plugin-html";
14
+ import MagicString from "magic-string";
13
15
  //#region connect-utils/constants.ts
14
16
  const EXTERNAL_PACKAGES_IMPORT = "PH:EXTERNAL_PACKAGES";
15
17
  const IMPORT_SCRIPT_FILE = "external-packages.js";
16
18
  const LOCAL_PACKAGE_ID = "ph:local-package";
17
19
  const PH_DIR_NAME = ".ph";
18
20
  //#endregion
21
+ //#region connect-utils/externalize-vendor.ts
22
+ /**
23
+ * Prebuild the heavy, stable Connect dependencies into a static ESM "vendor"
24
+ * bundle so the dev server never runs (or holds) the dependency optimizer /
25
+ * module graph for them.
26
+ *
27
+ * The reactor-project preview dev-optimizes a large UI graph (Connect itself,
28
+ * design-system, document-engineering, reactor-browser, …) every session —
29
+ * ~1–2 GB resident — even though those libs don't change; only the project's
30
+ * editors do. This module builds them ONCE with `vite build` (in a throwaway
31
+ * subprocess that exits, freeing the build's peak memory): `vite build` handles
32
+ * CSS/asset imports, web workers, and WASM (Connect's in-browser PGlite ships
33
+ * all three), and a multi-entry build with `preserveEntrySignatures: 'strict'`
34
+ * dedupes shared code into shared chunks. Entries for CJS deps re-export the
35
+ * module's named API explicitly (so `import { createRoot }` works); ESM deps
36
+ * use `export *`. The build runs under `base = VENDOR_URL_PREFIX` so emitted
37
+ * asset URLs (`new URL(…, import.meta.url)` for .wasm/.data/workers) resolve to
38
+ * the dev middleware, not the SPA root.
39
+ *
40
+ * The React family stays EXTERNAL (see `VENDOR_EXTERNAL`); `esmExternalRequirePlugin`
41
+ * owns that externalization and rewrites CJS `require("react")` → import. Vendor
42
+ * chunks keep bare React imports, which the dev import map resolves to Vite's
43
+ * pre-bundled React (`devReactImportmapPlugin`) — one React instance across the
44
+ * vendor, the project's editors, and CDN-loaded editors.
45
+ *
46
+ * `devReactImportmapPlugin` consumes the result: it externalizes these
47
+ * specifiers in the long-lived dev server, points the page import map at the
48
+ * vendor URLs, and serves the bundle. With Connect vendored, the dev server only
49
+ * processes the project's own `main` + local package; HMR for them is unaffected.
50
+ */
51
+ /**
52
+ * The stable Connect libraries worth prebuilding. The React family is NOT here —
53
+ * it's externalized from the build (see `VENDOR_EXTERNAL`) so the vendor shares
54
+ * the dev server's single React instance via the import map.
55
+ */
56
+ const DEFAULT_VENDOR_INCLUDE = [
57
+ "@powerhousedao/connect",
58
+ "document-model",
59
+ "zod",
60
+ "@powerhousedao/design-system/connect",
61
+ "@powerhousedao/reactor-browser",
62
+ "@powerhousedao/document-engineering"
63
+ ];
64
+ /**
65
+ * React-family specifiers kept external to the vendor build. The vendor's chunks
66
+ * emit bare imports for these; `devReactImportmapPlugin`'s import map resolves
67
+ * them to Vite's pre-bundled React, so there is exactly one React instance.
68
+ */
69
+ const VENDOR_EXTERNAL = [
70
+ "react",
71
+ "react-dom",
72
+ "react-dom/client",
73
+ "react/jsx-runtime",
74
+ "react/jsx-dev-runtime",
75
+ "ph-bundled-packages-virtual"
76
+ ];
77
+ /** URL prefix the vendor bundle is served under by the dev middleware. */
78
+ const VENDOR_URL_PREFIX = "/__vendor__/";
79
+ /**
80
+ * Build the prebuilt vendor (once, in a throwaway subprocess) and return its dir
81
+ * + import map. Idempotent: reuses a cached vendor built for the same dep set.
82
+ * Returns null on any failure (caller falls back to a normal dev server).
83
+ */
84
+ async function prebuildConnectVendor(options) {
85
+ const include = expandIncludeSubpaths(options.dirname, options.include ?? DEFAULT_VENDOR_INCLUDE);
86
+ const external = options.external ?? VENDOR_EXTERNAL;
87
+ const vendorDir = options.vendorDir ?? join(options.dirname, "node_modules/.ph-vendor");
88
+ const importMapPath = join(vendorDir, "import-map.json");
89
+ const versionDigest = resolveVersionDigest(options.dirname, [...include, ...external]);
90
+ try {
91
+ const hit = readCacheHit(importMapPath, include, external, versionDigest);
92
+ if (hit) return {
93
+ vendorDir,
94
+ imports: hit
95
+ };
96
+ const lockDir = `${vendorDir}.lock`;
97
+ const lock = acquireLock(lockDir);
98
+ if (!lock) {
99
+ const imports = await waitForCacheHit(importMapPath, include, external, versionDigest, lockDir);
100
+ return imports ? {
101
+ vendorDir,
102
+ imports
103
+ } : null;
104
+ }
105
+ try {
106
+ const raced = readCacheHit(importMapPath, include, external, versionDigest);
107
+ if (raced) return {
108
+ vendorDir,
109
+ imports: raced
110
+ };
111
+ const imports = await buildVendorAtomic(options.dirname, vendorDir, include, external, versionDigest);
112
+ return imports ? {
113
+ vendorDir,
114
+ imports
115
+ } : null;
116
+ } finally {
117
+ releaseLock(lock);
118
+ }
119
+ } catch (err) {
120
+ if (options.errorRef) options.errorRef.message = err instanceof Error ? err.message : String(err);
121
+ return null;
122
+ }
123
+ }
124
+ function readCacheHit(importMapPath, include, external, versionDigest) {
125
+ if (!existsSync(importMapPath)) return null;
126
+ try {
127
+ const cached = JSON.parse(readFileSync(importMapPath, "utf8"));
128
+ if (sameSet(cached.include, include) && sameSet(cached.external, external) && cached.versionDigest === versionDigest) return cached.imports;
129
+ } catch {}
130
+ return null;
131
+ }
132
+ function resolveVersionDigest(dirname, specs) {
133
+ const seen = /* @__PURE__ */ new Map();
134
+ for (const spec of specs) {
135
+ const { pkg } = parsePkg(spec);
136
+ if (seen.has(pkg)) continue;
137
+ let version = "missing";
138
+ try {
139
+ const pkgRoot = realpathSync(join(dirname, "node_modules", pkg));
140
+ const meta = JSON.parse(readFileSync(join(pkgRoot, "package.json"), "utf8"));
141
+ version = String(meta.version ?? "unknown");
142
+ } catch {}
143
+ seen.set(pkg, version);
144
+ }
145
+ const h = createHash("sha256");
146
+ const workerHash = createHash("sha256").update(VENDOR_BUILD_WORKER).digest("hex");
147
+ h.update(`worker:${workerHash}\n`);
148
+ for (const pkg of [...seen.keys()].sort()) h.update(`${pkg}@${seen.get(pkg)}\n`);
149
+ return h.digest("hex").slice(0, 16);
150
+ }
151
+ function sameSet(a, b) {
152
+ if (!a || a.length !== b.length) return false;
153
+ const s = new Set(a);
154
+ return b.every((x) => s.has(x));
155
+ }
156
+ const LOCK_STALE_MS = 5 * 6e4;
157
+ const LOCK_HEARTBEAT_MS = 6e4;
158
+ const ownerFile = (lockDir) => join(lockDir, "owner");
159
+ function lockIsStale(lockDir) {
160
+ try {
161
+ return Date.now() - statSync(ownerFile(lockDir)).mtimeMs > LOCK_STALE_MS;
162
+ } catch {
163
+ return false;
164
+ }
165
+ }
166
+ function acquireLock(lockDir) {
167
+ let made = false;
168
+ try {
169
+ mkdirSync(lockDir);
170
+ made = true;
171
+ } catch {
172
+ if (lockIsStale(lockDir)) try {
173
+ rmSync(lockDir, {
174
+ recursive: true,
175
+ force: true
176
+ });
177
+ mkdirSync(lockDir);
178
+ made = true;
179
+ } catch {}
180
+ }
181
+ if (!made) return null;
182
+ const token = `${process.pid}-${randomUUID()}`;
183
+ writeFileSync(ownerFile(lockDir), token);
184
+ const timer = setInterval(() => {
185
+ try {
186
+ if (readFileSync(ownerFile(lockDir), "utf8") === token) writeFileSync(ownerFile(lockDir), token);
187
+ else clearInterval(timer);
188
+ } catch {
189
+ clearInterval(timer);
190
+ }
191
+ }, LOCK_HEARTBEAT_MS);
192
+ timer.unref();
193
+ return {
194
+ dir: lockDir,
195
+ token,
196
+ timer
197
+ };
198
+ }
199
+ function releaseLock(lock) {
200
+ clearInterval(lock.timer);
201
+ try {
202
+ if (readFileSync(ownerFile(lock.dir), "utf8") === lock.token) rmSync(lock.dir, {
203
+ recursive: true,
204
+ force: true
205
+ });
206
+ } catch {}
207
+ }
208
+ async function waitForCacheHit(importMapPath, include, external, versionDigest, lockDir) {
209
+ const deadline = Date.now() + LOCK_STALE_MS;
210
+ while (Date.now() < deadline) {
211
+ const hit = readCacheHit(importMapPath, include, external, versionDigest);
212
+ if (hit) return hit;
213
+ if (!existsSync(lockDir)) return readCacheHit(importMapPath, include, external, versionDigest);
214
+ await new Promise((r) => setTimeout(r, 250));
215
+ }
216
+ return null;
217
+ }
218
+ function importTarget(value) {
219
+ if (typeof value === "string") return value;
220
+ if (!value || typeof value !== "object") return null;
221
+ const o = value;
222
+ for (const c of [
223
+ "browser",
224
+ "import",
225
+ "module",
226
+ "default"
227
+ ]) if (c in o) {
228
+ const t = importTarget(o[c]);
229
+ if (t) return t;
230
+ }
231
+ return null;
232
+ }
233
+ function parsePkg(spec) {
234
+ if (spec.startsWith("@")) {
235
+ const parts = spec.split("/");
236
+ return {
237
+ pkg: parts.slice(0, 2).join("/"),
238
+ sub: parts.slice(2).join("/")
239
+ };
240
+ }
241
+ const i = spec.indexOf("/");
242
+ return i === -1 ? {
243
+ pkg: spec,
244
+ sub: ""
245
+ } : {
246
+ pkg: spec.slice(0, i),
247
+ sub: spec.slice(i + 1)
248
+ };
249
+ }
250
+ /**
251
+ * Connect imports the heavy libs by subpath too (e.g.
252
+ * `@powerhousedao/design-system/connect/toast`, `zod/v4/core`). Those bare
253
+ * imports need their own import-map entry, so expand each listed spec to its
254
+ * package's concrete (non-wildcard, JS) subpath exports that share the spec's
255
+ * prefix. Unresolvable / CSS / JSON targets are skipped. Failures leave the
256
+ * original spec untouched.
257
+ */
258
+ function expandIncludeSubpaths(dirname, include) {
259
+ const out = new Set(include);
260
+ for (const spec of include) {
261
+ const { pkg, sub } = parsePkg(spec);
262
+ let exp;
263
+ let pkgRoot;
264
+ try {
265
+ pkgRoot = realpathSync(join(dirname, "node_modules", pkg));
266
+ exp = JSON.parse(readFileSync(join(pkgRoot, "package.json"), "utf8")).exports;
267
+ } catch {
268
+ continue;
269
+ }
270
+ if (!exp || typeof exp !== "object") continue;
271
+ const expMap = exp;
272
+ const prefixKey = sub ? `./${sub}` : ".";
273
+ for (const key of Object.keys(expMap)) {
274
+ if (key.includes("*") || key === "./package.json") continue;
275
+ if (/\.(css|json|scss)$/.test(key)) continue;
276
+ if (!(prefixKey === "." ? key === "." || key.startsWith("./") : key === prefixKey || key.startsWith(`${prefixKey}/`))) continue;
277
+ const target = importTarget(expMap[key]);
278
+ if (!target || /\.(css|json|scss)$/.test(target)) continue;
279
+ if (!existsSync(join(pkgRoot, target))) continue;
280
+ out.add(key === "." ? pkg : pkg + key.slice(1));
281
+ }
282
+ }
283
+ return [...out];
284
+ }
285
+ /**
286
+ * Build the vendor into a unique temp dir, then atomically swap it into place,
287
+ * so a concurrent reader never sees a partial bundle or import-map.json. Runs
288
+ * the build in a throwaway subprocess (its peak memory is reclaimed on exit);
289
+ * the parent augments the import map with the version digest and does the swap.
290
+ * Returns the published import map, or null on failure.
291
+ */
292
+ async function buildVendorAtomic(dirname$1, vendorDir, include, external, versionDigest) {
293
+ const parent = dirname(vendorDir);
294
+ mkdirSync(parent, { recursive: true });
295
+ const tmpDir = mkdtempSync(join(parent, ".ph-vendor.tmp-"));
296
+ try {
297
+ const { ok, stderr } = await runBuildWorker(dirname$1, tmpDir, include, external);
298
+ const tmpMap = join(tmpDir, "import-map.json");
299
+ if (!ok || !existsSync(tmpMap)) {
300
+ const detail = stderr.trim();
301
+ throw new Error(`vendor build failed${detail ? `:\n${detail}` : " (no output captured)"}`);
302
+ }
303
+ const meta = JSON.parse(readFileSync(tmpMap, "utf8"));
304
+ meta.versionDigest = versionDigest;
305
+ writeFileSync(tmpMap, JSON.stringify(meta, null, 2));
306
+ const oldDir = `${vendorDir}.old-${process.pid}-${Date.now()}`;
307
+ if (existsSync(vendorDir)) renameSync(vendorDir, oldDir);
308
+ renameSync(tmpDir, vendorDir);
309
+ rmSync(oldDir, {
310
+ recursive: true,
311
+ force: true
312
+ });
313
+ return meta.imports;
314
+ } catch (err) {
315
+ rmSync(tmpDir, {
316
+ recursive: true,
317
+ force: true
318
+ });
319
+ throw err;
320
+ }
321
+ }
322
+ /**
323
+ * Spawn a throwaway worker that generates one entry per specifier (CJS deps get
324
+ * explicit named re-exports, ESM deps `export *`), `vite build`s them into the
325
+ * given out dir, and writes the import map. Captures stdout/stderr so a failure
326
+ * is debuggable; the normal path stays quiet.
327
+ */
328
+ function runBuildWorker(dirname, outDir, include, external) {
329
+ const workerPath = join(outDir, "build-worker.mjs");
330
+ writeFileSync(workerPath, VENDOR_BUILD_WORKER);
331
+ return new Promise((resolve) => {
332
+ const child = spawn(process.execPath, [
333
+ workerPath,
334
+ dirname,
335
+ outDir,
336
+ JSON.stringify(include),
337
+ VENDOR_URL_PREFIX,
338
+ JSON.stringify(external)
339
+ ], {
340
+ cwd: dirname,
341
+ stdio: [
342
+ "ignore",
343
+ "pipe",
344
+ "pipe"
345
+ ]
346
+ });
347
+ let stderr = "";
348
+ child.stdout.on("data", (d) => {
349
+ stderr += String(d);
350
+ });
351
+ child.stderr.on("data", (d) => {
352
+ stderr += String(d);
353
+ });
354
+ child.on("exit", (code) => resolve({
355
+ ok: code === 0,
356
+ stderr
357
+ }));
358
+ child.on("error", (e) => resolve({
359
+ ok: false,
360
+ stderr: String(e)
361
+ }));
362
+ });
363
+ }
364
+ /**
365
+ * The vendor build worker, written to disk and run as a subprocess. Self-contained
366
+ * (only Node + the project's `vite`). argv: dirname, vendorDir, includeJSON, urlPrefix.
367
+ */
368
+ const VENDOR_BUILD_WORKER = `
369
+ import { createRequire } from 'node:module';
370
+ import { mkdirSync, writeFileSync, rmSync } from 'node:fs';
371
+ import { join, isAbsolute } from 'node:path';
372
+ import { fileURLToPath } from 'node:url';
373
+ const [dirname, vendorDir, includeJSON, urlPrefix, externalJSON] = process.argv.slice(2);
374
+ const include = JSON.parse(includeJSON);
375
+ const external = JSON.parse(externalJSON ?? '[]');
376
+ const externalSet = new Set(external);
377
+ const reqProj = createRequire(join(dirname, 'noop.js'));
378
+ const { build, esmExternalRequirePlugin } = await import(reqProj.resolve('vite'));
379
+ const srcDir = join(vendorDir, '.entries');
380
+ mkdirSync(srcDir, { recursive: true });
381
+ const entryName = (spec) => spec.replace(/[^\\w]+/g, '_');
382
+ const RESERVED = new Set('enum void null function in instanceof typeof new delete do if else return switch case break continue for while this true false class const let var default export import extends super with yield debugger finally throw try catch await implements interface package private protected public static eval arguments'.split(' '));
383
+ const input = {};
384
+ for (const spec of include) {
385
+ const name = entryName(spec);
386
+ let src;
387
+ try {
388
+ // Import the bare spec (not reqProj.resolve(spec)) so Node picks the same
389
+ // import/browser-condition module the build resolves — resolving first can
390
+ // pick a CJS sibling whose default-export shape differs from the ESM build.
391
+ const ns = await import(spec);
392
+ // Only fall back to re-exporting from default when the module exposes NO
393
+ // top-level named exports (a true CJS-interop module). If it does (e.g. zod
394
+ // exposes \`z\`), \`export *\` captures them; destructuring default would miss
395
+ // top-level names that aren't keys of the default object.
396
+ const named = Object.keys(ns).filter((k) => k !== 'default');
397
+ const cjs = named.length === 0 && ns.default && typeof ns.default === 'object';
398
+ if (cjs) {
399
+ const names = Object.keys(ns.default).filter((k) => k !== 'default' && k !== '__esModule' && /^[A-Za-z_$][\\w$]*$/.test(k));
400
+ const plain = names.filter((n) => !RESERVED.has(n));
401
+ const reserved = names.filter((n) => RESERVED.has(n));
402
+ src = 'import d from ' + JSON.stringify(spec) + ';\\nexport default d;\\n'
403
+ + (plain.length ? 'export const { ' + plain.join(', ') + ' } = d;\\n' : '');
404
+ // reserved words are invalid as const-binding names but valid as export
405
+ // aliases (export { x as enum }).
406
+ reserved.forEach((n, i) => {
407
+ src += 'const __r' + i + ' = d[' + JSON.stringify(n) + '];\\nexport { __r' + i + ' as ' + n + ' };\\n';
408
+ });
409
+ } else {
410
+ src = 'export * from ' + JSON.stringify(spec) + ';\\n'
411
+ + (('default' in ns) ? 'export { default } from ' + JSON.stringify(spec) + ';\\n' : '');
412
+ }
413
+ } catch {
414
+ src = 'export * from ' + JSON.stringify(spec) + ';\\n';
415
+ }
416
+ const file = join(srcDir, name + '.js');
417
+ writeFileSync(file, src);
418
+ input[name] = file;
419
+ }
420
+ // Prefer the bundler's browser-condition-aware resolution; fall back to resolving
421
+ // from the worker (real install path) only for Rolldown's realpath-anchoring bug.
422
+ const phResolveCache = new Map();
423
+ const phVendorResolve = {
424
+ name: 'ph-vendor-resolve', enforce: 'pre',
425
+ async resolveId(source, importer, options) {
426
+ if (externalSet.has(source)) return null;
427
+ if (source[0] === '\\0' || source[0] === '.' || isAbsolute(source)) return null;
428
+ if (source.startsWith('node:') || source.startsWith('data:')) return null;
429
+ let viaBundler = null;
430
+ try { viaBundler = await this.resolve(source, importer, { ...options, skipSelf: true }); } catch {}
431
+ if (viaBundler) return viaBundler;
432
+ if (phResolveCache.has(source)) return phResolveCache.get(source);
433
+ let resolved = null;
434
+ try { resolved = fileURLToPath(import.meta.resolve(source)); } catch {}
435
+ phResolveCache.set(source, resolved);
436
+ return resolved;
437
+ },
438
+ };
439
+ await build({
440
+ root: dirname, configFile: false, logLevel: 'error',
441
+ // Emit asset URLs (pglite .wasm/.data, workers via \`new URL(..., import.meta.url)\`)
442
+ // under the vendor prefix so they resolve to the middleware, not the SPA root.
443
+ base: urlPrefix,
444
+ define: { 'process.env.NODE_ENV': '"development"' },
445
+ // phVendorResolve runs first (pre) so bare specifiers resolve from the worker,
446
+ // not the importing module's realpath. esmExternalRequirePlugin then OWNS
447
+ // externalization of \`external\` (react family + dev virtuals): it marks them
448
+ // external AND rewrites CJS require("react") to import (Rolldown keeps bare
449
+ // require() for external ids, which fails in the browser). It must be the sole
450
+ // externalizer — also listing them in rollupOptions.external prevents the
451
+ // require() rewrite (see vite-config.ts).
452
+ plugins: [phVendorResolve, esmExternalRequirePlugin({ external })],
453
+ // pglite (and any worker dep) ships web workers; emit them as ES-module asset
454
+ // chunks into the vendor so the middleware can serve them.
455
+ worker: { format: 'es' },
456
+ build: {
457
+ outDir: vendorDir, emptyOutDir: false, minify: false, target: 'esnext', cssCodeSplit: true,
458
+ rollupOptions: {
459
+ input, preserveEntrySignatures: 'strict',
460
+ output: { format: 'es', entryFileNames: '[name].js', chunkFileNames: 'chunks/[name]-[hash].js', assetFileNames: 'assets/[name]-[hash][extname]' },
461
+ },
462
+ },
463
+ });
464
+ rmSync(srcDir, { recursive: true, force: true });
465
+ const imports = {};
466
+ for (const spec of include) imports[spec] = urlPrefix + entryName(spec) + '.js';
467
+ writeFileSync(join(vendorDir, 'import-map.json'), JSON.stringify({ include, external, imports }, null, 2));
468
+ `;
469
+ //#endregion
19
470
  //#region connect-utils/helpers.ts
20
471
  const DEFAULT_CONNECT_OUTDIR = ".ph/connect-build/dist/";
21
472
  function resolveViteConfigPath(options) {
@@ -246,6 +697,54 @@ const runtimeConfigSchema = {
246
697
  connect: phConnectRuntimeConfigSchema
247
698
  }
248
699
  };
700
+ const RESOLVED_VIRTUAL_ID = "\0virtual:ph-bundled-packages-virtual";
701
+ const BUNDLED_PACKAGES_DEV_URL = "@id/" + RESOLVED_VIRTUAL_ID.replace("\0", "__x00__");
702
+ function readBundledPackageVersion(projectRoot, name) {
703
+ try {
704
+ const raw = fs.readFileSync(path.join(projectRoot, "node_modules", name, "package.json"), "utf-8");
705
+ const pkg = JSON.parse(raw);
706
+ return typeof pkg.version === "string" ? pkg.version : void 0;
707
+ } catch {
708
+ return;
709
+ }
710
+ }
711
+ function makeRegisterModule(packages, projectRoot) {
712
+ if (packages.length === 0) return "export default () => {};\n";
713
+ const imports = [];
714
+ const calls = [];
715
+ packages.forEach((name, i) => {
716
+ const moduleName = `pkg${i}`;
717
+ const version = readBundledPackageVersion(projectRoot, name);
718
+ imports.push(`import * as ${moduleName} from ${JSON.stringify(name)};`);
719
+ imports.push(`import ${JSON.stringify(`${name}/style.css`)};`);
720
+ calls.push(` pm.addLocalPackage(${JSON.stringify(name)}, ${moduleName}, ${JSON.stringify(version)});`);
721
+ });
722
+ return `${imports.join("\n")}\n\nexport default function register(pm) {\n${calls.join("\n")}\n};\n`;
723
+ }
724
+ /**
725
+ * Emits a virtual module `ph-bundled-packages-virtual` whose default export
726
+ * is a `register(packageManager)` function. When called at runtime (from
727
+ * Connect's bootstrap), it registers each bundled package with the package
728
+ * manager the same way Common/Vetra are registered — meaning they work
729
+ * offline without the registry being reachable.
730
+ *
731
+ * When the list is empty, the module exports a no-op function so Connect's
732
+ * bootstrap code can always import it unconditionally.
733
+ */
734
+ function phBundledPackagesPlugin(options) {
735
+ const projectRoot = options.projectRoot ?? process.cwd();
736
+ const moduleSource = makeRegisterModule(options.packages, projectRoot);
737
+ return {
738
+ name: "vite-plugin-ph-bundled-packages",
739
+ enforce: "pre",
740
+ resolveId(id) {
741
+ if (id === "ph-bundled-packages-virtual") return RESOLVED_VIRTUAL_ID;
742
+ },
743
+ load(id) {
744
+ if (id === "\0virtual:ph-bundled-packages-virtual") return moduleSource;
745
+ }
746
+ };
747
+ }
249
748
  //#endregion
250
749
  //#region connect-utils/vite-plugins/dev-external-react.ts
251
750
  const REACT_DEPS = [
@@ -257,9 +756,36 @@ const REACT_DEPS = [
257
756
  ];
258
757
  const SHIM_PATH = "__ph/dev-react-shim/";
259
758
  const VITE_DEPS_PATH = "node_modules/.vite/deps";
759
+ const VENDOR_MIME = {
760
+ ".css": "text/css",
761
+ ".map": "application/json",
762
+ ".json": "application/json",
763
+ ".wasm": "application/wasm",
764
+ ".data": "application/octet-stream",
765
+ ".woff2": "font/woff2",
766
+ ".woff": "font/woff",
767
+ ".ttf": "font/ttf",
768
+ ".svg": "image/svg+xml",
769
+ ".png": "image/png",
770
+ ".jpg": "image/jpeg",
771
+ ".jpeg": "image/jpeg",
772
+ ".gif": "image/gif",
773
+ ".webp": "image/webp"
774
+ };
775
+ const VENDOR_MODE = process.env.PH_CONNECT_EXTERNALIZE_VENDOR === "1";
776
+ const VENDOR_EXTRA = (process.env.PH_CONNECT_VENDOR_EXTRA ?? "").split(",").map((s) => s.trim()).filter(Boolean);
777
+ const HEAVY_LIBS = [...DEFAULT_VENDOR_INCLUDE, ...VENDOR_EXTRA];
260
778
  function withBase(base, p) {
261
779
  return `${base}${p}`.replace(/\/{2,}/g, "/");
262
780
  }
781
+ function externalizePlugin(externals) {
782
+ try {
783
+ const mod = createRequire(import.meta.url)("vite-plugin-externalize-dependencies");
784
+ return (mod.default ?? mod)({ externals });
785
+ } catch {
786
+ return;
787
+ }
788
+ }
263
789
  /**
264
790
  * Dev-only sibling of `esmExternalRequirePlugin`. The build path externalizes
265
791
  * React via Rolldown so an importmap hands the same React instance to both
@@ -268,7 +794,7 @@ function withBase(base, p) {
268
794
  * export — so a CDN editor that does `import { lazy } from "react"` would
269
795
  * fail with "no named export 'lazy'".
270
796
  *
271
- * This plugin:
797
+ * This plugin owns the page import map. It always:
272
798
  * 1. Forces React into `optimizeDeps.include` so the optimizer always knows
273
799
  * about it.
274
800
  * 2. Serves a shim per React module at a stable URL. Each shim imports
@@ -276,14 +802,46 @@ function withBase(base, p) {
276
802
  * and re-exports React's named members so editors importing
277
803
  * `{ lazy }`, `{ jsx }`, etc. work.
278
804
  * 3. Rewrites the page importmap to point at those shim URLs.
805
+ *
806
+ * When PH_CONNECT_EXTERNALIZE_VENDOR=1 it additionally prebuilds the heavy
807
+ * stable libs (design-system, reactor-browser, …) into a static vendor bundle,
808
+ * externalizes them from the dev server, serves the bundle, and adds them to
809
+ * the SAME import map. The vendor's React imports stay bare → resolve through
810
+ * the React shims above → one React instance across Connect, the vendor, and
811
+ * CDN editors.
279
812
  */
280
- function devReactImportmapPlugin() {
813
+ async function devReactImportmapPlugin(projectRoot = process.cwd()) {
281
814
  let namedExports = /* @__PURE__ */ new Map();
282
815
  let base = "/";
283
- return {
816
+ let vendor = null;
817
+ if (VENDOR_MODE) {
818
+ const errorRef = {};
819
+ vendor = await prebuildConnectVendor({
820
+ dirname: projectRoot,
821
+ include: HEAVY_LIBS,
822
+ errorRef
823
+ });
824
+ if (!vendor) {
825
+ const detail = errorRef.message ? `: ${errorRef.message}` : "";
826
+ console.warn(`[connect] PH_CONNECT_EXTERNALIZE_VENDOR set but vendor prebuild failed; falling back to dep-optimizing the heavy libs${detail}`);
827
+ }
828
+ }
829
+ const ext = vendor ? externalizePlugin([(id) => id in vendor.imports]) : void 0;
830
+ if (vendor && !ext) console.warn("[connect] PH_CONNECT_EXTERNALIZE_VENDOR set and vendor prebuilt, but vite-plugin-externalize-dependencies is not installed; falling back to dep-optimizing the heavy libs (install it to enable the vendor)");
831
+ const vendorActive = !!(vendor && ext);
832
+ const main = {
284
833
  name: "ph-dev-react-importmap",
285
834
  apply: "serve",
286
- config: () => ({ optimizeDeps: { include: REACT_DEPS } }),
835
+ config(cfg) {
836
+ cfg.optimizeDeps ??= {};
837
+ const include = new Set(cfg.optimizeDeps.include ?? []);
838
+ REACT_DEPS.forEach((d) => include.add(d));
839
+ if (vendorActive) {
840
+ HEAVY_LIBS.forEach((d) => include.delete(d));
841
+ cfg.optimizeDeps.exclude = [...new Set([...cfg.optimizeDeps.exclude ?? [], ...HEAVY_LIBS])];
842
+ }
843
+ cfg.optimizeDeps.include = [...include];
844
+ },
287
845
  configResolved(config) {
288
846
  base = config.base;
289
847
  },
@@ -297,6 +855,26 @@ function devReactImportmapPlugin() {
297
855
  return [id, []];
298
856
  }
299
857
  }));
858
+ if (vendorActive) {
859
+ const vendorDir = vendor.vendorDir;
860
+ server.middlewares.use(VENDOR_URL_PREFIX, (req, res, next) => {
861
+ const name = (req.url ?? "").split("?")[0].replace(/^\/+/, "");
862
+ const file = path.join(vendorDir, name);
863
+ const rel = path.relative(vendorDir, file);
864
+ if (!name || rel.startsWith("..") || path.isAbsolute(rel)) return next();
865
+ const stream = createReadStream(file);
866
+ stream.on("error", () => {
867
+ if (!res.headersSent) next();
868
+ });
869
+ stream.once("open", () => {
870
+ const ext = file.slice(file.lastIndexOf("."));
871
+ res.setHeader("Content-Type", VENDOR_MIME[ext] ?? "text/javascript");
872
+ const hashed = name.startsWith("chunks/") || name.startsWith("assets/");
873
+ res.setHeader("Cache-Control", hashed ? "public, max-age=31536000, immutable" : "no-cache");
874
+ stream.pipe(res);
875
+ });
876
+ });
877
+ }
300
878
  const shimPrefixes = [withBase(base, SHIM_PATH), `/${SHIM_PATH}`];
301
879
  server.middlewares.use((req, res, next) => {
302
880
  const prefix = shimPrefixes.find((p) => req.url?.startsWith(p));
@@ -324,10 +902,103 @@ function devReactImportmapPlugin() {
324
902
  if (!browserHash) return;
325
903
  const shimPrefix = withBase(base, SHIM_PATH);
326
904
  const imports = Object.fromEntries(REACT_DEPS.map((id) => [id, `${shimPrefix}${id}.js?v=${browserHash}`]));
905
+ if (vendorActive) {
906
+ Object.assign(imports, vendor.imports);
907
+ if (vendor.imports["@powerhousedao/connect"]) imports["ph-bundled-packages-virtual"] = withBase(base, BUNDLED_PACKAGES_DEV_URL);
908
+ }
327
909
  return html.replace(/<script type="importmap">[\s\S]*?<\/script>/, `<script type="importmap">${JSON.stringify({ imports }, null, 2)}<\/script>`);
328
910
  }
329
911
  }
330
912
  };
913
+ return vendorActive ? [ext, main] : main;
914
+ }
915
+ //#endregion
916
+ //#region connect-utils/vite-plugins/dynamic-base.ts
917
+ /**
918
+ * Placeholder base used when Connect is built in dynamic-base mode. The Vite
919
+ * `base` option is set to this token; this plugin rewrites it in the emitted
920
+ * output so the effective base is resolved at serve time from a global instead
921
+ * of being baked at build time.
922
+ *
923
+ * Trailing slash matches `normalizeBasePath` output, so the token sits in the
924
+ * same syntactic position as a concrete base would.
925
+ */
926
+ const DYNAMIC_BASE_PLACEHOLDER = "/__PH_DYNAMIC_BASE__/";
927
+ /**
928
+ * Global the runtime (ph-clint proxy) must set before the entry bundle loads.
929
+ * Value is the normalized deploy base, e.g. "/myagent/" or "/". The JS rewrite
930
+ * below resolves all asset / lazy-chunk / BASE_URL references against it.
931
+ */
932
+ const RUNTIME_GLOBAL = "globalThis.__PH_DYNAMIC_BASE__";
933
+ const BASE_EXPR = `(${RUNTIME_GLOBAL}||"/")`;
934
+ /**
935
+ * Worker scope has its own global object; the proxy injects the runtime global
936
+ * into the main-thread `<head>` only, so a worker chunk's rewritten references
937
+ * would fall back to "/" and fetch siblings (pglite .data/.wasm) without the
938
+ * deploy prefix. Prepended to worker chunks, this defines the global in worker
939
+ * scope from the worker's own script URL: the worker loads from
940
+ * `<base>assets/<name>-<hash>.js`, so stripping `assets/<file>` off
941
+ * `self.location.pathname` yields the same trailing-slash base the main thread
942
+ * holds ("/myagent/" or "/").
943
+ */
944
+ const WORKER_PRELUDE = `${RUNTIME_GLOBAL}=self.location.pathname.replace(/assets\\/[^/]*$/,"");\n`;
945
+ const PLACEHOLDER_LITERAL = new RegExp(`(["'\`])${escapeForRegExp(DYNAMIC_BASE_PLACEHOLDER)}((?:(?!\\1)[^$])*)\\1`, "g");
946
+ /**
947
+ * Rewrites the placeholder base in emitted JS chunks to a runtime expression so
948
+ * one built `dist/connect` serves under any subpath. Rolldown-native: per-match
949
+ * MagicString edits in `renderChunk`, no AST splicing (the SWC byte-offset
950
+ * splicing in vite-plugin-dynamic-base corrupts Rolldown chunks). Each edited
951
+ * chunk returns a hires sourcemap that the bundler composes into the chunk's
952
+ * map chain, so the emitted `.map` files track the rewrite (and the worker
953
+ * prelude's line shift).
954
+ *
955
+ * What gets rewritten in JS, all of which emit the base as a quoted string
956
+ * literal beginning with the placeholder:
957
+ * - asset URLs (`new URL("/__PH_DYNAMIC_BASE__/assets/x.png", ...)`)
958
+ * - dynamic-import / lazy-chunk preload URLs
959
+ * - the inlined `import.meta.env.BASE_URL` (drives Connect's router basename
960
+ * and BASE_URL-relative fetches such as `${BASE_URL}ph-packages.json`)
961
+ *
962
+ * A literal `"/__PH_DYNAMIC_BASE__/foo"` becomes `((globalThis.__PH_DYNAMIC_BASE__||"/")+"foo")`;
963
+ * a bare `"/__PH_DYNAMIC_BASE__/"` (the BASE_URL value) becomes `(globalThis.__PH_DYNAMIC_BASE__||"/")`.
964
+ *
965
+ * HTML is left untouched: the entry `<script>`/`<link>` tags keep the literal
966
+ * placeholder so the proxy substitutes it with the concrete base at serve time
967
+ * (the same proxy also sets the runtime global for the JS rewrite). See the
968
+ * plugin's module doc / report for the exact serve-time contract.
969
+ *
970
+ * CSS `url(...)` references resolve relative to the stylesheet's own URL, so
971
+ * they need no rewrite once the stylesheet itself is loaded from the right
972
+ * prefix (which the HTML substitution handles).
973
+ */
974
+ function connectDynamicBasePlugin(options = {}) {
975
+ return {
976
+ name: "ph-connect-dynamic-base",
977
+ enforce: "post",
978
+ renderChunk(code, chunk) {
979
+ if (!code.includes("/__PH_DYNAMIC_BASE__/")) return null;
980
+ const s = new MagicString(code);
981
+ for (const match of code.matchAll(PLACEHOLDER_LITERAL)) {
982
+ const rest = match[2];
983
+ s.overwrite(match.index, match.index + match[0].length, rest.length === 0 ? BASE_EXPR : `(${BASE_EXPR}+${JSON.stringify(rest)})`);
984
+ }
985
+ if (options.forWorker) {
986
+ s.prepend(WORKER_PRELUDE);
987
+ this.info(`dynamic-base: worker prelude prepended to ${chunk.fileName}`);
988
+ }
989
+ if (!s.hasChanged()) return null;
990
+ return {
991
+ code: s.toString(),
992
+ map: s.generateMap({ hires: true })
993
+ };
994
+ },
995
+ generateBundle(_options, bundle) {
996
+ for (const file of Object.values(bundle)) if ((file.type === "chunk" ? file.code : file.fileName.endsWith(".css") && typeof file.source === "string" ? file.source : void 0)?.includes("/__PH_DYNAMIC_BASE__/")) this.error(`dynamic-base: unrewritten placeholder ${DYNAMIC_BASE_PLACEHOLDER} remains in ${file.fileName}`);
997
+ }
998
+ };
999
+ }
1000
+ function escapeForRegExp(s) {
1001
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
331
1002
  }
332
1003
  //#endregion
333
1004
  //#region connect-utils/vite-plugins/favicon.ts
@@ -365,56 +1036,6 @@ function connectFaviconPlugin() {
365
1036
  };
366
1037
  }
367
1038
  //#endregion
368
- //#region connect-utils/vite-plugins/ph-bundled-packages.ts
369
- const VIRTUAL_ID = "ph-bundled-packages-virtual";
370
- const RESOLVED_VIRTUAL_ID = "\0virtual:" + VIRTUAL_ID;
371
- function readBundledPackageVersion(projectRoot, name) {
372
- try {
373
- const raw = fs.readFileSync(path.join(projectRoot, "node_modules", name, "package.json"), "utf-8");
374
- const pkg = JSON.parse(raw);
375
- return typeof pkg.version === "string" ? pkg.version : void 0;
376
- } catch {
377
- return;
378
- }
379
- }
380
- function makeRegisterModule(packages, projectRoot) {
381
- if (packages.length === 0) return "export default () => {};\n";
382
- const imports = [];
383
- const calls = [];
384
- packages.forEach((name, i) => {
385
- const moduleName = `pkg${i}`;
386
- const version = readBundledPackageVersion(projectRoot, name);
387
- imports.push(`import * as ${moduleName} from ${JSON.stringify(name)};`);
388
- imports.push(`import ${JSON.stringify(`${name}/style.css`)};`);
389
- calls.push(` pm.addLocalPackage(${JSON.stringify(name)}, ${moduleName}, ${JSON.stringify(version)});`);
390
- });
391
- return `${imports.join("\n")}\n\nexport default function register(pm) {\n${calls.join("\n")}\n};\n`;
392
- }
393
- /**
394
- * Emits a virtual module `ph-bundled-packages-virtual` whose default export
395
- * is a `register(packageManager)` function. When called at runtime (from
396
- * Connect's bootstrap), it registers each bundled package with the package
397
- * manager the same way Common/Vetra are registered — meaning they work
398
- * offline without the registry being reachable.
399
- *
400
- * When the list is empty, the module exports a no-op function so Connect's
401
- * bootstrap code can always import it unconditionally.
402
- */
403
- function phBundledPackagesPlugin(options) {
404
- const projectRoot = options.projectRoot ?? process.cwd();
405
- const moduleSource = makeRegisterModule(options.packages, projectRoot);
406
- return {
407
- name: "vite-plugin-ph-bundled-packages",
408
- enforce: "pre",
409
- resolveId(id) {
410
- if (id === VIRTUAL_ID) return RESOLVED_VIRTUAL_ID;
411
- },
412
- load(id) {
413
- if (id === RESOLVED_VIRTUAL_ID) return moduleSource;
414
- }
415
- };
416
- }
417
- //#endregion
418
1039
  //#region connect-utils/vite-plugins/ph-config.ts
419
1040
  function readProjectPackageInfo(projectRoot) {
420
1041
  if (!projectRoot) return null;
@@ -670,7 +1291,7 @@ function getConnectBaseViteConfig(options) {
670
1291
  return {
671
1292
  configFile: false,
672
1293
  mode,
673
- base: connectBasePath ? normalizeBasePath(connectBasePath) : void 0,
1294
+ base: options.dynamicBase ? DYNAMIC_BASE_PLACEHOLDER : connectBasePath ? normalizeBasePath(connectBasePath) : void 0,
674
1295
  server: {
675
1296
  watch: { ignored: ["**/backup-documents/**", "**/.ph/**"] },
676
1297
  fs: { allow: [searchForWorkspaceRoot(options.dirname), ...linkedRoots] }
@@ -704,16 +1325,20 @@ function getConnectBaseViteConfig(options) {
704
1325
  packages: localPackagesFromConfig,
705
1326
  projectRoot: options.dirname
706
1327
  }),
707
- devReactImportmapPlugin(),
1328
+ devReactImportmapPlugin(options.dirname),
708
1329
  ...plugins,
709
1330
  esmExternalRequirePlugin({ external: reactExternal }),
710
- connectFaviconPlugin()
1331
+ connectFaviconPlugin(),
1332
+ ...options.dynamicBase ? [connectDynamicBasePlugin()] : []
711
1333
  ],
712
- worker: { format: "es" },
1334
+ worker: {
1335
+ format: "es",
1336
+ ...options.dynamicBase ? { plugins: () => [connectDynamicBasePlugin({ forWorker: true })] } : {}
1337
+ },
713
1338
  build: { sourcemap: true }
714
1339
  };
715
1340
  }
716
1341
  //#endregion
717
- export { DEFAULT_CONNECT_OUTDIR, EXTERNAL_PACKAGES_IMPORT, IMPORT_SCRIPT_FILE, LOCAL_PACKAGE_ID, PH_DIR_NAME, RUNTIME_CONFIG_SCHEMA_ID, RUNTIME_CONFIG_SCHEMA_URL, appendToHtmlHead, backupIndexHtml, copyConnect, ensureNodeVersion, getConnectBaseViteConfig, getConnectHtmlTags, makeImportScriptFromPackages, phConfigPlugin, prependToHtmlHead, readJsonFile, removeBase64EnvValues, resolveConnectBundle, resolveConnectPackageJson, resolveConnectPublicDir, resolvePackage, resolveViteConfigPath, runShellScriptPlugin, runTsc, runtimeConfigSchema, stripVersionFromPackage };
1342
+ export { 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, VENDOR_EXTERNAL, VENDOR_URL_PREFIX, appendToHtmlHead, backupIndexHtml, connectDynamicBasePlugin, copyConnect, ensureNodeVersion, getConnectBaseViteConfig, getConnectHtmlTags, makeImportScriptFromPackages, phConfigPlugin, prebuildConnectVendor, prependToHtmlHead, readJsonFile, removeBase64EnvValues, resolveConnectBundle, resolveConnectPackageJson, resolveConnectPublicDir, resolvePackage, resolveViteConfigPath, runShellScriptPlugin, runTsc, runtimeConfigSchema, stripVersionFromPackage };
718
1343
 
719
1344
  //# sourceMappingURL=index.mjs.map