@base44-preview/vite-plugin 1.0.37-pr.121.de0cee3 → 1.0.38-pr.121.f18ee18

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.
Files changed (37) hide show
  1. package/README.md +1 -1
  2. package/dist/base44-runtime-plugin.d.ts +3 -0
  3. package/dist/base44-runtime-plugin.d.ts.map +1 -0
  4. package/dist/base44-runtime-plugin.js +95 -0
  5. package/dist/base44-runtime-plugin.js.map +1 -0
  6. package/dist/build-status-plugin.d.ts.map +1 -1
  7. package/dist/build-status-plugin.js +41 -21
  8. package/dist/build-status-plugin.js.map +1 -1
  9. package/dist/bundled-dev.js +1 -1
  10. package/dist/bundled-dev.js.map +1 -1
  11. package/dist/error-overlay-plugin.d.ts.map +1 -1
  12. package/dist/error-overlay-plugin.js +9 -0
  13. package/dist/error-overlay-plugin.js.map +1 -1
  14. package/dist/html-injections-plugin.d.ts +10 -8
  15. package/dist/html-injections-plugin.d.ts.map +1 -1
  16. package/dist/html-injections-plugin.js +45 -9
  17. package/dist/html-injections-plugin.js.map +1 -1
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +6 -0
  20. package/dist/index.js.map +1 -1
  21. package/dist/injections/sandbox-build-error-notifier.d.ts +4 -0
  22. package/dist/injections/sandbox-build-error-notifier.d.ts.map +1 -1
  23. package/dist/injections/sandbox-build-error-notifier.js +22 -25
  24. package/dist/injections/sandbox-build-error-notifier.js.map +1 -1
  25. package/dist/injections/sandbox-hmr-socket-relay.d.ts +5 -0
  26. package/dist/injections/sandbox-hmr-socket-relay.d.ts.map +1 -1
  27. package/dist/injections/sandbox-hmr-socket-relay.js +22 -18
  28. package/dist/injections/sandbox-hmr-socket-relay.js.map +1 -1
  29. package/package.json +1 -1
  30. package/src/base44-runtime-plugin.ts +103 -0
  31. package/src/build-status-plugin.ts +46 -22
  32. package/src/bundled-dev.ts +1 -1
  33. package/src/error-overlay-plugin.ts +11 -0
  34. package/src/html-injections-plugin.ts +50 -17
  35. package/src/index.ts +6 -0
  36. package/src/injections/sandbox-build-error-notifier.ts +28 -25
  37. package/src/injections/sandbox-hmr-socket-relay.ts +28 -15
@@ -1 +1 @@
1
- {"version":3,"file":"sandbox-build-error-notifier.js","sourceRoot":"","sources":["../../src/injections/sandbox-build-error-notifier.ts"],"names":[],"mappings":"AAAA,qCAAqC;AAErC,wEAAwE;AACxE,+EAA+E;AAC/E,qEAAqE;AACrE,IAAI,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC;IACpB,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,YAAY,EAAE,CAAC,OAA0C,EAAE,EAAE;QAC9E,MAAM,GAAG,GAAG,OAAO,EAAE,GAAG,IAAI,EAAE,CAAC;QAC/B,MAAM,OAAO,GAAG,OAAO,GAAG,CAAC,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,aAAa,CAAC;QAC9E,MAAM,KAAK,GAAG,OAAO,GAAG,CAAC,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC;QACpE,MAAM,GAAG,GAAG,GAAG,CAAC,GAAoE,CAAC;QACrF,MAAM,IAAI,GAAG,GAAG,EAAE,IAAI,IAAI,CAAC,OAAO,GAAG,CAAC,EAAE,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;QAC5E,MAAM,KAAK,GAAG,IAAI,IAAI,GAAG,EAAE,IAAI,IAAI,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI,IAAI,GAAG,CAAC,IAAI,IAAI,GAAG,CAAC,MAAM,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;QAC1F,MAAM,OAAO,GAAG,CAAC,KAAK,EAAE,OAAO,GAAG,CAAC,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,EAAE,KAAK,CAAC;aAClF,MAAM,CAAC,OAAO,CAAC;aACf,IAAI,CAAC,IAAI,CAAC,CAAC;QACd,MAAM,CAAC,MAAM,EAAE,WAAW,CACxB;YACE,IAAI,EAAE,WAAW;YACjB,KAAK,EAAE;gBACL,KAAK,EAAE,OAAO;gBACd,OAAO,EAAE,OAAO,IAAI,SAAS;gBAC7B,aAAa,EAAE,SAAS;gBACxB,aAAa,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,EAAE,EAAE,GAAG,CAAC,EAAE,EAAE,GAAG,EAAE;gBACtE,KAAK;aACN;SACF,EACD,GAAG,CACJ,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC;AAED,OAAO,EAAE,CAAC"}
1
+ {"version":3,"file":"sandbox-build-error-notifier.js","sourceRoot":"","sources":["../../src/injections/sandbox-build-error-notifier.ts"],"names":[],"mappings":"AAAA,qCAAqC;AAQrC,MAAM,UAAU,gBAAgB,CAAC,OAA0B,EAAE,MAAc,MAAM;IAC/E,MAAM,GAAG,GAAG,OAAO,EAAE,GAAG,IAAI,EAAE,CAAC;IAC/B,MAAM,OAAO,GAAG,OAAO,GAAG,CAAC,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,aAAa,CAAC;IAC9E,MAAM,KAAK,GAAG,OAAO,GAAG,CAAC,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC;IACpE,MAAM,GAAG,GAAG,GAAG,CAAC,GAAoE,CAAC;IACrF,MAAM,IAAI,GAAG,GAAG,EAAE,IAAI,IAAI,CAAC,OAAO,GAAG,CAAC,EAAE,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;IAC5E,MAAM,KAAK,GAAG,IAAI,IAAI,GAAG,EAAE,IAAI,IAAI,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI,IAAI,GAAG,CAAC,IAAI,IAAI,GAAG,CAAC,MAAM,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;IAC1F,MAAM,OAAO,GAAG,CAAC,KAAK,EAAE,OAAO,GAAG,CAAC,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,EAAE,KAAK,CAAC;SAClF,MAAM,CAAC,OAAO,CAAC;SACf,IAAI,CAAC,IAAI,CAAC,CAAC;IACd,GAAG,CAAC,MAAM,EAAE,WAAW,CACrB;QACE,IAAI,EAAE,WAAW;QACjB,KAAK,EAAE;YACL,KAAK,EAAE,OAAO;YACd,OAAO,EAAE,OAAO,IAAI,SAAS;YAC7B,aAAa,EAAE,SAAS;YACxB,aAAa,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,EAAE,EAAE,GAAG,CAAC,EAAE,EAAE,GAAG,EAAE;YACtE,KAAK;SACN;KACF,EACD,GAAG,CACJ,CAAC;AACJ,CAAC;AAED,IAAI,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC;IACpB,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,YAAY,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,gBAAgB,CAAC,OAA4B,CAAC,CAAC,CAAC;AAChG,CAAC"}
@@ -1,2 +1,7 @@
1
+ type HotEvents = {
2
+ on: (event: string, cb: (payload?: unknown) => unknown) => void;
3
+ };
4
+ export declare const PREVIEW_FE_VERSION_PARAM = "previewFE_version";
5
+ export declare function attachHmrSocketRelay(hot: HotEvents, win?: Window): void;
1
6
  export {};
2
7
  //# sourceMappingURL=sandbox-hmr-socket-relay.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"sandbox-hmr-socket-relay.d.ts","sourceRoot":"","sources":["../../src/injections/sandbox-hmr-socket-relay.ts"],"names":[],"mappings":"AAyBA,OAAO,EAAE,CAAC"}
1
+ {"version":3,"file":"sandbox-hmr-socket-relay.d.ts","sourceRoot":"","sources":["../../src/injections/sandbox-hmr-socket-relay.ts"],"names":[],"mappings":"AAQA,KAAK,SAAS,GAAG;IACf,EAAE,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,OAAO,CAAC,EAAE,OAAO,KAAK,OAAO,KAAK,IAAI,CAAC;CACjE,CAAC;AAIF,eAAO,MAAM,wBAAwB,sBAAsB,CAAC;AAE5D,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,SAAS,EAAE,GAAG,GAAE,MAAe,QAkBxE"}
@@ -1,24 +1,28 @@
1
1
  /// <reference types="vite/client" />
2
- // The preview proxy's bridge watches the HMR socket by importing /@vite/client,
3
- // which bundled dev mode does not serve. This module ships inside the bundle and
4
- // reports the same events; the bridge skips its own attach when it sees the flag.
5
- window.__base44_bundled_dev__ = true;
6
- if (import.meta.hot && window.self !== window.top) {
7
- const previewFEVersion = Number(new URLSearchParams(window.location.search).get("previewFE_version")) || 0;
8
- import.meta.hot.on("vite:ws:connect", () => {
9
- window.parent?.postMessage({ type: "SANDBOX_WS_CONNECT" }, "*");
2
+ // Query param the editor adds to the preview URL when it can swap in a fresh
3
+ // iframe on a full reload (mirrors builder_bridge_inject.py in base44-dev/apper).
4
+ export const PREVIEW_FE_VERSION_PARAM = "previewFE_version";
5
+ export function attachHmrSocketRelay(hot, win = window) {
6
+ win.__base44_bundled_dev__ = true;
7
+ if (win.self === win.top)
8
+ return;
9
+ const previewFEVersion = Number(new URLSearchParams(win.location.search).get(PREVIEW_FE_VERSION_PARAM)) || 0;
10
+ hot.on("vite:ws:connect", () => {
11
+ win.parent?.postMessage({ type: "SANDBOX_WS_CONNECT" }, "*");
10
12
  });
11
- import.meta.hot.on("vite:ws:disconnect", () => {
12
- window.parent?.postMessage({ type: "SANDBOX_WS_DISCONNECT" }, "*");
13
+ hot.on("vite:ws:disconnect", () => {
14
+ win.parent?.postMessage({ type: "SANDBOX_WS_DISCONNECT" }, "*");
13
15
  });
14
- // Vite awaits these listeners before reloading; a pending promise lets the
15
- // editor swap in a fresh iframe instead of reloading in place.
16
- import.meta.hot.on("vite:beforeFullReload", () => {
17
- window.parent?.postMessage({ type: "sandbox:beforeFullReload" }, "*");
18
- if (previewFEVersion > 0)
19
- return new Promise(() => { });
20
- return undefined;
16
+ // Vite's bundled client awaits `vite:beforeFullReload` listeners before it
17
+ // reloads (verified against vite 8.2 bundledDevClient), so a never-settling
18
+ // promise holds the reload and lets the editor swap in a fresh iframe instead.
19
+ hot.on("vite:beforeFullReload", () => {
20
+ win.parent?.postMessage({ type: "sandbox:beforeFullReload" }, "*");
21
+ return previewFEVersion > 0 ? new Promise(() => { }) : undefined;
21
22
  });
22
23
  }
23
- export {};
24
+ // The socket opens asynchronously, after the whole bundle has evaluated, so the
25
+ // listeners above are always registered before the first connect event.
26
+ if (import.meta.hot)
27
+ attachHmrSocketRelay(import.meta.hot);
24
28
  //# sourceMappingURL=sandbox-hmr-socket-relay.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"sandbox-hmr-socket-relay.js","sourceRoot":"","sources":["../../src/injections/sandbox-hmr-socket-relay.ts"],"names":[],"mappings":"AAAA,qCAAqC;AAErC,gFAAgF;AAChF,iFAAiF;AACjF,kFAAkF;AACjF,MAA6C,CAAC,sBAAsB,GAAG,IAAI,CAAC;AAE7E,IAAI,MAAM,CAAC,IAAI,CAAC,GAAG,IAAI,MAAM,CAAC,IAAI,KAAK,MAAM,CAAC,GAAG,EAAE,CAAC;IAClD,MAAM,gBAAgB,GACpB,MAAM,CAAC,IAAI,eAAe,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,mBAAmB,CAAC,CAAC,IAAI,CAAC,CAAC;IACpF,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,iBAAiB,EAAE,GAAG,EAAE;QACzC,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,EAAE,IAAI,EAAE,oBAAoB,EAAE,EAAE,GAAG,CAAC,CAAC;IAClE,CAAC,CAAC,CAAC;IACH,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,oBAAoB,EAAE,GAAG,EAAE;QAC5C,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,EAAE,IAAI,EAAE,uBAAuB,EAAE,EAAE,GAAG,CAAC,CAAC;IACrE,CAAC,CAAC,CAAC;IACH,2EAA2E;IAC3E,+DAA+D;IAC/D,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,uBAAuB,EAAE,GAAG,EAAE;QAC/C,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,EAAE,IAAI,EAAE,0BAA0B,EAAE,EAAE,GAAG,CAAC,CAAC;QACtE,IAAI,gBAAgB,GAAG,CAAC;YAAE,OAAO,IAAI,OAAO,CAAO,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;QAC7D,OAAO,SAAS,CAAC;IACnB,CAAC,CAAC,CAAC;AACL,CAAC;AAED,OAAO,EAAE,CAAC"}
1
+ {"version":3,"file":"sandbox-hmr-socket-relay.js","sourceRoot":"","sources":["../../src/injections/sandbox-hmr-socket-relay.ts"],"names":[],"mappings":"AAAA,qCAAqC;AAYrC,6EAA6E;AAC7E,kFAAkF;AAClF,MAAM,CAAC,MAAM,wBAAwB,GAAG,mBAAmB,CAAC;AAE5D,MAAM,UAAU,oBAAoB,CAAC,GAAc,EAAE,MAAc,MAAM;IACtE,GAA0C,CAAC,sBAAsB,GAAG,IAAI,CAAC;IAC1E,IAAI,GAAG,CAAC,IAAI,KAAK,GAAG,CAAC,GAAG;QAAE,OAAO;IACjC,MAAM,gBAAgB,GACpB,MAAM,CAAC,IAAI,eAAe,CAAC,GAAG,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,wBAAwB,CAAC,CAAC,IAAI,CAAC,CAAC;IACtF,GAAG,CAAC,EAAE,CAAC,iBAAiB,EAAE,GAAG,EAAE;QAC7B,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,EAAE,IAAI,EAAE,oBAAoB,EAAE,EAAE,GAAG,CAAC,CAAC;IAC/D,CAAC,CAAC,CAAC;IACH,GAAG,CAAC,EAAE,CAAC,oBAAoB,EAAE,GAAG,EAAE;QAChC,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,EAAE,IAAI,EAAE,uBAAuB,EAAE,EAAE,GAAG,CAAC,CAAC;IAClE,CAAC,CAAC,CAAC;IACH,2EAA2E;IAC3E,4EAA4E;IAC5E,+EAA+E;IAC/E,GAAG,CAAC,EAAE,CAAC,uBAAuB,EAAE,GAAG,EAAE;QACnC,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,EAAE,IAAI,EAAE,0BAA0B,EAAE,EAAE,GAAG,CAAC,CAAC;QACnE,OAAO,gBAAgB,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,OAAO,CAAO,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IACxE,CAAC,CAAC,CAAC;AACL,CAAC;AAED,gFAAgF;AAChF,wEAAwE;AACxE,IAAI,MAAM,CAAC,IAAI,CAAC,GAAG;IAAE,oBAAoB,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@base44-preview/vite-plugin",
3
- "version": "1.0.37-pr.121.de0cee3",
3
+ "version": "1.0.38-pr.121.f18ee18",
4
4
  "description": "The Vite plugin for base44 based applications",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -0,0 +1,103 @@
1
+ import type { Plugin } from "vite";
2
+
3
+ /**
4
+ * Serves `base44:runtime` to full-stack server routes, so a route reads secrets
5
+ * and schedules background work with the SAME import a backend function uses:
6
+ *
7
+ * import { secrets, waitUntil } from "base44:runtime";
8
+ *
9
+ * WHY A BUILD-TIME VIRTUAL MODULE. The Workers runtime cannot resolve a custom
10
+ * scheme: the legacy module registry special-cases only `node:` / `cloudflare:` /
11
+ * `workerd:`, and the new registry (`new_module_registry`) explicitly rejects a
12
+ * bundle module whose name carries a non-`file:` scheme. Import maps are not
13
+ * supported either. So `base44:runtime` has to be resolved before upload — there
14
+ * is no runtime path to it, and no amount of wrapping the deployed entry creates
15
+ * one.
16
+ *
17
+ * WHY NO ASYNCLOCALSTORAGE / NO WRAPPER ENTRY. Cloudflare now ships both
18
+ * primitives ambiently from `cloudflare:workers` (`env` since 2025-03-17,
19
+ * `waitUntil` since 2025-08-08), backed by workerd's own AsyncContextFrame and
20
+ * IoContext. The backend-function bundler predates that and threads a context
21
+ * through `globalThis.Base44` because it owns the generated entry; here the
22
+ * framework owns the entry, and borrowing the platform's ambient accessors means
23
+ * we never have to.
24
+ */
25
+
26
+ const SPECIFIER = "base44:runtime";
27
+ // Rollup convention: a resolved virtual id starts with NUL so no other plugin
28
+ // (and no filesystem lookup) tries to claim it.
29
+ const RESOLVED = "\0base44:runtime";
30
+
31
+ // Reserved so a route importing it fails with an explanation instead of a bare
32
+ // "failed to resolve import". Actors are never imported — they live in the app's
33
+ // actors directory, in server routes exactly as in backend functions.
34
+ const ACTORS_REASON =
35
+ "Actors are defined in the app's actors directory, not imported into a route.";
36
+
37
+ function unsupportedReason(id: string): string | null {
38
+ if (id === "base44:runtime/actors") return ACTORS_REASON;
39
+ return null;
40
+ }
41
+
42
+ /**
43
+ * `cloudflare:workers` stays external — workerd provides it, and Nitro's
44
+ * cloudflare preset already treats `cloudflare:*` that way.
45
+ *
46
+ * There is deliberately no separate dev body. Nitro's `cloudflareDev` preset
47
+ * (aliased for `cloudflare-module`, which is what these apps build with) sets
48
+ * `devServer.runner: "miniflare"`, so dev runs in workerd too and the same
49
+ * import resolves there. A `process.env` fallback would buy nothing and cost a
50
+ * dev/prod behaviour split — the shape that makes background work silently
51
+ * vanish locally while working in production. If someone overrides the runner
52
+ * away from workerd, this fails loudly at resolution rather than quietly
53
+ * serving a different value.
54
+ *
55
+ * `waitUntil` is WRAPPED rather than re-exported: ours returns the promise so it
56
+ * composes (`infra/base44-userapp-bundler/src/runtime/index.ts` in apper),
57
+ * Cloudflare's returns void. A bare re-export would make `const p = waitUntil(x)`
58
+ * work in a backend function and silently yield `undefined` here — one specifier
59
+ * with two behaviours is the exact failure this module exists to prevent.
60
+ */
61
+ const RUNTIME_MODULE = `
62
+ import { env, waitUntil as cfWaitUntil } from "cloudflare:workers";
63
+
64
+ export function waitUntil(promise) {
65
+ cfWaitUntil(promise);
66
+ return promise;
67
+ }
68
+
69
+ export const secrets = {
70
+ get(name) {
71
+ const value = env[name];
72
+ return typeof value === "string" ? value : undefined;
73
+ },
74
+ };
75
+ `;
76
+
77
+ export function base44RuntimePlugin(): Plugin {
78
+ return {
79
+ name: "base44-runtime",
80
+ // Ahead of Vite's own resolution so the bare `base44:` specifier is never
81
+ // mistaken for a URL protocol.
82
+ enforce: "pre",
83
+
84
+ resolveId(id) {
85
+ const reason = unsupportedReason(id);
86
+ if (reason) {
87
+ this.error(`"${id}" is not available in server routes. ${reason}`);
88
+ }
89
+ if (id !== SPECIFIER) return null;
90
+
91
+ // `cloudflare:workers` has no browser equivalent, so a client-side import
92
+ // would bundle a module that cannot run. Fail with the reason instead.
93
+ if (this.environment?.name === "client") {
94
+ this.error(`"${SPECIFIER}" is server-only. Move this import into a server route.`);
95
+ }
96
+ return RESOLVED;
97
+ },
98
+
99
+ load(id) {
100
+ return id === RESOLVED ? RUNTIME_MODULE : null;
101
+ },
102
+ };
103
+ }
@@ -1,4 +1,4 @@
1
- import type { Connect, Plugin, ViteDevServer } from "vite";
1
+ import type { Connect, Logger, Plugin, ViteDevServer } from "vite";
2
2
  import type { ServerResponse } from "http";
3
3
  import { isBundledDev } from "./bundled-dev.js";
4
4
 
@@ -21,11 +21,11 @@ import { isBundledDev } from "./bundled-dev.js";
21
21
  * The status is always reported in the body with HTTP `200`, even on error:
22
22
  * a `500` here would trip the preview proxy's own error path.
23
23
  *
24
- * Cold-start caveat: errors thrown before any client has connected are not
25
- * sent over the HMR channel, so a boot-time transform failure may briefly
26
- * read `ok: true`. That window is covered server-side by the existing
27
- * dev_server_error_classifier. This endpoint reports post-load
28
- * HMR / compile errors.
24
+ * Cold-start caveat (middleware dev server): errors thrown before any client
25
+ * has connected are not sent over the HMR channel, so a boot-time transform
26
+ * failure may briefly read `ok: true`. That window is covered server-side by
27
+ * the existing dev_server_error_classifier. Under bundled dev the build hooks
28
+ * and logger below observe boot failures too.
29
29
  */
30
30
 
31
31
  const ENDPOINT = "/__build_status";
@@ -37,6 +37,18 @@ const MAX_FRAME = 2000;
37
37
 
38
38
  type SendChannel = { send: (...args: unknown[]) => void };
39
39
 
40
+ const PATCHED = Symbol.for("base44.buildStatus.loggerPatched");
41
+ type PatchableLogger = Omit<Logger, "error"> & {
42
+ error: Logger["error"] & { [PATCHED]?: boolean };
43
+ };
44
+
45
+ // Plugin hooks run once per environment in bundled dev; only the client's
46
+ // build is the preview. Older Vite has no `this.environment`.
47
+ function isClientEnvironment(ctx: unknown): boolean {
48
+ const name = (ctx as { environment?: { name?: string } } | undefined)?.environment?.name;
49
+ return name === undefined || name === "client";
50
+ }
51
+
40
52
  const LOOPBACK_ADDRESSES = new Set([
41
53
  "127.0.0.1",
42
54
  "::1",
@@ -131,29 +143,41 @@ export function buildStatusPlugin(): Plugin {
131
143
  configResolved(config) {
132
144
  bundled = isBundledDev(config);
133
145
  },
134
- // Bundled dev runs the rolldown build hooks on every rebuild; a build that
135
- // reaches generateBundle superseded whatever error was recorded before.
146
+ // Bundled dev runs the rolldown build hooks on every rebuild of the client
147
+ // environment; a build that reaches generateBundle superseded whatever error
148
+ // was recorded before. Other environments' builds say nothing about the preview.
136
149
  buildEnd(error) {
137
- if (bundled && error) lastError = error;
150
+ if (bundled && error && isClientEnvironment(this)) lastError = error;
138
151
  },
139
152
  generateBundle() {
140
- if (bundled) lastError = null;
153
+ if (bundled && isClientEnvironment(this)) lastError = null;
141
154
  },
142
155
  configureServer(server: ViteDevServer) {
143
- try {
144
- if (bundled) {
145
- // Bundled dev reports build failures through the environment logger
146
- // (`{ error }` option) and per-client sends, never the broadcast
147
- // channel patched below.
148
- const logger = server.environments.client.logger;
149
- const origError = logger.error.bind(logger);
150
- logger.error = (msg, options) => {
151
- if (options?.error) lastError = options.error;
152
- return origError(msg, options);
153
- };
156
+ if (bundled) {
157
+ // Bundled dev reports build failures through the environment logger
158
+ // (`{ error }` option) and per-client sends, never the broadcast
159
+ // channel patched below. Any logged `{ error }` counts until the next
160
+ // successful build: the fail-safe direction for a health endpoint.
161
+ // Kept out of the endpoint's try so a logger surprise never unmounts it.
162
+ try {
163
+ const logger = server.environments.client.logger as PatchableLogger;
164
+ if (!logger.error[PATCHED]) {
165
+ const origError = logger.error.bind(logger);
166
+ const patched: PatchableLogger["error"] = (msg, options) => {
167
+ if (options?.error) lastError = options.error;
168
+ return origError(msg, options);
169
+ };
170
+ patched[PATCHED] = true;
171
+ logger.error = patched;
172
+ }
173
+ } catch {
174
+ // Build failures then surface only via buildEnd; the endpoint still mounts.
154
175
  }
176
+ }
177
+ try {
155
178
  // Observe the HMR channel: an `error` payload records the last
156
- // error; a successful `update` / `full-reload` clears it.
179
+ // error; a successful `update` / `full-reload` clears it. Left on in
180
+ // bundled dev too: nothing is broadcast there, so it is inert.
157
181
  const ch = (server.hot ?? server.ws) as unknown as SendChannel | undefined;
158
182
  if (ch && typeof ch.send === "function") {
159
183
  const orig = ch.send.bind(ch);
@@ -9,5 +9,5 @@
9
9
  export function isBundledDev(config: unknown): boolean {
10
10
  const experimental = (config as { experimental?: { bundledDev?: unknown } } | undefined)
11
11
  ?.experimental;
12
- return experimental?.bundledDev === true;
12
+ return Boolean(experimental?.bundledDev);
13
13
  }
@@ -17,6 +17,17 @@ export function errorOverlayPlugin() {
17
17
  // Vite's overlay off and let sandbox-build-error-notifier forward the error.
18
18
  config: (config) =>
19
19
  isBundledDev(config) ? { server: { hmr: { overlay: false } } } : null,
20
+ // `config` sees only the user config; a later plugin or CLI flag can still
21
+ // turn bundled dev on, leaving Vite's overlay up next to the notifier.
22
+ configResolved(config) {
23
+ const hmr = config.server.hmr;
24
+ if (isBundledDev(config) && hmr !== false && (hmr === true || hmr?.overlay !== false)) {
25
+ console.warn(
26
+ "[error-overlay] Bundled dev mode is on but server.hmr.overlay is not false — " +
27
+ "Vite's own overlay will show alongside the app_error sent to the parent."
28
+ );
29
+ }
30
+ },
20
31
  transform(code, id, opts = {}) {
21
32
  if (opts?.ssr) return;
22
33
 
@@ -1,13 +1,14 @@
1
1
  import { createReadStream, existsSync, statSync } from "node:fs";
2
2
  import path from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
- import type { Plugin, HtmlTagDescriptor, ViteDevServer } from "vite";
4
+ import type { ServerResponse } from "node:http";
5
+ import type { Connect, Plugin, HtmlTagDescriptor, ViteDevServer } from "vite";
5
6
  import { loadEnv } from "vite";
6
7
  import { isBundledDev } from "./bundled-dev.js";
7
8
 
8
9
  type Injection = {
9
- /** Script src path for dev mode (served from node_modules) */
10
- src?: string;
10
+ /** Script path under this package's dist/, served from node_modules in dev. */
11
+ file?: string;
11
12
  injectTo: "head" | "body";
12
13
  mode: "dev" | "production";
13
14
  /** Inline content for production builds (src paths don't work in built output) */
@@ -50,36 +51,42 @@ if (window.self === window.top) {
50
51
  `;
51
52
 
52
53
  const DIST_URL_PREFIX = "/node_modules/@base44/vite-plugin/dist";
54
+ /**
55
+ * Bundled dev resolves module-script `src` through the bundler instead of the
56
+ * dev server, so a node_modules URL would break on any non-flat install. These
57
+ * ids are mapped onto the plugin's own dist tree by `resolveId` below.
58
+ */
59
+ export const BUNDLED_INJECTION_PREFIX = "virtual:base44-vite-plugin/";
53
60
 
54
61
  const INJECTIONS: Record<string, Injection> = {
55
62
  unhandledErrors: {
56
- src: `${DIST_URL_PREFIX}/injections/unhandled-errors-handlers.js`,
63
+ file: "injections/unhandled-errors-handlers.js",
57
64
  injectTo: "head",
58
65
  mode: "dev",
59
66
  },
60
67
  sandboxMount: {
61
- src: `${DIST_URL_PREFIX}/injections/sandbox-mount-observer.js`,
68
+ file: "injections/sandbox-mount-observer.js",
62
69
  injectTo: "body",
63
70
  mode: "dev",
64
71
  },
65
72
  hmrNotifier: {
66
- src: `${DIST_URL_PREFIX}/injections/sandbox-hmr-notifier.js`,
73
+ file: "injections/sandbox-hmr-notifier.js",
67
74
  injectTo: "head",
68
75
  mode: "dev",
69
76
  },
70
77
  navigationNotifier: {
71
- src: `${DIST_URL_PREFIX}/injections/navigation-notifier.js`,
78
+ file: "injections/navigation-notifier.js",
72
79
  injectTo: "head",
73
80
  mode: "dev",
74
81
  },
75
82
  buildErrorNotifier: {
76
- src: `${DIST_URL_PREFIX}/injections/sandbox-build-error-notifier.js`,
83
+ file: "injections/sandbox-build-error-notifier.js",
77
84
  injectTo: "head",
78
85
  mode: "dev",
79
86
  bundledOnly: true,
80
87
  },
81
88
  hmrSocketRelay: {
82
- src: `${DIST_URL_PREFIX}/injections/sandbox-hmr-socket-relay.js`,
89
+ file: "injections/sandbox-hmr-socket-relay.js",
83
90
  injectTo: "head",
84
91
  mode: "dev",
85
92
  bundledOnly: true,
@@ -99,6 +106,12 @@ const CONTENT_TYPES: Record<string, string> = {
99
106
 
100
107
  /** This file lives in dist/ once built, so its directory is the served root. */
101
108
  export const PLUGIN_DIST_DIR = path.dirname(fileURLToPath(import.meta.url));
109
+ const VISUAL_EDIT_AGENT_FILE = "statics/index.mjs";
110
+
111
+ export function resolveBundledInjection(id: string, distDir = PLUGIN_DIST_DIR): string | undefined {
112
+ if (!id.startsWith(BUNDLED_INJECTION_PREFIX)) return undefined;
113
+ return path.join(distDir, id.slice(BUNDLED_INJECTION_PREFIX.length));
114
+ }
102
115
 
103
116
  /**
104
117
  * Bundled dev mounts no static/transform middleware, so the visual-edit agent's
@@ -106,14 +119,16 @@ export const PLUGIN_DIST_DIR = path.dirname(fileURLToPath(import.meta.url));
106
119
  * fallback and receive index.html. Serve the plugin's own dist tree instead.
107
120
  */
108
121
  export function serveDistMiddleware(distDir: string) {
109
- return (
110
- req: { url?: string | undefined },
111
- res: { statusCode: number; setHeader: (k: string, v: string) => void; end: (b?: string) => void },
112
- next: (err?: unknown) => void
113
- ) => {
122
+ return (req: Connect.IncomingMessage, res: ServerResponse, next: Connect.NextFunction) => {
114
123
  const url = (req.url ?? "").split("?")[0] ?? "";
115
124
  if (!url.startsWith(`${DIST_URL_PREFIX}/`)) return next();
116
- const relative = decodeURIComponent(url.slice(DIST_URL_PREFIX.length + 1));
125
+ if (req.method !== "GET" && req.method !== "HEAD") return next();
126
+ let relative: string;
127
+ try {
128
+ relative = decodeURIComponent(url.slice(DIST_URL_PREFIX.length + 1));
129
+ } catch {
130
+ return next();
131
+ }
117
132
  const filePath = path.resolve(distDir, relative);
118
133
  const type = CONTENT_TYPES[path.extname(filePath)];
119
134
  if (
@@ -127,7 +142,8 @@ export function serveDistMiddleware(distDir: string) {
127
142
  res.statusCode = 200;
128
143
  res.setHeader("Content-Type", type);
129
144
  res.setHeader("Cache-Control", "no-store");
130
- createReadStream(filePath).pipe(res as unknown as NodeJS.WritableStream);
145
+ // Headers are already out, so a read failure can only end the response.
146
+ createReadStream(filePath).on("error", () => res.end()).pipe(res);
131
147
  };
132
148
  }
133
149
 
@@ -142,6 +158,9 @@ export function htmlInjectionsPlugin({
142
158
  visualEditAgent: boolean;
143
159
  analyticsTracker: boolean;
144
160
  }): Plugin[] {
161
+ // The two bundled-only scripts stand in for things that are unconditional in
162
+ // the middleware server (the error overlay swap and the preview bridge's
163
+ // `/@vite/client` import), so they do not follow the opt-in toggles.
145
164
  const enabledInjections: Record<string, boolean> = {
146
165
  unhandledErrors: true,
147
166
  sandboxMount: true,
@@ -152,6 +171,8 @@ export function htmlInjectionsPlugin({
152
171
  analyticsTracker,
153
172
  };
154
173
 
174
+ // Written by `dev.configResolved`; the three plugins below are always
175
+ // registered together and read this shared state.
155
176
  let resolvedEnv: Record<string, string> = {};
156
177
  let isServe = false;
157
178
  let bundled = false;
@@ -178,9 +199,12 @@ export function htmlInjectionsPlugin({
178
199
  injectTo: injection.injectTo,
179
200
  };
180
201
  }
202
+ const src = bundled
203
+ ? `${BUNDLED_INJECTION_PREFIX}${injection.file}`
204
+ : `${DIST_URL_PREFIX}/${injection.file}`;
181
205
  return {
182
206
  tag: "script",
183
- attrs: { src: injection.src, type: "module" },
207
+ attrs: { src, type: "module" },
184
208
  injectTo: injection.injectTo,
185
209
  };
186
210
  });
@@ -221,6 +245,12 @@ export function htmlInjectionsPlugin({
221
245
  },
222
246
  configureServer(server: ViteDevServer) {
223
247
  if (!bundled) return;
248
+ if (visualEditAgent && !existsSync(path.join(PLUGIN_DIST_DIR, VISUAL_EDIT_AGENT_FILE))) {
249
+ console.warn(
250
+ `[html-injections] ${VISUAL_EDIT_AGENT_FILE} not found under ${PLUGIN_DIST_DIR}; ` +
251
+ "the visual edit agent will not load in bundled dev mode."
252
+ );
253
+ }
224
254
  server.middlewares.use(serveDistMiddleware(PLUGIN_DIST_DIR));
225
255
  },
226
256
  transformIndexHtml: {
@@ -236,6 +266,9 @@ export function htmlInjectionsPlugin({
236
266
  // injecting after its own HTML processing, exactly as before.
237
267
  const devBundled: Plugin = {
238
268
  name: "html-injections-bundled",
269
+ resolveId(id) {
270
+ return resolveBundledInjection(id);
271
+ },
239
272
  transformIndexHtml: {
240
273
  order: "pre",
241
274
  handler() {
package/src/index.ts CHANGED
@@ -6,6 +6,7 @@ import { visualEditPlugin } from "./visual-edit-plugin.js";
6
6
  import { filterPackagesInProject } from "./utils.js";
7
7
  import { htmlInjectionsPlugin } from "./html-injections-plugin.js";
8
8
  import { buildStatusPlugin } from "./build-status-plugin.js";
9
+ import { base44RuntimePlugin } from "./base44-runtime-plugin.js";
9
10
 
10
11
  const isRunningInSandbox = !!process.env.MODAL_SANDBOX_ID;
11
12
 
@@ -288,5 +289,10 @@ export default function vitePlugin(
288
289
  ...(process.env.BASE44_BUILD_STATUS_ENABLED === "1"
289
290
  ? [buildStatusPlugin()]
290
291
  : []),
292
+ // Serves `base44:runtime` to full-stack server routes. Unconditional: it
293
+ // only claims the `base44:` specifiers, so an app with no server dir never
294
+ // notices it. Ordering comes from its own `enforce: "pre"`, not from its
295
+ // position here — several tests index this array from the front.
296
+ base44RuntimePlugin(),
291
297
  ];
292
298
  }
@@ -3,31 +3,34 @@
3
3
  // Bundled dev mode keeps Vite's own client, so the ErrorOverlay swap in
4
4
  // error-overlay-plugin never runs. The overlay is disabled server-side instead
5
5
  // and build errors are forwarded here in the same `app_error` shape.
6
- if (import.meta.hot) {
7
- import.meta.hot.on("vite:error", (payload: { err?: Record<string, unknown> }) => {
8
- const err = payload?.err ?? {};
9
- const message = typeof err.message === "string" ? err.message : "Build error";
10
- const stack = typeof err.stack === "string" ? err.stack : undefined;
11
- const loc = err.loc as { file?: string; line?: number; column?: number } | undefined;
12
- const file = loc?.file ?? (typeof err.id === "string" ? err.id : undefined);
13
- const where = file && loc?.line != null ? `${file}:${loc.line}:${loc.column ?? 0}` : file;
14
- const details = [where, typeof err.frame === "string" ? err.frame : undefined, stack]
15
- .filter(Boolean)
16
- .join("\n");
17
- window.parent?.postMessage(
18
- {
19
- type: "app_error",
20
- error: {
21
- title: message,
22
- details: details || undefined,
23
- componentName: undefined,
24
- originalError: { message, stack, plugin: err.plugin, id: err.id, loc },
25
- stack,
26
- },
6
+
7
+ type BuildErrorPayload = { err?: Record<string, unknown> } | undefined;
8
+
9
+ export function reportBuildError(payload: BuildErrorPayload, win: Window = window) {
10
+ const err = payload?.err ?? {};
11
+ const message = typeof err.message === "string" ? err.message : "Build error";
12
+ const stack = typeof err.stack === "string" ? err.stack : undefined;
13
+ const loc = err.loc as { file?: string; line?: number; column?: number } | undefined;
14
+ const file = loc?.file ?? (typeof err.id === "string" ? err.id : undefined);
15
+ const where = file && loc?.line != null ? `${file}:${loc.line}:${loc.column ?? 0}` : file;
16
+ const details = [where, typeof err.frame === "string" ? err.frame : undefined, stack]
17
+ .filter(Boolean)
18
+ .join("\n");
19
+ win.parent?.postMessage(
20
+ {
21
+ type: "app_error",
22
+ error: {
23
+ title: message,
24
+ details: details || undefined,
25
+ componentName: undefined,
26
+ originalError: { message, stack, plugin: err.plugin, id: err.id, loc },
27
+ stack,
27
28
  },
28
- "*"
29
- );
30
- });
29
+ },
30
+ "*"
31
+ );
31
32
  }
32
33
 
33
- export {};
34
+ if (import.meta.hot) {
35
+ import.meta.hot.on("vite:error", (payload) => reportBuildError(payload as BuildErrorPayload));
36
+ }
@@ -2,25 +2,38 @@
2
2
 
3
3
  // The preview proxy's bridge watches the HMR socket by importing /@vite/client,
4
4
  // which bundled dev mode does not serve. This module ships inside the bundle and
5
- // reports the same events; the bridge skips its own attach when it sees the flag.
6
- (window as unknown as Record<string, unknown>).__base44_bundled_dev__ = true;
5
+ // reports the same events. The flag below is set only once the bundle runs, so
6
+ // the bridge (builder_bridge_inject.py) polls it for a while after its import
7
+ // fails before reporting the socket as unreachable.
7
8
 
8
- if (import.meta.hot && window.self !== window.top) {
9
+ type HotEvents = {
10
+ on: (event: string, cb: (payload?: unknown) => unknown) => void;
11
+ };
12
+
13
+ // Query param the editor adds to the preview URL when it can swap in a fresh
14
+ // iframe on a full reload (mirrors builder_bridge_inject.py in base44-dev/apper).
15
+ export const PREVIEW_FE_VERSION_PARAM = "previewFE_version";
16
+
17
+ export function attachHmrSocketRelay(hot: HotEvents, win: Window = window) {
18
+ (win as unknown as Record<string, unknown>).__base44_bundled_dev__ = true;
19
+ if (win.self === win.top) return;
9
20
  const previewFEVersion =
10
- Number(new URLSearchParams(window.location.search).get("previewFE_version")) || 0;
11
- import.meta.hot.on("vite:ws:connect", () => {
12
- window.parent?.postMessage({ type: "SANDBOX_WS_CONNECT" }, "*");
21
+ Number(new URLSearchParams(win.location.search).get(PREVIEW_FE_VERSION_PARAM)) || 0;
22
+ hot.on("vite:ws:connect", () => {
23
+ win.parent?.postMessage({ type: "SANDBOX_WS_CONNECT" }, "*");
13
24
  });
14
- import.meta.hot.on("vite:ws:disconnect", () => {
15
- window.parent?.postMessage({ type: "SANDBOX_WS_DISCONNECT" }, "*");
25
+ hot.on("vite:ws:disconnect", () => {
26
+ win.parent?.postMessage({ type: "SANDBOX_WS_DISCONNECT" }, "*");
16
27
  });
17
- // Vite awaits these listeners before reloading; a pending promise lets the
18
- // editor swap in a fresh iframe instead of reloading in place.
19
- import.meta.hot.on("vite:beforeFullReload", () => {
20
- window.parent?.postMessage({ type: "sandbox:beforeFullReload" }, "*");
21
- if (previewFEVersion > 0) return new Promise<void>(() => {});
22
- return undefined;
28
+ // Vite's bundled client awaits `vite:beforeFullReload` listeners before it
29
+ // reloads (verified against vite 8.2 bundledDevClient), so a never-settling
30
+ // promise holds the reload and lets the editor swap in a fresh iframe instead.
31
+ hot.on("vite:beforeFullReload", () => {
32
+ win.parent?.postMessage({ type: "sandbox:beforeFullReload" }, "*");
33
+ return previewFEVersion > 0 ? new Promise<void>(() => {}) : undefined;
23
34
  });
24
35
  }
25
36
 
26
- export {};
37
+ // The socket opens asynchronously, after the whole bundle has evaluated, so the
38
+ // listeners above are always registered before the first connect event.
39
+ if (import.meta.hot) attachHmrSocketRelay(import.meta.hot);