From 88439fc5221ab61e3f0566c6b5a4786bdf1a4e72 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 29 Aug 2026 21:30:45 +0000 Subject: [PATCH] fix(components): scope the browser `process` shim to the package source MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `src/global.d.ts` declared `const process: { env: { NODE_ENV: string } }`, and `tsconfig.test.json` globs `src/**/*.d.ts` in for the ambient declarations its tests rely on. That project sets `"types": ["node"]`, so `@types/node` is in the program — but the ambient declaration REPLACES the node global rather than augmenting it, and because `@types/node` spells its module as `export = process` it also became what `import process from 'node:process'` resolved to. Every spelling of `process.cwd()` in this package's tests failed with `TS2339: Property 'cwd' does not exist on type '{ env: { NODE_ENV: string; }; }'` while `types: ["node"]` sat in the config, correct and configured. Measured before choosing a repair, with `tsc --listFiles` on both projects: the SOURCE project contains zero `@types/node` files and the test project contains 82, and an ablation of the declaration turned the source project red with five `TS2591: Cannot find name 'process'` (renderers/basic/div.tsx, renderers/basic/span.tsx, renderers/form/form.tsx x3). The shim is load-bearing for the source, so it is narrowed rather than deleted: - the declaration moves to `src/browser-process-shim.d.ts`; - `tsconfig.test.json` names that one file in `exclude`; - `src/__tests__/browser-process-shim-scope.test.ts` pins both halves. Its own compilation under `tsc -p tsconfig.test.json` is the compile-time pin — a runtime assertion cannot see this defect, because all three spellings always worked at runtime. No release: declaration files and `tsconfig.test.json` are checking-only inputs and the built `dist/**/*.d.ts` carries no `process` declaration at all. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01CRJge11jso9TpXRWFt1Z49 --- .../6809-browser-process-shim-source-only.md | 34 +++++ .../browser-process-shim-scope.test.ts | 141 ++++++++++++++++++ .../components/src/browser-process-shim.d.ts | 43 ++++++ packages/components/src/global.d.ts | 12 +- packages/components/tsconfig.test.json | 14 +- 5 files changed, 237 insertions(+), 7 deletions(-) create mode 100644 .changeset/6809-browser-process-shim-source-only.md create mode 100644 packages/components/src/__tests__/browser-process-shim-scope.test.ts create mode 100644 packages/components/src/browser-process-shim.d.ts diff --git a/.changeset/6809-browser-process-shim-source-only.md b/.changeset/6809-browser-process-shim-source-only.md new file mode 100644 index 0000000000..15827927e0 --- /dev/null +++ b/.changeset/6809-browser-process-shim-source-only.md @@ -0,0 +1,34 @@ +--- +--- + +Scope `packages/components`' browser `process` shim to the package SOURCE, so +its own test project keeps node's real `process` global. + +`src/global.d.ts` declared `const process: { env: { NODE_ENV: string } }`, and +`tsconfig.test.json` globs `src/**/*.d.ts` in for the ambient declarations its +tests rely on. That project sets `"types": ["node"]`, so `@types/node` is in the +program — but the ambient declaration REPLACES the node global rather than +augmenting it, and because `@types/node` spells its module as `export = process` +it also became what `import process from 'node:process'` resolved to. All three +obvious spellings failed identically with `TS2339: Property 'cwd' does not exist +on type '{ env: { NODE_ENV: string; }; }'`, while `types: ["node"]` sat in the +config, correct. The plain `join(process.cwd(), …)` idiom `packages/i18n`'s +ratchet tests use did not compile one directory over. + +Measured before choosing a repair (`tsc --listFiles`, both projects): the source +project contains **zero** `@types/node` files and the test project contains 82, +and removing the declaration turns the SOURCE project red with five +`TS2591: Cannot find name 'process'` across `renderers/basic/div.tsx`, +`renderers/basic/span.tsx` and `renderers/form/form.tsx`. The shim is +load-bearing for the source — so it was narrowed, not deleted: + +- the declaration moved to `src/browser-process-shim.d.ts`; +- `tsconfig.test.json` names that one file in `exclude`; +- `src/__tests__/browser-process-shim-scope.test.ts` pins both halves — its own + compilation is the compile-time pin (a runtime assertion cannot see this + defect, since all three spellings always worked at runtime). + +No release: declaration files and `tsconfig.test.json` are checking-only inputs. +Verified that neither `src/global.d.ts` nor the new shim is emitted — the built +`dist/**/*.d.ts` contains no `process` declaration at all — so the published +surface is unchanged. diff --git a/packages/components/src/__tests__/browser-process-shim-scope.test.ts b/packages/components/src/__tests__/browser-process-shim-scope.test.ts new file mode 100644 index 0000000000..5e7a4edd3f --- /dev/null +++ b/packages/components/src/__tests__/browser-process-shim-scope.test.ts @@ -0,0 +1,141 @@ +/** + * ObjectUI + * Copyright (c) 2024-present ObjectStack Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +/** + * The browser `process` shim is SOURCE-ONLY — objectui#6809. + * + * ## What was wrong + * + * `src/global.d.ts` carried an ambient `declare const process: { env: { + * NODE_ENV: string } }`, and `tsconfig.test.json` globs `src/**` + `/*.d.ts` in + * for the ambient declarations its tests rely on. That project also sets + * `"types": ["node"]`, so `@types/node` IS in the program — but the ambient + * declaration does not AUGMENT the node global, it REPLACES it, and because + * `@types/node` spells its module as `export = process` it also became what + * `import process from 'node:process'` resolved to. All three obvious spellings + * failed identically: + * + * process.cwd() TS2339 + * import process from 'node:process'; process.cwd() TS2339 + * import nodeProcess from 'node:process'; nodeProcess.cwd() TS2339 + * + * error TS2339: Property 'cwd' does not exist on type + * '{ env: { NODE_ENV: string; }; }' + * + * Runtime was plain node in every case and all three worked — only the + * declaration was wrong. The asymmetry is what cost real time: `packages/i18n` + * tests read their ratchet baselines with the plain `join(process.cwd(), …)` + * idiom, which simply did not compile one directory over. + * + * ## Why the shim was NOT simply deleted + * + * Measured before choosing (`tsc --listFiles`, both projects): the SOURCE + * project (`tsconfig.json`, which names no `types`) contains **zero** + * `@types/node` files, while the TEST project contains 82. Five source sites + * read `process.env.NODE_ENV`, and an ablation — the declaration removed, the + * source project recompiled — turned it red with five `TS2591: Cannot find name + * 'process'` across `renderers/basic/div.tsx`, `renderers/basic/span.tsx` and + * `renderers/form/form.tsx`. The shim is load-bearing for the source. So it was + * NARROWED, not removed: it moved to `src/browser-process-shim.d.ts` and + * `tsconfig.test.json` names that one file in `exclude`. + * + * ## What this file pins, and how + * + * The compile-time half is THIS FILE COMPILING. `packages/components/ + * tsconfig.json` excludes tests, so `tsc --noEmit` says nothing about it — + * `tsc -p tsconfig.test.json` (the second half of the package's `type-check` + * script) is the only thing that reads it, and `process.cwd()` below is exactly + * the spelling that failed. Put the shim back in `global.d.ts`, or drop the + * `exclude`, and `type-check` goes red here. That is deliberate: a runtime + * assertion cannot see this defect at all, because at runtime all three + * spellings always worked. + * + * The runtime half guards the two ways the arrangement can be dismantled while + * still compiling: deleting the shim outright (which breaks the SOURCE build, + * a different project than the one that reads this file) and re-adding a + * `process` declaration to the shared `global.d.ts`. + */ + +import { describe, expect, it } from 'vitest'; +import { existsSync, readFileSync } from 'node:fs'; +import { join } from 'node:path'; + +// Root-form Vitest only (`scripts/vitest-invocation-guard.mjs` rejects a +// package-cwd run), so `process.cwd()` is the repo root — the same idiom +// `packages/i18n`'s ratchet tests use, and the one that did not compile in this +// package before objectui#6809. +const REPO_ROOT = process.cwd(); +const PKG = join(REPO_ROOT, 'packages/components'); + +const read = (rel: string): string => { + const abs = join(PKG, rel); + // A path that silently does not exist reads as an empty string and every + // "does not contain" assertion below would pass vacuously — the one direction + // this pin must not fail in. + expect(existsSync(abs), `missing file: ${abs}`).toBe(true); + return readFileSync(abs, 'utf8'); +}; + +describe('browser `process` shim scope (objectui#6809)', () => { + it('compiles the plain `process.cwd()` spelling the shim used to break', () => { + // The assertion that matters is that this file TYPE-CHECKS: before the fix + // this line was `TS2339: Property 'cwd' does not exist on type + // '{ env: { NODE_ENV: string; }; }'`. The runtime check just proves the + // binding is the real one rather than a compile-time fiction. + const cwd: string = process.cwd(); + expect(typeof cwd).toBe('string'); + expect(existsSync(join(cwd, 'pnpm-workspace.yaml'))).toBe(true); + }); + + it('sees node\'s process surface, not the two-property browser shim', () => { + // `platform` and `versions.node` exist on `NodeJS.Process` and on neither + // shape the shim could produce, so this is a second, independent spelling + // of "the node global won". + const platform: string = process.platform; + const nodeVersion: string = process.versions.node; + expect(platform.length).toBeGreaterThan(0); + expect(nodeVersion.length).toBeGreaterThan(0); + }); + + it('keeps the shim — the SOURCE project has no `@types/node` and needs it', () => { + const shim = read('src/browser-process-shim.d.ts'); + expect( + /declare const process\s*:/.test(shim), + 'The browser `process` shim is load-bearing for this package\'s SOURCE ' + + 'project: `tsconfig.json` names no `types`, `@types/node` is not ' + + 'reachable from packages/components, and five source sites read ' + + '`process.env.NODE_ENV`. Removing this declaration fails the SOURCE ' + + 'build with TS2591 — a different tsconfig project than the one that ' + + 'compiles this test, so nothing here would have caught it. See ' + + 'objectui#6809.' + ).toBe(true); + }); + + it('keeps the shim out of `global.d.ts`, which BOTH projects read', () => { + const shared = read('src/global.d.ts'); + expect( + /declare (const|var|let)\s+process\b/.test(shared), + '`src/global.d.ts` is globbed into `tsconfig.test.json` as well as the ' + + 'source project, so a `process` declaration here REPLACES the real node ' + + 'global that `types: ["node"]` provides — reintroducing objectui#6809. ' + + 'Source-only ambients belong in `src/browser-process-shim.d.ts`.' + ).toBe(false); + }); + + it('pins the `exclude` that makes the shim source-only', () => { + const testProject = read('tsconfig.test.json'); + expect( + testProject.includes('"exclude": ["src/browser-process-shim.d.ts"]'), + '`tsconfig.test.json` must exclude `src/browser-process-shim.d.ts`. Its ' + + '`include` globs `src/**/*.d.ts`, so without the exclusion the ' + + 'source-only shim is back in the test program and objectui#6809 ' + + 'returns. Dropping it also turns this file red at compile time; this ' + + 'assertion is here to name the cause in one line.' + ).toBe(true); + }); +}); diff --git a/packages/components/src/browser-process-shim.d.ts b/packages/components/src/browser-process-shim.d.ts new file mode 100644 index 0000000000..8f5a6b12b8 --- /dev/null +++ b/packages/components/src/browser-process-shim.d.ts @@ -0,0 +1,43 @@ +/** + * ObjectUI + * Copyright (c) 2024-present ObjectStack Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +// The browser `process` shim — SOURCE-ONLY, and that is the whole reason this +// declaration lives in its own file instead of in `global.d.ts` next door. +// +// This package's source is a browser bundle: `tsconfig.json` names no `types`, +// and `@types/node` is not reachable from `packages/components/node_modules`, +// so the source program contains ZERO node typings (measured with +// `tsc --listFiles`). Five source sites still read the bundler-replaced +// `process.env.NODE_ENV` idiom — `renderers/basic/div.tsx`, +// `renderers/basic/span.tsx` and `renderers/form/form.tsx` (×3) — and without +// this declaration all five fail `TS2591: Cannot find name 'process'`. Adding +// `"types": ["node"]` to `tsconfig.json` instead would be the wrong repair: it +// would hand a browser library the whole `fs`/`path`/`child_process` surface. +// +// ⛔ But this shim must NOT reach `tsconfig.test.json`. That project DOES set +// `"types": ["node"]`, so the real node global is available there — and an +// ambient `declare const process` of this narrow shape WINS over it, and (since +// `@types/node` spells its module as `export = process`) also becomes what +// `import process from 'node:process'` resolves to. The result was a +// self-contradicting diagnostic in this package's tests: `types: ["node"]` is +// configured, the import resolves, and the compiler still says +// `TS2339: Property 'cwd' does not exist on type '{ env: { NODE_ENV: string; }; }'` +// — for `process.cwd()`, for `import process from 'node:process'`, and for a +// renamed binding alike. The plain `join(process.cwd(), …)` idiom that +// `packages/i18n` tests use happily simply did not compile here (objectui#6809). +// +// The separation is mechanical, not a convention: `tsconfig.test.json` globs in +// `src/**/*.d.ts` for the ambient declarations its tests DO rely on, and names +// THIS file — and only this file — in its `exclude`. Keep the shim here; do not +// move it back into `global.d.ts`, and do not add a second source-only ambient +// to `global.d.ts` without giving it the same treatment. +declare const process: { + env: { + NODE_ENV: string; + }; +}; diff --git a/packages/components/src/global.d.ts b/packages/components/src/global.d.ts index 1129a9ae4a..6072634176 100644 --- a/packages/components/src/global.d.ts +++ b/packages/components/src/global.d.ts @@ -19,9 +19,9 @@ declare namespace NodeJS { } } -// Global process for browser environments -declare const process: { - env: { - NODE_ENV: string; - }; -}; +// The browser `process` shim that used to sit here now lives in +// `browser-process-shim.d.ts`, which `tsconfig.test.json` excludes. It replaced +// (not augmented) the real node global in this package's TEST project, where +// `@types/node` IS configured — see that file's header and objectui#6809. +// Whatever stays in THIS file reaches both projects, so keep it to declarations +// that are correct in both. diff --git a/packages/components/tsconfig.test.json b/packages/components/tsconfig.test.json index 9a978ce7ee..cd6358517d 100644 --- a/packages/components/tsconfig.test.json +++ b/packages/components/tsconfig.test.json @@ -65,5 +65,17 @@ }, // `src/**/*.d.ts` picks up `src/global.d.ts`, whose ambient declarations the // test files rely on. - "include": ["src/**/*.test.ts", "src/**/*.test.tsx", "src/**/*.d.ts"] + "include": ["src/**/*.test.ts", "src/**/*.test.tsx", "src/**/*.d.ts"], + // ...but NOT `src/browser-process-shim.d.ts`. That file is the browser + // `process` shim the package SOURCE needs (its project has no `@types/node` + // at all). Globbed in here it does not augment the node global `types: ["node"]` + // above provides — it REPLACES it, and `@types/node` spelling its module as + // `export = process` means it also becomes what `import process from + // 'node:process'` resolves to. Every spelling of `process.cwd()` in this + // package's tests then failed `TS2339: Property 'cwd' does not exist on type + // '{ env: { NODE_ENV: string; }; }'` while `types: ["node"]` sat right here, + // configured and correct — objectui#6809. Excluding the one file is what makes + // the shim source-only; `src/__tests__/browser-process-shim-scope.test.ts` + // pins both halves. + "exclude": ["src/browser-process-shim.d.ts"] }