@tschk/moonshine-compiler 0.3.3 → 0.3.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tschk/moonshine-compiler",
3
- "version": "0.3.3",
3
+ "version": "0.3.5",
4
4
  "license": "ISC",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -15,8 +15,8 @@
15
15
  "test": "bun test"
16
16
  },
17
17
  "dependencies": {
18
- "@tschk/moonshine-framework": "^0.3.3",
19
- "@tschk/moonshine-router": "^0.3.3"
18
+ "@tschk/moonshine-framework": "^0.3.5",
19
+ "@tschk/moonshine-router": "^0.3.5"
20
20
  },
21
21
  "devDependencies": {
22
22
  "@types/bun": "1.3.14",
package/src/build.ts CHANGED
@@ -108,6 +108,19 @@ async function runBuild(
108
108
  naming: "[name].js",
109
109
  splitting: false,
110
110
  sourcemap: "none",
111
+ // Browsers have no `process`. Any dependency that reads `process.env.X` —
112
+ // and most of them do, behind a `typeof` guard or not — otherwise throws
113
+ // `ReferenceError: process is not defined` while the entry module is being
114
+ // evaluated. That happens before any render, so the app silently mounts
115
+ // nothing: no error boundary runs and the page is simply blank.
116
+ //
117
+ // `??=` so an app that injects real values ahead of this bundle keeps them;
118
+ // this only guarantees the property exists. Values are deliberately not
119
+ // inlined here — inlining the process environment into a browser bundle
120
+ // would ship every secret the build host happens to hold.
121
+ ...(target === "browser"
122
+ ? { banner: "globalThis.process ??= { env: {} };" }
123
+ : {}),
111
124
  });
112
125
  if (!result.success) {
113
126
  const messages = result.logs.map((log) => log.message).join("\n");
@@ -0,0 +1,73 @@
1
+ import { afterAll, describe, expect, test } from "bun:test";
2
+ import { existsSync, rmSync } from "node:fs";
3
+ import { join, resolve } from "node:path";
4
+ import { buildProject } from "../src/manifest";
5
+
6
+ /**
7
+ * A browser bundle must not carry a bare `process` reference into a page.
8
+ *
9
+ * Client code and its dependencies routinely read `process.env.X`. `process`
10
+ * does not exist in a browser, so the reference throws while the client entry
11
+ * module is still evaluating — before any component renders and before any
12
+ * error boundary can mount. The page stays blank and the console stays empty,
13
+ * which is close to undiagnosable from the outside.
14
+ */
15
+
16
+ const projectDir = resolve(import.meta.dir, "fixtures", "project");
17
+ const outDir = join(projectDir, ".moonshine");
18
+
19
+ function clean() {
20
+ if (existsSync(outDir)) {
21
+ rmSync(outDir, { recursive: true, force: true });
22
+ }
23
+ }
24
+
25
+ afterAll(clean);
26
+
27
+ describe("client bundle process shim", () => {
28
+ test("defines globalThis.process before any module code runs", async () => {
29
+ await buildProject({ projectDir });
30
+
31
+ const client = await Bun.file(join(outDir, "public", "client.js")).text();
32
+
33
+ expect(client.startsWith("globalThis.process ??= { env: {} };")).toBe(true);
34
+ });
35
+
36
+ test("evaluating the bundle does not throw on a process.env read", async () => {
37
+ await buildProject({ projectDir });
38
+
39
+ const client = await Bun.file(join(outDir, "public", "client.js")).text();
40
+ // The banner is the whole fix, so assert it actually guards a real read
41
+ // rather than trusting that the fixture still contains one.
42
+ expect(client).toContain("process.env");
43
+
44
+ // Evaluate the banner in isolation with no ambient `process`, which is what
45
+ // a browser gives the bundle.
46
+ const scope: Record<string, unknown> = {};
47
+ const run = new Function(
48
+ "globalThis",
49
+ "return (globalThis.process ??= { env: {} });",
50
+ );
51
+ expect(run(scope)).toEqual({ env: {} });
52
+ });
53
+
54
+ test("does not overwrite an environment injected ahead of the bundle", async () => {
55
+ // Apps prepend their own public values; `??=` must leave those intact.
56
+ const scope: Record<string, unknown> = { process: { env: { A: "1" } } };
57
+ const run = new Function(
58
+ "globalThis",
59
+ "return (globalThis.process ??= { env: {} });",
60
+ );
61
+ expect(run(scope)).toEqual({ env: { A: "1" } });
62
+ });
63
+
64
+ test("the server bundle is left alone", async () => {
65
+ await buildProject({ projectDir });
66
+
67
+ const server = await Bun.file(join(outDir, "dist", "server.js")).text();
68
+
69
+ // Bun and Node both have a real `process`; shimming it there would mask
70
+ // genuine environment problems at runtime.
71
+ expect(server.startsWith("globalThis.process ??=")).toBe(false);
72
+ });
73
+ });
@@ -1,3 +1,8 @@
1
+ // Reads process.env deliberately: browsers have no `process`, so this is the
2
+ // shape that used to throw `ReferenceError: process is not defined` while the
3
+ // client entry evaluated, leaving the page blank with no error boundary.
4
+ const apiBase = process.env.MOONSHINE_TEST_API_BASE ?? 'unset';
5
+
1
6
  export default function ClientPage() {
2
- return <h1>client</h1>;
7
+ return <h1>client {apiBase}</h1>;
3
8
  }