Skip to content

The primary import must construct correctly on every toolchain ​

Cluster: packaging / DX

Scenario ​

A developer installs the library, writes the most natural import for their toolchain, and constructs the top-level workbook type. Depending on bundler and module system, whether a webpack-backed React and TypeScript app, a vite or remix build, a nuxt/nitro server, plain Node ESM, or a browser bundle, the constructor comes back undefined and instantiation throws "X.Workbook is not a constructor", or the imported namespace is an empty object. The only way forward is trial-and-error import shapes: reaching through a .default property, destructuring the named class off a default import, swapping namespace against default import syntax, or toggling bundler caching. The same class name that works in one project is unconstructable in another purely because of module-interop packaging, nothing to do with spreadsheet data.

Desired behavior ​

The primary public API must be importable identically and predictably across every mainstream toolchain, with the ergonomic import being the correct one. A single documented form, import { Workbook } from '<pkg>', must construct correctly under Node ESM, TypeScript (NodeNext or bundler resolution), and the common bundlers (webpack, vite, rollup, esbuild, remix, nuxt/nitro). No .default reach-through, no empty-namespace failure mode, and no divergence between what the types say is importable and what exists at runtime.

Root cause ​

The ecosystem failures stem from shipping a CommonJS-only entry whose CJS/ESM interop places the real exports under a synthetic default. Bundlers and Node's ESM-over-CJS interop then disagree about whether the top-level named export exists, producing undefined constructors or empty namespaces. The converged workarounds (pkg.default.Workbook, const { Workbook } = pkg, default-against-namespace swaps) are symptoms of that packaging, not fixes the library should require.

How the fork precludes this ​

Ship real ESM as the source of truth with named exports. If CJS is supported at all, provide a correct dual export map (exports with import, require and types conditions); otherwise drop CJS entirely per the fork's ESM-only stance and eliminate the interop join. Ensure the emitted .d.ts matches the runtime shape exactly so import type and value imports agree. Lock it down with type-level export-surface tests plus a small matrix of import-and-construct smoke checks across resolvers and bundlers, so a regression cannot ship. The plain named import must be the easy path; no interop workaround should ever be necessary. This directly serves the constitution's "ESM, modern idioms only, correct use is the easy path".

Open questions ​

  • Does the fork support CJS consumers at all, or is it strictly ESM, which makes the dual export map unnecessary and removes the interop join entirely?
  • Which resolver and bundler matrix is the committed support surface for the import smoke tests?
  • Single default export, named exports only, or both, reflected consistently in the export map and the type declarations?

Related notes: public-types-node-stream-portability, browser-safe-io-boundary, no-global-polyfill-in-browser-bundle, the same "the package must behave predictably wherever it is consumed" principle applied to types, IO, and browser globals.

Released under the MIT License.