From 35f39ddb2a2d732ce5ea4963226e524aff6bfc9a Mon Sep 17 00:00:00 2001 From: KCM Date: Thu, 1 Jan 2026 11:40:15 -0600 Subject: [PATCH 1/4] test: wildcard re-export. --- test/fixtures/exportAll.mjs | 1 + test/module.ts | 17 +++++++++++++++++ 2 files changed, 18 insertions(+) create mode 100644 test/fixtures/exportAll.mjs diff --git a/test/fixtures/exportAll.mjs b/test/fixtures/exportAll.mjs new file mode 100644 index 0000000..4989922 --- /dev/null +++ b/test/fixtures/exportAll.mjs @@ -0,0 +1 @@ +export * from './values.mjs' diff --git a/test/module.ts b/test/module.ts index ff3c798..8cd3380 100644 --- a/test/module.ts +++ b/test/module.ts @@ -139,6 +139,23 @@ describe('@knighted/module', () => { assert.ok(/type: module/.test(conditional!.message)) }) + it('lowers export star reexports to cjs', async t => { + const fixturePath = join(fixtures, 'exportAll.mjs') + const outFile = join(fixtures, 'exportAll.cjs') + + t.after(() => rm(outFile, { force: true })) + + const result = await transform(fixturePath, { target: 'commonjs' }) + await writeFile(outFile, result) + + const requireFromHere = createRequire(import.meta.url) + const mod = requireFromHere(outFile) + + assert.equal(mod.foo, 'bar') + assert.equal(mod.esmodule, true) + assert.equal(Object.prototype.hasOwnProperty.call(mod, 'default'), false) + }) + it('transforms __filename', async t => { const result = await transform(join(fixtures, '__filename.cjs'), { target: 'module', From 18df05bb37bda26c8027b5de5dfd3c0f43c30a53 Mon Sep 17 00:00:00 2001 From: KCM Date: Thu, 1 Jan 2026 11:46:34 -0600 Subject: [PATCH 2/4] docs: add potentially breaking to roadmap. --- docs/roadmap.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/roadmap.md b/docs/roadmap.md index 1690ef9..0d74eef 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -21,3 +21,8 @@ Next: - Emit source maps and clearer diagnostics for transform choices. - Benchmark scope analysis choices: compare `periscopic`, `scope-analyzer`, and `eslint-scope` on fixtures and pick the final adapter. + +## Potential Breaking Changes (flag/document clearly) + +- Template literal specifier rewriting: skip or gate rewriting when `TemplateLiteral` has expressions to avoid touching non-static specifiers; needs opt-in or documented behavior change. +- Cycle detection hardening: expand extensions (.ts/.tsx/.mts/.cts) and normalize/realpath paths, which may surface new cycle warnings/errors, especially on Windows or mixed TS/JS projects. From 5a31b091e1b0803285aa121692b92870b6a41fb8 Mon Sep 17 00:00:00 2001 From: KCM Date: Thu, 1 Jan 2026 12:20:48 -0600 Subject: [PATCH 3/4] fix: fixes #44. --- README.md | 4 +- docs/roadmap.md | 4 +- package-lock.json | 4 +- package.json | 2 +- src/cli.ts | 34 ++++++++---- src/format.ts | 12 +--- src/module.ts | 70 ++++++++++++++---------- src/types.ts | 2 + src/utils/builtinSpecifiers.ts | 13 +++++ test/cli.ts | 27 +++++++++ test/fixtures/cycles/tsA.cts | 2 + test/fixtures/cycles/tsB.cts | 2 + test/fixtures/specifier/templateExpr.mjs | 6 ++ test/module.ts | 47 ++++++++++++++++ 14 files changed, 172 insertions(+), 57 deletions(-) create mode 100644 src/utils/builtinSpecifiers.ts create mode 100644 test/fixtures/cycles/tsA.cts create mode 100644 test/fixtures/cycles/tsB.cts create mode 100644 test/fixtures/specifier/templateExpr.mjs diff --git a/README.md b/README.md index dd8f015..2b7e1c3 100644 --- a/README.md +++ b/README.md @@ -125,6 +125,7 @@ type ModuleOptions = { | '.mts' | '.cts' | ((value: string) => string | null | undefined) + rewriteTemplateLiterals?: 'allow' | 'static-only' dirFilename?: 'inject' | 'preserve' | 'error' importMeta?: 'preserve' | 'shim' | 'error' importMetaMain?: 'shim' | 'warn' | 'error' @@ -151,6 +152,7 @@ type ModuleOptions = { - `appendJsExtension` (`relative-only` when targeting ESM): append `.js` to relative specifiers; never touches bare specifiers. - `appendDirectoryIndex` (`index.js`): when a relative specifier ends with a slash, append this index filename (set `false` to disable). - `appenders` precedence: `rewriteSpecifier` runs first; if it returns a string, that result is used. If it returns `undefined` or `null`, `appendJsExtension` and `appendDirectoryIndex` still run. Bare specifiers are never modified by appenders. +- `rewriteTemplateLiterals` (`allow`): when `static-only`, interpolated template literals are left untouched by specifier rewriting; string literals and non-interpolated templates still rewrite. - `dirFilename` (`inject`): inject `__dirname`/`__filename`, preserve existing, or throw. - `importMeta` (`shim`): rewrite `import.meta.*` to CommonJS equivalents. - `importMetaMain` (`shim`): gate `import.meta.main` with shimming/warning/error when Node support is too old. @@ -159,7 +161,7 @@ type ModuleOptions = { - `detectCircularRequires` (`off`): optionally detect relative static require cycles and warn/throw. - `detectDualPackageHazard` (`warn`): flag when `import` and `require` mix for the same package or root/subpath are combined in ways that can resolve to separate module instances (dual packages). Set to `error` to fail the transform. - `dualPackageHazardScope` (`file`): `file` preserves the legacy per-file detector; `project` aggregates package usage across all CLI inputs (useful in monorepos/hoisted installs) and emits one diagnostic per package. -- `topLevelAwait` (`error`): throw, wrap, or preserve when TLA appears in CommonJS output. +- `topLevelAwait` (`error`): throw, wrap, or preserve when TLA appears in CommonJS output. `wrap` runs the file body inside an async IIFE (exports may resolve after the initial tick); `preserve` leaves `await` at top level, which Node will reject for CJS. - `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`. - `cjsDefault` (`auto`): bundler-style default interop vs direct `module.exports`. diff --git a/docs/roadmap.md b/docs/roadmap.md index 0d74eef..cf3627b 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -24,5 +24,5 @@ Next: ## Potential Breaking Changes (flag/document clearly) -- Template literal specifier rewriting: skip or gate rewriting when `TemplateLiteral` has expressions to avoid touching non-static specifiers; needs opt-in or documented behavior change. -- Cycle detection hardening: expand extensions (.ts/.tsx/.mts/.cts) and normalize/realpath paths, which may surface new cycle warnings/errors, especially on Windows or mixed TS/JS projects. +- Template literal specifier rewriting: if we ever default to skipping interpolated `TemplateLiteral` specifiers, it would change outputs. Current implementation is opt-in via `rewriteTemplateLiterals: 'static-only'` (non-breaking); future default flips would need a major/minor note. +- Cycle detection hardening: expanding extensions (.ts/.tsx/.mts/.cts) and normalize/realpath paths may surface new cycle warnings/errors, especially on Windows or mixed TS/JS projects. diff --git a/package-lock.json b/package-lock.json index 364ae8e..8dfeb43 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@knighted/module", - "version": "1.4.0-rc.2", + "version": "1.4.0-rc.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@knighted/module", - "version": "1.4.0-rc.2", + "version": "1.4.0-rc.3", "license": "MIT", "dependencies": { "glob": "^13.0.0", diff --git a/package.json b/package.json index 7e1e942..c12c7c8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@knighted/module", - "version": "1.4.0-rc.2", + "version": "1.4.0-rc.3", "description": "Bidirectional transform for ES modules and CommonJS.", "type": "module", "main": "dist/module.js", diff --git a/src/cli.ts b/src/cli.ts index cf007df..64b5658 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -7,15 +7,17 @@ import { import { parseArgs } from 'node:util' import { readFile, mkdir } from 'node:fs/promises' import { dirname, resolve, relative, join } from 'node:path' -import { builtinModules } from 'node:module' import { glob } from 'glob' +import type { TemplateLiteral } from '@oxc-project/types' + import { transform, collectProjectDualPackageHazards } from './module.js' import { parse } from './parse.js' import { format } from './format.js' import { specifier } from './specifier.js' import { getLangFromExt } from './utils/lang.js' import type { ModuleOptions, Diagnostic } from './types.js' +import { builtinSpecifiers } from './utils/builtinSpecifiers.js' const defaultOptions: ModuleOptions = { target: 'commonjs', @@ -23,6 +25,7 @@ const defaultOptions: ModuleOptions = { transformSyntax: true, liveBindings: 'strict', rewriteSpecifier: undefined, + rewriteTemplateLiterals: 'allow', appendJsExtension: undefined, appendDirectoryIndex: 'index.js', dirFilename: 'inject', @@ -108,16 +111,6 @@ const colorize = (enabled: boolean) => { } } -const builtinSpecifiers = new Set( - builtinModules - .map(mod => (mod.startsWith('node:') ? mod.slice(5) : mod)) - .flatMap(mod => { - const parts = mod.split('/') - const base = parts[0] - return parts.length > 1 ? [mod, base] : [mod] - }), -) - const collapseSpecifier = (value: string) => value.replace(/['"`+)\s]|new String\(/g, '') const appendExtensionIfNeeded = ( @@ -195,6 +188,12 @@ const optionsTable = [ type: 'string', desc: 'Rewrite import specifiers (.js/.mjs/.cjs/.ts/.mts/.cts)', }, + { + long: 'rewrite-template-literals', + short: undefined, + type: 'string', + desc: 'Rewrite template literals (allow|static-only)', + }, { long: 'append-js-extension', short: 'j', @@ -377,6 +376,11 @@ const toModuleOptions = (values: ParsedValues): ModuleOptions => { const transformSyntax = parseTransformSyntax( values['transform-syntax'] as string | undefined, ) + const rewriteTemplateLiterals = + parseEnum( + values['rewrite-template-literals'] as string | undefined, + ['allow', 'static-only'] as const, + ) ?? defaultOptions.rewriteTemplateLiterals const appendJsExtension = parseEnum( values['append-js-extension'] as string | undefined, ['off', 'relative-only', 'all'] as const, @@ -391,6 +395,7 @@ const toModuleOptions = (values: ParsedValues): ModuleOptions => { transformSyntax, rewriteSpecifier: (values['rewrite-specifier'] as ModuleOptions['rewriteSpecifier']) ?? undefined, + rewriteTemplateLiterals, appendJsExtension: appendJsExtension, appendDirectoryIndex, detectCircularRequires: @@ -513,6 +518,13 @@ const applySpecifierUpdates = async ( const lang = getLangFromExt(filename) const updated = await specifier.updateSrc(source, lang, spec => { + if ( + spec.type === 'TemplateLiteral' && + opts.rewriteTemplateLiterals === 'static-only' + ) { + const node = spec.node as TemplateLiteral + if (node.expressions.length > 0) return + } const normalized = normalizeBuiltinSpecifier(spec.value) const rewritten = rewriteSpecifierValue( normalized ?? spec.value, diff --git a/src/format.ts b/src/format.ts index 976ed98..c9ab813 100644 --- a/src/format.ts +++ b/src/format.ts @@ -1,4 +1,3 @@ -import { builtinModules } from 'node:module' import { dirname, join, resolve as pathResolve } from 'node:path' import { readFile as fsReadFile, stat as fsStat } from 'node:fs/promises' import type { Node, ParseResult } from 'oxc-parser' @@ -31,6 +30,7 @@ import type { Diagnostic, ExportsMeta, FormatterOptions } from './types.js' import { collectCjsExports } from './utils/exports.js' import { collectModuleIdentifiers } from './utils/identifiers.js' import { isValidUrl } from './utils/url.js' +import { builtinSpecifiers } from './utils/builtinSpecifiers.js' import { ancestorWalk } from './walk.js' const isRequireMainMember = (node: Node, shadowed: Set) => @@ -41,16 +41,6 @@ const isRequireMainMember = (node: Node, shadowed: Set) => node.property.type === 'Identifier' && node.property.name === 'main' -const builtinSpecifiers = new Set( - builtinModules - .map(mod => (mod.startsWith('node:') ? mod.slice(5) : mod)) - .flatMap(mod => { - const parts = mod.split('/') - const base = parts[0] - return parts.length > 1 ? [mod, base] : [mod] - }), -) - const stripQuery = (value: string) => value.includes('?') || value.includes('#') ? (value.split(/[?#]/)[0] ?? value) : value diff --git a/src/module.ts b/src/module.ts index 3328e04..57f8067 100644 --- a/src/module.ts +++ b/src/module.ts @@ -14,28 +14,18 @@ import { } from './format.js' import { getLangFromExt } from './utils/lang.js' import type { ModuleOptions, Diagnostic } from './types.js' -import { builtinModules } from 'node:module' import { resolve as pathResolve, dirname as pathDirname, extname, join } from 'node:path' -import { readFile as fsReadFile, stat } from 'node:fs/promises' +import { readFile as fsReadFile, stat, realpath } from 'node:fs/promises' import { parse as parseModule } from './parse.js' import { walk } from './walk.js' import { collectModuleIdentifiers } from './utils/identifiers.js' +import { builtinSpecifiers } from './utils/builtinSpecifiers.js' type AppendJsExtensionMode = NonNullable type DetectCircularRequires = NonNullable const collapseSpecifier = (value: string) => value.replace(/['"`+)\s]|new String\(/g, '') -const builtinSpecifiers = new Set( - builtinModules - .map(mod => (mod.startsWith('node:') ? mod.slice(5) : mod)) - .flatMap(mod => { - const parts = mod.split('/') - const base = parts[0] - return parts.length > 1 ? [mod, base] : [mod] - }), -) - const appendExtensionIfNeeded = ( spec: Spec, mode: AppendJsExtensionMode, @@ -118,6 +108,8 @@ const fileExists = async (candidate: string) => { } } +const normalizePath = async (p: string) => pathResolve(await realpath(p).catch(() => p)) + const resolveRequirePath = async (fromFile: string, spec: string, dirIndex: string) => { if (!spec.startsWith('./') && !spec.startsWith('../')) return null const base = pathResolve(pathDirname(fromFile), spec) @@ -127,12 +119,19 @@ const resolveRequirePath = async (fromFile: string, spec: string, dirIndex: stri if (ext) { candidates.push(base) } else { - candidates.push(`${base}.js`, `${base}.cjs`, `${base}.mjs`) + candidates.push( + `${base}.js`, + `${base}.cjs`, + `${base}.mjs`, + `${base}.ts`, + `${base}.mts`, + `${base}.cts`, + ) candidates.push(join(base, dirIndex)) } for (const candidate of candidates) { - if (await fileExists(candidate)) return candidate + if (await fileExists(candidate)) return await normalizePath(candidate) } return null @@ -180,8 +179,10 @@ const detectCircularRequireGraph = async ( const visited = new Set() const dfs = async (file: string, stack: string[]) => { - if (visiting.has(file)) { - const cycle = [...stack, file] + const normalized = await normalizePath(file) + + if (visiting.has(normalized)) { + const cycle = [...stack, normalized] const msg = `Circular require detected: ${cycle.join(' -> ')}` if (mode === 'error') { throw new Error(msg) @@ -191,14 +192,14 @@ const detectCircularRequireGraph = async ( return } - if (visited.has(file)) return - visiting.add(file) - stack.push(file) + if (visited.has(normalized)) return + visiting.add(normalized) + stack.push(normalized) - let deps = cache.get(file) + let deps = cache.get(normalized) if (!deps) { - deps = await collectStaticRequires(file, dirIndex) - cache.set(file, deps) + deps = await collectStaticRequires(normalized, dirIndex) + cache.set(normalized, deps) } for (const dep of deps) { @@ -206,11 +207,11 @@ const detectCircularRequireGraph = async ( } stack.pop() - visiting.delete(file) - visited.add(file) + visiting.delete(normalized) + visited.add(normalized) } - await dfs(entryFile, []) + await dfs(await normalizePath(entryFile), []) } const mergeUsageMaps = ( @@ -271,12 +272,13 @@ const collectProjectDualPackageHazards = async (files: string[], opts: ModuleOpt return byFile } -const defaultOptions = { +const createDefaultOptions = (): ModuleOptions => ({ target: 'commonjs', sourceType: 'auto', transformSyntax: true, liveBindings: 'strict', rewriteSpecifier: undefined, + rewriteTemplateLiterals: 'allow', appendJsExtension: undefined, appendDirectoryIndex: 'index.js', dirFilename: 'inject', @@ -295,9 +297,12 @@ const defaultOptions = { cwd: undefined, out: undefined, inPlace: false, -} satisfies ModuleOptions -const transform = async (filename: string, options: ModuleOptions = defaultOptions) => { - const opts = { ...defaultOptions, ...options, filePath: filename } +}) +const transform = async (filename: string, options?: ModuleOptions) => { + const base = createDefaultOptions() + const opts = options + ? { ...base, ...options, filePath: filename } + : { ...base, filePath: filename } const cwdBase = opts.cwd ? resolve(opts.cwd) : process.cwd() const appendMode: AppendJsExtensionMode = options?.appendJsExtension ?? (opts.target === 'module' ? 'relative-only' : 'off') @@ -311,6 +316,13 @@ const transform = async (filename: string, options: ModuleOptions = defaultOptio if (opts.rewriteSpecifier || appendMode !== 'off' || dirIndex) { const code = await specifier.updateSrc(source, getLangFromExt(filename), spec => { + if ( + spec.type === 'TemplateLiteral' && + opts.rewriteTemplateLiterals === 'static-only' + ) { + const node = spec.node as TemplateLiteral + if (node.expressions.length > 0) return + } const normalized = normalizeBuiltinSpecifier(spec.value) const rewritten = rewriteSpecifierValue( normalized ?? spec.value, diff --git a/src/types.ts b/src/types.ts index 5dd8493..2adb5a2 100644 --- a/src/types.ts +++ b/src/types.ts @@ -34,6 +34,8 @@ export type ModuleOptions = { liveBindings?: 'strict' | 'loose' | 'off' /** Rewrite import specifiers (e.g. add extensions). */ rewriteSpecifier?: RewriteSpecifier + /** Whether to rewrite template literals that contain expressions. Default allows rewrites; set to 'static-only' to skip interpolated templates. */ + rewriteTemplateLiterals?: 'allow' | 'static-only' /** Whether to append .js to relative imports. */ appendJsExtension?: 'off' | 'relative-only' | 'all' /** Add directory index (e.g. /index.js) or disable. */ diff --git a/src/utils/builtinSpecifiers.ts b/src/utils/builtinSpecifiers.ts new file mode 100644 index 0000000..b39ed11 --- /dev/null +++ b/src/utils/builtinSpecifiers.ts @@ -0,0 +1,13 @@ +import { builtinModules } from 'node:module' + +const builtinSpecifiers = new Set( + builtinModules + .map(mod => (mod.startsWith('node:') ? mod.slice(5) : mod)) + .flatMap(mod => { + const parts = mod.split('/') + const base = parts[0] + return parts.length > 1 ? [mod, base] : [mod] + }), +) + +export { builtinSpecifiers } diff --git a/test/cli.ts b/test/cli.ts index 20b2216..b9a239c 100644 --- a/test/cli.ts +++ b/test/cli.ts @@ -638,6 +638,33 @@ test('rewrites specifiers with --rewrite-specifier', () => { assert.match(result.stdout, /\.\/foo\.js'/) }) +test('--rewrite-template-literals guards interpolated templates', () => { + const source = [ + "const side = 'alpha'", + "import './file.ts'", + 'import(`./tmpl/${side}.ts`)', + '', + ].join('\n') + + const result = runCli( + [ + '--target', + 'module', + '--stdin-filename', + 'input.mjs', + '--rewrite-specifier', + '.js', + '--rewrite-template-literals', + 'static-only', + ], + source, + ) + + assert.equal(result.status, 0) + assert.ok(result.stdout.includes("import './file.js'")) + assert.ok(result.stdout.includes('import(`./tmpl/${side}.ts`)')) +}) + test('help example: out-dir mirror', async t => { const temp = await mkdtemp(join(tmpdir(), 'module-cli-')) const srcDir = join(temp, 'src') diff --git a/test/fixtures/cycles/tsA.cts b/test/fixtures/cycles/tsA.cts new file mode 100644 index 0000000..51490cd --- /dev/null +++ b/test/fixtures/cycles/tsA.cts @@ -0,0 +1,2 @@ +const b = require('./tsB.cts') +module.exports = { b } diff --git a/test/fixtures/cycles/tsB.cts b/test/fixtures/cycles/tsB.cts new file mode 100644 index 0000000..1bdbc48 --- /dev/null +++ b/test/fixtures/cycles/tsB.cts @@ -0,0 +1,2 @@ +const a = require('./tsA.cts') +module.exports = { a } diff --git a/test/fixtures/specifier/templateExpr.mjs b/test/fixtures/specifier/templateExpr.mjs new file mode 100644 index 0000000..04f6e5e --- /dev/null +++ b/test/fixtures/specifier/templateExpr.mjs @@ -0,0 +1,6 @@ +const section = 'alpha' + +await import('./file.js') +await import(`./tmpl/${section}.js`) +require('./file.js') +require(`./tmpl/${section}.js`) diff --git a/test/module.ts b/test/module.ts index 8cd3380..3a1b58f 100644 --- a/test/module.ts +++ b/test/module.ts @@ -848,6 +848,18 @@ describe('@knighted/module', () => { const outFile = join(fixtures, `${file.replace('.mjs', '')}.out.cjs`) const requireCjs = createRequire(import.meta.url) + it('detects circular requires across ts/cts files', async () => { + const fixturePath = join(fixtures, 'cycles', 'tsA.cts') + + await assert.rejects( + () => + transform(fixturePath, { + target: 'module', + detectCircularRequires: 'error', + }), + /Circular require detected/, + ) + }) t.after(() => { rm(outFile, { force: true }) }) @@ -1284,6 +1296,41 @@ describe('@knighted/module', () => { ) }) + it('skips rewrites for interpolated template literals', async t => { + const specifierRoot = join(fixtures, 'specifier') + const fixturePath = join(specifierRoot, 'templateExpr.mjs') + const outFile = join(specifierRoot, 'templateExpr.out.cjs') + const fileCjs = join(specifierRoot, 'file.cjs') + const tmplDir = join(specifierRoot, 'tmpl') + const tmplAlpha = join(tmplDir, 'alpha.js') + + await mkdir(tmplDir, { recursive: true }) + await writeFile(fileCjs, "module.exports = { value: 'file-cjs' }\n") + await writeFile(tmplAlpha, "module.exports = { value: 'alpha' }\n") + + const result = await transform(fixturePath, { + target: 'commonjs', + rewriteSpecifier: '.cjs', + rewriteTemplateLiterals: 'static-only', + topLevelAwait: 'wrap', + }) + + t.after(() => { + rm(outFile, { force: true }) + rm(fileCjs, { force: true }) + rm(tmplAlpha, { force: true }) + }) + + await writeFile(outFile, result) + const { status } = spawnSync('node', [outFile], { stdio: 'inherit' }) + assert.equal(status, 0) + + assert.ok(result.includes("import('./file.cjs')")) + assert.ok(result.includes("require('./file.cjs')")) + assert.ok(result.includes('import(`./tmpl/${section}.js`)')) + assert.ok(result.includes('require(`./tmpl/${section}.js`)')) + }) + it('appends .js to relative specifiers when targeting module', async t => { const specifierRoot = join(fixtures, 'specifier') const fixturePath = join(specifierRoot, 'noext.cjs') From 308da7692d884bff88e6c8ec0018aea6efe67521 Mon Sep 17 00:00:00 2001 From: KCM Date: Thu, 1 Jan 2026 12:54:23 -0600 Subject: [PATCH 4/4] test: fix nesting and helper placement. --- test/module.ts | 57 +++++++++++++++++++++++++------------------------- 1 file changed, 29 insertions(+), 28 deletions(-) diff --git a/test/module.ts b/test/module.ts index 3a1b58f..e98309d 100644 --- a/test/module.ts +++ b/test/module.ts @@ -29,6 +29,24 @@ const isValidFilename = async (filename: string) => { } describe('@knighted/module', () => { + const transformEsmToCjs = async (t: any, file: string) => { + const fixturePath = join(fixtures, file) + const result = await transform(fixturePath, { target: 'commonjs' }) + const outFile = join(fixtures, `${file.replace('.mjs', '')}.out.cjs`) + const requireCjs = createRequire(import.meta.url) + + t.after(() => { + rm(outFile, { force: true }) + }) + + await writeFile(outFile, result) + const { status } = spawnSync('node', [outFile], { stdio: 'inherit' }) + assert.equal(status, 0) + const exportsObj = requireCjs(outFile) + + return { exportsObj, result } + } + it('warns on dual package hazard by default', async t => { const temp = await mkdtemp(join(tmpdir(), 'module-dual-hazard-')) const file = join(temp, 'entry.mjs') @@ -842,35 +860,18 @@ describe('@knighted/module', () => { ) }) - const transformEsmToCjs = async (t: any, file: string) => { - const fixturePath = join(fixtures, file) - const result = await transform(fixturePath, { target: 'commonjs' }) - const outFile = join(fixtures, `${file.replace('.mjs', '')}.out.cjs`) - const requireCjs = createRequire(import.meta.url) + it('detects circular requires across ts/cts files', async () => { + const fixturePath = join(fixtures, 'cycles', 'tsA.cts') - it('detects circular requires across ts/cts files', async () => { - const fixturePath = join(fixtures, 'cycles', 'tsA.cts') - - await assert.rejects( - () => - transform(fixturePath, { - target: 'module', - detectCircularRequires: 'error', - }), - /Circular require detected/, - ) - }) - t.after(() => { - rm(outFile, { force: true }) - }) - - await writeFile(outFile, result) - const { status } = spawnSync('node', [outFile], { stdio: 'inherit' }) - assert.equal(status, 0) - const exportsObj = requireCjs(outFile) - - return { exportsObj, result } - } + await assert.rejects( + () => + transform(fixturePath, { + target: 'module', + detectCircularRequires: 'error', + }), + /Circular require detected/, + ) + }) it('lowers side-effect import when targeting commonjs', async t => { const { exportsObj, result } = await transformEsmToCjs(t, 'importSideEffect.mjs')