Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions .changeset/6809-browser-process-shim-source-only.md
Original file line number Diff line number Diff line change
@@ -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.
141 changes: 141 additions & 0 deletions packages/components/src/__tests__/browser-process-shim-scope.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
43 changes: 43 additions & 0 deletions packages/components/src/browser-process-shim.d.ts
Original file line number Diff line number Diff line change
@@ -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;
};
};
12 changes: 6 additions & 6 deletions packages/components/src/global.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
14 changes: 13 additions & 1 deletion packages/components/tsconfig.test.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
}
Loading