From 20a8dd1fc2bfa50782455a873acc8baec4d05e7d Mon Sep 17 00:00:00 2001 From: KCM Date: Fri, 2 Jan 2026 21:13:27 -0600 Subject: [PATCH 1/2] feat: source map support. --- README.md | 2 ++ docs/cli.md | 1 + docs/roadmap.md | 1 - package-lock.json | 4 +-- package.json | 2 +- src/cli.ts | 78 ++++++++++++++++++++++++++++++++++++++++++++--- src/format.ts | 34 ++++++++++++--------- src/module.ts | 54 +++++++++++++++++++++++++++++--- src/specifier.ts | 35 ++++++++++++++++++--- src/types.ts | 2 ++ test/cli.ts | 78 +++++++++++++++++++++++++++++++++++++++++++++++ test/module.ts | 19 ++++++++++++ 12 files changed, 276 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index bb5e416..a84b633 100644 --- a/README.md +++ b/README.md @@ -114,6 +114,7 @@ type ModuleOptions = { target: 'module' | 'commonjs' sourceType?: 'auto' | 'module' | 'commonjs' transformSyntax?: boolean | 'globals-only' + sourceMap?: boolean liveBindings?: 'strict' | 'loose' | 'off' appendJsExtension?: 'off' | 'relative-only' | 'all' appendDirectoryIndex?: string | false @@ -167,6 +168,7 @@ type ModuleOptions = { - `cjsDefault` (`auto`): bundler-style default interop vs direct `module.exports`. - `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. - `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. +- `sourceMap` (`false`): when true, returns `{ code, map }` from `transform` and writes the map if you also set `out`/`inPlace`. Maps are generated from the same MagicString pipeline used for the code. - `cwd` (`process.cwd()`): Base directory used to resolve relative `out` paths. > [!NOTE] diff --git a/docs/cli.md b/docs/cli.md index 7f57fb0..edb1fb1 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -75,6 +75,7 @@ Short and long forms are supported. | -d | --cjs-default | Default interop (module-exports \| auto \| none) | | -e | --idiomatic-exports | Emit idiomatic exports when safe (off \| safe \| aggressive) | | -m | --import-meta-prelude | Emit import.meta prelude (off \| auto \| on) | +| | --source-map | Emit a source map (sidecar); use --source-map=inline for stdout | | -n | --nested-require-strategy | Rewrite nested require (create-require \| dynamic-import) | | -R | --require-main-strategy | Detect main (import-meta-main \| realpath) | | -l | --live-bindings | Live binding strategy (strict \| loose \| off) | diff --git a/docs/roadmap.md b/docs/roadmap.md index da69df7..93b66af 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -14,7 +14,6 @@ Status: draft ## Tooling & Diagnostics -- 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) diff --git a/package-lock.json b/package-lock.json index 2417450..556ee5e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@knighted/module", - "version": "1.4.0", + "version": "1.5.0-rc.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@knighted/module", - "version": "1.4.0", + "version": "1.5.0-rc.0", "license": "MIT", "dependencies": { "glob": "^13.0.0", diff --git a/package.json b/package.json index 4231640..cbbfc38 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@knighted/module", - "version": "1.4.0", + "version": "1.5.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 64b5658..6caa805 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -5,8 +5,8 @@ import { stderr as defaultStderr, } from 'node:process' import { parseArgs } from 'node:util' -import { readFile, mkdir } from 'node:fs/promises' -import { dirname, resolve, relative, join } from 'node:path' +import { readFile, mkdir, writeFile } from 'node:fs/promises' +import { dirname, resolve, relative, join, basename } from 'node:path' import { glob } from 'glob' import type { TemplateLiteral } from '@oxc-project/types' @@ -41,6 +41,7 @@ const defaultOptions: ModuleOptions = { idiomaticExports: 'safe', importMetaPrelude: 'auto', topLevelAwait: 'error', + sourceMap: false, cwd: undefined, out: undefined, inPlace: false, @@ -248,6 +249,12 @@ const optionsTable = [ type: 'string', desc: 'Emit import.meta prelude (off|auto|on)', }, + { + long: 'source-map', + short: undefined, + type: 'boolean', + desc: 'Emit a source map alongside transformed output (use --source-map=inline for stdout)', + }, { long: 'nested-require-strategy', short: 'n', @@ -448,6 +455,7 @@ const toModuleOptions = (values: ParsedValues): ModuleOptions => { values['live-bindings'] as string | undefined, ['strict', 'loose', 'off'] as const, ) ?? defaultOptions.liveBindings, + sourceMap: Boolean(values['source-map']), cwd: values.cwd ? resolve(String(values.cwd)) : defaultOptions.cwd, } @@ -462,6 +470,35 @@ const readStdin = async (stdin: typeof defaultStdin) => { return Buffer.concat(chunks).toString('utf8') } +const normalizeSourceMapArgv = (argv: string[]) => { + let sourceMapInline = false + const normalized: string[] = [] + + for (let i = 0; i < argv.length; i += 1) { + const arg = argv[i] + + if (arg === '--source-map' && argv[i + 1] === 'inline') { + sourceMapInline = true + normalized.push('--source-map') + i += 1 + continue + } + + if (arg.startsWith('--source-map=')) { + const value = arg.slice('--source-map='.length) + if (value === 'inline') { + sourceMapInline = true + normalized.push('--source-map') + continue + } + } + + normalized.push(arg) + } + + return { argv: normalized, sourceMapInline } +} + const expandFiles = async (patterns: string[], cwd: string, ignore?: string[]) => { const files = new Set() for (const pattern of patterns) { @@ -577,6 +614,7 @@ const runFiles = async ( outDir?: string inPlace: boolean allowStdout: boolean + sourceMapInline: boolean }, ) => { const results: FileResult[] = [] @@ -618,8 +656,11 @@ const runFiles = async ( } } - const output = await transform(file, perFileOpts) + const transformed = await transform(file, perFileOpts) + const output = typeof transformed === 'string' ? transformed : transformed.code + const map = typeof transformed === 'string' ? null : transformed.map const changed = output !== original + let finalOutput = output if (projectHazards) { const extras = projectHazards.get(file) @@ -630,8 +671,26 @@ const runFiles = async ( logger.info(file) } + if (map && flags.sourceMapInline && !writeTarget && !perFileOpts.inPlace) { + const mapUri = Buffer.from(JSON.stringify(map)).toString('base64') + finalOutput = `${output.replace(/\/\/# sourceMappingURL=.*/g, '').trimEnd()}\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,${mapUri}\n` + } else if (map && (writeTarget || perFileOpts.inPlace)) { + const target = writeTarget ?? file + const mapPath = `${target}.map` + map.file = basename(mapPath) + + const updated = `${output.replace(/\/\/# sourceMappingURL=.*/g, '').trimEnd()}\n//# sourceMappingURL=${map.file}\n` + await writeFile(mapPath, JSON.stringify(map)) + + if (writeTarget) { + await writeFile(writeTarget, updated) + } else if (perFileOpts.inPlace) { + await writeFile(file, updated) + } + } + if (!flags.dryRun && !flags.list && !writeTarget && !perFileOpts.inPlace) { - io.stdout.write(output) + io.stdout.write(finalOutput) } results.push({ filePath: file, changed, diagnostics }) @@ -664,8 +723,10 @@ const runCli = async ({ stdout = defaultStdout, stderr = defaultStderr, }: CliOptions = {}) => { + const { argv: normalizedArgv, sourceMapInline } = normalizeSourceMapArgv(argv) + const { values, positionals } = parseArgs({ - args: argv, + args: normalizedArgv, allowPositionals: true, options: Object.fromEntries( optionsTable.map(opt => [ @@ -694,6 +755,7 @@ const runCli = async ({ } const moduleOpts = toModuleOptions(values) + if (sourceMapInline) moduleOpts.sourceMap = true const cwd = moduleOpts.cwd ?? process.cwd() const allowStdout = positionals.length <= 1 const fromStdin = positionals.length === 0 || positionals.includes('-') @@ -710,6 +772,11 @@ const runCli = async ({ const summary = Boolean(values.summary) const json = Boolean(values.json) + if (sourceMapInline && (outDir || inPlace)) { + logger.error('Inline source maps are only supported when writing to stdout') + return 2 + } + if (outDir && inPlace) { logger.error('Choose either --out-dir or --in-place, not both') return 2 @@ -769,6 +836,7 @@ const runCli = async ({ outDir, inPlace, allowStdout, + sourceMapInline, }, ) diff --git a/src/format.ts b/src/format.ts index c9ab813..b01303b 100644 --- a/src/format.ts +++ b/src/format.ts @@ -419,12 +419,13 @@ const detectDualPackageHazards = async (params: { } } -/** - * Node added support for import.meta.main. - * Added in: v24.2.0, v22.18.0 - * @see https://nodejs.org/api/esm.html#importmetamain - */ -const format = async (src: string, ast: ParseResult, opts: FormatterOptions) => { +function format( + src: string, + ast: ParseResult, + opts: FormatterOptions & { sourceMap: true }, +): Promise +function format(src: string, ast: ParseResult, opts: FormatterOptions): Promise +async function format(src: string, ast: ParseResult, opts: FormatterOptions) { const code = new MagicString(src) const exportsMeta = { hasExportsBeenReassigned: false, @@ -706,19 +707,22 @@ const format = async (src: string, ast: ParseResult, opts: FormatterOptions) => } if (opts.target === 'commonjs' && fullTransform && containsTopLevelAwait) { - const body = code.toString() - if (opts.topLevelAwait === 'wrap') { - const tlaPromise = `const __tla = (async () => {\n${body}\nreturn module.exports;\n})();\n` - const setPromise = `const __setTla = target => {\n if (!target) return;\n const type = typeof target;\n if (type !== 'object' && type !== 'function') return;\n target.__tla = __tla;\n};\n` - const attach = `__setTla(module.exports);\n__tla.then(resolved => __setTla(resolved), err => { throw err; });\n` - return `${tlaPromise}${setPromise}${attach}` + code.prepend('const __tla = (async () => {\n') + code.append('\nreturn module.exports;\n})();\n') + code.append( + 'const __setTla = target => {\n if (!target) return;\n const type = typeof target;\n if (type !== "object" && type !== "function") return;\n target.__tla = __tla;\n};\n', + ) + code.append( + '__setTla(module.exports);\n__tla.then(resolved => __setTla(resolved), err => { throw err; });\n', + ) + } else { + code.prepend(';(async () => {\n') + code.append('\n})();\n') } - - return `;(async () => {\n${body}\n})();\n` } - return code.toString() + return opts.sourceMap ? code : code.toString() } export { format, collectDualPackageUsage, dualPackageHazardDiagnostics } diff --git a/src/module.ts b/src/module.ts index 57f8067..a039326 100644 --- a/src/module.ts +++ b/src/module.ts @@ -14,6 +14,8 @@ import { } from './format.js' import { getLangFromExt } from './utils/lang.js' import type { ModuleOptions, Diagnostic } from './types.js' +import type MagicString from 'magic-string' +import type { SourceMap } from 'magic-string' import { resolve as pathResolve, dirname as pathDirname, extname, join } from 'node:path' import { readFile as fsReadFile, stat, realpath } from 'node:fs/promises' import { parse as parseModule } from './parse.js' @@ -294,11 +296,24 @@ const createDefaultOptions = (): ModuleOptions => ({ idiomaticExports: 'safe', importMetaPrelude: 'auto', topLevelAwait: 'error', + sourceMap: false, cwd: undefined, out: undefined, inPlace: false, }) -const transform = async (filename: string, options?: ModuleOptions) => { +function transform( + filename: string, + options: ModuleOptions & { sourceMap: true }, +): Promise<{ code: string; map: SourceMap }> +function transform( + filename: string, + options?: ModuleOptions & { sourceMap?: false | undefined }, +): Promise +function transform( + filename: string, + options?: ModuleOptions, +): Promise +async function transform(filename: string, options?: ModuleOptions) { const base = createDefaultOptions() const opts = options ? { ...base, ...options, filePath: filename } @@ -312,10 +327,18 @@ const transform = async (filename: string, options?: ModuleOptions) => { const file = resolve(cwdBase, filename) const code = (await readFile(file)).toString() const ast = parse(filename, code) - let source = await format(code, ast, opts) + let sourceCode: MagicString | null = null + let source: string + + if (opts.sourceMap) { + sourceCode = await format(code, ast, { ...opts, sourceMap: true }) + source = sourceCode.toString() + } else { + source = await format(code, ast, opts) + } if (opts.rewriteSpecifier || appendMode !== 'off' || dirIndex) { - const code = await specifier.updateSrc(source, getLangFromExt(filename), spec => { + const applyRewrite = (spec: any) => { if ( spec.type === 'TemplateLiteral' && opts.rewriteTemplateLiterals === 'static-only' @@ -332,9 +355,19 @@ const transform = async (filename: string, options?: ModuleOptions) => { const appended = appendExtensionIfNeeded(spec, appendMode, dirIndex, baseValue) return appended ?? rewritten ?? normalized ?? undefined - }) + } - source = code + if (opts.sourceMap && sourceCode) { + await specifier.updateMagicString(sourceCode, code, ast, applyRewrite) + source = sourceCode.toString() + } else { + const rewritten = await specifier.updateSrc( + source, + getLangFromExt(filename), + applyRewrite, + ) + source = rewritten + } } if (detectCycles !== 'off' && opts.target === 'module' && opts.transformSyntax) { @@ -351,6 +384,17 @@ const transform = async (filename: string, options?: ModuleOptions) => { await writeFile(outputPath, source) } + if (opts.sourceMap && sourceCode) { + const map = sourceCode.generateMap({ + hires: true, + includeContent: true, + file: outputPath ?? filename, + source: opts.filePath ?? filename, + }) + + return { code: source, map } + } + return source } diff --git a/src/specifier.ts b/src/specifier.ts index baa0a99..343c846 100644 --- a/src/specifier.ts +++ b/src/specifier.ts @@ -45,6 +45,12 @@ type SpecifierApi = { lang: ParserOptions['lang'], callback: Callback, ) => Promise + updateMagicString: ( + code: MagicString, + src: string, + ast: ParseResult, + callback: Callback, + ) => Promise } const isStringLiteral = (node: Node): node is StringLiteral => { @@ -60,8 +66,12 @@ const isCallExpression = (node: Node): node is CallExpression => { return node.type === 'CallExpression' && node.callee !== undefined } -const formatSpecifiers = async (src: string, ast: ParseResult, cb: Callback) => { - const code = new MagicString(src) +const formatSpecifiers = async ( + src: string, + ast: ParseResult, + cb: Callback, + code: MagicString = new MagicString(src), +) => { const formatExpression = (expression: ImportExpression | CallExpression) => { const node = isCallExpression(expression) ? expression.arguments[0] @@ -278,7 +288,7 @@ const formatSpecifiers = async (src: string, ast: ParseResult, cb: Callback) => }, }) - return code.toString() + return code } const isValidFilename = async (filename: string) => { @@ -309,7 +319,9 @@ const specifier = { const src = (await readFile(filename)).toString() const ast = parseSync(filename, src) - return await formatSpecifiers(src, ast, callback) + const code = await formatSpecifiers(src, ast, callback) + + return code.toString() }, async updateSrc(src: string, lang: ParserOptions['lang'], callback: Callback) { @@ -323,7 +335,20 @@ const specifier = { : 'file.jsx' const ast = parseSync(filename, src) - return await formatSpecifiers(src, ast, callback) + const code = await formatSpecifiers(src, ast, callback) + + return code.toString() + }, + + async updateMagicString( + code: MagicString, + src: string, + ast: ParseResult, + callback: Callback, + ) { + await formatSpecifiers(src, ast, callback, code) + + return code }, } satisfies SpecifierApi diff --git a/src/types.ts b/src/types.ts index 2adb5a2..2891088 100644 --- a/src/types.ts +++ b/src/types.ts @@ -23,6 +23,8 @@ export type ModuleOptions = { target: 'module' | 'commonjs' /** Explicit source type; auto infers from file extension. */ sourceType?: 'auto' | 'module' | 'commonjs' + /** Emit a source map alongside the transformed code. */ + sourceMap?: boolean /** * Enable syntax transforms beyond parsing. * - true: full CJS↔ESM lowering/raising diff --git a/test/cli.ts b/test/cli.ts index b9a239c..e97c4aa 100644 --- a/test/cli.ts +++ b/test/cli.ts @@ -1,6 +1,7 @@ import { test } from 'node:test' import assert from 'node:assert/strict' import { spawnSync } from 'node:child_process' +import { Buffer } from 'node:buffer' import { resolve, join, relative } from 'node:path' import { pathToFileURL } from 'node:url' import { tmpdir } from 'node:os' @@ -692,6 +693,83 @@ test('help example: out-dir mirror', async t => { await runCodeInNode(written, { type: 'module' }) }) +test('--source-map=inline writes inline map to stdout', () => { + const result = runCli(['--target', 'module', '--source-map=inline', fixtureRel]) + + assert.equal(result.status, 0) + const match = + /sourceMappingURL=data:application\/json;charset=utf-8;base64,([^\n]+)/.exec( + result.stdout, + ) + assert.ok(match) + const map = JSON.parse(Buffer.from(match?.[1] ?? '', 'base64').toString('utf8')) + assert.equal(map.version, 3) + assert.ok((map.sources ?? []).length > 0) +}) + +test('--source-map writes map files with out-dir', async t => { + const temp = await mkdtemp(join(tmpdir(), 'module-cli-sourcemap-')) + const input = join(temp, 'entry.cjs') + await copyFile(fixture, input) + + t.after(() => rm(temp, { recursive: true, force: true })) + + const result = runCli( + [ + '--target', + 'module', + '--source-map', + '--cwd', + temp, + '--out-dir', + 'dist', + 'entry.cjs', + ], + undefined, + { cwd: temp }, + ) + + assert.equal(result.status, 0) + const outFile = join(temp, 'dist', 'entry.cjs') + const mapFile = `${outFile}.map` + const written = await readFile(outFile, 'utf8') + assert.match(written, /sourceMappingURL=entry.cjs.map/) + + const map = JSON.parse(await readFile(mapFile, 'utf8')) + assert.equal(map.file, 'entry.cjs.map') + assert.ok((map.sources ?? []).some((s: string) => s.endsWith('entry.cjs'))) + assert.ok(String(map.mappings || '').length > 0) +}) + +test('--source-map=inline errors when targeting files', async t => { + const temp = await mkdtemp(join(tmpdir(), 'module-cli-sourcemap-inline-file-')) + const input = join(temp, 'entry.cjs') + await copyFile(fixture, input) + + t.after(() => rm(temp, { recursive: true, force: true })) + + const result = runCli( + [ + '--target', + 'module', + '--source-map=inline', + '--cwd', + temp, + '--out-dir', + 'dist', + 'entry.cjs', + ], + undefined, + { cwd: temp }, + ) + + assert.equal(result.status, 2) + assert.match( + result.stderr, + /Inline source maps are only supported when writing to stdout/, + ) +}) + test('--in-place rewrites files', async t => { const temp = await mkdtemp(join(tmpdir(), 'module-cli-')) const tempFile = join(temp, 'input.cjs') diff --git a/test/module.ts b/test/module.ts index 47121a7..688b1a1 100644 --- a/test/module.ts +++ b/test/module.ts @@ -47,6 +47,25 @@ describe('@knighted/module', () => { return { exportsObj, result } } + it('returns code and source map when requested', async t => { + const temp = await mkdtemp(join(tmpdir(), 'module-sourcemap-')) + const file = join(temp, 'entry.ts') + + await writeFile(file, 'export const answer: number = 41 + 1\n', 'utf8') + + t.after(() => rm(temp, { recursive: true, force: true })) + + const result = await transform(file, { target: 'commonjs', sourceMap: true }) + + assert.ok(typeof result !== 'string', 'expected {code,map} when sourceMap is true') + const { code, map } = result + assert.equal(typeof code, 'string') + assert.ok(map) + assert.equal(map.version, 3) + assert.ok((map.sources ?? []).length > 0) + assert.ok(String(map.mappings || '').length > 0) + }) + 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') From 7b5a96c65a65af2ae5ef52932c07ec2aea6d1248 Mon Sep 17 00:00:00 2001 From: KCM Date: Fri, 2 Jan 2026 21:36:24 -0600 Subject: [PATCH 2/2] refactor: update for v3 source maps, prevent double writes. --- src/cli.ts | 72 +++++++++++++++++++++++++++++++++++++++++------------ test/cli.ts | 2 +- 2 files changed, 57 insertions(+), 17 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index 6caa805..155c967 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -472,16 +472,28 @@ const readStdin = async (stdin: typeof defaultStdin) => { const normalizeSourceMapArgv = (argv: string[]) => { let sourceMapInline = false + let invalidSourceMapValue: string | null = null const normalized: string[] = [] + const recordInvalid = (value: string) => { + if (!invalidSourceMapValue) invalidSourceMapValue = value + } for (let i = 0; i < argv.length; i += 1) { const arg = argv[i] - if (arg === '--source-map' && argv[i + 1] === 'inline') { - sourceMapInline = true - normalized.push('--source-map') - i += 1 - continue + if (arg === '--source-map') { + const next = argv[i + 1] + if (next === 'inline') { + sourceMapInline = true + normalized.push('--source-map') + i += 1 + continue + } + if (next === 'true' || next === 'false') { + normalized.push(`--source-map=${next}`) + i += 1 + continue + } } if (arg.startsWith('--source-map=')) { @@ -491,12 +503,23 @@ const normalizeSourceMapArgv = (argv: string[]) => { normalized.push('--source-map') continue } + if (value === 'true' || value === 'false') { + normalized.push(arg) + continue + } + recordInvalid(value) + continue + } + + if (arg === '--source-map' && argv[i + 1] && argv[i + 1].startsWith('--')) { + normalized.push('--source-map') + continue } normalized.push(arg) } - return { argv: normalized, sourceMapInline } + return { argv: normalized, sourceMapInline, invalidSourceMapValue } } const expandFiles = async (patterns: string[], cwd: string, ignore?: string[]) => { @@ -642,8 +665,11 @@ const runFiles = async ( hazardScope === 'project' ? 'off' : moduleOpts.detectDualPackageHazard, } + const allowWrites = !flags.dryRun && !flags.list + const writeInPlace = allowWrites && flags.inPlace let writeTarget: string | undefined - if (!flags.dryRun && !flags.list) { + + if (allowWrites) { if (flags.inPlace) { perFileOpts.inPlace = true } else if (outPath) { @@ -656,6 +682,11 @@ const runFiles = async ( } } + if (moduleOpts.sourceMap && (writeTarget || writeInPlace)) { + perFileOpts.out = undefined + perFileOpts.inPlace = false + } + const transformed = await transform(file, perFileOpts) const output = typeof transformed === 'string' ? transformed : transformed.code const map = typeof transformed === 'string' ? null : transformed.map @@ -671,25 +702,26 @@ const runFiles = async ( logger.info(file) } - if (map && flags.sourceMapInline && !writeTarget && !perFileOpts.inPlace) { + if (map && flags.sourceMapInline && !writeTarget && !writeInPlace) { const mapUri = Buffer.from(JSON.stringify(map)).toString('base64') finalOutput = `${output.replace(/\/\/# sourceMappingURL=.*/g, '').trimEnd()}\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,${mapUri}\n` - } else if (map && (writeTarget || perFileOpts.inPlace)) { + } else if (map && (writeTarget || writeInPlace)) { const target = writeTarget ?? file const mapPath = `${target}.map` - map.file = basename(mapPath) + const mapFile = basename(mapPath) + map.file = basename(target) - const updated = `${output.replace(/\/\/# sourceMappingURL=.*/g, '').trimEnd()}\n//# sourceMappingURL=${map.file}\n` + const updated = `${output.replace(/\/\/# sourceMappingURL=.*/g, '').trimEnd()}\n//# sourceMappingURL=${mapFile}\n` await writeFile(mapPath, JSON.stringify(map)) if (writeTarget) { await writeFile(writeTarget, updated) - } else if (perFileOpts.inPlace) { + } else if (writeInPlace) { await writeFile(file, updated) } } - if (!flags.dryRun && !flags.list && !writeTarget && !perFileOpts.inPlace) { + if (!flags.dryRun && !flags.list && !writeTarget && !writeInPlace) { io.stdout.write(finalOutput) } @@ -723,7 +755,17 @@ const runCli = async ({ stdout = defaultStdout, stderr = defaultStderr, }: CliOptions = {}) => { - const { argv: normalizedArgv, sourceMapInline } = normalizeSourceMapArgv(argv) + const logger = makeLogger(stdout, stderr) + const { + argv: normalizedArgv, + sourceMapInline, + invalidSourceMapValue, + } = normalizeSourceMapArgv(argv) + + if (invalidSourceMapValue) { + logger.error(`Invalid --source-map value: ${invalidSourceMapValue}`) + return 2 + } const { values, positionals } = parseArgs({ args: normalizedArgv, @@ -739,8 +781,6 @@ const runCli = async ({ ), }) - const logger = makeLogger(stdout, stderr) - if (values.help) { stdout.write(buildHelp(stdout.isTTY ?? false)) return 0 diff --git a/test/cli.ts b/test/cli.ts index e97c4aa..700eacf 100644 --- a/test/cli.ts +++ b/test/cli.ts @@ -736,7 +736,7 @@ test('--source-map writes map files with out-dir', async t => { assert.match(written, /sourceMappingURL=entry.cjs.map/) const map = JSON.parse(await readFile(mapFile, 'utf8')) - assert.equal(map.file, 'entry.cjs.map') + assert.equal(map.file, 'entry.cjs') assert.ok((map.sources ?? []).some((s: string) => s.endsWith('entry.cjs'))) assert.ok(String(map.mappings || '').length > 0) })