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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ type ModuleOptions = {
importMetaMain?: 'shim' | 'warn' | 'error'
requireMainStrategy?: 'import-meta-main' | 'realpath'
detectCircularRequires?: 'off' | 'warn' | 'error'
detectDualPackageHazard?: 'off' | 'warn' | 'error'
requireSource?: 'builtin' | 'create-require'
importMetaPrelude?: 'off' | 'auto' | 'on'
cjsDefault?: 'module-exports' | 'auto' | 'none'
Expand All @@ -155,6 +156,7 @@ type ModuleOptions = {
- `requireMainStrategy` (`import-meta-main`): use `import.meta.main` or the realpath-based `pathToFileURL(realpathSync(process.argv[1])).href` check.
- `importMetaPrelude` (`auto`): emit a no-op `void import.meta.filename;` touch. `on` always emits; `off` never emits; `auto` emits only when helpers that reference `import.meta.*` are synthesized (e.g., `__dirname`/`__filename` in CJS→ESM, require-main shims, createRequire helpers). Useful for bundlers/transpilers that do usage-based `import.meta` polyfilling.
- `detectCircularRequires` (`off`): optionally detect relative static require cycles and warn/throw.
- `detectDualPackageHazard` (`warn`): flag when a file mixes `import` and `require` of the same package or combines root and subpath specifiers that can resolve to separate module instances (dual packages). Set to `error` to fail the transform.
- `topLevelAwait` (`error`): throw, wrap, or preserve when TLA appears in CommonJS output.
- `rewriteSpecifier` (off): rewrite relative specifiers to a chosen extension or via a callback. Precedence: the callback (if provided) runs first; if it returns a string, that wins. If it returns `undefined` or `null`, the appenders still apply.
- `requireSource` (`builtin`): whether `require` comes from Node or `createRequire`.
Expand Down
1 change: 1 addition & 0 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ Short and long forms are supported.
| -j | --append-js-extension | Append .js to relative imports (off \| relative-only \| all) |
| -i | --append-directory-index | Append directory index (e.g. index.js) or false |
| -c | --detect-circular-requires | Warn/error on circular require (off \| warn \| error) |
| -H | --detect-dual-package-hazard | Warn/error on mixed import/require of dual packages (off \| warn \| error) |
| -a | --top-level-await | TLA handling (error \| wrap \| preserve) |
| -d | --cjs-default | Default interop (module-exports \| auto \| none) |
| -e | --idiomatic-exports | Emit idiomatic exports when safe (off \| safe \| aggressive) |
Expand Down
40 changes: 40 additions & 0 deletions docs/dual-package-hazard.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Dual Package Hazard Diagnostics

This tool can warn or error when a file mixes specifiers that may trigger the dual package hazard (ESM vs CJS instances of the same package).

## Option

- `detectDualPackageHazard`: `off` | `warn` (default) | `error`
- CLI: `--detect-dual-package-hazard`, short `-H`.
- `warn`: emit diagnostics but continue.
- `error`: diagnostics are emitted and the transform exits non-zero.
- `off`: skip detection.

## What we detect (per file)

- Mixed import/require of the same bare package (including subpaths).
- Diagnostic: `dual-package-mixed-specifiers`.
- Root vs subpath specifiers of the same package (e.g., `pkg` and `pkg/module`).
- Diagnostic: `dual-package-subpath`.
- When both import and require occur, and package.json shows divergent entrypoints (conditional exports, module/main disagreements, or type: module with CJS main).
- Diagnostic: `dual-package-conditional-exports`.

## How it works

- Static string specifiers only (import/export-from, import(), require literals).
- Computes the package root from bare specifiers (ignores relative/absolute, node: builtins, URLs).
- Looks up package.json under `node_modules/<pkg>` relative to the current file/cwd when available.
- Best-effort: if the manifest cannot be read, the manifest-based diagnostic is skipped.

## What is not covered

- Cross-file or whole-project graph analysis; detection is per file only.
- Dynamic or template specifiers; non-literal specifiers are ignored.
- Loader/bundler resolution differences (pnpm linking, aliases, custom conditions).
- Exact equality of root vs subpath targets; we do not stat/resolve to see if they point to the same file, so a root/subpath warning may be conservative.

## Guidance

- Prefer a single specifier form for a given package: either all import or all require, and avoid mixing root and subpath unless you know they share the same build.
- Use `-H error` (or `detectDualPackageHazard: 'error'`) in CI to block new hazards once noise is acceptable for your codebase.
- If you need to suppress noise temporarily, set the option to `warn` while you align specifiers or package metadata.
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@knighted/module",
"version": "1.3.1",
"version": "1.4.0-rc.0",
"description": "Bidirectional transform for ES modules and CommonJS.",
"type": "module",
"main": "dist/module.js",
Expand Down
12 changes: 12 additions & 0 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const defaultOptions: ModuleOptions = {
importMetaMain: 'shim',
requireMainStrategy: 'import-meta-main',
detectCircularRequires: 'off',
detectDualPackageHazard: 'warn',
requireSource: 'builtin',
nestedRequireStrategy: 'create-require',
cjsDefault: 'auto',
Expand Down Expand Up @@ -211,6 +212,12 @@ const optionsTable = [
type: 'string',
desc: 'Warn/error on circular require (off|warn|error)',
},
{
long: 'detect-dual-package-hazard',
short: 'H',
type: 'string',
desc: 'Warn/error on mixed import/require of dual packages (off|warn|error)',
},
{
long: 'top-level-await',
short: 'a',
Expand Down Expand Up @@ -382,6 +389,11 @@ const toModuleOptions = (values: ParsedValues): ModuleOptions => {
values['detect-circular-requires'] as string | undefined,
['off', 'warn', 'error'] as const,
) ?? defaultOptions.detectCircularRequires,
detectDualPackageHazard:
parseEnum(
values['detect-dual-package-hazard'] as string | undefined,
['off', 'warn', 'error'] as const,
) ?? defaultOptions.detectDualPackageHazard,
topLevelAwait:
parseEnum(
values['top-level-await'] as string | undefined,
Expand Down
Loading