Skip to content

Commit fde15f4

Browse files
feat: cli. (#36)
1 parent 8f88df3 commit fde15f4

11 files changed

Lines changed: 1494 additions & 140 deletions

File tree

README.md

Lines changed: 7 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ Highlights
1616
- Configurable lowering modes: full syntax transforms or globals-only.
1717
- Specifier tools: add extensions, add directory indexes, or map with a custom callback.
1818
- Output control: write to disk (`out`/`inPlace`) or return the transformed string.
19+
- CLI: `dub` for batch transforms, dry-run/list/summary, stdin/stdout, and colorized diagnostics. See [docs/cli.md](docs/cli.md).
1920

2021
> [!IMPORTANT]
2122
> All parsing logic is applied under the assumption the code is in [strict mode](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Strict_mode) which [modules run under by default](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules#other_differences_between_modules_and_classic_scripts).
@@ -143,7 +144,7 @@ type ModuleOptions = {
143144
### Behavior notes (defaults in parentheses)
144145
145146
- `target` (`commonjs`): output module system.
146-
- `transformSyntax` (true): enable/disable the ESM↔CJS lowering pass; set to `'globals-only'` to rewrite module globals (`import.meta.*`, `__dirname`, `__filename`, `require.main` shims) while leaving import/export syntax untouched. In `'globals-only'`, no helpers are injected (e.g., `__requireResolve`), `require.resolve` rewrites to `import.meta.resolve`, and `idiomaticExports` is skipped. See [globals-only](#globals-only-scope).
147+
- `transformSyntax` (`true`): enable/disable the ESM↔CJS lowering pass; set to `'globals-only'` to rewrite module globals (`import.meta.*`, `__dirname`, `__filename`, `require.main` shims) while leaving import/export syntax untouched. In `'globals-only'`, no helpers are injected (e.g., `__requireResolve`), `require.resolve` rewrites to `import.meta.resolve`, and `idiomaticExports` is skipped. See [globals-only](docs/globals-only.md).
147148
- `liveBindings` (`strict`): getter-based live bindings, or snapshot (`loose`/`off`).
148149
- `appendJsExtension` (`relative-only` when targeting ESM): append `.js` to relative specifiers; never touches bare specifiers.
149150
- `appendDirectoryIndex` (`index.js`): when a relative specifier ends with a slash, append this index filename (set `false` to disable).
@@ -159,7 +160,7 @@ type ModuleOptions = {
159160
- `requireSource` (`builtin`): whether `require` comes from Node or `createRequire`.
160161
- `cjsDefault` (`auto`): bundler-style default interop vs direct `module.exports`.
161162
- `idiomaticExports` (`safe`): when raising CJS to ESM, attempt to synthesize `export` statements directly when it is safe. `off` always uses the helper bag; `aggressive` currently matches `safe` heuristics.
162-
- `out`/`inPlace`: write the transformed code to a file; otherwise the function returns the transformed string only.
163+
- `out`/`inPlace`: choose output location. Default returns the transformed string (CLI emits to stdout). `out` writes to the provided path. `inPlace` overwrites the input files on disk and does not return/emit the code.
163164
- `cwd` (`process.cwd()`): Base directory used to resolve relative `out` paths.
164165
165166
> [!NOTE]
@@ -170,13 +171,6 @@ See [docs/esm-to-cjs.md](docs/esm-to-cjs.md) for deeper notes on live bindings,
170171
> [!NOTE]
171172
> Known limitations: `with` and unshadowed `eval` are rejected when raising CJS to ESM because the rewrite would be unsound; bare specifiers are not rewritten—only relative specifiers participate in `rewriteSpecifier`.
172173
173-
### Globals-only scope
174-
175-
- Rewrites module globals (`import.meta.*`, `__dirname`, `__filename`, `require.main` shims) for the target side.
176-
- Optional specifier rewrites still run (`rewriteSpecifier`, `appendJsExtension`, `appendDirectoryIndex`).
177-
- Leaves imports/exports and interop untouched (no export bag, no idiomaticExports, no live-binding synthesis, no helpers like `__requireResolve`).
178-
- CJS→ESM: `require.resolve` maps to `import.meta.resolve` (URL return, ESM resolver) and may differ from CJS resolution. ESM→CJS: `import.meta` maps to CJS globals; no import lowering.
179-
180174
### Diagnostics callback example
181175
182176
Pass a `diagnostics` callback to surface CJS→ESM edge cases (mixed `module.exports`/`exports`, top-level `return`, legacy `require.cache`/`require.extensions`, live-binding reassignments, string-literal export names):
@@ -213,20 +207,9 @@ TypeScript reports asymmetric module-global errors (e.g., `import.meta` in CJS,
213207

214208
Minimal flow:
215209

216-
```js
217-
import { glob } from 'glob'
218-
import { transform } from '@knighted/module'
219-
220-
const files = await glob('src/**/*.{ts,js,mts,cts}', { ignore: 'node_modules/**' })
221-
222-
for (const file of files) {
223-
await transform(file, {
224-
target: 'commonjs', // or 'module' when raising CJS → ESM
225-
inPlace: true,
226-
transformSyntax: true,
227-
})
228-
}
229-
// then run `tsc`
210+
```bash
211+
dub -t commonjs "src/**/*.{ts,js,mts,cts}" --ignore node_modules/** --transform-syntax globals-only --in-place
212+
tsc
230213
```
231214

232-
This pre-`tsc` step removes the flagged globals in the compiled orientation; runtime semantics still match the target build.
215+
This pre-`tsc` step rewrites globals-only (keeps import/export syntax) so the TypeScript checker sees already-rewritten sources; runtime semantics still match the target build.

docs/cli.md

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
# CLI: `dub`
2+
3+
Command-line wrapper around `@knighted/module` for transforming files between ESM and CommonJS.
4+
5+
## Requirements
6+
7+
- Node >= 22.21.1 (or Node 24+)
8+
9+
## Installation
10+
11+
The CLI is shipped with the package. Install locally or use `npx`:
12+
13+
```bash
14+
npm install @knighted/module
15+
npx dub --help
16+
```
17+
18+
## Usage
19+
20+
```bash
21+
dub [options] <files...>
22+
```
23+
24+
Examples:
25+
26+
- Transform CJS to ESM into an output directory:
27+
28+
```bash
29+
dub -t module src/**/*.cjs --out-dir dist
30+
```
31+
32+
- In-place ESM to CJS rewrite:
33+
34+
```bash
35+
dub -t commonjs src/**/*.mjs --in-place
36+
```
37+
38+
- Preview changes without writing:
39+
40+
```bash
41+
dub -t module src/**/*.cjs --out-dir dist --dry-run --list --summary
42+
```
43+
44+
- Stdin/stdout pipeline:
45+
46+
```bash
47+
cat input.cjs | dub -t module --stdin-filename input.cjs > output.mjs
48+
```
49+
50+
## Options
51+
52+
Short and long forms are supported.
53+
54+
<!-- prettier-ignore-start -->
55+
| Short | Long | Description |
56+
| ----- | -------------------------- | --------------------------------------------------------------- |
57+
| -t | --target | Output format (module \| commonjs) |
58+
| -x | --transform-syntax | Syntax transforms (true \| false \| globals-only) |
59+
| -r | --rewrite-specifier | Rewrite import specifiers (.js/.mjs/.cjs/.ts/.mts/.cts) |
60+
| -j | --append-js-extension | Append .js to relative imports (off \| relative-only \| all) |
61+
| -i | --append-directory-index | Append directory index (e.g. index.js) or false |
62+
| -c | --detect-circular-requires | Warn/error on circular require (off \| warn \| error) |
63+
| -a | --top-level-await | TLA handling (error \| wrap \| preserve) |
64+
| -d | --cjs-default | Default interop (module-exports \| auto \| none) |
65+
| -e | --idiomatic-exports | Emit idiomatic exports when safe (off \| safe \| aggressive) |
66+
| -m | --import-meta-prelude | Emit import.meta prelude (off \| auto \| on) |
67+
| -n | --nested-require-strategy | Rewrite nested require (create-require \| dynamic-import) |
68+
| -R | --require-main-strategy | Detect main (import-meta-main \| realpath) |
69+
| -l | --live-bindings | Live binding strategy (strict \| loose \| off) |
70+
| -o | --out-dir | Write outputs to a directory mirror |
71+
| -p | --in-place | Rewrite files in place |
72+
| -y | --dry-run | Do not write files; report planned changes |
73+
| -L | --list | List files that would change |
74+
| -s | --summary | Print a summary of work performed |
75+
| -J | --json | Emit machine-readable JSON summary/diagnostics |
76+
| -C | --cwd | Working directory for resolving files/out paths |
77+
| -f | --stdin-filename | Virtual filename when reading from stdin |
78+
| -g | --ignore | Glob pattern(s) to ignore (repeatable) |
79+
| -h | --help | Show help |
80+
| -v | --version | Show version |
81+
<!-- prettier-ignore-end -->
82+
83+
Notes:
84+
85+
- When reading from stdin, output is sent to stdout; `--out-dir` or `--in-place` are not allowed in that mode.
86+
- Specify either `--out-dir` or `--in-place` for file inputs; stdout is used only when a single file is given and neither flag is set.
87+
- Diagnostics are printed to stderr; use `--json` for machine-readable output.

docs/globals-only.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# Globals-only transform
2+
3+
`transformSyntax: 'globals-only'` rewrites well-known module globals lexically without emitting any runtime helpers or preludes. Imports/exports and interop are left untouched; runtime correctness depends on the target environment providing the globals you opt into.
4+
5+
## Scope
6+
7+
- Pure identifier/member rewrites; no injected helpers, shims, or prelude imports.
8+
- Works in both directions (CJS ➜ ESM, ESM ➜ CJS).
9+
- Diagnostics still surface for legacy constructs (e.g., `require.extensions`, `module.parent`).
10+
11+
## Rewrites at a glance
12+
13+
<!-- prettier-ignore-start -->
14+
| Direction | Input | Output | Notes |
15+
| --- | --- | --- | --- |
16+
| CJS ➜ ESM | `__dirname` | `import.meta.dirname` | lexical swap |
17+
| CJS ➜ ESM | `__filename` | `import.meta.filename` | lexical swap |
18+
| CJS ➜ ESM | `require.main` | `import.meta.main` | skipped inside equality checks |
19+
| CJS ➜ ESM | `require.resolve(...)` | `import.meta.resolve(...)` | no helper emitted* |
20+
| CJS ➜ ESM | `module.require(...)` | `require(...)` | collapses to ambient require |
21+
| CJS ➜ ESM | `require.cache` | `{}` | best-effort stub + warning |
22+
| CJS ➜ ESM | `require.extensions` | (unchanged) | warning emitted |
23+
| CJS ➜ ESM | `module.parent / module.children` | (unchanged) | warning emitted |
24+
| ESM ➜ CJS | `import.meta` | `module` | bare meta expression |
25+
| ESM ➜ CJS | `import.meta.url` | `require("node:url").pathToFileURL(__filename).href` | URL-shaped contract |
26+
| ESM ➜ CJS | `import.meta.filename` | `__filename` | lexical swap |
27+
| ESM ➜ CJS | `import.meta.dirname` | `__dirname` | lexical swap |
28+
| ESM ➜ CJS | `import.meta.resolve(...)` | `require.resolve(...)` | string-path semantics |
29+
| ESM ➜ CJS | `import.meta.main` | `process.argv[1] === __filename` | inline check |
30+
| ESM ➜ CJS | other `import.meta.*` | `module.*` | no extra objects created |
31+
<!-- prettier-ignore-end -->
32+
33+
\* In globals-only, we do not inject the CJS-style `require.resolve` helper. The rewrite relies on the host's native `import.meta.resolve`, whose semantics differ (URL-based, parent handling). Use full transforms if you need the helper that preserves CJS resolution behavior.
34+
35+
## When to use it
36+
37+
- Pre-`tsc` mitigation for TypeScript’s [asymmetry on module globals](https://github.com/microsoft/TypeScript/issues/58658) (see `test/cli.ts`): rewrite globals so the checker sees the target-side shapes without altering import/export syntax.
38+
- Tooling pipelines that only need syntax-level compatibility and will supply the required globals at runtime.
39+
40+
## When not to use it
41+
42+
- When you need runtime shims (e.g., `createRequire`, import.meta polyfills) or interop helpers: use `transformSyntax: true` instead.
43+
44+
## Caveats
45+
46+
- Behavior relies on the target runtime’s built-ins; no guarantees are made about availability or semantics of the rewritten globals.
47+
- Legacy fields like `require.cache`, `require.extensions`, `module.parent`, and `module.children` only receive warnings/stubs—behavior may differ from Node.

docs/roadmap.md

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,14 @@ Shipped: `idiomaticExports: 'safe'` is now the default for CJS → ESM, with fal
99
Next:
1010

1111
- Explore a true `'aggressive'` mode (mixed exports/module.exports, limited reassignments, identifier-safe computed keys) with guarded semantics and explicit diagnostics.
12+
- In `'auto'`, allow idiomatic `module.exports = { foo, bar }` when the object literal is simple: single top-level assignment, no spreads/getters/computed/duplicate keys, only identifier keys, RHS values limited to safe literals/identifiers/function|class expressions, no `require()` inside RHS, and no `__proto__`/`prototype` keys. Add a shorthand-object fixture test and keep falling back to the helper for anything more complex.
1213
- Consider a constrained ESM → CJS “pretty” path where live-binding and TLA semantics permit it.
1314

1415
## CLI
1516

16-
- Deliver a `knighted-module` CLI that wraps the core transform with parity to API options (targets, rewriteSpecifier, appendJsExtension/appendDirectoryIndex, detectCircularRequires, topLevelAwait, cjsDefault, diagnostics hooks, out/in-place).
17-
- Input handling: accept file/glob lists plus stdin/stdout piping; respect `package.json` `type` and `.cjs/.mjs` extensions; allow per-invocation overrides via flags and a config file.
18-
- Output handling: in-place rewrite or out-dir mirroring with extension rewriting; emit diagnostics to stderr and machine-readable JSON when requested; non-zero exit on diagnostics of severity error.
19-
- Performance ergonomics: batch parse/format where possible, optional concurrency flag, and a `--watch` mode that rebuilds on change with minimal restarts.
20-
- DX: `--dry-run` to preview planned rewrites, `--list` to show which files would change, `--summary` to print counts of transformed specifiers/globals, and `--help`/`--version` aligned with package metadata.
17+
- Shipped parity CLI wrapping the core transform (targets, rewriteSpecifier, appendJsExtension/appendDirectoryIndex, detectCircularRequires, topLevelAwait, cjsDefault, diagnostics hooks, out/in-place) with stdin/stdout support, JSON/summary, and list/dry-run paths.
18+
- Next: optional concurrency flag, `--watch` mode with minimal restarts, and a tiny stream type surface to keep test stubs and embedding clean.
19+
- DX polish: keep help/examples in sync with tests; retain single-fixture CLI coverage unless new CLI-specific behaviors emerge (e.g., multi-ext glob ordering, large-input streaming), since transform semantics are already exercised in module fixtures.
2120

2221
## Tooling & Diagnostics
2322

oxlint.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
}
3232
},
3333
{
34-
"files": ["test/**"],
34+
"files": ["test/**", "!test/cli.ts"],
3535
"rules": {
3636
"@typescript-eslint/no-explicit-any": "off"
3737
}

0 commit comments

Comments
 (0)