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
14 changes: 14 additions & 0 deletions .changeset/types-drop-dead-require-condition.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
'@object-ui/types': patch
---

Removed the dead `require` condition from `exports["."]` in `@object-ui/types`'s `package.json`. It pointed at `dist/index.cjs`, a file the package's `"build": "tsc"` script (bare `tsc`, no bundler) structurally never emits — verified on a clean rebuild (`rm -rf dist tsconfig.tsbuildinfo && tsc`): zero `.cjs` files under `dist/`.

**Judged non-breaking (`patch`), because the condition never resolved to anything a consumer could depend on** — measured both ways from a real `require()` call through the package's own resolved workspace symlink (not asserted):

- **Before this change**: `require('@object-ui/types')` → `MODULE_NOT_FOUND: Cannot find module '.../dist/index.cjs'` (the condition existed but its target was never written by the build).
- **After this change**: `require('@object-ui/types')` → `ERR_PACKAGE_PATH_NOT_EXPORTED: No "exports" main defined ...` (no matching condition).

Both throw. No working `require()` call is turned into a failing one — there was no working one to begin with, in this repo or in any published version, since the build has never emitted `dist/index.cjs`. The `import` condition (`./dist/index.js`, real and always present) and the `types` condition are unchanged.

The package declares `"type": "module"` and ships no bundler, so ESM-only is the contract-honest shape going forward; adding a second build format to satisfy a condition nothing used was the alternative and was not taken.
3 changes: 1 addition & 2 deletions packages/types/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"require": "./dist/index.cjs"
"import": "./dist/index.js"
},
"./base": {
"types": "./dist/base.d.ts",
Expand Down
90 changes: 90 additions & 0 deletions packages/types/src/__tests__/package-exports-manifest.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/**
* Pins `@object-ui/types`' `package.json` `exports` map to the shape its
* bare-`tsc` build can actually produce (objectui#4896).
*
* ## The defect this guards against
*
* The shipped `exports["."].require` condition used to point at
* `dist/index.cjs` — a file the package's `"build": "tsc"` script structurally
* never emits (`tsc` alone writes `.js` + `.d.ts`, never `.cjs`; a second
* output format needs a bundler or an explicit dual-emit step, and this
* package has neither). Any CJS consumer resolving the `require` condition
* therefore hit `MODULE_NOT_FOUND` against a published tarball that declared
* an entry point it never shipped.
*
* Measured directly against a clean rebuild before this fix (`rm -rf dist
* tsconfig.tsbuildinfo && tsc`): zero `.cjs` files under `dist/`, and
* `require.resolve('.../packages/types/dist/index.cjs')` throws
* `MODULE_NOT_FOUND`. The package declares `"type": "module"` and is
* types-first (no bundler in its build), so ESM-only — dropping the dead
* `require` condition rather than adding a second build format — is the
* contract-honest fix; see the PR body for the full premise verification
* (nothing in-repo `require()`s this package, and the one in-repo CJS-format
* bundle consumer, `packages/vscode-extension` via `tsup --format cjs` with
* `noExternal: ['@object-ui/types', ...]`, builds byte-identically with the
* condition present or absent — it resolves the value imports it re-exports
* via the `import` condition regardless of its own output format).
*
* ## What this test is, and is not
*
* This is a MANIFEST-level pin — it reads `package.json` as text and asserts
* on its shape. It does NOT build the package or inspect `dist/`: this
* repo's per-PR `test` job (`ci.yml`) runs `pnpm test` with no build step
* ahead of it (turbo's `test` task only `dependsOn: ["^build"]` — the
* DEPENDENCY closure, never the package's own build — see
* `scripts/check-package-self-import.mjs`'s header for the same trap hitting
* a different gate), so a test that required a fresh `dist/` to exist would
* be vacuously absent-or-red on a cold CI cache, not a meaningful signal.
* The artifact-level claim (a clean `tsc` build never emits `.cjs`) was
* verified by hand for this fix and is not expected to regress silently: the
* package's `"build": "tsc"` script and lack of any bundler/dual-emit step
* are exactly the two other things this test pins, so a future edit that
* reintroduces a `require` condition without ALSO reintroducing the emit step
* would still need to touch this file's expectations to pass CI.
*/
import { readFileSync } from 'node:fs';
import { createRequire } from 'node:module';
import { describe, it, expect } from 'vitest';

const require = createRequire(import.meta.url);
const pkg = JSON.parse(
readFileSync(require.resolve('../../package.json'), 'utf8'),
) as {
type?: string;
scripts?: Record<string, string>;
exports?: Record<string, Record<string, string> | string>;
};

describe('@object-ui/types package.json exports map (objectui#4896)', () => {
it('declares "type": "module" — the premise for an ESM-only exports map', () => {
expect(pkg.type).toBe('module');
});

it('builds with bare tsc — no bundler that could emit a second (CJS) format', () => {
// If this ever changes to a bundler/dual-emit build, the `require`-less
// exports map below should be revisited rather than assumed to still be
// correct.
expect(pkg.scripts?.build).toBe('tsc');
});

it('the root "." export carries exactly {types, import} — no "require" condition', () => {
const rootExport = pkg.exports?.['.'];
expect(rootExport).toEqual({
types: './dist/index.d.ts',
import: './dist/index.js',
});
});

it('no export condition anywhere in the map points at a bare-tsc build never emits (*.cjs)', () => {
const offenders: string[] = [];
for (const [subpath, conditions] of Object.entries(pkg.exports ?? {})) {
if (typeof conditions === 'string') continue;
for (const [condition, target] of Object.entries(conditions)) {
if (typeof target === 'string' && target.endsWith('.cjs')) {
offenders.push(`${subpath} -> ${condition}: ${target}`);
}
}
}
expect(offenders).toEqual([]);
});
});
Loading