Skip to content

feat(cli): auto-detect import file extension from tsconfig#2705

Merged
ymc9 merged 1 commit into
devfrom
fix/ts-import-extension-autodetect
Jun 12, 2026
Merged

feat(cli): auto-detect import file extension from tsconfig#2705
ymc9 merged 1 commit into
devfrom
fix/ts-import-extension-autodetect

Conversation

@ymc9

@ymc9 ymc9 commented Jun 12, 2026

Copy link
Copy Markdown
Member

Summary

Closes #2686.

Generated TypeScript schema files import from each other with relative specifiers (./schema). Projects using native ESM module resolution (node16/nodenext) require an explicit .js extension on such imports, otherwise the generated code fails to type-check. Until now this required manually setting the importWithFileExtension plugin option.

This PR makes the TypeScript plugin auto-detect the right extension when the option is not explicitly declared:

  • It inspects the nearest tsconfig.json to the output directory (resolving extends chains and module/moduleResolution defaults).
  • Emits .js for node16/nodenext resolution (where it is required).
  • Keeps the idiomatic extensionless form for bundler/node resolution (the maximally-compatible form for those toolchains).
  • An explicitly-set importWithFileExtension still takes precedence; passing "" disables the extension entirely.

Why not just emit .js unconditionally?

Extensionless imports only fail under native Node ESM — which is exactly the node16/nodenext case that detection already covers. For every other target (bundler/node), extensionless is the idiomatic form and avoids changing output for existing users. Auto-detection emits the form each project would hand-write, and is guaranteed safe regardless of how a downstream bundler resolves .js.ts.

Changes

  • packages/sdk/src/tsconfig-utils.ts (new) — detectImportFileExtension(dir) helper, exported from the SDK.
  • packages/cli/src/plugins/typescript.ts — wire auto-detection in; only runs when the option is undefined.
  • packages/cli/test/import-extension.test.ts (new) — end-to-end generate-then-tsc tests covering bundler (no extension, compiles), nodenext (auto-adds .js, compiles), a negative control proving the extension is load-bearing under nodenext, and explicit-override precedence.
  • packages/cli/tsconfig.json — include test/**/*.ts (matching the zod/testtools convention) so test files type-check; this surfaced and fixed a few pre-existing strict-mode errors in ts-schema-gen, proxy, and db/pull tests.

Testing

  • pnpm --filter @zenstackhq/cli test import-extension — 4 passing e2e tests (real zenstack generate + tsc --noEmit).
  • tsc --noEmit -p packages/cli/tsconfig.json — clean, now including tests.
  • pnpm --filter @zenstackhq/sdk build and pnpm --filter @zenstackhq/cli build — clean.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • TypeScript plugin now auto-detects import file extensions based on your project's module resolution settings (e.g., Node16/NodeNext ESM).
    • Plugin respects explicit configuration when provided.
  • Improvements

    • SDK now exports TypeScript configuration utilities.
  • Tests

    • Added comprehensive test coverage for import extension handling across different module resolution scenarios.

The TypeScript generator's `importWithFileExtension` option had to be set
manually for projects using native ESM module resolution
(`node16`/`nodenext`), which requires explicit `.js` extensions on relative
imports. Without it, generated code failed to type-check in those projects.

When the option is not explicitly declared, the TypeScript plugin now
inspects the nearest `tsconfig.json` to the output directory and emits a
`.js` extension for `node16`/`nodenext` resolution, while keeping the
idiomatic extensionless form for `bundler`/`node` resolution. An explicit
option still takes precedence.

Also include `test/**/*.ts` in the CLI package's tsconfig (matching the
zod/testtools convention) so test files type-check, fixing a handful of
pre-existing strict-mode errors surfaced by that change.

Fixes #2686

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 36601f7f-1550-470d-bfdd-517b66d80f56

📥 Commits

Reviewing files that changed from the base of the PR and between 431ad02 and 0caf899.

📒 Files selected for processing (8)
  • packages/cli/src/plugins/typescript.ts
  • packages/cli/test/db/pull.test.ts
  • packages/cli/test/import-extension.test.ts
  • packages/cli/test/proxy.test.ts
  • packages/cli/test/ts-schema-gen.test.ts
  • packages/cli/tsconfig.json
  • packages/sdk/src/index.ts
  • packages/sdk/src/tsconfig-utils.ts

📝 Walkthrough

Walkthrough

The PR implements auto-detection of TypeScript module resolution settings to determine whether generated schema imports should include .js file extensions. A new utility detects module resolution from the nearest tsconfig.json, integrates into the TypeScript plugin to conditionally apply extensions, and includes comprehensive test coverage validating behavior across different module resolution modes.

Changes

TypeScript Import Extension Auto-Detection

Layer / File(s) Summary
TypeScript config utilities
packages/sdk/src/tsconfig-utils.ts, packages/sdk/src/index.ts
New detectImportFileExtension(fromDir) utility locates the nearest tsconfig.json, parses module resolution settings via TypeScript APIs, and returns '.js' for node16/nodenext resolution or undefined otherwise. Re-exported from SDK index.
TypeScript plugin integration
packages/cli/src/plugins/typescript.ts
Plugin imports the detection utility and auto-detects importWithFileExtension when unset; validates explicit values are strings; forwards the resulting value to TsSchemaGenerator.generate.
Import extension test coverage
packages/cli/test/import-extension.test.ts
Four test cases validate: bundler resolution omits extensions and type-checks; NodeNext includes .js and type-checks; explicit suppression fails type-check; explicit override applies across resolution modes. Includes helper utilities for tsconfig.json setup and compilation validation.
Test suite and config updates
packages/cli/tsconfig.json, packages/cli/test/db/pull.test.ts, packages/cli/test/proxy.test.ts, packages/cli/test/ts-schema-gen.test.ts
Extend TypeScript config to include test files; adjust test destructuring and casting; use bracket notation in assertions.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 A rabbit hops through tsconfig trees,
Detecting node16, with utmost ease,
When .js extensions should dance in the code,
The plugin now knows the proper load!
Tests verify bundlers both new and old,
A tale of imports, rightly told. 🌟

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly describes the main change: auto-detecting import file extensions from tsconfig configuration.
Linked Issues check ✅ Passed The PR fully implements the objective from #2686: auto-detecting import file extensions based on tsconfig module resolution settings, with sensible defaults and explicit override support.
Out of Scope Changes check ✅ Passed All changes are scoped to implementing the auto-detection feature and supporting test infrastructure; minor tsconfig updates and test fixes are directly related to this objective.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/ts-import-extension-autodetect

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install timed out. The project may have too many dependencies for the sandbox.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@claude

claude Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Claude finished @ymc9's task —— View job


Code Review for Auto-Detect Import File Extension Feature

Todo List

  • Read and analyze core implementation files
  • Review TypeScript plugin integration
  • Examine test coverage and quality
  • Check for security and performance considerations
  • Provide comprehensive feedback

Review Summary

This is a well-implemented feature that solves a real pain point for ESM projects. The implementation is thoughtful, thorough, and follows good engineering practices.

Strengths

1. Correct TypeScript Resolution Logic (packages/sdk/src/tsconfig-utils.ts:15-54)

  • Properly uses TypeScript's own APIs (ts.findConfigFile, ts.parseJsonConfigFileContent) to mimic compiler behavior
  • Correctly handles extends chains and module resolution defaults
  • Maps Node16/18/20 module kinds to NodeNext resolution as TypeScript does
  • Smart fallback behavior when no tsconfig is found

2. Clean Integration (packages/cli/src/plugins/typescript.ts:25-32)

  • Auto-detection only runs when option is undefined, preserving explicit user configuration
  • Proper error handling with descriptive message for invalid option types
  • Minimal code changes to existing plugin architecture

3. Comprehensive Test Coverage (packages/cli/test/import-extension.test.ts)

  • End-to-end validation: Actually runs zenstack generate + tsc --noEmit
  • Tests all key scenarios: bundler (no extension), nodenext (auto-adds .js), explicit override
  • Negative control test: Proves the extension is required by showing nodenext compilation failure
  • Real-world testing approach that catches integration issues

4. Backward Compatibility

  • Zero breaking changes - existing projects continue working unchanged
  • Explicit configuration still takes precedence over auto-detection
  • Conservative approach: only adds extensions where absolutely required

🔍 Code Quality Observations

Performance

  • Tsconfig parsing happens once per generation, not per file - acceptable overhead
  • Uses efficient TypeScript APIs designed for this purpose
  • Early returns minimize unnecessary work

Security

  • Uses TypeScript's built-in file system abstractions (ts.sys)
  • No direct file manipulation or shell execution
  • Error handling prevents crashes on malformed tsconfig files

Error Handling

  • Graceful degradation when tsconfig is missing or malformed (packages/sdk/src/tsconfig-utils.ts:17-24)
  • Clear error message for invalid plugin options (packages/cli/src/plugins/typescript.ts:31-32)

🎯 Technical Accuracy

The logic correctly implements TypeScript's module resolution behavior:

  • node16/nodenext require explicit .js extensions on relative imports
  • bundler/node work with extensionless imports
  • ✅ Handles edge cases like missing moduleResolution (derives from module)
  • ✅ Returns .js (not .ts) - correct for runtime imports

📋 Test Quality

The tests are exemplary:

  • Real compilation testing - not just string matching
  • Negative controls - proves the feature is necessary
  • Edge case coverage - explicit override, missing config, different resolutions
  • Clear test names that describe expected behavior

🛡️ Minor Considerations

1. TypeScript Version Compatibility

  • Code uses stable TypeScript APIs, but consider documenting minimum TS version requirements
  • ModuleKind.Node18/Node20 are relatively recent additions

2. Tsconfig Location

  • Uses ts.findConfigFile which searches upward from output directory - this is correct behavior
  • Could potentially be unexpected if output dir is far from project root, but this matches TypeScript's own behavior

3. Future Module Kinds

  • If TypeScript adds new Node module kinds, they'd need to be added to the switch statement
  • Consider a comment about this maintenance requirement

🎉 Overall Assessment

This PR demonstrates excellent engineering practices:

  • Solves a real user problem with minimal API surface changes
  • Uses the right tools (TypeScript's own APIs) for the job
  • Comprehensive testing that validates the entire feature
  • Clear documentation and good error handling
  • Maintains backward compatibility

The auto-detection logic is technically sound and the implementation is production-ready. The tests provide high confidence that this works correctly across different project configurations.

Recommendation: ✅ Approve - This is a high-quality implementation that will significantly improve the developer experience for ESM projects.


@ymc9 ymc9 merged commit 0da9f81 into dev Jun 12, 2026
8 checks passed
@ymc9 ymc9 deleted the fix/ts-import-extension-autodetect branch June 12, 2026 19:24
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.

Generated schema files should use ".js" import for best compatibility

1 participant