@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.
- package/README.md +1 -1
- package/dist/base44-runtime-plugin.d.ts +3 -0
- package/dist/base44-runtime-plugin.d.ts.map +1 -0
- package/dist/base44-runtime-plugin.js +95 -0
- package/dist/base44-runtime-plugin.js.map +1 -0
- package/dist/build-status-plugin.d.ts.map +1 -1
- package/dist/build-status-plugin.js +41 -21
- package/dist/build-status-plugin.js.map +1 -1
- package/dist/bundled-dev.js +1 -1
- package/dist/bundled-dev.js.map +1 -1
- package/dist/error-overlay-plugin.d.ts.map +1 -1
- package/dist/error-overlay-plugin.js +9 -0
- package/dist/error-overlay-plugin.js.map +1 -1
- package/dist/html-injections-plugin.d.ts +10 -8
- package/dist/html-injections-plugin.d.ts.map +1 -1
- package/dist/html-injections-plugin.js +45 -9
- package/dist/html-injections-plugin.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -1
- package/dist/injections/sandbox-build-error-notifier.d.ts +4 -0
- package/dist/injections/sandbox-build-error-notifier.d.ts.map +1 -1
- package/dist/injections/sandbox-build-error-notifier.js +22 -25
- package/dist/injections/sandbox-build-error-notifier.js.map +1 -1
- package/dist/injections/sandbox-hmr-socket-relay.d.ts +5 -0
- package/dist/injections/sandbox-hmr-socket-relay.d.ts.map +1 -1
- package/dist/injections/sandbox-hmr-socket-relay.js +22 -18
- package/dist/injections/sandbox-hmr-socket-relay.js.map +1 -1
- package/package.json +1 -1
- package/src/base44-runtime-plugin.ts +103 -0
- package/src/build-status-plugin.ts +46 -22
- package/src/bundled-dev.ts +1 -1
- package/src/error-overlay-plugin.ts +11 -0
- package/src/html-injections-plugin.ts +50 -17
- package/src/index.ts +6 -0
- package/src/injections/sandbox-build-error-notifier.ts +28 -25
- 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;
|
|
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":"
|
|
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
|
-
//
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
12
|
-
|
|
13
|
+
hot.on("vite:ws:disconnect", () => {
|
|
14
|
+
win.parent?.postMessage({ type: "SANDBOX_WS_DISCONNECT" }, "*");
|
|
13
15
|
});
|
|
14
|
-
// Vite awaits
|
|
15
|
-
//
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
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;
|
|
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
|
@@ -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
|
|
25
|
-
* sent over the HMR channel, so a boot-time transform
|
|
26
|
-
* read `ok: true`. That window is covered server-side by
|
|
27
|
-
* dev_server_error_classifier.
|
|
28
|
-
*
|
|
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
|
|
135
|
-
// reaches generateBundle superseded whatever error
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
logger
|
|
151
|
-
|
|
152
|
-
|
|
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);
|
package/src/bundled-dev.ts
CHANGED
|
@@ -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 {
|
|
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
|
|
10
|
-
|
|
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
|
-
|
|
63
|
+
file: "injections/unhandled-errors-handlers.js",
|
|
57
64
|
injectTo: "head",
|
|
58
65
|
mode: "dev",
|
|
59
66
|
},
|
|
60
67
|
sandboxMount: {
|
|
61
|
-
|
|
68
|
+
file: "injections/sandbox-mount-observer.js",
|
|
62
69
|
injectTo: "body",
|
|
63
70
|
mode: "dev",
|
|
64
71
|
},
|
|
65
72
|
hmrNotifier: {
|
|
66
|
-
|
|
73
|
+
file: "injections/sandbox-hmr-notifier.js",
|
|
67
74
|
injectTo: "head",
|
|
68
75
|
mode: "dev",
|
|
69
76
|
},
|
|
70
77
|
navigationNotifier: {
|
|
71
|
-
|
|
78
|
+
file: "injections/navigation-notifier.js",
|
|
72
79
|
injectTo: "head",
|
|
73
80
|
mode: "dev",
|
|
74
81
|
},
|
|
75
82
|
buildErrorNotifier: {
|
|
76
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
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
|
|
6
|
-
(
|
|
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
|
-
|
|
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(
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
15
|
-
|
|
25
|
+
hot.on("vite:ws:disconnect", () => {
|
|
26
|
+
win.parent?.postMessage({ type: "SANDBOX_WS_DISCONNECT" }, "*");
|
|
16
27
|
});
|
|
17
|
-
// Vite awaits
|
|
18
|
-
//
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
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);
|