diff --git a/README.md b/README.md index 352c361..cc5af32 100644 --- a/README.md +++ b/README.md @@ -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' @@ -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`. diff --git a/docs/cli.md b/docs/cli.md index da5d89e..c961ccb 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -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) | diff --git a/docs/dual-package-hazard.md b/docs/dual-package-hazard.md new file mode 100644 index 0000000..454250b --- /dev/null +++ b/docs/dual-package-hazard.md @@ -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/` 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. diff --git a/package-lock.json b/package-lock.json index 2f40144..7ed7d8a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@knighted/module", - "version": "1.3.1", + "version": "1.4.0-rc.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@knighted/module", - "version": "1.3.1", + "version": "1.4.0-rc.0", "license": "MIT", "dependencies": { "glob": "^13.0.0", diff --git a/package.json b/package.json index 7058d7c..96882c8 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/cli.ts b/src/cli.ts index 83bfb2a..d318b6c 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -30,6 +30,7 @@ const defaultOptions: ModuleOptions = { importMetaMain: 'shim', requireMainStrategy: 'import-meta-main', detectCircularRequires: 'off', + detectDualPackageHazard: 'warn', requireSource: 'builtin', nestedRequireStrategy: 'create-require', cjsDefault: 'auto', @@ -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', @@ -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, diff --git a/src/format.ts b/src/format.ts index 933a259..65a9da7 100644 --- a/src/format.ts +++ b/src/format.ts @@ -1,3 +1,6 @@ +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' import MagicString from 'magic-string' @@ -38,6 +41,320 @@ 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 + +const packageFromSpecifier = (spec: string) => { + const cleaned = stripQuery(spec) + if (!cleaned) return null + if (cleaned.startsWith('node:')) return null + if (/^(?:\.?\.?\/|\/)/.test(cleaned)) return null + if (/^[a-zA-Z][a-zA-Z+.-]*:/.test(cleaned)) return null + + const parts = cleaned.split('/') + + if (cleaned.startsWith('@')) { + if (parts.length < 2) return null + const pkg = `${parts[0]}/${parts[1]}` + if (builtinSpecifiers.has(pkg) || builtinSpecifiers.has(parts[1] ?? '')) return null + const subpath = parts.slice(2).join('/') + return { pkg, subpath } + } + + const pkg = parts[0] ?? '' + if (!pkg || builtinSpecifiers.has(pkg)) return null + const subpath = parts.slice(1).join('/') + return { pkg, subpath } +} + +const fileExists = async (filename: string) => { + try { + const stats = await fsStat(filename) + return stats.isFile() + } catch { + return false + } +} + +const findPackageManifest = async ( + pkg: string, + filePath: string | undefined, + cwd: string | undefined, +) => { + const startDir = filePath + ? dirname(pathResolve(filePath)) + : pathResolve(cwd ?? process.cwd()) + const seen = new Set() + let dir = startDir + + while (!seen.has(dir)) { + seen.add(dir) + const candidate = join(dir, 'node_modules', pkg, 'package.json') + if (await fileExists(candidate)) return candidate + + const parent = dirname(dir) + if (parent === dir) break + dir = parent + } + + return null +} + +const readPackageManifest = async ( + pkg: string, + filePath: string | undefined, + cwd: string | undefined, + cache: Map, +) => { + const start = pathResolve(filePath ? dirname(filePath) : (cwd ?? process.cwd())) + const cacheKey = `${pkg}@${start}` + if (cache.has(cacheKey)) return cache.get(cacheKey) + + const manifestPath = await findPackageManifest(pkg, filePath, cwd) + if (!manifestPath) { + cache.set(cacheKey, null) + return null + } + + try { + const raw = await fsReadFile(manifestPath, 'utf8') + const json = JSON.parse(raw) + cache.set(cacheKey, json) + return json + } catch { + cache.set(cacheKey, null) + return null + } +} + +const analyzeExportsTargets = (exportsField: unknown) => { + const root = + exportsField && typeof exportsField === 'object' && !Array.isArray(exportsField) + ? // @ts-expect-error -- loose lookup of root export condition + (exportsField['.'] ?? exportsField) + : exportsField + + if (typeof root === 'string') { + return { importTarget: root, requireTarget: root } + } + + if (root && typeof root === 'object') { + const record = root as Record + const importTarget = typeof record.import === 'string' ? record.import : undefined + const requireTarget = typeof record.require === 'string' ? record.require : undefined + const defaultTarget = typeof record.default === 'string' ? record.default : undefined + + return { + importTarget: importTarget ?? defaultTarget, + requireTarget: requireTarget ?? defaultTarget, + } + } + + return { importTarget: undefined, requireTarget: undefined } +} + +const describeDualPackage = (pkgJson: any) => { + const { importTarget, requireTarget } = analyzeExportsTargets(pkgJson?.exports) + const moduleField = typeof pkgJson?.module === 'string' ? pkgJson.module : undefined + const mainField = typeof pkgJson?.main === 'string' ? pkgJson.main : undefined + const typeField = typeof pkgJson?.type === 'string' ? pkgJson.type : undefined + + const divergentExports = importTarget && requireTarget && importTarget !== requireTarget + const divergentModuleMain = moduleField && mainField && moduleField !== mainField + const typeModuleMainCjs = + typeField === 'module' && typeof mainField === 'string' && mainField.endsWith('.cjs') + + const hasHazardSignals = divergentExports || divergentModuleMain || typeModuleMainCjs + + const details: string[] = [] + if (divergentExports) { + details.push(`exports import -> ${importTarget}, require -> ${requireTarget}`) + } + if (divergentModuleMain) { + details.push(`module -> ${moduleField}, main -> ${mainField}`) + } + if (typeModuleMainCjs) { + details.push(`type: module with CommonJS main (${mainField})`) + } + + return { hasHazardSignals, details, importTarget, requireTarget } +} + +type HazardLevel = 'warning' | 'error' + +type PackageUse = { + spec: string + subpath: string + loc?: { start: number; end: number } +} + +type PackageUsage = { + imports: PackageUse[] + requires: PackageUse[] +} + +const detectDualPackageHazards = async (params: { + program: Node + shadowedBindings: Set + hazardLevel: HazardLevel + filePath?: string + cwd?: string + diagOnce: ( + level: HazardLevel, + codeId: string, + message: string, + loc?: { start: number; end: number }, + ) => void +}) => { + const { program, shadowedBindings, hazardLevel, filePath, cwd, diagOnce } = params + const usages = new Map() + const manifestCache = new Map() + + const record = ( + pkg: string, + kind: 'import' | 'require', + spec: string, + subpath: string, + loc?: { start: number; end: number }, + ) => { + const existing = usages.get(pkg) ?? { imports: [], requires: [] } + const bucket = kind === 'import' ? existing.imports : existing.requires + bucket.push({ spec, subpath, loc }) + usages.set(pkg, existing) + } + + await ancestorWalk(program, { + enter(node) { + if ( + node.type === 'ImportDeclaration' && + node.source.type === 'Literal' && + typeof node.source.value === 'string' + ) { + const pkg = packageFromSpecifier(node.source.value) + if (pkg) + record(pkg.pkg, 'import', node.source.value, pkg.subpath, { + start: node.source.start, + end: node.source.end, + }) + } + + if ( + node.type === 'ExportNamedDeclaration' && + node.source && + node.source.type === 'Literal' && + typeof node.source.value === 'string' + ) { + const pkg = packageFromSpecifier(node.source.value) + if (pkg) + record(pkg.pkg, 'import', node.source.value, pkg.subpath, { + start: node.source.start, + end: node.source.end, + }) + } + + if ( + node.type === 'ExportAllDeclaration' && + node.source.type === 'Literal' && + typeof node.source.value === 'string' + ) { + const pkg = packageFromSpecifier(node.source.value) + if (pkg) + record(pkg.pkg, 'import', node.source.value, pkg.subpath, { + start: node.source.start, + end: node.source.end, + }) + } + + if ( + node.type === 'ImportExpression' && + node.source.type === 'Literal' && + typeof node.source.value === 'string' + ) { + const pkg = packageFromSpecifier(node.source.value) + if (pkg) + record(pkg.pkg, 'import', node.source.value, pkg.subpath, { + start: node.source.start, + end: node.source.end, + }) + } + + if (node.type === 'CallExpression' && isStaticRequire(node, shadowedBindings)) { + const arg = node.arguments[0] + if (arg?.type === 'Literal' && typeof arg.value === 'string') { + const pkg = packageFromSpecifier(arg.value) + if (pkg) + record(pkg.pkg, 'require', arg.value, pkg.subpath, { + start: arg.start, + end: arg.end, + }) + } + } + }, + }) + + for (const [pkg, usage] of usages) { + const hasImport = usage.imports.length > 0 + const hasRequire = usage.requires.length > 0 + const combined = [...usage.imports, ...usage.requires] + const hasRoot = combined.some(entry => !entry.subpath) + const hasSubpath = combined.some(entry => Boolean(entry.subpath)) + + if (hasImport && hasRequire) { + const importSpecs = usage.imports.map(u => + u.subpath ? `${pkg}/${u.subpath}` : pkg, + ) + const requireSpecs = usage.requires.map(u => + u.subpath ? `${pkg}/${u.subpath}` : pkg, + ) + + diagOnce( + hazardLevel, + 'dual-package-mixed-specifiers', + `Package '${pkg}' is loaded via import (${importSpecs.join(', ')}) and require (${requireSpecs.join(', ')}); conditional exports can instantiate it twice.`, + usage.imports[0]?.loc ?? usage.requires[0]?.loc, + ) + } + + if (hasRoot && hasSubpath) { + const subpaths = combined + .filter(entry => entry.subpath) + .map(entry => `${pkg}/${entry.subpath}`) + diagOnce( + hazardLevel, + 'dual-package-subpath', + `Package '${pkg}' is referenced via root specifier '${pkg}' and subpath(s) ${subpaths.join(', ')}; mixing them loads separate module instances.`, + combined.find(entry => entry.subpath)?.loc ?? combined[0]?.loc, + ) + } + + if (hasImport && hasRequire) { + const manifest = await readPackageManifest(pkg, filePath, cwd, manifestCache) + if (manifest) { + const meta = describeDualPackage(manifest) + if (meta.hasHazardSignals) { + const detail = meta.details.length ? ` (${meta.details.join('; ')})` : '' + diagOnce( + hazardLevel, + 'dual-package-conditional-exports', + `Package '${pkg}' exposes different entry points for import vs require${detail}. Mixed usage can produce distinct instances.`, + usage.imports[0]?.loc ?? usage.requires[0]?.loc, + ) + } + } + } + } +} + /** * Node added support for import.meta.main. * Added in: v24.2.0, v22.18.0 @@ -67,22 +384,22 @@ const format = async (src: string, ast: ParseResult, opts: FormatterOptions) => // eslint-disable-next-line no-console -- used for opt-in diagnostics console.error(diag.message) } - const warnOnce: WarnOnce = ( + const diagOnce = ( + level: Diagnostic['level'], codeId: string, message: string, loc?: { start: number; end: number }, ) => { - const key = `${codeId}:${loc?.start ?? ''}` + const key = `${level}:${codeId}:${loc?.start ?? ''}` if (warned.has(key)) return warned.add(key) - emitDiagnostic({ - level: 'warning', - code: codeId, - message, - filePath: opts.filePath, - loc, - }) + emitDiagnostic({ level, code: codeId, message, filePath: opts.filePath, loc }) } + const warnOnce: WarnOnce = ( + codeId: string, + message: string, + loc?: { start: number; end: number }, + ) => diagOnce('warning', codeId, message, loc) const transformMode = opts.transformSyntax const fullTransform = transformMode === true const moduleIdentifiers = await collectModuleIdentifiers(ast.program) @@ -92,6 +409,19 @@ const format = async (src: string, ast: ParseResult, opts: FormatterOptions) => .map(([name]) => name), ) + const hazardMode = opts.detectDualPackageHazard ?? 'warn' + if (hazardMode !== 'off') { + const hazardLevel: HazardLevel = hazardMode === 'error' ? 'error' : 'warning' + await detectDualPackageHazards({ + program: ast.program, + shadowedBindings, + hazardLevel, + filePath: opts.filePath, + cwd: opts.cwd, + diagOnce, + }) + } + if (opts.target === 'module' && fullTransform) { if (shadowedBindings.has('module') || shadowedBindings.has('exports')) { throw new Error( diff --git a/src/module.ts b/src/module.ts index aadb539..22f07b1 100644 --- a/src/module.ts +++ b/src/module.ts @@ -220,6 +220,7 @@ const defaultOptions = { importMetaMain: 'shim', requireMainStrategy: 'import-meta-main', detectCircularRequires: 'off', + detectDualPackageHazard: 'warn', requireSource: 'builtin', nestedRequireStrategy: 'create-require', cjsDefault: 'auto', diff --git a/src/types.ts b/src/types.ts index e2c2611..fc778a9 100644 --- a/src/types.ts +++ b/src/types.ts @@ -49,6 +49,8 @@ export type ModuleOptions = { requireMainStrategy?: 'import-meta-main' | 'realpath' /** Detect circular require usage level. */ detectCircularRequires?: 'off' | 'warn' | 'error' + /** Detect divergent import/require usage of the same dual package (default warn). */ + detectDualPackageHazard?: 'off' | 'warn' | 'error' /** Source used to provide require in ESM output. */ requireSource?: 'builtin' | 'create-require' /** How to rewrite nested or non-hoistable require calls. */ diff --git a/test/cli.ts b/test/cli.ts index 31e5dfc..3bd532a 100644 --- a/test/cli.ts +++ b/test/cli.ts @@ -122,6 +122,58 @@ test('--ignore excludes glob matches', async () => { } }) +test('-H error exits on dual package hazard', async () => { + const temp = await mkdtemp(join(tmpdir(), 'module-cli-dual-hazard-')) + const file = join(temp, 'entry.mjs') + const pkgDir = join(temp, 'node_modules', 'x-core') + + await mkdir(pkgDir, { recursive: true }) + await writeFile( + join(pkgDir, 'package.json'), + JSON.stringify( + { + name: 'x-core', + version: '1.0.0', + exports: { + '.': { import: './x-core.mjs', require: './x-core.cjs' }, + './module': './x-core.mjs', + }, + main: './x-core.cjs', + }, + null, + 2, + ), + 'utf8', + ) + await writeFile( + file, + [ + "import { X } from 'x-core/module'", + "const core = require('x-core')", + 'console.log(core, X)', + '', + ].join('\n'), + 'utf8', + ) + + try { + const result = runCli([ + '-H', + 'error', + '--target', + 'commonjs', + '--cwd', + temp, + 'entry.mjs', + ]) + + assert.equal(result.status, 1) + assert.match(result.stderr, /dual-package-mixed-specifiers/) + } finally { + await rm(temp, { recursive: true, force: true }) + } +}) + test('rewrites __dirname for ESM TS projects (NodeNext)', async () => { const temp = await mkdtemp(join(tmpdir(), 'module-cli-ts-node-next-')) const srcDir = join(temp, 'src') diff --git a/test/module.ts b/test/module.ts index 1a86072..ff3c798 100644 --- a/test/module.ts +++ b/test/module.ts @@ -2,9 +2,10 @@ import { describe, it } from 'node:test' import assert from 'node:assert/strict' import { spawnSync } from 'node:child_process' import { resolve, join } from 'node:path' +import { tmpdir } from 'node:os' import { pathToFileURL } from 'node:url' import { createRequire } from 'node:module' -import { copyFile, mkdir, rm, stat, writeFile } from 'node:fs/promises' +import { copyFile, mkdir, mkdtemp, rm, stat, writeFile } from 'node:fs/promises' import type { Stats } from 'node:fs' import { transform } from '../src/module.js' @@ -28,6 +29,116 @@ const isValidFilename = async (filename: string) => { } describe('@knighted/module', () => { + 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') + const pkgDir = join(temp, 'node_modules', 'x-core') + + await mkdir(pkgDir, { recursive: true }) + await writeFile( + join(pkgDir, 'package.json'), + JSON.stringify( + { + name: 'x-core', + version: '1.0.0', + exports: { + '.': { import: './x-core.mjs', require: './x-core.cjs' }, + './module': './x-core.mjs', + }, + main: './x-core.cjs', + }, + null, + 2, + ), + 'utf8', + ) + await writeFile( + file, + [ + "import { X } from 'x-core/module'", + "const core = require('x-core')", + 'console.log(core, X)', + '', + ].join('\n'), + 'utf8', + ) + + t.after(() => rm(temp, { recursive: true, force: true })) + + const diagnostics: any[] = [] + await transform(file, { + target: 'commonjs', + topLevelAwait: 'wrap', + diagnostics: diag => diagnostics.push(diag), + cwd: temp, + }) + + const codes = diagnostics.map(d => d.code) + assert.ok(codes.includes('dual-package-mixed-specifiers')) + assert.ok(codes.includes('dual-package-subpath')) + assert.ok(codes.includes('dual-package-conditional-exports')) + assert.ok(diagnostics.every(d => d.level === 'warning')) + }) + + it('warns on hazard across export forms and dynamic import', async t => { + const temp = await mkdtemp(join(tmpdir(), 'module-dual-hazard-exports-')) + const file = join(temp, 'entry.mjs') + const pkgDir = join(temp, 'node_modules', 'x-core') + + await mkdir(pkgDir, { recursive: true }) + await writeFile( + join(pkgDir, 'package.json'), + JSON.stringify( + { + name: 'x-core', + version: '1.1.0', + exports: { + '.': { import: './x-core.mjs', require: './x-core.cjs' }, + './module': './x-core.mjs', + }, + module: './x-core.mjs', + main: './x-core.cjs', + type: 'module', + }, + null, + 2, + ), + 'utf8', + ) + await writeFile( + file, + [ + "export * from 'x-core'", + "export { Y } from 'x-core/module?query'", + "await import('x-core/module')", + "const core = require('x-core')", + "await import('node:fs')", + '', + ].join('\n'), + 'utf8', + ) + + t.after(() => rm(temp, { recursive: true, force: true })) + + const diagnostics: any[] = [] + await transform(file, { + target: 'commonjs', + topLevelAwait: 'wrap', + diagnostics: diag => diagnostics.push(diag), + cwd: temp, + }) + + const codes = diagnostics.map(d => d.code) + assert.ok(codes.includes('dual-package-mixed-specifiers')) + assert.ok(codes.includes('dual-package-subpath')) + const conditional = diagnostics.find( + d => d.code === 'dual-package-conditional-exports', + ) + assert.ok(conditional) + assert.ok(/module ->/.test(conditional!.message)) + assert.ok(/type: module/.test(conditional!.message)) + }) + it('transforms __filename', async t => { const result = await transform(join(fixtures, '__filename.cjs'), { target: 'module',