Skip to content

Commit 9d33406

Browse files
feat: cwd. (#34)
1 parent e517b7e commit 9d33406

7 files changed

Lines changed: 78 additions & 24 deletions

File tree

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ type ModuleOptions = {
135135
idiomaticExports?: 'off' | 'safe' | 'aggressive'
136136
topLevelAwait?: 'error' | 'wrap' | 'preserve'
137137
out?: string
138+
cwd?: string
138139
inPlace?: boolean
139140
}
140141
```
@@ -159,7 +160,7 @@ type ModuleOptions = {
159160
- `cjsDefault` (`auto`): bundler-style default interop vs direct `module.exports`.
160161
- `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.
161162
- `out`/`inPlace`: write the transformed code to a file; otherwise the function returns the transformed string only.
162-
- CommonJS → ESM lowering will throw on `with` statements and unshadowed `eval` calls to avoid unsound rewrites.
163+
- `cwd` (`process.cwd()`): Base directory used to resolve relative `out` paths.
163164
164165
> [!NOTE]
165166
> Package-level metadata (`package.json` updates such as setting `"type": "module"` or authoring `exports`) is not edited by this tool today; plan that change outside the per-file transform.

package-lock.json

Lines changed: 2 additions & 17 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@knighted/module",
3-
"version": "1.1.1",
3+
"version": "1.2.0",
44
"description": "Bidirectional transform for ES modules and CommonJS.",
55
"type": "module",
66
"main": "dist/module.js",
@@ -27,7 +27,7 @@
2727
"prettier": "prettier -w .",
2828
"prettier:check": "prettier -c .",
2929
"lint": "oxlint --config oxlint.json .",
30-
"prepare": "husky install",
30+
"prepare": "husky",
3131
"test": "c8 --reporter=text --reporter=text-summary --reporter=lcov tsx --test --test-reporter=spec test/*.ts",
3232
"build:types": "tsc --emitDeclarationOnly",
3333
"build:dual": "babel-dual-package src --extensions .ts",
@@ -72,7 +72,6 @@
7272
},
7373
"dependencies": {
7474
"magic-string": "^0.30.21",
75-
"node-module-type": "^1.0.4",
7675
"oxc-parser": "^0.105.0",
7776
"periscopic": "^4.0.2"
7877
},

src/module.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -226,17 +226,19 @@ const defaultOptions = {
226226
idiomaticExports: 'safe',
227227
importMetaPrelude: 'auto',
228228
topLevelAwait: 'error',
229+
cwd: undefined,
229230
out: undefined,
230231
inPlace: false,
231232
} satisfies ModuleOptions
232233
const transform = async (filename: string, options: ModuleOptions = defaultOptions) => {
233234
const opts = { ...defaultOptions, ...options, filePath: filename }
235+
const cwdBase = opts.cwd ? resolve(opts.cwd) : process.cwd()
234236
const appendMode: AppendJsExtensionMode =
235237
options?.appendJsExtension ?? (opts.target === 'module' ? 'relative-only' : 'off')
236238
const dirIndex =
237239
opts.appendDirectoryIndex === undefined ? 'index.js' : opts.appendDirectoryIndex
238240
const detectCycles: DetectCircularRequires = opts.detectCircularRequires ?? 'off'
239-
const file = resolve(filename)
241+
const file = resolve(cwdBase, filename)
240242
const code = (await readFile(file)).toString()
241243
const ast = parse(filename, code)
242244
let source = await format(code, ast, opts)
@@ -261,7 +263,11 @@ const transform = async (filename: string, options: ModuleOptions = defaultOptio
261263
await detectCircularRequireGraph(file, detectCycles, dirIndex || 'index.js')
262264
}
263265

264-
const outputPath = opts.inPlace ? file : opts.out ? resolve(opts.out) : undefined
266+
const outputPath = opts.inPlace
267+
? file
268+
: opts.out
269+
? resolve(cwdBase, opts.out)
270+
: undefined
265271

266272
if (outputPath) {
267273
await writeFile(outputPath, source)

src/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ export type ModuleOptions = {
6565
diagnostics?: (diag: Diagnostic) => void
6666
/** Optional source file path used for diagnostics context. */
6767
filePath?: string
68+
/** Base directory used to resolve relative `out` paths; defaults to process.cwd(). */
69+
cwd?: string
6870
/** Output directory or file path when writing. */
6971
out?: string
7072
/** Overwrite input files instead of writing to out. */

test/fixtures/cwdFile.cjs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
const value = { answer: 42 }
2+
3+
module.exports = value

test/module.ts

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { spawnSync } from 'node:child_process'
44
import { resolve, join } from 'node:path'
55
import { pathToFileURL } from 'node:url'
66
import { createRequire } from 'node:module'
7-
import { rm, stat, writeFile } from 'node:fs/promises'
7+
import { copyFile, mkdir, rm, stat, writeFile } from 'node:fs/promises'
88
import type { Stats } from 'node:fs'
99

1010
import { transform } from '../src/module.js'
@@ -1819,6 +1819,64 @@ describe('@knighted/module', () => {
18191819
assert.ok(result.includes('__dirname'))
18201820
})
18211821

1822+
it('resolves relative out paths against cwd option', async t => {
1823+
const source = join(fixtures, 'file.cjs')
1824+
const relOut = join('cwd-out', 'out.mjs')
1825+
const expectedDir = join(fixtures, 'cwd-out')
1826+
const expected = join(expectedDir, 'out.mjs')
1827+
const unintendedDir = join(process.cwd(), 'cwd-out')
1828+
const valuesSrc = join(fixtures, 'values.cjs')
1829+
const valuesDst = join(expectedDir, 'values.cjs')
1830+
1831+
await mkdir(expectedDir, { recursive: true })
1832+
await copyFile(valuesSrc, valuesDst)
1833+
1834+
t.after(async () => {
1835+
await rm(expectedDir, { recursive: true, force: true })
1836+
if (unintendedDir !== expectedDir) {
1837+
await rm(unintendedDir, { recursive: true, force: true })
1838+
}
1839+
})
1840+
1841+
await transform(source, { target: 'module', out: relOut, cwd: fixtures })
1842+
1843+
assert.equal(await isValidFilename(expected), true)
1844+
if (unintendedDir !== expectedDir) {
1845+
assert.equal(await isValidFilename(join(unintendedDir, 'out.mjs')), false)
1846+
}
1847+
1848+
const { status } = spawnSync('node', [expected], { stdio: 'inherit' })
1849+
assert.equal(status, 0)
1850+
})
1851+
1852+
it('resolves relative input and output against cwd', async t => {
1853+
const relSource = 'cwdFile.cjs'
1854+
const relOut = join('cwd-rel', 'out.mjs')
1855+
const expectedDir = join(fixtures, 'cwd-rel')
1856+
const expectedOut = join(expectedDir, 'out.mjs')
1857+
const unintendedDir = join(process.cwd(), 'cwd-rel')
1858+
1859+
await mkdir(expectedDir, { recursive: true })
1860+
1861+
t.after(async () => {
1862+
await rm(expectedDir, { recursive: true, force: true })
1863+
if (unintendedDir !== expectedDir) {
1864+
await rm(unintendedDir, { recursive: true, force: true })
1865+
}
1866+
})
1867+
1868+
await transform(relSource, { target: 'module', cwd: fixtures, out: relOut })
1869+
1870+
assert.equal(await isValidFilename(expectedOut), true)
1871+
if (unintendedDir !== expectedDir) {
1872+
assert.equal(await isValidFilename(join(unintendedDir, 'out.mjs')), false)
1873+
}
1874+
1875+
const mod = await import(pathToFileURL(expectedOut).href)
1876+
const exported = (mod as any).default ?? (mod as any)
1877+
assert.equal(exported.answer, 42)
1878+
})
1879+
18221880
it('writes transformed source to a file when option enabled', async t => {
18231881
const mjs = join(fixtures, 'transformed.mjs')
18241882
const cjs = join(fixtures, 'transformed.cjs')

0 commit comments

Comments
 (0)