@powerhousedao/builder-tools 6.2.0-dev.8 → 6.2.0-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,8 +1,11 @@
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";
6
+ import { fileURLToPath } from "node:url";
7
+ import MagicString from "magic-string";
4
8
  import { readFile, writeFile } from "node:fs/promises";
5
- import path, { join, resolve } from "node:path";
6
9
  import { cwd } from "node:process";
7
10
  import { DEFAULT_CONNECT_CONFIG, buildRuntimeConfig, deepMerge, loadConnectEnv, normalizeBasePath, phConnectRuntimeConfigSchema, powerhousePackageSchema, setConnectEnv } from "@powerhousedao/shared/connect";
8
11
  import { getConfig } from "@powerhousedao/config/node";
@@ -10,13 +13,552 @@ import tailwind from "@tailwindcss/vite";
10
13
  import react from "@vitejs/plugin-react";
11
14
  import { createLogger, esmExternalRequirePlugin, loadEnv, searchForWorkspaceRoot } from "vite";
12
15
  import { createHtmlPlugin } from "vite-plugin-html";
13
- import MagicString from "magic-string";
14
16
  //#region connect-utils/constants.ts
15
17
  const EXTERNAL_PACKAGES_IMPORT = "PH:EXTERNAL_PACKAGES";
16
18
  const IMPORT_SCRIPT_FILE = "external-packages.js";
17
19
  const LOCAL_PACKAGE_ID = "ph:local-package";
18
20
  const PH_DIR_NAME = ".ph";
19
21
  //#endregion
22
+ //#region connect-utils/vite-plugins/dynamic-base.ts
23
+ /**
24
+ * Placeholder base used when Connect is built in dynamic-base mode. The Vite
25
+ * `base` option is set to this token; this plugin rewrites it in the emitted
26
+ * output so the effective base is resolved at serve time from a global instead
27
+ * of being baked at build time.
28
+ *
29
+ * Trailing slash matches `normalizeBasePath` output, so the token sits in the
30
+ * same syntactic position as a concrete base would.
31
+ */
32
+ const DYNAMIC_BASE_PLACEHOLDER = "/__PH_DYNAMIC_BASE__/";
33
+ /**
34
+ * Global the runtime (ph-clint proxy) must set before the entry bundle loads.
35
+ * Value is the normalized deploy base, e.g. "/myagent/" or "/". The JS rewrite
36
+ * below resolves all asset / lazy-chunk / BASE_URL references against it.
37
+ */
38
+ const RUNTIME_GLOBAL = "globalThis.__PH_DYNAMIC_BASE__";
39
+ const BASE_EXPR = `(${RUNTIME_GLOBAL}||"/")`;
40
+ function workerPrelude(stripPrefix) {
41
+ return `${RUNTIME_GLOBAL}=self.location.pathname.replace(/${escapeForRegExp(stripPrefix).replace(/\//g, "\\/")}assets\\/[^/]*$/,"");\n`;
42
+ }
43
+ const PLACEHOLDER_LITERAL = new RegExp(`(["'\`])${escapeForRegExp(DYNAMIC_BASE_PLACEHOLDER)}((?:(?!\\1)[^$])*)\\1`, "g");
44
+ /**
45
+ * Rewrites the placeholder base in emitted JS chunks to a runtime expression so
46
+ * one built `dist/connect` serves under any subpath. Rolldown-native: per-match
47
+ * MagicString edits in `renderChunk`, no AST splicing (the SWC byte-offset
48
+ * splicing in vite-plugin-dynamic-base corrupts Rolldown chunks). Each edited
49
+ * chunk returns a hires sourcemap that the bundler composes into the chunk's
50
+ * map chain, so the emitted `.map` files track the rewrite (and the worker
51
+ * prelude's line shift).
52
+ *
53
+ * What gets rewritten in JS, all of which emit the base as a quoted string
54
+ * literal beginning with the placeholder:
55
+ * - asset URLs (`new URL("/__PH_DYNAMIC_BASE__/assets/x.png", ...)`)
56
+ * - dynamic-import / lazy-chunk preload URLs
57
+ * - the inlined `import.meta.env.BASE_URL` (drives Connect's router basename
58
+ * and BASE_URL-relative fetches such as `${BASE_URL}ph-packages.json`)
59
+ *
60
+ * A literal `"/__PH_DYNAMIC_BASE__/foo"` becomes `((globalThis.__PH_DYNAMIC_BASE__||"/")+"foo")`;
61
+ * a bare `"/__PH_DYNAMIC_BASE__/"` (the BASE_URL value) becomes `(globalThis.__PH_DYNAMIC_BASE__||"/")`.
62
+ *
63
+ * HTML is left untouched: the entry `<script>`/`<link>` tags keep the literal
64
+ * placeholder so the proxy substitutes it with the concrete base at serve time
65
+ * (the same proxy also sets the runtime global for the JS rewrite). See the
66
+ * plugin's module doc / report for the exact serve-time contract.
67
+ *
68
+ * CSS `url(...)` references resolve relative to the stylesheet's own URL, so
69
+ * they need no rewrite once the stylesheet itself is loaded from the right
70
+ * prefix (which the HTML substitution handles).
71
+ */
72
+ function connectDynamicBasePlugin(options = {}) {
73
+ return {
74
+ name: "ph-connect-dynamic-base",
75
+ enforce: "post",
76
+ renderChunk(code, chunk) {
77
+ if (!code.includes("/__PH_DYNAMIC_BASE__/")) return null;
78
+ const s = new MagicString(code);
79
+ for (const match of code.matchAll(PLACEHOLDER_LITERAL)) {
80
+ const rest = match[2];
81
+ s.overwrite(match.index, match.index + match[0].length, rest.length === 0 ? BASE_EXPR : `(${BASE_EXPR}+${JSON.stringify(rest)})`);
82
+ }
83
+ if (options.forWorker) {
84
+ s.prepend(workerPrelude(options.workerStripPrefix ?? ""));
85
+ this.info(`dynamic-base: worker prelude prepended to ${chunk.fileName}`);
86
+ }
87
+ if (!s.hasChanged()) return null;
88
+ return {
89
+ code: s.toString(),
90
+ map: s.generateMap({ hires: true })
91
+ };
92
+ },
93
+ generateBundle(_options, bundle) {
94
+ 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}`);
95
+ }
96
+ };
97
+ }
98
+ function escapeForRegExp(s) {
99
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
100
+ }
101
+ //#endregion
102
+ //#region connect-utils/externalize-vendor.ts
103
+ /**
104
+ * Prebuild the heavy, stable Connect dependencies into a static ESM "vendor"
105
+ * bundle so the dev server never runs (or holds) the dependency optimizer /
106
+ * module graph for them.
107
+ *
108
+ * The reactor-project preview dev-optimizes a large UI graph (Connect itself,
109
+ * design-system, document-engineering, reactor-browser, …) every session —
110
+ * ~1–2 GB resident — even though those libs don't change; only the project's
111
+ * editors do. This module builds them ONCE with `vite build` (in a throwaway
112
+ * subprocess that exits, freeing the build's peak memory): `vite build` handles
113
+ * CSS/asset imports, web workers, and WASM (Connect's in-browser PGlite ships
114
+ * all three), and a multi-entry build with `preserveEntrySignatures: 'strict'`
115
+ * dedupes shared code into shared chunks. Entries for CJS deps re-export the
116
+ * module's named API explicitly (so `import { createRoot }` works); ESM deps
117
+ * use `export *`. The build runs under a dynamic-base placeholder + vendor
118
+ * segment (`connectDynamicBasePlugin`), so emitted asset/chunk URLs
119
+ * (`new URL(…, import.meta.url)` for .wasm/.data/workers) carry the placeholder
120
+ * and resolve at serve time to `<deploy-base>__vendor__/`, while the vendored
121
+ * Connect's `import.meta.env.BASE_URL` resolves to the deploy base.
122
+ *
123
+ * The React family stays EXTERNAL (see `VENDOR_EXTERNAL`); `esmExternalRequirePlugin`
124
+ * owns that externalization and rewrites CJS `require("react")` → import. Vendor
125
+ * chunks keep bare React imports, which the dev import map resolves to Vite's
126
+ * pre-bundled React (`devReactImportmapPlugin`) — one React instance across the
127
+ * vendor, the project's editors, and CDN-loaded editors.
128
+ *
129
+ * `devReactImportmapPlugin` consumes the result: it externalizes these
130
+ * specifiers in the long-lived dev server, points the page import map at the
131
+ * vendor URLs, and serves the bundle. With Connect vendored, the dev server only
132
+ * processes the project's own `main` + local package; HMR for them is unaffected.
133
+ */
134
+ /**
135
+ * The stable Connect libraries worth prebuilding. The React family is NOT here —
136
+ * it's externalized from the build (see `VENDOR_EXTERNAL`) so the vendor shares
137
+ * the dev server's single React instance via the import map.
138
+ */
139
+ const DEFAULT_VENDOR_INCLUDE = [
140
+ "@powerhousedao/connect",
141
+ "document-model",
142
+ "zod",
143
+ "@powerhousedao/design-system/connect",
144
+ "@powerhousedao/reactor-browser",
145
+ "@powerhousedao/document-engineering"
146
+ ];
147
+ /**
148
+ * React-family specifiers kept external to the vendor build. The vendor's chunks
149
+ * emit bare imports for these; `devReactImportmapPlugin`'s import map resolves
150
+ * them to Vite's pre-bundled React, so there is exactly one React instance.
151
+ */
152
+ const VENDOR_EXTERNAL = [
153
+ "react",
154
+ "react-dom",
155
+ "react-dom/client",
156
+ "react/jsx-runtime",
157
+ "react/jsx-dev-runtime",
158
+ "ph-bundled-packages-virtual"
159
+ ];
160
+ /** URL prefix the vendor bundle is served under by the dev middleware. */
161
+ const VENDOR_URL_PREFIX = "/__vendor__/";
162
+ const VENDOR_DYNAMIC_BASE = `${DYNAMIC_BASE_PLACEHOLDER}${VENDOR_URL_PREFIX.replace(/^\/+/, "")}`;
163
+ /**
164
+ * Build the prebuilt vendor (once, in a throwaway subprocess) and return its dir
165
+ * + import map. Idempotent: reuses a cached vendor built for the same dep set.
166
+ * Returns null on any failure (caller falls back to a normal dev server).
167
+ */
168
+ async function prebuildConnectVendor(options) {
169
+ const include = expandIncludeSubpaths(options.dirname, options.include ?? DEFAULT_VENDOR_INCLUDE);
170
+ const external = options.external ?? VENDOR_EXTERNAL;
171
+ const vendorDir = options.vendorDir ?? join(options.dirname, "node_modules/.ph-vendor");
172
+ const importMapPath = join(vendorDir, "import-map.json");
173
+ const versionDigest = resolveVersionDigest(options.dirname, [...include, ...external]);
174
+ try {
175
+ const hit = readCacheHit(importMapPath, include, external, versionDigest);
176
+ if (hit) return {
177
+ vendorDir,
178
+ imports: hit
179
+ };
180
+ const lockDir = `${vendorDir}.lock`;
181
+ const lock = acquireLock(lockDir);
182
+ if (!lock) {
183
+ const imports = await waitForCacheHit(importMapPath, include, external, versionDigest, lockDir);
184
+ return imports ? {
185
+ vendorDir,
186
+ imports
187
+ } : null;
188
+ }
189
+ try {
190
+ const raced = readCacheHit(importMapPath, include, external, versionDigest);
191
+ if (raced) return {
192
+ vendorDir,
193
+ imports: raced
194
+ };
195
+ const imports = await buildVendorAtomic(options.dirname, vendorDir, include, external, versionDigest);
196
+ return imports ? {
197
+ vendorDir,
198
+ imports
199
+ } : null;
200
+ } finally {
201
+ releaseLock(lock);
202
+ }
203
+ } catch (err) {
204
+ if (options.errorRef) options.errorRef.message = err instanceof Error ? err.message : String(err);
205
+ return null;
206
+ }
207
+ }
208
+ function readCacheHit(importMapPath, include, external, versionDigest) {
209
+ if (!existsSync(importMapPath)) return null;
210
+ try {
211
+ const cached = JSON.parse(readFileSync(importMapPath, "utf8"));
212
+ if (sameSet(cached.include, include) && sameSet(cached.external, external) && cached.versionDigest === versionDigest) return cached.imports;
213
+ } catch {}
214
+ return null;
215
+ }
216
+ function resolveVersionDigest(dirname, specs) {
217
+ const seen = /* @__PURE__ */ new Map();
218
+ for (const spec of specs) {
219
+ const { pkg } = parsePkg(spec);
220
+ if (seen.has(pkg)) continue;
221
+ let version = "missing";
222
+ try {
223
+ const pkgRoot = realpathSync(join(dirname, "node_modules", pkg));
224
+ const meta = JSON.parse(readFileSync(join(pkgRoot, "package.json"), "utf8"));
225
+ version = String(meta.version ?? "unknown");
226
+ } catch {}
227
+ seen.set(pkg, version);
228
+ }
229
+ const h = createHash("sha256");
230
+ const workerHash = createHash("sha256").update(VENDOR_BUILD_WORKER).digest("hex");
231
+ h.update(`worker:${workerHash}\n`);
232
+ for (const pkg of [...seen.keys()].sort()) h.update(`${pkg}@${seen.get(pkg)}\n`);
233
+ return h.digest("hex").slice(0, 16);
234
+ }
235
+ function sameSet(a, b) {
236
+ if (!a || a.length !== b.length) return false;
237
+ const s = new Set(a);
238
+ return b.every((x) => s.has(x));
239
+ }
240
+ const LOCK_STALE_MS = 5 * 6e4;
241
+ const LOCK_HEARTBEAT_MS = 6e4;
242
+ const ownerFile = (lockDir) => join(lockDir, "owner");
243
+ function lockIsStale(lockDir) {
244
+ try {
245
+ return Date.now() - statSync(ownerFile(lockDir)).mtimeMs > LOCK_STALE_MS;
246
+ } catch {
247
+ return false;
248
+ }
249
+ }
250
+ function acquireLock(lockDir) {
251
+ let made = false;
252
+ try {
253
+ mkdirSync(lockDir);
254
+ made = true;
255
+ } catch {
256
+ if (lockIsStale(lockDir)) try {
257
+ rmSync(lockDir, {
258
+ recursive: true,
259
+ force: true
260
+ });
261
+ mkdirSync(lockDir);
262
+ made = true;
263
+ } catch {}
264
+ }
265
+ if (!made) return null;
266
+ const token = `${process.pid}-${randomUUID()}`;
267
+ writeFileSync(ownerFile(lockDir), token);
268
+ const timer = setInterval(() => {
269
+ try {
270
+ if (readFileSync(ownerFile(lockDir), "utf8") === token) writeFileSync(ownerFile(lockDir), token);
271
+ else clearInterval(timer);
272
+ } catch {
273
+ clearInterval(timer);
274
+ }
275
+ }, LOCK_HEARTBEAT_MS);
276
+ timer.unref();
277
+ return {
278
+ dir: lockDir,
279
+ token,
280
+ timer
281
+ };
282
+ }
283
+ function releaseLock(lock) {
284
+ clearInterval(lock.timer);
285
+ try {
286
+ if (readFileSync(ownerFile(lock.dir), "utf8") === lock.token) rmSync(lock.dir, {
287
+ recursive: true,
288
+ force: true
289
+ });
290
+ } catch {}
291
+ }
292
+ async function waitForCacheHit(importMapPath, include, external, versionDigest, lockDir) {
293
+ const deadline = Date.now() + LOCK_STALE_MS;
294
+ while (Date.now() < deadline) {
295
+ const hit = readCacheHit(importMapPath, include, external, versionDigest);
296
+ if (hit) return hit;
297
+ if (!existsSync(lockDir)) return readCacheHit(importMapPath, include, external, versionDigest);
298
+ await new Promise((r) => setTimeout(r, 250));
299
+ }
300
+ return null;
301
+ }
302
+ function importTarget$1(value) {
303
+ if (typeof value === "string") return value;
304
+ if (!value || typeof value !== "object") return null;
305
+ const o = value;
306
+ for (const c of [
307
+ "browser",
308
+ "import",
309
+ "module",
310
+ "default"
311
+ ]) if (c in o) {
312
+ const t = importTarget$1(o[c]);
313
+ if (t) return t;
314
+ }
315
+ return null;
316
+ }
317
+ function parsePkg(spec) {
318
+ if (spec.startsWith("@")) {
319
+ const parts = spec.split("/");
320
+ return {
321
+ pkg: parts.slice(0, 2).join("/"),
322
+ sub: parts.slice(2).join("/")
323
+ };
324
+ }
325
+ const i = spec.indexOf("/");
326
+ return i === -1 ? {
327
+ pkg: spec,
328
+ sub: ""
329
+ } : {
330
+ pkg: spec.slice(0, i),
331
+ sub: spec.slice(i + 1)
332
+ };
333
+ }
334
+ /**
335
+ * Connect imports the heavy libs by subpath too (e.g.
336
+ * `@powerhousedao/design-system/connect/toast`, `zod/v4/core`). Those bare
337
+ * imports need their own import-map entry, so expand each listed spec to its
338
+ * package's concrete (non-wildcard, JS) subpath exports that share the spec's
339
+ * prefix. Unresolvable / CSS / JSON targets are skipped. Failures leave the
340
+ * original spec untouched.
341
+ */
342
+ function expandIncludeSubpaths(dirname, include) {
343
+ const out = new Set(include);
344
+ for (const spec of include) {
345
+ const { pkg, sub } = parsePkg(spec);
346
+ let exp;
347
+ let pkgRoot;
348
+ try {
349
+ pkgRoot = realpathSync(join(dirname, "node_modules", pkg));
350
+ exp = JSON.parse(readFileSync(join(pkgRoot, "package.json"), "utf8")).exports;
351
+ } catch {
352
+ continue;
353
+ }
354
+ if (!exp || typeof exp !== "object") continue;
355
+ const expMap = exp;
356
+ const prefixKey = sub ? `./${sub}` : ".";
357
+ for (const key of Object.keys(expMap)) {
358
+ if (key.includes("*") || key === "./package.json") continue;
359
+ if (/\.(css|json|scss)$/.test(key)) continue;
360
+ if (!(prefixKey === "." ? key === "." || key.startsWith("./") : key === prefixKey || key.startsWith(`${prefixKey}/`))) continue;
361
+ const target = importTarget$1(expMap[key]);
362
+ if (!target || /\.(css|json|scss)$/.test(target)) continue;
363
+ if (!existsSync(join(pkgRoot, target))) continue;
364
+ out.add(key === "." ? pkg : pkg + key.slice(1));
365
+ }
366
+ }
367
+ return [...out];
368
+ }
369
+ /**
370
+ * Build the vendor into a unique temp dir, then atomically swap it into place,
371
+ * so a concurrent reader never sees a partial bundle or import-map.json. Runs
372
+ * the build in a throwaway subprocess (its peak memory is reclaimed on exit);
373
+ * the parent augments the import map with the version digest and does the swap.
374
+ * Returns the published import map, or null on failure.
375
+ */
376
+ async function buildVendorAtomic(dirname$2, vendorDir, include, external, versionDigest) {
377
+ const parent = dirname(vendorDir);
378
+ mkdirSync(parent, { recursive: true });
379
+ const tmpDir = mkdtempSync(join(parent, ".ph-vendor.tmp-"));
380
+ try {
381
+ const { ok, stderr } = await runBuildWorker(dirname$2, tmpDir, include, external);
382
+ const tmpMap = join(tmpDir, "import-map.json");
383
+ if (!ok || !existsSync(tmpMap)) {
384
+ const detail = stderr.trim();
385
+ throw new Error(`vendor build failed${detail ? `:\n${detail}` : " (no output captured)"}`);
386
+ }
387
+ const meta = JSON.parse(readFileSync(tmpMap, "utf8"));
388
+ meta.versionDigest = versionDigest;
389
+ writeFileSync(tmpMap, JSON.stringify(meta, null, 2));
390
+ const oldDir = `${vendorDir}.old-${process.pid}-${Date.now()}`;
391
+ if (existsSync(vendorDir)) renameSync(vendorDir, oldDir);
392
+ renameSync(tmpDir, vendorDir);
393
+ rmSync(oldDir, {
394
+ recursive: true,
395
+ force: true
396
+ });
397
+ return meta.imports;
398
+ } catch (err) {
399
+ rmSync(tmpDir, {
400
+ recursive: true,
401
+ force: true
402
+ });
403
+ throw err;
404
+ }
405
+ }
406
+ /**
407
+ * Spawn a throwaway worker that generates one entry per specifier (CJS deps get
408
+ * explicit named re-exports, ESM deps `export *`), `vite build`s them into the
409
+ * given out dir, and writes the import map. Captures stdout/stderr so a failure
410
+ * is debuggable; the normal path stays quiet.
411
+ */
412
+ function runBuildWorker(dirname, outDir, include, external) {
413
+ const workerPath = join(outDir, "build-worker.mjs");
414
+ writeFileSync(workerPath, VENDOR_BUILD_WORKER);
415
+ const selfModulePath = fileURLToPath(import.meta.url);
416
+ return new Promise((resolve) => {
417
+ const child = spawn(process.execPath, [
418
+ workerPath,
419
+ dirname,
420
+ outDir,
421
+ JSON.stringify(include),
422
+ VENDOR_URL_PREFIX,
423
+ JSON.stringify(external),
424
+ VENDOR_DYNAMIC_BASE,
425
+ selfModulePath
426
+ ], {
427
+ cwd: dirname,
428
+ stdio: [
429
+ "ignore",
430
+ "pipe",
431
+ "pipe"
432
+ ]
433
+ });
434
+ let stderr = "";
435
+ child.stdout.on("data", (d) => {
436
+ stderr += String(d);
437
+ });
438
+ child.stderr.on("data", (d) => {
439
+ stderr += String(d);
440
+ });
441
+ child.on("exit", (code) => resolve({
442
+ ok: code === 0,
443
+ stderr
444
+ }));
445
+ child.on("error", (e) => resolve({
446
+ ok: false,
447
+ stderr: String(e)
448
+ }));
449
+ });
450
+ }
451
+ /**
452
+ * The vendor build worker, written to disk and run as a subprocess. Loads
453
+ * `vite` from the project and `connectDynamicBasePlugin` from builder-tools' own
454
+ * built bundle (selfModulePath). argv: dirname, vendorDir, includeJSON,
455
+ * urlPrefix, externalJSON, dynamicBase, selfModulePath.
456
+ */
457
+ const VENDOR_BUILD_WORKER = `
458
+ import { createRequire } from 'node:module';
459
+ import { mkdirSync, writeFileSync, rmSync } from 'node:fs';
460
+ import { join, isAbsolute } from 'node:path';
461
+ import { fileURLToPath, pathToFileURL } from 'node:url';
462
+ const [dirname, vendorDir, includeJSON, urlPrefix, externalJSON, dynamicBase, selfModulePath] = process.argv.slice(2);
463
+ const include = JSON.parse(includeJSON);
464
+ const external = JSON.parse(externalJSON ?? '[]');
465
+ const externalSet = new Set(external);
466
+ const reqProj = createRequire(join(dirname, 'noop.js'));
467
+ const { build, esmExternalRequirePlugin } = await import(reqProj.resolve('vite'));
468
+ // Load the dynamic-base plugin from builder-tools' own built bundle (passed as
469
+ // an absolute path) — it isn't resolvable as a bare specifier from the worker.
470
+ const { connectDynamicBasePlugin, DYNAMIC_BASE_PLACEHOLDER } = await import(pathToFileURL(selfModulePath));
471
+ const srcDir = join(vendorDir, '.entries');
472
+ mkdirSync(srcDir, { recursive: true });
473
+ const entryName = (spec) => spec.replace(/[^\\w]+/g, '_');
474
+ 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(' '));
475
+ const input = {};
476
+ for (const spec of include) {
477
+ const name = entryName(spec);
478
+ let src;
479
+ try {
480
+ // Import the bare spec (not reqProj.resolve(spec)) so Node picks the same
481
+ // import/browser-condition module the build resolves — resolving first can
482
+ // pick a CJS sibling whose default-export shape differs from the ESM build.
483
+ const ns = await import(spec);
484
+ // Only fall back to re-exporting from default when the module exposes NO
485
+ // top-level named exports (a true CJS-interop module). If it does (e.g. zod
486
+ // exposes \`z\`), \`export *\` captures them; destructuring default would miss
487
+ // top-level names that aren't keys of the default object.
488
+ const named = Object.keys(ns).filter((k) => k !== 'default');
489
+ const cjs = named.length === 0 && ns.default && typeof ns.default === 'object';
490
+ if (cjs) {
491
+ const names = Object.keys(ns.default).filter((k) => k !== 'default' && k !== '__esModule' && /^[A-Za-z_$][\\w$]*$/.test(k));
492
+ const plain = names.filter((n) => !RESERVED.has(n));
493
+ const reserved = names.filter((n) => RESERVED.has(n));
494
+ src = 'import d from ' + JSON.stringify(spec) + ';\\nexport default d;\\n'
495
+ + (plain.length ? 'export const { ' + plain.join(', ') + ' } = d;\\n' : '');
496
+ // reserved words are invalid as const-binding names but valid as export
497
+ // aliases (export { x as enum }).
498
+ reserved.forEach((n, i) => {
499
+ src += 'const __r' + i + ' = d[' + JSON.stringify(n) + '];\\nexport { __r' + i + ' as ' + n + ' };\\n';
500
+ });
501
+ } else {
502
+ src = 'export * from ' + JSON.stringify(spec) + ';\\n'
503
+ + (('default' in ns) ? 'export { default } from ' + JSON.stringify(spec) + ';\\n' : '');
504
+ }
505
+ } catch {
506
+ src = 'export * from ' + JSON.stringify(spec) + ';\\n';
507
+ }
508
+ const file = join(srcDir, name + '.js');
509
+ writeFileSync(file, src);
510
+ input[name] = file;
511
+ }
512
+ // Prefer the bundler's browser-condition-aware resolution; fall back to resolving
513
+ // from the worker (real install path) only for Rolldown's realpath-anchoring bug.
514
+ const phResolveCache = new Map();
515
+ const phVendorResolve = {
516
+ name: 'ph-vendor-resolve', enforce: 'pre',
517
+ async resolveId(source, importer, options) {
518
+ if (externalSet.has(source)) return null;
519
+ if (source[0] === '\\0' || source[0] === '.' || isAbsolute(source)) return null;
520
+ if (source.startsWith('node:') || source.startsWith('data:')) return null;
521
+ let viaBundler = null;
522
+ try { viaBundler = await this.resolve(source, importer, { ...options, skipSelf: true }); } catch {}
523
+ if (viaBundler) return viaBundler;
524
+ if (phResolveCache.has(source)) return phResolveCache.get(source);
525
+ let resolved = null;
526
+ try { resolved = fileURLToPath(import.meta.resolve(source)); } catch {}
527
+ phResolveCache.set(source, resolved);
528
+ return resolved;
529
+ },
530
+ };
531
+ await build({
532
+ root: dirname, configFile: false, logLevel: 'error',
533
+ // Dynamic-base placeholder + vendor segment: connectDynamicBasePlugin rewrites
534
+ // emitted chunk/asset URLs to resolve against the deploy base at serve time.
535
+ base: dynamicBase,
536
+ define: {
537
+ 'process.env.NODE_ENV': '"development"',
538
+ // BASE_URL resolves to the deploy base (not the vendor prefix) so vendored
539
+ // Connect's router basename + BASE_URL-relative fetches use the right path.
540
+ 'import.meta.env.BASE_URL': JSON.stringify(DYNAMIC_BASE_PLACEHOLDER),
541
+ },
542
+ // phVendorResolve (pre) resolves bares from the worker; esmExternalRequirePlugin owns
543
+ // react/virtual externalization; connectDynamicBasePlugin (post) rewrites the placeholder base.
544
+ plugins: [phVendorResolve, esmExternalRequirePlugin({ external }), connectDynamicBasePlugin()],
545
+ // pglite ships web workers as ES-module chunks. workerStripPrefix is the vendor
546
+ // segment so the worker recovers the deploy base, not the vendor prefix.
547
+ worker: { format: 'es', plugins: () => [connectDynamicBasePlugin({ forWorker: true, workerStripPrefix: urlPrefix.replace(/^\\/+/, '') })] },
548
+ build: {
549
+ outDir: vendorDir, emptyOutDir: false, minify: false, target: 'esnext', cssCodeSplit: true,
550
+ rollupOptions: {
551
+ input, preserveEntrySignatures: 'strict',
552
+ output: { format: 'es', entryFileNames: '[name].js', chunkFileNames: 'chunks/[name]-[hash].js', assetFileNames: 'assets/[name]-[hash][extname]' },
553
+ },
554
+ },
555
+ });
556
+ rmSync(srcDir, { recursive: true, force: true });
557
+ const imports = {};
558
+ for (const spec of include) imports[spec] = urlPrefix + entryName(spec) + '.js';
559
+ writeFileSync(join(vendorDir, 'import-map.json'), JSON.stringify({ include, external, imports }, null, 2));
560
+ `;
561
+ //#endregion
20
562
  //#region connect-utils/helpers.ts
21
563
  const DEFAULT_CONNECT_OUTDIR = ".ph/connect-build/dist/";
22
564
  function resolveViteConfigPath(options) {
@@ -247,6 +789,54 @@ const runtimeConfigSchema = {
247
789
  connect: phConnectRuntimeConfigSchema
248
790
  }
249
791
  };
792
+ const RESOLVED_VIRTUAL_ID = "\0virtual:ph-bundled-packages-virtual";
793
+ const BUNDLED_PACKAGES_DEV_URL = "@id/" + RESOLVED_VIRTUAL_ID.replace("\0", "__x00__");
794
+ function readBundledPackageVersion(projectRoot, name) {
795
+ try {
796
+ const raw = fs.readFileSync(path.join(projectRoot, "node_modules", name, "package.json"), "utf-8");
797
+ const pkg = JSON.parse(raw);
798
+ return typeof pkg.version === "string" ? pkg.version : void 0;
799
+ } catch {
800
+ return;
801
+ }
802
+ }
803
+ function makeRegisterModule(packages, projectRoot) {
804
+ if (packages.length === 0) return "export default () => {};\n";
805
+ const imports = [];
806
+ const calls = [];
807
+ packages.forEach((name, i) => {
808
+ const moduleName = `pkg${i}`;
809
+ const version = readBundledPackageVersion(projectRoot, name);
810
+ imports.push(`import * as ${moduleName} from ${JSON.stringify(name)};`);
811
+ imports.push(`import ${JSON.stringify(`${name}/style.css`)};`);
812
+ calls.push(` pm.addLocalPackage(${JSON.stringify(name)}, ${moduleName}, ${JSON.stringify(version)});`);
813
+ });
814
+ return `${imports.join("\n")}\n\nexport default function register(pm) {\n${calls.join("\n")}\n};\n`;
815
+ }
816
+ /**
817
+ * Emits a virtual module `ph-bundled-packages-virtual` whose default export
818
+ * is a `register(packageManager)` function. When called at runtime (from
819
+ * Connect's bootstrap), it registers each bundled package with the package
820
+ * manager the same way Common/Vetra are registered — meaning they work
821
+ * offline without the registry being reachable.
822
+ *
823
+ * When the list is empty, the module exports a no-op function so Connect's
824
+ * bootstrap code can always import it unconditionally.
825
+ */
826
+ function phBundledPackagesPlugin(options) {
827
+ const projectRoot = options.projectRoot ?? process.cwd();
828
+ const moduleSource = makeRegisterModule(options.packages, projectRoot);
829
+ return {
830
+ name: "vite-plugin-ph-bundled-packages",
831
+ enforce: "pre",
832
+ resolveId(id) {
833
+ if (id === "ph-bundled-packages-virtual") return RESOLVED_VIRTUAL_ID;
834
+ },
835
+ load(id) {
836
+ if (id === "\0virtual:ph-bundled-packages-virtual") return moduleSource;
837
+ }
838
+ };
839
+ }
250
840
  //#endregion
251
841
  //#region connect-utils/vite-plugins/dev-external-react.ts
252
842
  const REACT_DEPS = [
@@ -258,9 +848,36 @@ const REACT_DEPS = [
258
848
  ];
259
849
  const SHIM_PATH = "__ph/dev-react-shim/";
260
850
  const VITE_DEPS_PATH = "node_modules/.vite/deps";
851
+ const VENDOR_MIME = {
852
+ ".css": "text/css",
853
+ ".map": "application/json",
854
+ ".json": "application/json",
855
+ ".wasm": "application/wasm",
856
+ ".data": "application/octet-stream",
857
+ ".woff2": "font/woff2",
858
+ ".woff": "font/woff",
859
+ ".ttf": "font/ttf",
860
+ ".svg": "image/svg+xml",
861
+ ".png": "image/png",
862
+ ".jpg": "image/jpeg",
863
+ ".jpeg": "image/jpeg",
864
+ ".gif": "image/gif",
865
+ ".webp": "image/webp"
866
+ };
867
+ const VENDOR_MODE = process.env.PH_CONNECT_EXTERNALIZE_VENDOR === "1";
868
+ const VENDOR_EXTRA = (process.env.PH_CONNECT_VENDOR_EXTRA ?? "").split(",").map((s) => s.trim()).filter(Boolean);
869
+ const HEAVY_LIBS = [...DEFAULT_VENDOR_INCLUDE, ...VENDOR_EXTRA];
261
870
  function withBase(base, p) {
262
871
  return `${base}${p}`.replace(/\/{2,}/g, "/");
263
872
  }
873
+ function externalizePlugin(externals) {
874
+ try {
875
+ const mod = createRequire(import.meta.url)("vite-plugin-externalize-dependencies");
876
+ return (mod.default ?? mod)({ externals });
877
+ } catch {
878
+ return;
879
+ }
880
+ }
264
881
  /**
265
882
  * Dev-only sibling of `esmExternalRequirePlugin`. The build path externalizes
266
883
  * React via Rolldown so an importmap hands the same React instance to both
@@ -269,7 +886,7 @@ function withBase(base, p) {
269
886
  * export — so a CDN editor that does `import { lazy } from "react"` would
270
887
  * fail with "no named export 'lazy'".
271
888
  *
272
- * This plugin:
889
+ * This plugin owns the page import map. It always:
273
890
  * 1. Forces React into `optimizeDeps.include` so the optimizer always knows
274
891
  * about it.
275
892
  * 2. Serves a shim per React module at a stable URL. Each shim imports
@@ -277,14 +894,46 @@ function withBase(base, p) {
277
894
  * and re-exports React's named members so editors importing
278
895
  * `{ lazy }`, `{ jsx }`, etc. work.
279
896
  * 3. Rewrites the page importmap to point at those shim URLs.
897
+ *
898
+ * When PH_CONNECT_EXTERNALIZE_VENDOR=1 it additionally prebuilds the heavy
899
+ * stable libs (design-system, reactor-browser, …) into a static vendor bundle,
900
+ * externalizes them from the dev server, serves the bundle, and adds them to
901
+ * the SAME import map. The vendor's React imports stay bare → resolve through
902
+ * the React shims above → one React instance across Connect, the vendor, and
903
+ * CDN editors.
280
904
  */
281
- function devReactImportmapPlugin() {
905
+ async function devReactImportmapPlugin(projectRoot = process.cwd()) {
282
906
  let namedExports = /* @__PURE__ */ new Map();
283
907
  let base = "/";
284
- return {
908
+ let vendor = null;
909
+ if (VENDOR_MODE) {
910
+ const errorRef = {};
911
+ vendor = await prebuildConnectVendor({
912
+ dirname: projectRoot,
913
+ include: HEAVY_LIBS,
914
+ errorRef
915
+ });
916
+ if (!vendor) {
917
+ const detail = errorRef.message ? `: ${errorRef.message}` : "";
918
+ console.warn(`[connect] PH_CONNECT_EXTERNALIZE_VENDOR set but vendor prebuild failed; falling back to dep-optimizing the heavy libs${detail}`);
919
+ }
920
+ }
921
+ const ext = vendor ? externalizePlugin([(id) => id in vendor.imports]) : void 0;
922
+ 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)");
923
+ const vendorActive = !!(vendor && ext);
924
+ const main = {
285
925
  name: "ph-dev-react-importmap",
286
926
  apply: "serve",
287
- config: () => ({ optimizeDeps: { include: REACT_DEPS } }),
927
+ config(cfg) {
928
+ cfg.optimizeDeps ??= {};
929
+ const include = new Set(cfg.optimizeDeps.include ?? []);
930
+ REACT_DEPS.forEach((d) => include.add(d));
931
+ if (vendorActive) {
932
+ HEAVY_LIBS.forEach((d) => include.delete(d));
933
+ cfg.optimizeDeps.exclude = [...new Set([...cfg.optimizeDeps.exclude ?? [], ...HEAVY_LIBS])];
934
+ }
935
+ cfg.optimizeDeps.include = [...include];
936
+ },
288
937
  configResolved(config) {
289
938
  base = config.base;
290
939
  },
@@ -298,6 +947,30 @@ function devReactImportmapPlugin() {
298
947
  return [id, []];
299
948
  }
300
949
  }));
950
+ if (vendorActive) {
951
+ const vendorDir = vendor.vendorDir;
952
+ const vendorPrefixes = [withBase(base, VENDOR_URL_PREFIX), VENDOR_URL_PREFIX];
953
+ server.middlewares.use((req, res, next) => {
954
+ const url = (req.url ?? "").split("?")[0];
955
+ const prefix = vendorPrefixes.find((p) => url.startsWith(p));
956
+ if (!prefix) return next();
957
+ const name = url.slice(prefix.length).replace(/^\/+/, "");
958
+ const file = path.join(vendorDir, name);
959
+ const rel = path.relative(vendorDir, file);
960
+ if (!name || rel.startsWith("..") || path.isAbsolute(rel)) return next();
961
+ const stream = createReadStream(file);
962
+ stream.on("error", () => {
963
+ if (!res.headersSent) next();
964
+ });
965
+ stream.once("open", () => {
966
+ const ext = file.slice(file.lastIndexOf("."));
967
+ res.setHeader("Content-Type", VENDOR_MIME[ext] ?? "text/javascript");
968
+ const hashed = name.startsWith("chunks/") || name.startsWith("assets/");
969
+ res.setHeader("Cache-Control", hashed ? "public, max-age=31536000, immutable" : "no-cache");
970
+ stream.pipe(res);
971
+ });
972
+ });
973
+ }
301
974
  const shimPrefixes = [withBase(base, SHIM_PATH), `/${SHIM_PATH}`];
302
975
  server.middlewares.use((req, res, next) => {
303
976
  const prefix = shimPrefixes.find((p) => req.url?.startsWith(p));
@@ -325,98 +998,17 @@ function devReactImportmapPlugin() {
325
998
  if (!browserHash) return;
326
999
  const shimPrefix = withBase(base, SHIM_PATH);
327
1000
  const imports = Object.fromEntries(REACT_DEPS.map((id) => [id, `${shimPrefix}${id}.js?v=${browserHash}`]));
328
- return html.replace(/<script type="importmap">[\s\S]*?<\/script>/, `<script type="importmap">${JSON.stringify({ imports }, null, 2)}<\/script>`);
329
- }
330
- }
331
- };
332
- }
333
- //#endregion
334
- //#region connect-utils/vite-plugins/dynamic-base.ts
335
- /**
336
- * Placeholder base used when Connect is built in dynamic-base mode. The Vite
337
- * `base` option is set to this token; this plugin rewrites it in the emitted
338
- * output so the effective base is resolved at serve time from a global instead
339
- * of being baked at build time.
340
- *
341
- * Trailing slash matches `normalizeBasePath` output, so the token sits in the
342
- * same syntactic position as a concrete base would.
343
- */
344
- const DYNAMIC_BASE_PLACEHOLDER = "/__PH_DYNAMIC_BASE__/";
345
- /**
346
- * Global the runtime (ph-clint proxy) must set before the entry bundle loads.
347
- * Value is the normalized deploy base, e.g. "/myagent/" or "/". The JS rewrite
348
- * below resolves all asset / lazy-chunk / BASE_URL references against it.
349
- */
350
- const RUNTIME_GLOBAL = "globalThis.__PH_DYNAMIC_BASE__";
351
- const BASE_EXPR = `(${RUNTIME_GLOBAL}||"/")`;
352
- /**
353
- * Worker scope has its own global object; the proxy injects the runtime global
354
- * into the main-thread `<head>` only, so a worker chunk's rewritten references
355
- * would fall back to "/" and fetch siblings (pglite .data/.wasm) without the
356
- * deploy prefix. Prepended to worker chunks, this defines the global in worker
357
- * scope from the worker's own script URL: the worker loads from
358
- * `<base>assets/<name>-<hash>.js`, so stripping `assets/<file>` off
359
- * `self.location.pathname` yields the same trailing-slash base the main thread
360
- * holds ("/myagent/" or "/").
361
- */
362
- const WORKER_PRELUDE = `${RUNTIME_GLOBAL}=self.location.pathname.replace(/assets\\/[^/]*$/,"");\n`;
363
- const PLACEHOLDER_LITERAL = new RegExp(`(["'\`])${escapeForRegExp(DYNAMIC_BASE_PLACEHOLDER)}((?:(?!\\1)[^$])*)\\1`, "g");
364
- /**
365
- * Rewrites the placeholder base in emitted JS chunks to a runtime expression so
366
- * one built `dist/connect` serves under any subpath. Rolldown-native: per-match
367
- * MagicString edits in `renderChunk`, no AST splicing (the SWC byte-offset
368
- * splicing in vite-plugin-dynamic-base corrupts Rolldown chunks). Each edited
369
- * chunk returns a hires sourcemap that the bundler composes into the chunk's
370
- * map chain, so the emitted `.map` files track the rewrite (and the worker
371
- * prelude's line shift).
372
- *
373
- * What gets rewritten in JS, all of which emit the base as a quoted string
374
- * literal beginning with the placeholder:
375
- * - asset URLs (`new URL("/__PH_DYNAMIC_BASE__/assets/x.png", ...)`)
376
- * - dynamic-import / lazy-chunk preload URLs
377
- * - the inlined `import.meta.env.BASE_URL` (drives Connect's router basename
378
- * and BASE_URL-relative fetches such as `${BASE_URL}ph-packages.json`)
379
- *
380
- * A literal `"/__PH_DYNAMIC_BASE__/foo"` becomes `((globalThis.__PH_DYNAMIC_BASE__||"/")+"foo")`;
381
- * a bare `"/__PH_DYNAMIC_BASE__/"` (the BASE_URL value) becomes `(globalThis.__PH_DYNAMIC_BASE__||"/")`.
382
- *
383
- * HTML is left untouched: the entry `<script>`/`<link>` tags keep the literal
384
- * placeholder so the proxy substitutes it with the concrete base at serve time
385
- * (the same proxy also sets the runtime global for the JS rewrite). See the
386
- * plugin's module doc / report for the exact serve-time contract.
387
- *
388
- * CSS `url(...)` references resolve relative to the stylesheet's own URL, so
389
- * they need no rewrite once the stylesheet itself is loaded from the right
390
- * prefix (which the HTML substitution handles).
391
- */
392
- function connectDynamicBasePlugin(options = {}) {
393
- return {
394
- name: "ph-connect-dynamic-base",
395
- enforce: "post",
396
- renderChunk(code, chunk) {
397
- if (!code.includes("/__PH_DYNAMIC_BASE__/")) return null;
398
- const s = new MagicString(code);
399
- for (const match of code.matchAll(PLACEHOLDER_LITERAL)) {
400
- const rest = match[2];
401
- s.overwrite(match.index, match.index + match[0].length, rest.length === 0 ? BASE_EXPR : `(${BASE_EXPR}+${JSON.stringify(rest)})`);
402
- }
403
- if (options.forWorker) {
404
- s.prepend(WORKER_PRELUDE);
405
- this.info(`dynamic-base: worker prelude prepended to ${chunk.fileName}`);
1001
+ let dynamicBaseScript = "";
1002
+ if (vendorActive) {
1003
+ for (const [spec, url] of Object.entries(vendor.imports)) imports[spec] = withBase(base, url);
1004
+ if (vendor.imports["@powerhousedao/connect"]) imports["ph-bundled-packages-virtual"] = withBase(base, BUNDLED_PACKAGES_DEV_URL);
1005
+ dynamicBaseScript = `<script>globalThis.__PH_DYNAMIC_BASE__=${JSON.stringify(base)};<\/script>\n`;
1006
+ }
1007
+ return html.replace(/<script type="importmap">[\s\S]*?<\/script>/, `${dynamicBaseScript}<script type="importmap">${JSON.stringify({ imports }, null, 2)}<\/script>`);
406
1008
  }
407
- if (!s.hasChanged()) return null;
408
- return {
409
- code: s.toString(),
410
- map: s.generateMap({ hires: true })
411
- };
412
- },
413
- generateBundle(_options, bundle) {
414
- 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}`);
415
1009
  }
416
1010
  };
417
- }
418
- function escapeForRegExp(s) {
419
- return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1011
+ return vendorActive ? [ext, main] : main;
420
1012
  }
421
1013
  //#endregion
422
1014
  //#region connect-utils/vite-plugins/favicon.ts
@@ -454,56 +1046,6 @@ function connectFaviconPlugin() {
454
1046
  };
455
1047
  }
456
1048
  //#endregion
457
- //#region connect-utils/vite-plugins/ph-bundled-packages.ts
458
- const VIRTUAL_ID = "ph-bundled-packages-virtual";
459
- const RESOLVED_VIRTUAL_ID = "\0virtual:" + VIRTUAL_ID;
460
- function readBundledPackageVersion(projectRoot, name) {
461
- try {
462
- const raw = fs.readFileSync(path.join(projectRoot, "node_modules", name, "package.json"), "utf-8");
463
- const pkg = JSON.parse(raw);
464
- return typeof pkg.version === "string" ? pkg.version : void 0;
465
- } catch {
466
- return;
467
- }
468
- }
469
- function makeRegisterModule(packages, projectRoot) {
470
- if (packages.length === 0) return "export default () => {};\n";
471
- const imports = [];
472
- const calls = [];
473
- packages.forEach((name, i) => {
474
- const moduleName = `pkg${i}`;
475
- const version = readBundledPackageVersion(projectRoot, name);
476
- imports.push(`import * as ${moduleName} from ${JSON.stringify(name)};`);
477
- imports.push(`import ${JSON.stringify(`${name}/style.css`)};`);
478
- calls.push(` pm.addLocalPackage(${JSON.stringify(name)}, ${moduleName}, ${JSON.stringify(version)});`);
479
- });
480
- return `${imports.join("\n")}\n\nexport default function register(pm) {\n${calls.join("\n")}\n};\n`;
481
- }
482
- /**
483
- * Emits a virtual module `ph-bundled-packages-virtual` whose default export
484
- * is a `register(packageManager)` function. When called at runtime (from
485
- * Connect's bootstrap), it registers each bundled package with the package
486
- * manager the same way Common/Vetra are registered — meaning they work
487
- * offline without the registry being reachable.
488
- *
489
- * When the list is empty, the module exports a no-op function so Connect's
490
- * bootstrap code can always import it unconditionally.
491
- */
492
- function phBundledPackagesPlugin(options) {
493
- const projectRoot = options.projectRoot ?? process.cwd();
494
- const moduleSource = makeRegisterModule(options.packages, projectRoot);
495
- return {
496
- name: "vite-plugin-ph-bundled-packages",
497
- enforce: "pre",
498
- resolveId(id) {
499
- if (id === VIRTUAL_ID) return RESOLVED_VIRTUAL_ID;
500
- },
501
- load(id) {
502
- if (id === RESOLVED_VIRTUAL_ID) return moduleSource;
503
- }
504
- };
505
- }
506
- //#endregion
507
1049
  //#region connect-utils/vite-plugins/ph-config.ts
508
1050
  function readProjectPackageInfo(projectRoot) {
509
1051
  if (!projectRoot) return null;
@@ -564,14 +1106,174 @@ function phConfigPlugin(options) {
564
1106
  };
565
1107
  }
566
1108
  //#endregion
1109
+ //#region connect-utils/vite-plugins/react-self-host.ts
1110
+ const REACT_URL_DIR = "__react__";
1111
+ const REACT_PACKAGES = ["react", "react-dom"];
1112
+ function importTarget(value) {
1113
+ if (typeof value === "string") return value;
1114
+ if (!value || typeof value !== "object") return null;
1115
+ const o = value;
1116
+ for (const c of [
1117
+ "browser",
1118
+ "import",
1119
+ "module",
1120
+ "default"
1121
+ ]) if (c in o) {
1122
+ const t = importTarget(o[c]);
1123
+ if (t) return t;
1124
+ }
1125
+ return null;
1126
+ }
1127
+ function resolveReactEntries(dirname$1) {
1128
+ const require = createRequire(join(dirname$1, "noop.js"));
1129
+ const out = {};
1130
+ for (const pkg of REACT_PACKAGES) {
1131
+ let pkgRoot;
1132
+ let exp;
1133
+ try {
1134
+ const pkgJsonPath = require.resolve(`${pkg}/package.json`);
1135
+ pkgRoot = dirname(pkgJsonPath);
1136
+ exp = JSON.parse(readFileSync(pkgJsonPath, "utf8")).exports;
1137
+ } catch {
1138
+ continue;
1139
+ }
1140
+ if (!exp || typeof exp !== "object") continue;
1141
+ for (const key of Object.keys(exp)) {
1142
+ if (key === "./package.json" || key.includes("*")) continue;
1143
+ const target = importTarget(exp[key]);
1144
+ if (!target || /\.(json|css)$/.test(target)) continue;
1145
+ if (/\.(node|bun)\.js$/.test(target)) continue;
1146
+ const file = join(pkgRoot, target);
1147
+ if (!existsSync(file)) continue;
1148
+ out[key === "." ? pkg : `${pkg}${key.slice(1)}`] = file;
1149
+ }
1150
+ }
1151
+ return out;
1152
+ }
1153
+ function reactSelfHostPlugin(options) {
1154
+ const entries = resolveReactEntries(options.dirname);
1155
+ let absOutDir = resolve(options.dirname, "dist");
1156
+ let importMap = {};
1157
+ return {
1158
+ name: "ph-react-self-host",
1159
+ apply: "build",
1160
+ configResolved(config) {
1161
+ absOutDir = resolve(config.root, config.build.outDir);
1162
+ importMap = Object.fromEntries(Object.keys(entries).map((spec) => [spec, `${config.base}${REACT_URL_DIR}/${spec}.js`]));
1163
+ for (const pkg of REACT_PACKAGES) importMap[`${pkg}/`] = `${config.base}${REACT_URL_DIR}/${pkg}/`;
1164
+ },
1165
+ transformIndexHtml() {
1166
+ if (!Object.keys(importMap).length) return;
1167
+ return [{
1168
+ tag: "script",
1169
+ attrs: { type: "importmap" },
1170
+ children: JSON.stringify({ imports: importMap }),
1171
+ injectTo: "head-prepend"
1172
+ }];
1173
+ },
1174
+ async closeBundle() {
1175
+ if (!Object.keys(entries).length) return;
1176
+ const { ok, stderr } = await runReactBuild(options.dirname, join(absOutDir, REACT_URL_DIR), entries, options.dev ? "development" : "production");
1177
+ if (!ok) this.error(`react self-host build failed${stderr.trim() ? `:\n${stderr.trim()}` : ""}`);
1178
+ }
1179
+ };
1180
+ }
1181
+ function runReactBuild(dirname, outDir, entries, nodeEnv) {
1182
+ mkdirSync(outDir, { recursive: true });
1183
+ const workerPath = join(outDir, "build-worker.mjs");
1184
+ writeFileSync(workerPath, REACT_BUILD_WORKER);
1185
+ return new Promise((resolvePromise) => {
1186
+ const child = spawn(process.execPath, [
1187
+ workerPath,
1188
+ dirname,
1189
+ outDir,
1190
+ JSON.stringify(entries),
1191
+ nodeEnv
1192
+ ], {
1193
+ cwd: dirname,
1194
+ stdio: [
1195
+ "ignore",
1196
+ "pipe",
1197
+ "pipe"
1198
+ ]
1199
+ });
1200
+ let out = "";
1201
+ child.stdout.on("data", (d) => out += String(d));
1202
+ child.stderr.on("data", (d) => out += String(d));
1203
+ const done = (r) => {
1204
+ rmSync(workerPath, { force: true });
1205
+ resolvePromise(r);
1206
+ };
1207
+ child.on("exit", (code) => done({
1208
+ ok: code === 0,
1209
+ stderr: out
1210
+ }));
1211
+ child.on("error", (e) => done({
1212
+ ok: false,
1213
+ stderr: String(e)
1214
+ }));
1215
+ });
1216
+ }
1217
+ const REACT_BUILD_WORKER = `
1218
+ import { createRequire } from 'node:module';
1219
+ import { mkdirSync, writeFileSync, rmSync } from 'node:fs';
1220
+ import { join } from 'node:path';
1221
+ import { pathToFileURL } from 'node:url';
1222
+ const [dirname, outDir, entriesJSON, nodeEnv] = process.argv.slice(2);
1223
+ const entries = JSON.parse(entriesJSON);
1224
+ const reqProj = createRequire(join(dirname, 'noop.js'));
1225
+ const { build } = await import(reqProj.resolve('vite'));
1226
+ const srcDir = join(outDir, '.entries');
1227
+ mkdirSync(srcDir, { recursive: true });
1228
+ const entryName = (spec) => spec.replace(/[^\\w]+/g, '_');
1229
+ 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(' '));
1230
+ const input = {};
1231
+ for (const [spec, file] of Object.entries(entries)) {
1232
+ const name = entryName(spec);
1233
+ let names = [];
1234
+ try {
1235
+ const ns = await import(pathToFileURL(file).href);
1236
+ const obj = (ns.default && typeof ns.default === 'object') ? ns.default : ns;
1237
+ names = Object.keys(obj).filter((k) => k !== 'default' && k !== '__esModule' && /^[A-Za-z_$][\\w$]*$/.test(k));
1238
+ } catch {}
1239
+ const plain = names.filter((n) => !RESERVED.has(n));
1240
+ const reserved = names.filter((n) => RESERVED.has(n));
1241
+ let src = 'import __m from ' + JSON.stringify(spec) + ';\\nexport default __m;\\n';
1242
+ if (plain.length) src += 'export const { ' + plain.join(', ') + ' } = __m;\\n';
1243
+ reserved.forEach((n, i) => {
1244
+ src += 'const __r' + i + ' = __m[' + JSON.stringify(n) + '];\\nexport { __r' + i + ' as ' + n + ' };\\n';
1245
+ });
1246
+ const entryFile = join(srcDir, name + '.js');
1247
+ writeFileSync(entryFile, src);
1248
+ // Key by the full spec so the output path mirrors the subpath
1249
+ // (react-dom/client -> react-dom/client.js), matching the catch-all import map.
1250
+ input[spec] = entryFile;
1251
+ }
1252
+ try {
1253
+ await build({
1254
+ root: dirname, configFile: false, logLevel: 'error',
1255
+ base: './', publicDir: false,
1256
+ define: { 'process.env.NODE_ENV': JSON.stringify(nodeEnv) },
1257
+ resolve: { conditions: ['browser', 'import', 'module', 'default'] },
1258
+ build: {
1259
+ outDir, emptyOutDir: false, minify: nodeEnv === 'production', target: 'esnext',
1260
+ rollupOptions: {
1261
+ input, preserveEntrySignatures: 'strict',
1262
+ output: { format: 'es', entryFileNames: '[name].js', chunkFileNames: 'chunks/[name]-[hash].js', assetFileNames: 'assets/[name]-[hash][extname]' },
1263
+ },
1264
+ },
1265
+ });
1266
+ rmSync(srcDir, { recursive: true, force: true });
1267
+ // Force exit: rolldown can leave native worker threads alive that keep the
1268
+ // event loop open, hanging the parent's spawn() promise indefinitely.
1269
+ process.exit(0);
1270
+ } catch (err) {
1271
+ console.error(err instanceof Error ? (err.stack ?? err.message) : String(err));
1272
+ process.exit(1);
1273
+ }
1274
+ `;
1275
+ //#endregion
567
1276
  //#region connect-utils/vite-config.ts
568
- const REACT_VERSION = "19.2.0";
569
- const REACT_IMPORTMAP_IMPORTS = {
570
- react: `https://esm.sh/react@${REACT_VERSION}`,
571
- "react/": `https://esm.sh/react@${REACT_VERSION}/`,
572
- "react-dom": `https://esm.sh/react-dom@${REACT_VERSION}`,
573
- "react-dom/": `https://esm.sh/react-dom@${REACT_VERSION}/`
574
- };
575
1277
  function getConnectHtmlTags(options = {}) {
576
1278
  const { registryUrl, injectTo = "head" } = options;
577
1279
  return [
@@ -579,7 +1281,7 @@ function getConnectHtmlTags(options = {}) {
579
1281
  tag: "meta",
580
1282
  attrs: {
581
1283
  "http-equiv": "Content-Security-Policy",
582
- content: `script-src 'self' 'unsafe-inline' 'unsafe-eval' https://esm.sh${registryUrl ? " " + registryUrl : ""}; worker-src 'self' blob:; object-src 'none'; base-uri 'self';`
1284
+ content: `script-src 'self' 'unsafe-inline' 'unsafe-eval'${registryUrl ? " " + registryUrl : ""}; worker-src 'self' blob:; object-src 'none'; base-uri 'self';`
583
1285
  },
584
1286
  injectTo
585
1287
  },
@@ -711,17 +1413,18 @@ function getConnectBaseViteConfig(options) {
711
1413
  const release = process.env.WORKSPACE_VERSION ?? process.env.npm_package_version ?? env.PH_CONNECT_VERSION;
712
1414
  const uploadSentrySourcemaps = authToken && org && project;
713
1415
  const connectHtmlTags = getConnectHtmlTags({ registryUrl: phPackageRegistryUrl });
1416
+ const devImportmapTag = mode === "development" ? [{
1417
+ tag: "script",
1418
+ attrs: { type: "importmap" },
1419
+ children: JSON.stringify({ imports: {} }),
1420
+ injectTo: "head-prepend"
1421
+ }] : [];
714
1422
  const plugins = [
715
1423
  tailwind(),
716
1424
  react(),
717
1425
  createHtmlPlugin({
718
1426
  minify: false,
719
- inject: { tags: [...connectHtmlTags, {
720
- tag: "script",
721
- attrs: { type: "importmap" },
722
- children: JSON.stringify({ imports: REACT_IMPORTMAP_IMPORTS }, null, 2),
723
- injectTo: "head-prepend"
724
- }] }
1427
+ inject: { tags: [...connectHtmlTags, ...devImportmapTag] }
725
1428
  })
726
1429
  ];
727
1430
  if (uploadSentrySourcemaps) plugins.push(import("@sentry/vite-plugin").then(({ sentryVitePlugin }) => sentryVitePlugin({
@@ -735,7 +1438,8 @@ function getConnectBaseViteConfig(options) {
735
1438
  bundleSizeOptimizations: { excludeDebugStatements: true },
736
1439
  reactComponentAnnotation: { enabled: true }
737
1440
  })));
738
- const customLogger = process.env.LOG_LEVEL === "debug" || phConfig.connect?.app?.logLevel === "debug" ? void 0 : viteLogger({ silence: {
1441
+ const isDebug = process.env.LOG_LEVEL === "debug" || phConfig.connect?.app?.logLevel === "debug";
1442
+ const customLogger = isDebug ? void 0 : viteLogger({ silence: {
739
1443
  warnings: ["@import must precede all other statements (besides @charset or empty @layer)"],
740
1444
  errors: ["Unterminated string literal"]
741
1445
  } });
@@ -793,9 +1497,13 @@ function getConnectBaseViteConfig(options) {
793
1497
  packages: localPackagesFromConfig,
794
1498
  projectRoot: options.dirname
795
1499
  }),
796
- devReactImportmapPlugin(),
1500
+ devReactImportmapPlugin(options.dirname),
797
1501
  ...plugins,
798
1502
  esmExternalRequirePlugin({ external: reactExternal }),
1503
+ reactSelfHostPlugin({
1504
+ dirname: options.dirname,
1505
+ dev: mode !== "production" || isDebug
1506
+ }),
799
1507
  connectFaviconPlugin(),
800
1508
  ...options.dynamicBase ? [connectDynamicBasePlugin()] : []
801
1509
  ],
@@ -807,6 +1515,6 @@ function getConnectBaseViteConfig(options) {
807
1515
  };
808
1516
  }
809
1517
  //#endregion
810
- export { DEFAULT_CONNECT_OUTDIR, DYNAMIC_BASE_PLACEHOLDER, EXTERNAL_PACKAGES_IMPORT, IMPORT_SCRIPT_FILE, LOCAL_PACKAGE_ID, PH_DIR_NAME, RUNTIME_CONFIG_SCHEMA_ID, RUNTIME_CONFIG_SCHEMA_URL, appendToHtmlHead, backupIndexHtml, connectDynamicBasePlugin, copyConnect, ensureNodeVersion, getConnectBaseViteConfig, getConnectHtmlTags, makeImportScriptFromPackages, phConfigPlugin, prependToHtmlHead, readJsonFile, removeBase64EnvValues, resolveConnectBundle, resolveConnectPackageJson, resolveConnectPublicDir, resolvePackage, resolveViteConfigPath, runShellScriptPlugin, runTsc, runtimeConfigSchema, stripVersionFromPackage };
1518
+ 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 };
811
1519
 
812
1520
  //# sourceMappingURL=index.mjs.map