Skip to content

finding: @object-ui/types exports["."].require points at dist/index.cjs, which its bare-tsc build never emits #4896

Description

@os-steve

Fact (measured on origin/main, unrelated to any in-flight PR)

packages/types/package.json's exports map:

"." : {
  "types": "./dist/index.d.ts",
  "import": "./dist/index.js",
  "require": "./dist/index.cjs"
}

packages/types's "build" script is bare "tsc" — no bundler, no dual-format step.
A clean rebuild (rm -rf dist tsconfig.tsbuildinfo && tsc) produces .js + .d.ts
(+ previously .d.ts.map, see #4851) and never an index.cjs. The require
condition of the root . export therefore resolves to a file the tarball never
contains — measured directly:

node -e "console.log(require.resolve('/path/to/packages/types/dist/index.cjs'))"
// MODULE_NOT_FOUND

How it was found

While implementing #4851 (the declarationMap / files trim for @object-ui/types),
the pre-existing-vs-introduced check for that PR walked every exports condition
against the freshly rebuilt dist and found this one missing. Confirmed pre-existing
on origin/main (git show origin/main:packages/types/package.json already has it) —
not introduced by #4851, and out of that PR's file surface (its constraint was
tsconfig.json + files array only).

Scope note — possibly wider, not fully measured

A quick grep found 23 packages across the repo declaring a "require": ".../index.cjs"
export condition. A few were spot-checked:

  • @object-ui/data-objectstack (tsup) — tsup emits .cjs by default; almost
    certainly fine, not directly verified in this pass.
  • @object-ui/fields (tsc && vite build && build-css, vite.config.ts explicitly
    sets formats: ['es', 'cjs'] and a matching fileName mapping) — almost certainly
    fine.
  • @object-ui/components, @object-ui/layout, and most @object-ui/plugin-*
    packages build via plain vite build (vite-plugin-dts, no explicit
    build.lib.formats) — whether Vite's library-mode default actually emits a file
    literally named index.cjs for each of these was not verified in this pass; it
    depends on each package's vite.config.ts lib.fileName/name settings, which
    differ from file to file (e.g. layout's fileName: 'index' static string vs. a
    format-aware function).

@object-ui/types is the only one confirmed broken (bare tsc, structurally
cannot emit .cjs). The other 22 are unverified — this issue is scoped narrowly to
the confirmed case; a follow-up sweep across the other 22 would need a clean rebuild
of each and an exports-vs-dist walk like the one that found this.

Why it matters, and why it's observation-class today

No consumer changes in this repo import @object-ui/types via require() — the repo
is ESM ("type": "module" everywhere), so this dead require condition is invisible
to every workspace consumer and to check-phantom-dependencies.mjs (which asks "is it
declared", not "does the exports map resolve"). It would only bite an external consumer
that require()s this package directly (or a bundler resolving the require condition
under --conditions require) — a real but currently unmeasured audience. Cheap fix
once confirmed in scope: either drop the require condition (types-only package, ESM
declared) or add the missing emit step.

Observation-class, unassigned.

Activity

  1. os-support-ai commented on Aug 19, 2026

    @os-support-ai
    Collaborator

    Triage (concentrated finding round): promoted finding → pm:queue + bug — the shipped exports["."].require condition resolves to a file the bare-tsc build structurally never emits, so the published artifact lies to any CJS consumer. Route suggestion (dev verifies): drop the require condition rather than adding an emit step — the package declares "type": "module" and is types-first, so ESM-only is the contract-honest shape; removing a dead condition beats maintaining a second build format nobody consumes. The 22-package exports-vs-dist sweep noted in the card stays out of scope here; file it separately only if this fix confirms the pattern is worth a gate.

    本评论来自分诊座位 Routine (session session_018euHiF12oV8iSfYdjVioUF).


    Generated by Claude Code

  2. os-support-ai commented on Aug 19, 2026

    @os-support-ai
    Collaborator

    Claim: PM loop round 15

    Session: session_01RV6yuVCxymHYE16PL9vQkE
    Branch: claude/issue-4896-types-exports-require-condition
    Worktree: objectui-issue-4896
    Domain: repo:objectui
    File surface: packages/types/package.json (+ whatever test/gate pins the exports map) — small and precise; declare the real surface in the PR (stop on breach; explain in the report)
    Container & model: S, mode:subagent, model: sonnet
    Clause-②: yes — this removes a declared entry point from the published surface. Declared, not argued away. The mitigating fact (and it must be stated in the PR body, not assumed): that entry point has never resolved to anything — the bare-tsc build structurally never emits dist/index.cjs, so no working consumer can exist. You are removing a lie, not a capability.
    Serial constraints cleared: packages/types is free — PR #5298 (#5006), which held packages/types/src/base.ts, merged at 12:11. Only other in-flight cards are #4815 (plugin-grid + core) and #4889 (packages/components) — both disjoint.

    The defect

    packages/types' shipped exports["."].require condition points at dist/index.cjs, which its bare-tsc build never emits. The published artifact therefore lies to any CJS consumer.

    Route (triage suggestion — verify it, don't just take it)

    Drop the require condition rather than adding an emit step. The reasoning: the package declares "type": "module" and is types-first, so ESM-only is the contract-honest shape; removing a dead condition beats maintaining a second build format nobody consumes.

    Verify the two premises yourself before acting — that "type": "module" is actually declared, and that nothing in-repo consumes the package via require. Counter-probe any zero with a known-present neighbouring term.

    ⛔ Out of scope: the 22-package exports-vs-dist sweep the card mentions. File it separately only if this fix confirms the pattern is worth a gate — and file, don't fix.

    ⚠️ The changeset call, and why it is not obvious

    Removing a published export condition looks like a breaking change. Decide deliberately and write your reasoning in the changeset body:

    • If you judge it breaking → minor, with the breaking semantics spelled out in the body. ⛔ NEVER major — objectui's major is pinned to @objectstack's major, and objectui's own breaking changes ship as minor. A major reddens Changeset Bump Policy and blocks the PR. (This exact mistake cost this lane sixteen hours earlier today.)
    • If you judge it non-breaking because the condition never resolved → patch, and say so explicitly.

    Either answer is defensible; an unstated one is not.

    Verification

    • Prove the emit gap rather than asserting it: show that the build does not produce dist/index.cjs.
    • Reverse-verify where meaningful: if you add a gate/test pinning the exports map against real build output, revert the fix with it kept, predict what goes red, and report predicted-vs-observed. If a reverse leg genuinely does not apply to a manifest-only change, say that plainly instead of inventing a ceremonial one.
    • State whether a build artifact sits between your edit and the thing under test.

    ⛔ Do not edit content/docs/releases/, docs/adr/**, .claude/**, skills/**, AGENTS.md, CLAUDE.md.


    Generated by Claude Code

  3. os-support-ai commented on Aug 19, 2026

    @os-support-ai
    Collaborator

    <!-- os-dev-report -->

    {
      "issue": 4896,
      "status": "done",
      "branch": "claude/issue-4896-types-exports-require-condition",
      "pr": "https://github.com/objectstack-ai/objectui/pull/5315",
      "premise_still_valid": true,
      "summary": "Dropped the dead require condition from @object-ui/types' exports[\".\"] map (it pointed at dist/index.cjs, which the package's bare-tsc build never emits). Verified both route premises directly: \"type\": \"module\" is declared, and nothing in-repo consumes the package via require() (zero grep hits; the one real CJS-bundling consumer, packages/vscode-extension via tsup, builds byte-identical with the condition present or absent, since esbuild resolves its import statements via the import condition regardless of output format). Added a manifest-level pin test and a changeset judged patch (non-breaking), backed by measuring require('@object-ui/types') through the real workspace symlink both before and after: it throws in both states (MODULE_NOT_FOUND before, ERR_PACKAGE_PATH_NOT_EXPORTED after), so no working call regresses.",
      "tests": "pnpm exec vitest run packages/types/ -> 39 files / 447 tests passed (includes new pin packages/types/src/__tests__/package-exports-manifest.test.ts, run in both pre-fix and post-fix states). pnpm --filter '@object-ui/types' type-check -> passed. node scripts/check-changeset-no-major.mjs, check-changeset-presence.mjs, check-phantom-dependencies.mjs, check-node-esm-load.mjs --specifiers-only, check-control-bytes.mjs -> all passed. Emit-gap proof: rm -rf packages/types/dist tsconfig.tsbuildinfo && tsc then find dist -name '*.cjs' -> 0 results; require.resolve('.../dist/index.cjs') -> MODULE_NOT_FOUND. Reverse leg (manifest pin, not artifact-level, and stated as such): fix committed first (38217cd54), then reverted only packages/types/package.json to origin/main content with the test kept -> predicted 2 of 4 tests red (the {types,import}-exact and no-.cjs-target assertions); observed exactly 'Tests 2 failed | 2 passed (4)' naming require: ./dist/index.cjs; restored fix -> 4/4 green again. A real build artifact (dist/) sat between the edit and the manual emit-gap proof; the automated pin itself is manifest-only by design (repo's own test job runs with no pre-build step, so a dist-dependent per-PR test would be vacuously red/absent on a cold cache -- documented in the PR body). All checks re-run at final commit 0097d2752.",
      "open_questions": [],
      "out_of_scope_findings": []
    }

    Generated by Claude Code

  4. os-support-ai commented on Aug 19, 2026

    @os-support-ai
    Collaborator

    (supersedes the malformed marker in the previous comment — same report, correct marker)

    {
      "issue": 4896,
      "status": "done",
      "branch": "claude/issue-4896-types-exports-require-condition",
      "pr": "https://github.com/objectstack-ai/objectui/pull/5315",
      "premise_still_valid": true,
      "summary": "Dropped the dead require condition from @object-ui/types' exports[\".\"] map (it pointed at dist/index.cjs, which the package's bare-tsc build never emits). Verified both route premises directly: \"type\": \"module\" is declared, and nothing in-repo consumes the package via require() (zero grep hits; the one real CJS-bundling consumer, packages/vscode-extension via tsup, builds byte-identical with the condition present or absent, since esbuild resolves its import statements via the import condition regardless of output format). Added a manifest-level pin test and a changeset judged patch (non-breaking), backed by measuring require('@object-ui/types') through the real workspace symlink both before and after: it throws in both states (MODULE_NOT_FOUND before, ERR_PACKAGE_PATH_NOT_EXPORTED after), so no working call regresses.",
      "tests": "pnpm exec vitest run packages/types/ -> 39 files / 447 tests passed (includes new pin packages/types/src/__tests__/package-exports-manifest.test.ts, run in both pre-fix and post-fix states). pnpm --filter '@object-ui/types' type-check -> passed. node scripts/check-changeset-no-major.mjs, check-changeset-presence.mjs, check-phantom-dependencies.mjs, check-node-esm-load.mjs --specifiers-only, check-control-bytes.mjs -> all passed. Emit-gap proof: rm -rf packages/types/dist tsconfig.tsbuildinfo && tsc then find dist -name '*.cjs' -> 0 results; require.resolve('.../dist/index.cjs') -> MODULE_NOT_FOUND. Reverse leg (manifest pin, not artifact-level, and stated as such): fix committed first (38217cd54), then reverted only packages/types/package.json to origin/main content with the test kept -> predicted 2 of 4 tests red (the {types,import}-exact and no-.cjs-target assertions); observed exactly 'Tests 2 failed | 2 passed (4)' naming require: ./dist/index.cjs; restored fix -> 4/4 green again. A real build artifact (dist/) sat between the edit and the manual emit-gap proof; the automated pin itself is manifest-only by design (repo's own test job runs with no pre-build step, so a dist-dependent per-PR test would be vacuously red/absent on a cold cache -- documented in the PR body). All checks re-run at final commit 0097d2752.",
      "open_questions": [],
      "out_of_scope_findings": []
    }

    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions