Skip to content

chore(ts): moduleResolution node -> nodenext (CJS) / bundler (ESM) - #1834

Open
pyramation wants to merge 4 commits into
mainfrom
feat/ts-module-resolution-nodenext
Open

pyramation wants to merge 4 commits into
mainfrom
feat/ts-module-resolution-nodenext

Conversation

@pyramation

@pyramation pyramation commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

Summary

Same convention as constructive-io/constructive-db#3764 (tracked in constructive-planning#2056): retire moduleResolution: node (deprecated in TS 6, removed in TS 7) without changing what we emit or how we publish.

  • Root tsconfig.json: module: commonjs / moduleResolution: node -> nodenext / nodenext. No package sets type: module, so nodenext classifies every .ts as CJS and dist/ is byte-identical CommonJS.
  • Every tsconfig.esm.json (106 files): + "moduleResolution": "bundler" next to the existing module: es2022, describing what dist/esm is (bundler ESM consumed via the module field). No exports map, no .mjs, no type: module — dist-folder publishing untouched.

Things nodenext's stricter resolution surfaced and how they were fixed:

  • graphile-presigned-url-plugin: build.getTypeByName('JSON') is a GraphQLNamedType (may be an input type); real bug hidden by node resolution. Now isOutputType(t) ? t : null.
  • graphql/codegen/generate.ts: dropped three await import('./codegen/cli') / import('./output') calls whose targets were already statically imported in the same file (under nodenext a CJS import() is preserved, not downleveled, and needs an extension). Test-file import()s are untouched: ts-jest forces module: commonjs, so they still downlevel to require (verified, suites pass).
  • @agentic-kit/ollama resolved through its import entry has no default export; embedder.ts (sdk/constructive-cli) and the codegen template that generates it now use m.OllamaClient.
  • parse-package-name@1.0.0 ships types only for its require condition; added an ambient declare module in pgpm/core/src/types/.
  • graphile-llm was compiling its __tests__ into dist/; excluded like sibling packages.
  • Runtime await import() of hard deps / Node builtins in CJS packages is now preserved by tsc (no longer downleveled to require), and Jest's CJS runtime cannot execute it (A dynamic import callback was invoked without --experimental-vm-modules). This is what broke site-deploy-ssg, graphile-search (BM25 index discovery silently skipped) and the uploads preset tests on the first CI run. pg in graphile-search/bm25-codec.ts and graphile-realtime-subscriptions in graphile-cache/create-instance.ts are declared dependencies, so they're plain static imports now. site-deploy/walk.ts must stay browser-safe, so it loads fs/promises / path via process.getBuiltinModule (Node >= 22.3, engines is >=22), which works from CJS and ESM and is simply absent in a browser. Left alone on purpose: agentic/cli (ESM-only pi), db-tools/pg-fixups.ts (best-effort, pg undeclared), codegen/output/writer.ts (oxfmt is ESM-only, optional) and the ollama embedders (lazy by design, no CJS test crosses them).
  • agentic/pi-host depended on @constructive-io/coerce via literal ^0.4.1 (the only non-workspace: intra-repo dep). lerna version's syncWorkspaceLock step then runs pnpm install --lockfile-only and fails with ERR_PNPM_NO_MATCHING_VERSION for the bumped ^0.4.2. Switched to workspace:^; lockfile updated accordingly.

Verification on this branch (CI fully green): pnpm build clean (0 TS errors), pnpm lint 0 errors, graphql/codegen 21 suites / 364 tests + graphile-presigned-url-plugin 24 tests pass, lerna version --conventional-commits --no-push --no-git-tag-version succeeds (127 packages), and pnpm -r publish --dry-run --no-git-checks (what lerna publish delegates to) packs all 121 public packages from dist/ with workspace:^ rewritten to concrete ranges. Sample dist/package.json still has main/module/types and no exports; require('./dist') loads.

Follow-up (separate, mechanical): rewriteRelativeImportExtensions with .ts source imports + makage emitting dist/esm/package.json {"type":"module"} so dist/esm is also Node-loadable.

Link to Devin session: https://app.devin.ai/sessions/d34558e831ad4bbf986f64cc4bd509bd
Open in Devin Desktop: https://app.devin.ai/desktop/session/d34558e831ad4bbf986f64cc4bd509bd?variant=devin
Requested by: @pyramation

- root tsconfig: module/moduleResolution nodenext (files stay CJS, no type:module)
- every tsconfig.esm.json: moduleResolution bundler (dist/esm is bundler ESM)
- codegen generate.ts: drop redundant dynamic imports of already-imported modules
- embedder (template + generated sdk): use named OllamaClient export
- presigned-url-plugin: narrow getTypeByName result to an output type
- pgpm/core: ambient types for parse-package-name's untyped import entry
- graphile-llm: exclude tests from the build like sibling packages
lerna version's syncWorkspaceLock step resolves bumped versions against the
registry; a literal ^0.4.1 range on a workspace package fails there with
ERR_PNPM_NO_MATCHING_VERSION as soon as coerce is bumped.
@devin-ai-integration

Copy link
Copy Markdown
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@tenki-reviewer

tenki-reviewer Bot commented Sep 15, 2026

Copy link
Copy Markdown

Review complete. No issues found — approved ✅.


This PR introduces dual ESM builds by adding a tsconfig.esm.json to ~100 packages and switching the root tsconfig.json module/moduleResolution strategy, alongside a handful of targeted code changes. The bulk tsconfig additions are consistent and reference the shared base config, so the ESM rollout is low-risk.

Files Change
100+ tsconfig.esm.json across agentic/, graphile/, graphql/, packages/, pgpm/, postgres/, uploads/, sdk/ Add per-package ESM build config with module=es2022 / moduleResolution=bundler, pointing at dist/esm.
tsconfig.json Switch root module/moduleResolution settings to drive the new ESM output.
graphql/codegen/src/core/generate.ts, .../templates/embedder.ts, sdk/.../cli/embedder.ts Refactor dynamic imports to static and rename @agentic-kit/ollama access from m.default to m.OllamaClient.
graphile/graphile-presigned-url-plugin/src/plugin.ts Add isOutputType() guard before projecting the JSON scalar.
pgpm/core/src/types/parse-package-name.d.ts, agentic/pi-host/package.json Add type shim and adjust a dependency version.

Candidate findings surfaced during review were checked against the actual source and did not hold up, so no actionable issues remain.

Reviewed commit: 71a4b78

Under nodenext tsc preserves import() in CommonJS output instead of downleveling it to require(), which Jest's CJS runtime cannot execute. pg and graphile-realtime-subscriptions are declared dependencies, so import them statically; site-deploy loads fs/promises and path through process.getBuiltinModule so it stays browser-safe.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant