@knighted/module 1.4.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -114,6 +114,7 @@ type ModuleOptions = {
114
114
  target: 'module' | 'commonjs'
115
115
  sourceType?: 'auto' | 'module' | 'commonjs'
116
116
  transformSyntax?: boolean | 'globals-only'
117
+ sourceMap?: boolean
117
118
  liveBindings?: 'strict' | 'loose' | 'off'
118
119
  appendJsExtension?: 'off' | 'relative-only' | 'all'
119
120
  appendDirectoryIndex?: string | false
@@ -133,6 +134,7 @@ type ModuleOptions = {
133
134
  detectCircularRequires?: 'off' | 'warn' | 'error'
134
135
  detectDualPackageHazard?: 'off' | 'warn' | 'error'
135
136
  dualPackageHazardScope?: 'file' | 'project'
137
+ dualPackageHazardAllowlist?: string[]
136
138
  requireSource?: 'builtin' | 'create-require'
137
139
  importMetaPrelude?: 'off' | 'auto' | 'on'
138
140
  cjsDefault?: 'module-exports' | 'auto' | 'none'
@@ -161,12 +163,14 @@ type ModuleOptions = {
161
163
  - `detectCircularRequires` (`off`): optionally detect relative static require cycles across `.js`/`.mjs`/`.cjs`/`.ts`/`.mts`/`.cts` (realpath-normalized) and warn/throw.
162
164
  - `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.
163
165
  - `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.
166
+ - `dualPackageHazardAllowlist` (`[]`): suppress dual-package hazard diagnostics for the listed packages. Accepts an array in the API; entries are trimmed and empty values dropped. The CLI flag `--dual-package-hazard-allowlist pkg1,pkg2` parses a comma- or space-separated string into this array. Applies to both `file` and `project` scopes.
164
167
  - `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.
165
168
  - `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.
166
169
  - `requireSource` (`builtin`): whether `require` comes from Node or `createRequire`.
167
170
  - `cjsDefault` (`auto`): bundler-style default interop vs direct `module.exports`.
168
171
  - `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.
169
172
  - `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.
173
+ - `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.
170
174
  - `cwd` (`process.cwd()`): Base directory used to resolve relative `out` paths.
171
175
 
172
176
  > [!NOTE]
package/dist/cjs/cli.cjs CHANGED
@@ -32,12 +32,14 @@ const defaultOptions = {
32
32
  detectCircularRequires: 'off',
33
33
  detectDualPackageHazard: 'warn',
34
34
  dualPackageHazardScope: 'file',
35
+ dualPackageHazardAllowlist: [],
35
36
  requireSource: 'builtin',
36
37
  nestedRequireStrategy: 'create-require',
37
38
  cjsDefault: 'auto',
38
39
  idiomaticExports: 'safe',
39
40
  importMetaPrelude: 'auto',
40
41
  topLevelAwait: 'error',
42
+ sourceMap: false,
41
43
  cwd: undefined,
42
44
  out: undefined,
43
45
  inPlace: false
@@ -163,6 +165,11 @@ const optionsTable = [{
163
165
  short: undefined,
164
166
  type: 'string',
165
167
  desc: 'Scope for dual package hazard detection (file|project)'
168
+ }, {
169
+ long: 'dual-package-hazard-allowlist',
170
+ short: undefined,
171
+ type: 'string',
172
+ desc: 'Comma-separated packages to ignore for dual package hazard checks'
166
173
  }, {
167
174
  long: 'top-level-await',
168
175
  short: 'a',
@@ -183,6 +190,11 @@ const optionsTable = [{
183
190
  short: 'm',
184
191
  type: 'string',
185
192
  desc: 'Emit import.meta prelude (off|auto|on)'
193
+ }, {
194
+ long: 'source-map',
195
+ short: undefined,
196
+ type: 'boolean',
197
+ desc: 'Emit a source map alongside transformed output (use --source-map=inline for stdout)'
186
198
  }, {
187
199
  long: 'nested-require-strategy',
188
200
  short: 'n',
@@ -281,12 +293,17 @@ const parseAppendDirectoryIndex = value => {
281
293
  if (value === 'false') return false;
282
294
  return value;
283
295
  };
296
+ const parseAllowlist = value => {
297
+ const values = value === undefined ? [] : Array.isArray(value) ? value : [value];
298
+ return values.flatMap(entry => String(entry).split(',')).map(item => item.trim()).filter(Boolean);
299
+ };
284
300
  const toModuleOptions = values => {
285
301
  const target = parseEnum(values.target, ['module', 'commonjs']) ?? defaultOptions.target;
286
302
  const transformSyntax = parseTransformSyntax(values['transform-syntax']);
287
303
  const rewriteTemplateLiterals = parseEnum(values['rewrite-template-literals'], ['allow', 'static-only']) ?? defaultOptions.rewriteTemplateLiterals;
288
304
  const appendJsExtension = parseEnum(values['append-js-extension'], ['off', 'relative-only', 'all']);
289
305
  const appendDirectoryIndex = parseAppendDirectoryIndex(values['append-directory-index']);
306
+ const dualPackageHazardAllowlist = parseAllowlist(values['dual-package-hazard-allowlist']);
290
307
  const opts = {
291
308
  ...defaultOptions,
292
309
  target,
@@ -298,6 +315,7 @@ const toModuleOptions = values => {
298
315
  detectCircularRequires: parseEnum(values['detect-circular-requires'], ['off', 'warn', 'error']) ?? defaultOptions.detectCircularRequires,
299
316
  detectDualPackageHazard: parseEnum(values['detect-dual-package-hazard'], ['off', 'warn', 'error']) ?? defaultOptions.detectDualPackageHazard,
300
317
  dualPackageHazardScope: parseEnum(values['dual-package-hazard-scope'], ['file', 'project']) ?? defaultOptions.dualPackageHazardScope,
318
+ dualPackageHazardAllowlist,
301
319
  topLevelAwait: parseEnum(values['top-level-await'], ['error', 'wrap', 'preserve']) ?? defaultOptions.topLevelAwait,
302
320
  cjsDefault: parseEnum(values['cjs-default'], ['module-exports', 'auto', 'none']) ?? defaultOptions.cjsDefault,
303
321
  idiomaticExports: parseEnum(values['idiomatic-exports'], ['off', 'safe', 'aggressive']) ?? defaultOptions.idiomaticExports,
@@ -305,6 +323,7 @@ const toModuleOptions = values => {
305
323
  nestedRequireStrategy: parseEnum(values['nested-require-strategy'], ['create-require', 'dynamic-import']) ?? defaultOptions.nestedRequireStrategy,
306
324
  requireMainStrategy: parseEnum(values['require-main-strategy'], ['import-meta-main', 'realpath']) ?? defaultOptions.requireMainStrategy,
307
325
  liveBindings: parseEnum(values['live-bindings'], ['strict', 'loose', 'off']) ?? defaultOptions.liveBindings,
326
+ sourceMap: Boolean(values['source-map']),
308
327
  cwd: values.cwd ? (0, _nodePath.resolve)(String(values.cwd)) : defaultOptions.cwd
309
328
  };
310
329
  return opts;
@@ -316,6 +335,55 @@ const readStdin = async stdin => {
316
335
  }
317
336
  return Buffer.concat(chunks).toString('utf8');
318
337
  };
338
+ const normalizeSourceMapArgv = argv => {
339
+ let sourceMapInline = false;
340
+ let invalidSourceMapValue = null;
341
+ const normalized = [];
342
+ const recordInvalid = value => {
343
+ if (!invalidSourceMapValue) invalidSourceMapValue = value;
344
+ };
345
+ for (let i = 0; i < argv.length; i += 1) {
346
+ const arg = argv[i];
347
+ if (arg === '--source-map') {
348
+ const next = argv[i + 1];
349
+ if (next === 'inline') {
350
+ sourceMapInline = true;
351
+ normalized.push('--source-map');
352
+ i += 1;
353
+ continue;
354
+ }
355
+ if (next === 'true' || next === 'false') {
356
+ normalized.push(`--source-map=${next}`);
357
+ i += 1;
358
+ continue;
359
+ }
360
+ }
361
+ if (arg.startsWith('--source-map=')) {
362
+ const value = arg.slice('--source-map='.length);
363
+ if (value === 'inline') {
364
+ sourceMapInline = true;
365
+ normalized.push('--source-map');
366
+ continue;
367
+ }
368
+ if (value === 'true' || value === 'false') {
369
+ normalized.push(arg);
370
+ continue;
371
+ }
372
+ recordInvalid(value);
373
+ continue;
374
+ }
375
+ if (arg === '--source-map' && argv[i + 1] && argv[i + 1].startsWith('--')) {
376
+ normalized.push('--source-map');
377
+ continue;
378
+ }
379
+ normalized.push(arg);
380
+ }
381
+ return {
382
+ argv: normalized,
383
+ sourceMapInline,
384
+ invalidSourceMapValue
385
+ };
386
+ };
319
387
  const expandFiles = async (patterns, cwd, ignore) => {
320
388
  const files = new Set();
321
389
  for (const pattern of patterns) {
@@ -403,8 +471,10 @@ const runFiles = async (files, moduleOpts, io, flags) => {
403
471
  filePath: file,
404
472
  detectDualPackageHazard: hazardScope === 'project' ? 'off' : moduleOpts.detectDualPackageHazard
405
473
  };
474
+ const allowWrites = !flags.dryRun && !flags.list;
475
+ const writeInPlace = allowWrites && flags.inPlace;
406
476
  let writeTarget;
407
- if (!flags.dryRun && !flags.list) {
477
+ if (allowWrites) {
408
478
  if (flags.inPlace) {
409
479
  perFileOpts.inPlace = true;
410
480
  } else if (outPath) {
@@ -421,8 +491,15 @@ const runFiles = async (files, moduleOpts, io, flags) => {
421
491
  };
422
492
  }
423
493
  }
424
- const output = await (0, _module.transform)(file, perFileOpts);
494
+ if (moduleOpts.sourceMap && (writeTarget || writeInPlace)) {
495
+ perFileOpts.out = undefined;
496
+ perFileOpts.inPlace = false;
497
+ }
498
+ const transformed = await (0, _module.transform)(file, perFileOpts);
499
+ const output = typeof transformed === 'string' ? transformed : transformed.code;
500
+ const map = typeof transformed === 'string' ? null : transformed.map;
425
501
  const changed = output !== original;
502
+ let finalOutput = output;
426
503
  if (projectHazards) {
427
504
  const extras = projectHazards.get(file);
428
505
  if (extras?.length) diagnostics.push(...extras);
@@ -430,8 +507,24 @@ const runFiles = async (files, moduleOpts, io, flags) => {
430
507
  if (flags.list && changed) {
431
508
  logger.info(file);
432
509
  }
433
- if (!flags.dryRun && !flags.list && !writeTarget && !perFileOpts.inPlace) {
434
- io.stdout.write(output);
510
+ if (map && flags.sourceMapInline && !writeTarget && !writeInPlace) {
511
+ const mapUri = Buffer.from(JSON.stringify(map)).toString('base64');
512
+ finalOutput = `${output.replace(/\/\/# sourceMappingURL=.*/g, '').trimEnd()}\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,${mapUri}\n`;
513
+ } else if (map && (writeTarget || writeInPlace)) {
514
+ const target = writeTarget ?? file;
515
+ const mapPath = `${target}.map`;
516
+ const mapFile = (0, _nodePath.basename)(mapPath);
517
+ map.file = (0, _nodePath.basename)(target);
518
+ const updated = `${output.replace(/\/\/# sourceMappingURL=.*/g, '').trimEnd()}\n//# sourceMappingURL=${mapFile}\n`;
519
+ await (0, _promises.writeFile)(mapPath, JSON.stringify(map));
520
+ if (writeTarget) {
521
+ await (0, _promises.writeFile)(writeTarget, updated);
522
+ } else if (writeInPlace) {
523
+ await (0, _promises.writeFile)(file, updated);
524
+ }
525
+ }
526
+ if (!flags.dryRun && !flags.list && !writeTarget && !writeInPlace) {
527
+ io.stdout.write(finalOutput);
435
528
  }
436
529
  results.push({
437
530
  filePath: file,
@@ -468,11 +561,21 @@ const runCli = async ({
468
561
  stdout = _nodeProcess.stdout,
469
562
  stderr = _nodeProcess.stderr
470
563
  } = {}) => {
564
+ const logger = makeLogger(stdout, stderr);
565
+ const {
566
+ argv: normalizedArgv,
567
+ sourceMapInline,
568
+ invalidSourceMapValue
569
+ } = normalizeSourceMapArgv(argv);
570
+ if (invalidSourceMapValue) {
571
+ logger.error(`Invalid --source-map value: ${invalidSourceMapValue}`);
572
+ return 2;
573
+ }
471
574
  const {
472
575
  values,
473
576
  positionals
474
577
  } = (0, _nodeUtil.parseArgs)({
475
- args: argv,
578
+ args: normalizedArgv,
476
579
  allowPositionals: true,
477
580
  options: Object.fromEntries(optionsTable.map(opt => [opt.long, {
478
581
  type: opt.type,
@@ -481,7 +584,6 @@ const runCli = async ({
481
584
  } : {})
482
585
  }]))
483
586
  });
484
- const logger = makeLogger(stdout, stderr);
485
587
  if (values.help) {
486
588
  stdout.write(buildHelp(stdout.isTTY ?? false));
487
589
  return 0;
@@ -492,6 +594,7 @@ const runCli = async ({
492
594
  return 0;
493
595
  }
494
596
  const moduleOpts = toModuleOptions(values);
597
+ if (sourceMapInline) moduleOpts.sourceMap = true;
495
598
  const cwd = moduleOpts.cwd ?? process.cwd();
496
599
  const allowStdout = positionals.length <= 1;
497
600
  const fromStdin = positionals.length === 0 || positionals.includes('-');
@@ -504,6 +607,10 @@ const runCli = async ({
504
607
  const list = Boolean(values.list);
505
608
  const summary = Boolean(values.summary);
506
609
  const json = Boolean(values.json);
610
+ if (sourceMapInline && (outDir || inPlace)) {
611
+ logger.error('Inline source maps are only supported when writing to stdout');
612
+ return 2;
613
+ }
507
614
  if (outDir && inPlace) {
508
615
  logger.error('Choose either --out-dir or --in-place, not both');
509
616
  return 2;
@@ -558,7 +665,8 @@ const runCli = async ({
558
665
  json,
559
666
  outDir,
560
667
  inPlace,
561
- allowStdout
668
+ allowStdout,
669
+ sourceMapInline
562
670
  });
563
671
  if (typeof result.code === 'number' && result.code !== 0) return result.code;
564
672
  tasks.push(...result.results);
@@ -3,7 +3,8 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
- exports.format = exports.dualPackageHazardDiagnostics = exports.collectDualPackageUsage = void 0;
6
+ exports.dualPackageHazardDiagnostics = exports.collectDualPackageUsage = void 0;
7
+ exports.format = format;
7
8
  var _nodePath = require("node:path");
8
9
  var _promises = require("node:fs/promises");
9
10
  var _magicString = _interopRequireDefault(require("magic-string"));
@@ -148,6 +149,9 @@ const describeDualPackage = pkgJson => {
148
149
  requireTarget
149
150
  };
150
151
  };
152
+ const normalizeAllowlist = allowlist => {
153
+ return new Set([...(allowlist ?? [])].map(item => item.trim()).filter(item => item.length > 0));
154
+ };
151
155
  const recordUsage = (usages, pkg, kind, spec, subpath, loc, filePath) => {
152
156
  const existing = usages.get(pkg) ?? {
153
157
  imports: [],
@@ -217,8 +221,10 @@ const dualPackageHazardDiagnostics = async params => {
217
221
  cwd
218
222
  } = params;
219
223
  const manifestCache = params.manifestCache ?? new Map();
224
+ const allowlist = normalizeAllowlist(params.hazardAllowlist);
220
225
  const diags = [];
221
226
  for (const [pkg, usage] of usages) {
227
+ if (allowlist.has(pkg)) continue;
222
228
  const hasImport = usage.imports.length > 0;
223
229
  const hasRequire = usage.requires.length > 0;
224
230
  const combined = [...usage.imports, ...usage.requires];
@@ -285,19 +291,14 @@ const detectDualPackageHazards = async params => {
285
291
  hazardLevel,
286
292
  filePath,
287
293
  cwd,
288
- manifestCache
294
+ manifestCache,
295
+ hazardAllowlist: params.hazardAllowlist
289
296
  });
290
297
  for (const diag of diags) {
291
298
  diagOnce(diag.level, diag.code, diag.message, diag.loc);
292
299
  }
293
300
  };
294
-
295
- /**
296
- * Node added support for import.meta.main.
297
- * Added in: v24.2.0, v22.18.0
298
- * @see https://nodejs.org/api/esm.html#importmetamain
299
- */
300
- const format = async (src, ast, opts) => {
301
+ async function format(src, ast, opts) {
301
302
  const code = new _magicString.default(src);
302
303
  const exportsMeta = {
303
304
  hasExportsBeenReassigned: false,
@@ -346,7 +347,8 @@ const format = async (src, ast, opts) => {
346
347
  hazardLevel,
347
348
  filePath: opts.filePath,
348
349
  cwd: opts.cwd,
349
- diagOnce
350
+ diagOnce,
351
+ hazardAllowlist: opts.dualPackageHazardAllowlist
350
352
  });
351
353
  }
352
354
  if (opts.target === 'module' && fullTransform) {
@@ -522,15 +524,15 @@ const format = async (src, ast, opts) => {
522
524
  code.prepend(prelude);
523
525
  }
524
526
  if (opts.target === 'commonjs' && fullTransform && containsTopLevelAwait) {
525
- const body = code.toString();
526
527
  if (opts.topLevelAwait === 'wrap') {
527
- const tlaPromise = `const __tla = (async () => {\n${body}\nreturn module.exports;\n})();\n`;
528
- 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`;
529
- const attach = `__setTla(module.exports);\n__tla.then(resolved => __setTla(resolved), err => { throw err; });\n`;
530
- return `${tlaPromise}${setPromise}${attach}`;
528
+ code.prepend('const __tla = (async () => {\n');
529
+ code.append('\nreturn module.exports;\n})();\n');
530
+ 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');
531
+ code.append('__setTla(module.exports);\n__tla.then(resolved => __setTla(resolved), err => { throw err; });\n');
532
+ } else {
533
+ code.prepend(';(async () => {\n');
534
+ code.append('\n})();\n');
531
535
  }
532
- return `;(async () => {\n${body}\n})();\n`;
533
536
  }
534
- return code.toString();
535
- };
536
- exports.format = format;
537
+ return opts.sourceMap ? code : code.toString();
538
+ }
@@ -1,4 +1,5 @@
1
1
  import type { Node, ParseResult } from 'oxc-parser';
2
+ import MagicString from 'magic-string';
2
3
  import type { Diagnostic, FormatterOptions } from './types.cjs';
3
4
  type HazardLevel = 'warning' | 'error';
4
5
  export type PackageUse = {
@@ -21,11 +22,10 @@ declare const dualPackageHazardDiagnostics: (params: {
21
22
  filePath?: string;
22
23
  cwd?: string;
23
24
  manifestCache?: Map<string, any | null>;
25
+ hazardAllowlist?: Iterable<string>;
24
26
  }) => Promise<Diagnostic[]>;
25
- /**
26
- * Node added support for import.meta.main.
27
- * Added in: v24.2.0, v22.18.0
28
- * @see https://nodejs.org/api/esm.html#importmetamain
29
- */
30
- declare const format: (src: string, ast: ParseResult, opts: FormatterOptions) => Promise<string>;
27
+ declare function format(src: string, ast: ParseResult, opts: FormatterOptions & {
28
+ sourceMap: true;
29
+ }): Promise<MagicString>;
30
+ declare function format(src: string, ast: ParseResult, opts: FormatterOptions): Promise<string>;
31
31
  export { format, collectDualPackageUsage, dualPackageHazardDiagnostics };
@@ -3,7 +3,8 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
- exports.transform = exports.collectProjectDualPackageHazards = void 0;
6
+ exports.collectProjectDualPackageHazards = void 0;
7
+ exports.transform = transform;
7
8
  var _nodePath = require("node:path");
8
9
  var _promises = require("node:fs/promises");
9
10
  var _specifier = require("./specifier.cjs");
@@ -170,8 +171,10 @@ const collectProjectDualPackageHazards = async (files, opts) => {
170
171
  const diags = await (0, _format.dualPackageHazardDiagnostics)({
171
172
  usages,
172
173
  hazardLevel,
174
+ filePath: opts.filePath,
173
175
  cwd: opts.cwd,
174
- manifestCache
176
+ manifestCache,
177
+ hazardAllowlist: opts.dualPackageHazardAllowlist
175
178
  });
176
179
  const byFile = new Map();
177
180
  for (const diag of diags) {
@@ -199,17 +202,19 @@ const createDefaultOptions = () => ({
199
202
  detectCircularRequires: 'off',
200
203
  detectDualPackageHazard: 'warn',
201
204
  dualPackageHazardScope: 'file',
205
+ dualPackageHazardAllowlist: [],
202
206
  requireSource: 'builtin',
203
207
  nestedRequireStrategy: 'create-require',
204
208
  cjsDefault: 'auto',
205
209
  idiomaticExports: 'safe',
206
210
  importMetaPrelude: 'auto',
207
211
  topLevelAwait: 'error',
212
+ sourceMap: false,
208
213
  cwd: undefined,
209
214
  out: undefined,
210
215
  inPlace: false
211
216
  });
212
- const transform = async (filename, options) => {
217
+ async function transform(filename, options) {
213
218
  const base = createDefaultOptions();
214
219
  const opts = options ? {
215
220
  ...base,
@@ -226,9 +231,19 @@ const transform = async (filename, options) => {
226
231
  const file = (0, _nodePath.resolve)(cwdBase, filename);
227
232
  const code = (await (0, _promises.readFile)(file)).toString();
228
233
  const ast = (0, _parse.parse)(filename, code);
229
- let source = await (0, _format.format)(code, ast, opts);
234
+ let sourceCode = null;
235
+ let source;
236
+ if (opts.sourceMap) {
237
+ sourceCode = await (0, _format.format)(code, ast, {
238
+ ...opts,
239
+ sourceMap: true
240
+ });
241
+ source = sourceCode.toString();
242
+ } else {
243
+ source = await (0, _format.format)(code, ast, opts);
244
+ }
230
245
  if (opts.rewriteSpecifier || appendMode !== 'off' || dirIndex) {
231
- const code = await _specifier.specifier.updateSrc(source, (0, _lang.getLangFromExt)(filename), spec => {
246
+ const applyRewrite = spec => {
232
247
  if (spec.type === 'TemplateLiteral' && opts.rewriteTemplateLiterals === 'static-only') {
233
248
  const node = spec.node;
234
249
  if (node.expressions.length > 0) return;
@@ -238,8 +253,14 @@ const transform = async (filename, options) => {
238
253
  const baseValue = rewritten ?? normalized ?? spec.value;
239
254
  const appended = appendExtensionIfNeeded(spec, appendMode, dirIndex, baseValue);
240
255
  return appended ?? rewritten ?? normalized ?? undefined;
241
- });
242
- source = code;
256
+ };
257
+ if (opts.sourceMap && sourceCode) {
258
+ await _specifier.specifier.updateMagicString(sourceCode, code, ast, applyRewrite);
259
+ source = sourceCode.toString();
260
+ } else {
261
+ const rewritten = await _specifier.specifier.updateSrc(source, (0, _lang.getLangFromExt)(filename), applyRewrite);
262
+ source = rewritten;
263
+ }
243
264
  }
244
265
  if (detectCycles !== 'off' && opts.target === 'module' && opts.transformSyntax) {
245
266
  await detectCircularRequireGraph(file, detectCycles, dirIndex || 'index.js');
@@ -248,6 +269,17 @@ const transform = async (filename, options) => {
248
269
  if (outputPath) {
249
270
  await (0, _promises.writeFile)(outputPath, source);
250
271
  }
272
+ if (opts.sourceMap && sourceCode) {
273
+ const map = sourceCode.generateMap({
274
+ hires: true,
275
+ includeContent: true,
276
+ file: outputPath ?? filename,
277
+ source: opts.filePath ?? filename
278
+ });
279
+ return {
280
+ code: source,
281
+ map
282
+ };
283
+ }
251
284
  return source;
252
- };
253
- exports.transform = transform;
285
+ }
@@ -1,4 +1,17 @@
1
1
  import type { ModuleOptions, Diagnostic } from './types.cjs';
2
+ import type { SourceMap } from 'magic-string';
2
3
  declare const collectProjectDualPackageHazards: (files: string[], opts: ModuleOptions) => Promise<Map<string, Diagnostic[]>>;
3
- declare const transform: (filename: string, options?: ModuleOptions) => Promise<string>;
4
+ declare function transform(filename: string, options: ModuleOptions & {
5
+ sourceMap: true;
6
+ }): Promise<{
7
+ code: string;
8
+ map: SourceMap;
9
+ }>;
10
+ declare function transform(filename: string, options?: ModuleOptions & {
11
+ sourceMap?: false | undefined;
12
+ }): Promise<string>;
13
+ declare function transform(filename: string, options?: ModuleOptions): Promise<string | {
14
+ code: string;
15
+ map: SourceMap;
16
+ }>;
4
17
  export { transform, collectProjectDualPackageHazards };
@@ -20,8 +20,7 @@ const isBinaryExpression = node => {
20
20
  const isCallExpression = node => {
21
21
  return node.type === 'CallExpression' && node.callee !== undefined;
22
22
  };
23
- const formatSpecifiers = async (src, ast, cb) => {
24
- const code = new _magicString.default(src);
23
+ const formatSpecifiers = async (src, ast, cb, code = new _magicString.default(src)) => {
25
24
  const formatExpression = expression => {
26
25
  const node = isCallExpression(expression) ? expression.arguments[0] : expression.source;
27
26
  const {
@@ -235,7 +234,7 @@ const formatSpecifiers = async (src, ast, cb) => {
235
234
  }
236
235
  }
237
236
  });
238
- return code.toString();
237
+ return code;
239
238
  };
240
239
  const isValidFilename = async filename => {
241
240
  let stats;
@@ -258,11 +257,17 @@ const specifier = exports.specifier = {
258
257
  }
259
258
  const src = (await (0, _promises.readFile)(filename)).toString();
260
259
  const ast = (0, _oxcParser.parseSync)(filename, src);
261
- return await formatSpecifiers(src, ast, callback);
260
+ const code = await formatSpecifiers(src, ast, callback);
261
+ return code.toString();
262
262
  },
263
263
  async updateSrc(src, lang, callback) {
264
264
  const filename = lang === 'ts' ? 'file.ts' : lang === 'tsx' ? 'file.tsx' : lang === 'js' ? 'file.js' : 'file.jsx';
265
265
  const ast = (0, _oxcParser.parseSync)(filename, src);
266
- return await formatSpecifiers(src, ast, callback);
266
+ const code = await formatSpecifiers(src, ast, callback);
267
+ return code.toString();
268
+ },
269
+ async updateMagicString(code, src, ast, callback) {
270
+ await formatSpecifiers(src, ast, callback, code);
271
+ return code;
267
272
  }
268
273
  };
@@ -1,4 +1,5 @@
1
- import type { ParserOptions, StringLiteral, TemplateLiteral, BinaryExpression, NewExpression, ImportDeclaration, ExportNamedDeclaration, ExportAllDeclaration, TSImportType, ImportExpression, CallExpression } from 'oxc-parser';
1
+ import MagicString from 'magic-string';
2
+ import type { ParserOptions, ParseResult, StringLiteral, TemplateLiteral, BinaryExpression, NewExpression, ImportDeclaration, ExportNamedDeclaration, ExportAllDeclaration, TSImportType, ImportExpression, CallExpression } from 'oxc-parser';
2
3
  type Spec = {
3
4
  type: 'StringLiteral' | 'TemplateLiteral' | 'BinaryExpression' | 'NewExpression';
4
5
  node: StringLiteral | TemplateLiteral | BinaryExpression | NewExpression;
@@ -11,6 +12,7 @@ type Callback = (spec: Spec) => string | void;
11
12
  declare const specifier: {
12
13
  update(path: string, callback: Callback): Promise<string>;
13
14
  updateSrc(src: string, lang: ParserOptions["lang"], callback: Callback): Promise<string>;
15
+ updateMagicString(code: MagicString, src: string, ast: ParseResult, callback: Callback): Promise<MagicString>;
14
16
  };
15
17
  export { specifier };
16
18
  export type { Spec, Callback };
@@ -6,6 +6,8 @@ export type ModuleOptions = {
6
6
  target: 'module' | 'commonjs';
7
7
  /** Explicit source type; auto infers from file extension. */
8
8
  sourceType?: 'auto' | 'module' | 'commonjs';
9
+ /** Emit a source map alongside the transformed code. */
10
+ sourceMap?: boolean;
9
11
  /**
10
12
  * Enable syntax transforms beyond parsing.
11
13
  * - true: full CJS↔ESM lowering/raising
@@ -38,6 +40,8 @@ export type ModuleOptions = {
38
40
  detectDualPackageHazard?: 'off' | 'warn' | 'error';
39
41
  /** Scope for dual package hazard detection. */
40
42
  dualPackageHazardScope?: 'file' | 'project';
43
+ /** Packages to ignore for dual package hazard diagnostics. */
44
+ dualPackageHazardAllowlist?: string[];
41
45
  /** Source used to provide require in ESM output. */
42
46
  requireSource?: 'builtin' | 'create-require';
43
47
  /** How to rewrite nested or non-hoistable require calls. */
package/dist/cli.js CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import { stdin as defaultStdin, stdout as defaultStdout, stderr as defaultStderr } from 'node:process';
3
3
  import { parseArgs } from 'node:util';
4
- import { readFile, mkdir } from 'node:fs/promises';
5
- import { dirname, resolve, relative, join } from 'node:path';
4
+ import { readFile, mkdir, writeFile } from 'node:fs/promises';
5
+ import { dirname, resolve, relative, join, basename } from 'node:path';
6
6
  import { glob } from 'glob';
7
7
  import { transform, collectProjectDualPackageHazards } from './module.js';
8
8
  import { parse } from './parse.js';
@@ -26,12 +26,14 @@ const defaultOptions = {
26
26
  detectCircularRequires: 'off',
27
27
  detectDualPackageHazard: 'warn',
28
28
  dualPackageHazardScope: 'file',
29
+ dualPackageHazardAllowlist: [],
29
30
  requireSource: 'builtin',
30
31
  nestedRequireStrategy: 'create-require',
31
32
  cjsDefault: 'auto',
32
33
  idiomaticExports: 'safe',
33
34
  importMetaPrelude: 'auto',
34
35
  topLevelAwait: 'error',
36
+ sourceMap: false,
35
37
  cwd: undefined,
36
38
  out: undefined,
37
39
  inPlace: false
@@ -157,6 +159,11 @@ const optionsTable = [{
157
159
  short: undefined,
158
160
  type: 'string',
159
161
  desc: 'Scope for dual package hazard detection (file|project)'
162
+ }, {
163
+ long: 'dual-package-hazard-allowlist',
164
+ short: undefined,
165
+ type: 'string',
166
+ desc: 'Comma-separated packages to ignore for dual package hazard checks'
160
167
  }, {
161
168
  long: 'top-level-await',
162
169
  short: 'a',
@@ -177,6 +184,11 @@ const optionsTable = [{
177
184
  short: 'm',
178
185
  type: 'string',
179
186
  desc: 'Emit import.meta prelude (off|auto|on)'
187
+ }, {
188
+ long: 'source-map',
189
+ short: undefined,
190
+ type: 'boolean',
191
+ desc: 'Emit a source map alongside transformed output (use --source-map=inline for stdout)'
180
192
  }, {
181
193
  long: 'nested-require-strategy',
182
194
  short: 'n',
@@ -275,12 +287,17 @@ const parseAppendDirectoryIndex = value => {
275
287
  if (value === 'false') return false;
276
288
  return value;
277
289
  };
290
+ const parseAllowlist = value => {
291
+ const values = value === undefined ? [] : Array.isArray(value) ? value : [value];
292
+ return values.flatMap(entry => String(entry).split(',')).map(item => item.trim()).filter(Boolean);
293
+ };
278
294
  const toModuleOptions = values => {
279
295
  const target = parseEnum(values.target, ['module', 'commonjs']) ?? defaultOptions.target;
280
296
  const transformSyntax = parseTransformSyntax(values['transform-syntax']);
281
297
  const rewriteTemplateLiterals = parseEnum(values['rewrite-template-literals'], ['allow', 'static-only']) ?? defaultOptions.rewriteTemplateLiterals;
282
298
  const appendJsExtension = parseEnum(values['append-js-extension'], ['off', 'relative-only', 'all']);
283
299
  const appendDirectoryIndex = parseAppendDirectoryIndex(values['append-directory-index']);
300
+ const dualPackageHazardAllowlist = parseAllowlist(values['dual-package-hazard-allowlist']);
284
301
  const opts = {
285
302
  ...defaultOptions,
286
303
  target,
@@ -292,6 +309,7 @@ const toModuleOptions = values => {
292
309
  detectCircularRequires: parseEnum(values['detect-circular-requires'], ['off', 'warn', 'error']) ?? defaultOptions.detectCircularRequires,
293
310
  detectDualPackageHazard: parseEnum(values['detect-dual-package-hazard'], ['off', 'warn', 'error']) ?? defaultOptions.detectDualPackageHazard,
294
311
  dualPackageHazardScope: parseEnum(values['dual-package-hazard-scope'], ['file', 'project']) ?? defaultOptions.dualPackageHazardScope,
312
+ dualPackageHazardAllowlist,
295
313
  topLevelAwait: parseEnum(values['top-level-await'], ['error', 'wrap', 'preserve']) ?? defaultOptions.topLevelAwait,
296
314
  cjsDefault: parseEnum(values['cjs-default'], ['module-exports', 'auto', 'none']) ?? defaultOptions.cjsDefault,
297
315
  idiomaticExports: parseEnum(values['idiomatic-exports'], ['off', 'safe', 'aggressive']) ?? defaultOptions.idiomaticExports,
@@ -299,6 +317,7 @@ const toModuleOptions = values => {
299
317
  nestedRequireStrategy: parseEnum(values['nested-require-strategy'], ['create-require', 'dynamic-import']) ?? defaultOptions.nestedRequireStrategy,
300
318
  requireMainStrategy: parseEnum(values['require-main-strategy'], ['import-meta-main', 'realpath']) ?? defaultOptions.requireMainStrategy,
301
319
  liveBindings: parseEnum(values['live-bindings'], ['strict', 'loose', 'off']) ?? defaultOptions.liveBindings,
320
+ sourceMap: Boolean(values['source-map']),
302
321
  cwd: values.cwd ? resolve(String(values.cwd)) : defaultOptions.cwd
303
322
  };
304
323
  return opts;
@@ -310,6 +329,55 @@ const readStdin = async stdin => {
310
329
  }
311
330
  return Buffer.concat(chunks).toString('utf8');
312
331
  };
332
+ const normalizeSourceMapArgv = argv => {
333
+ let sourceMapInline = false;
334
+ let invalidSourceMapValue = null;
335
+ const normalized = [];
336
+ const recordInvalid = value => {
337
+ if (!invalidSourceMapValue) invalidSourceMapValue = value;
338
+ };
339
+ for (let i = 0; i < argv.length; i += 1) {
340
+ const arg = argv[i];
341
+ if (arg === '--source-map') {
342
+ const next = argv[i + 1];
343
+ if (next === 'inline') {
344
+ sourceMapInline = true;
345
+ normalized.push('--source-map');
346
+ i += 1;
347
+ continue;
348
+ }
349
+ if (next === 'true' || next === 'false') {
350
+ normalized.push(`--source-map=${next}`);
351
+ i += 1;
352
+ continue;
353
+ }
354
+ }
355
+ if (arg.startsWith('--source-map=')) {
356
+ const value = arg.slice('--source-map='.length);
357
+ if (value === 'inline') {
358
+ sourceMapInline = true;
359
+ normalized.push('--source-map');
360
+ continue;
361
+ }
362
+ if (value === 'true' || value === 'false') {
363
+ normalized.push(arg);
364
+ continue;
365
+ }
366
+ recordInvalid(value);
367
+ continue;
368
+ }
369
+ if (arg === '--source-map' && argv[i + 1] && argv[i + 1].startsWith('--')) {
370
+ normalized.push('--source-map');
371
+ continue;
372
+ }
373
+ normalized.push(arg);
374
+ }
375
+ return {
376
+ argv: normalized,
377
+ sourceMapInline,
378
+ invalidSourceMapValue
379
+ };
380
+ };
313
381
  const expandFiles = async (patterns, cwd, ignore) => {
314
382
  const files = new Set();
315
383
  for (const pattern of patterns) {
@@ -397,8 +465,10 @@ const runFiles = async (files, moduleOpts, io, flags) => {
397
465
  filePath: file,
398
466
  detectDualPackageHazard: hazardScope === 'project' ? 'off' : moduleOpts.detectDualPackageHazard
399
467
  };
468
+ const allowWrites = !flags.dryRun && !flags.list;
469
+ const writeInPlace = allowWrites && flags.inPlace;
400
470
  let writeTarget;
401
- if (!flags.dryRun && !flags.list) {
471
+ if (allowWrites) {
402
472
  if (flags.inPlace) {
403
473
  perFileOpts.inPlace = true;
404
474
  } else if (outPath) {
@@ -415,8 +485,15 @@ const runFiles = async (files, moduleOpts, io, flags) => {
415
485
  };
416
486
  }
417
487
  }
418
- const output = await transform(file, perFileOpts);
488
+ if (moduleOpts.sourceMap && (writeTarget || writeInPlace)) {
489
+ perFileOpts.out = undefined;
490
+ perFileOpts.inPlace = false;
491
+ }
492
+ const transformed = await transform(file, perFileOpts);
493
+ const output = typeof transformed === 'string' ? transformed : transformed.code;
494
+ const map = typeof transformed === 'string' ? null : transformed.map;
419
495
  const changed = output !== original;
496
+ let finalOutput = output;
420
497
  if (projectHazards) {
421
498
  const extras = projectHazards.get(file);
422
499
  if (extras?.length) diagnostics.push(...extras);
@@ -424,8 +501,24 @@ const runFiles = async (files, moduleOpts, io, flags) => {
424
501
  if (flags.list && changed) {
425
502
  logger.info(file);
426
503
  }
427
- if (!flags.dryRun && !flags.list && !writeTarget && !perFileOpts.inPlace) {
428
- io.stdout.write(output);
504
+ if (map && flags.sourceMapInline && !writeTarget && !writeInPlace) {
505
+ const mapUri = Buffer.from(JSON.stringify(map)).toString('base64');
506
+ finalOutput = `${output.replace(/\/\/# sourceMappingURL=.*/g, '').trimEnd()}\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,${mapUri}\n`;
507
+ } else if (map && (writeTarget || writeInPlace)) {
508
+ const target = writeTarget ?? file;
509
+ const mapPath = `${target}.map`;
510
+ const mapFile = basename(mapPath);
511
+ map.file = basename(target);
512
+ const updated = `${output.replace(/\/\/# sourceMappingURL=.*/g, '').trimEnd()}\n//# sourceMappingURL=${mapFile}\n`;
513
+ await writeFile(mapPath, JSON.stringify(map));
514
+ if (writeTarget) {
515
+ await writeFile(writeTarget, updated);
516
+ } else if (writeInPlace) {
517
+ await writeFile(file, updated);
518
+ }
519
+ }
520
+ if (!flags.dryRun && !flags.list && !writeTarget && !writeInPlace) {
521
+ io.stdout.write(finalOutput);
429
522
  }
430
523
  results.push({
431
524
  filePath: file,
@@ -462,11 +555,21 @@ const runCli = async ({
462
555
  stdout = defaultStdout,
463
556
  stderr = defaultStderr
464
557
  } = {}) => {
558
+ const logger = makeLogger(stdout, stderr);
559
+ const {
560
+ argv: normalizedArgv,
561
+ sourceMapInline,
562
+ invalidSourceMapValue
563
+ } = normalizeSourceMapArgv(argv);
564
+ if (invalidSourceMapValue) {
565
+ logger.error(`Invalid --source-map value: ${invalidSourceMapValue}`);
566
+ return 2;
567
+ }
465
568
  const {
466
569
  values,
467
570
  positionals
468
571
  } = parseArgs({
469
- args: argv,
572
+ args: normalizedArgv,
470
573
  allowPositionals: true,
471
574
  options: Object.fromEntries(optionsTable.map(opt => [opt.long, {
472
575
  type: opt.type,
@@ -475,7 +578,6 @@ const runCli = async ({
475
578
  } : {})
476
579
  }]))
477
580
  });
478
- const logger = makeLogger(stdout, stderr);
479
581
  if (values.help) {
480
582
  stdout.write(buildHelp(stdout.isTTY ?? false));
481
583
  return 0;
@@ -486,6 +588,7 @@ const runCli = async ({
486
588
  return 0;
487
589
  }
488
590
  const moduleOpts = toModuleOptions(values);
591
+ if (sourceMapInline) moduleOpts.sourceMap = true;
489
592
  const cwd = moduleOpts.cwd ?? process.cwd();
490
593
  const allowStdout = positionals.length <= 1;
491
594
  const fromStdin = positionals.length === 0 || positionals.includes('-');
@@ -498,6 +601,10 @@ const runCli = async ({
498
601
  const list = Boolean(values.list);
499
602
  const summary = Boolean(values.summary);
500
603
  const json = Boolean(values.json);
604
+ if (sourceMapInline && (outDir || inPlace)) {
605
+ logger.error('Inline source maps are only supported when writing to stdout');
606
+ return 2;
607
+ }
501
608
  if (outDir && inPlace) {
502
609
  logger.error('Choose either --out-dir or --in-place, not both');
503
610
  return 2;
@@ -552,7 +659,8 @@ const runCli = async ({
552
659
  json,
553
660
  outDir,
554
661
  inPlace,
555
- allowStdout
662
+ allowStdout,
663
+ sourceMapInline
556
664
  });
557
665
  if (typeof result.code === 'number' && result.code !== 0) return result.code;
558
666
  tasks.push(...result.results);
package/dist/format.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { Node, ParseResult } from 'oxc-parser';
2
+ import MagicString from 'magic-string';
2
3
  import type { Diagnostic, FormatterOptions } from './types.js';
3
4
  type HazardLevel = 'warning' | 'error';
4
5
  export type PackageUse = {
@@ -21,11 +22,10 @@ declare const dualPackageHazardDiagnostics: (params: {
21
22
  filePath?: string;
22
23
  cwd?: string;
23
24
  manifestCache?: Map<string, any | null>;
25
+ hazardAllowlist?: Iterable<string>;
24
26
  }) => Promise<Diagnostic[]>;
25
- /**
26
- * Node added support for import.meta.main.
27
- * Added in: v24.2.0, v22.18.0
28
- * @see https://nodejs.org/api/esm.html#importmetamain
29
- */
30
- declare const format: (src: string, ast: ParseResult, opts: FormatterOptions) => Promise<string>;
27
+ declare function format(src: string, ast: ParseResult, opts: FormatterOptions & {
28
+ sourceMap: true;
29
+ }): Promise<MagicString>;
30
+ declare function format(src: string, ast: ParseResult, opts: FormatterOptions): Promise<string>;
31
31
  export { format, collectDualPackageUsage, dualPackageHazardDiagnostics };
package/dist/format.js CHANGED
@@ -141,6 +141,9 @@ const describeDualPackage = pkgJson => {
141
141
  requireTarget
142
142
  };
143
143
  };
144
+ const normalizeAllowlist = allowlist => {
145
+ return new Set([...(allowlist ?? [])].map(item => item.trim()).filter(item => item.length > 0));
146
+ };
144
147
  const recordUsage = (usages, pkg, kind, spec, subpath, loc, filePath) => {
145
148
  const existing = usages.get(pkg) ?? {
146
149
  imports: [],
@@ -209,8 +212,10 @@ const dualPackageHazardDiagnostics = async params => {
209
212
  cwd
210
213
  } = params;
211
214
  const manifestCache = params.manifestCache ?? new Map();
215
+ const allowlist = normalizeAllowlist(params.hazardAllowlist);
212
216
  const diags = [];
213
217
  for (const [pkg, usage] of usages) {
218
+ if (allowlist.has(pkg)) continue;
214
219
  const hasImport = usage.imports.length > 0;
215
220
  const hasRequire = usage.requires.length > 0;
216
221
  const combined = [...usage.imports, ...usage.requires];
@@ -276,19 +281,14 @@ const detectDualPackageHazards = async params => {
276
281
  hazardLevel,
277
282
  filePath,
278
283
  cwd,
279
- manifestCache
284
+ manifestCache,
285
+ hazardAllowlist: params.hazardAllowlist
280
286
  });
281
287
  for (const diag of diags) {
282
288
  diagOnce(diag.level, diag.code, diag.message, diag.loc);
283
289
  }
284
290
  };
285
-
286
- /**
287
- * Node added support for import.meta.main.
288
- * Added in: v24.2.0, v22.18.0
289
- * @see https://nodejs.org/api/esm.html#importmetamain
290
- */
291
- const format = async (src, ast, opts) => {
291
+ async function format(src, ast, opts) {
292
292
  const code = new MagicString(src);
293
293
  const exportsMeta = {
294
294
  hasExportsBeenReassigned: false,
@@ -337,7 +337,8 @@ const format = async (src, ast, opts) => {
337
337
  hazardLevel,
338
338
  filePath: opts.filePath,
339
339
  cwd: opts.cwd,
340
- diagOnce
340
+ diagOnce,
341
+ hazardAllowlist: opts.dualPackageHazardAllowlist
341
342
  });
342
343
  }
343
344
  if (opts.target === 'module' && fullTransform) {
@@ -513,15 +514,16 @@ const format = async (src, ast, opts) => {
513
514
  code.prepend(prelude);
514
515
  }
515
516
  if (opts.target === 'commonjs' && fullTransform && containsTopLevelAwait) {
516
- const body = code.toString();
517
517
  if (opts.topLevelAwait === 'wrap') {
518
- const tlaPromise = `const __tla = (async () => {\n${body}\nreturn module.exports;\n})();\n`;
519
- 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`;
520
- const attach = `__setTla(module.exports);\n__tla.then(resolved => __setTla(resolved), err => { throw err; });\n`;
521
- return `${tlaPromise}${setPromise}${attach}`;
518
+ code.prepend('const __tla = (async () => {\n');
519
+ code.append('\nreturn module.exports;\n})();\n');
520
+ 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');
521
+ code.append('__setTla(module.exports);\n__tla.then(resolved => __setTla(resolved), err => { throw err; });\n');
522
+ } else {
523
+ code.prepend(';(async () => {\n');
524
+ code.append('\n})();\n');
522
525
  }
523
- return `;(async () => {\n${body}\n})();\n`;
524
526
  }
525
- return code.toString();
526
- };
527
+ return opts.sourceMap ? code : code.toString();
528
+ }
527
529
  export { format, collectDualPackageUsage, dualPackageHazardDiagnostics };
package/dist/module.d.ts CHANGED
@@ -1,4 +1,17 @@
1
1
  import type { ModuleOptions, Diagnostic } from './types.js';
2
+ import type { SourceMap } from 'magic-string';
2
3
  declare const collectProjectDualPackageHazards: (files: string[], opts: ModuleOptions) => Promise<Map<string, Diagnostic[]>>;
3
- declare const transform: (filename: string, options?: ModuleOptions) => Promise<string>;
4
+ declare function transform(filename: string, options: ModuleOptions & {
5
+ sourceMap: true;
6
+ }): Promise<{
7
+ code: string;
8
+ map: SourceMap;
9
+ }>;
10
+ declare function transform(filename: string, options?: ModuleOptions & {
11
+ sourceMap?: false | undefined;
12
+ }): Promise<string>;
13
+ declare function transform(filename: string, options?: ModuleOptions): Promise<string | {
14
+ code: string;
15
+ map: SourceMap;
16
+ }>;
4
17
  export { transform, collectProjectDualPackageHazards };
package/dist/module.js CHANGED
@@ -167,8 +167,10 @@ const collectProjectDualPackageHazards = async (files, opts) => {
167
167
  const diags = await dualPackageHazardDiagnostics({
168
168
  usages,
169
169
  hazardLevel,
170
+ filePath: opts.filePath,
170
171
  cwd: opts.cwd,
171
- manifestCache
172
+ manifestCache,
173
+ hazardAllowlist: opts.dualPackageHazardAllowlist
172
174
  });
173
175
  const byFile = new Map();
174
176
  for (const diag of diags) {
@@ -195,17 +197,19 @@ const createDefaultOptions = () => ({
195
197
  detectCircularRequires: 'off',
196
198
  detectDualPackageHazard: 'warn',
197
199
  dualPackageHazardScope: 'file',
200
+ dualPackageHazardAllowlist: [],
198
201
  requireSource: 'builtin',
199
202
  nestedRequireStrategy: 'create-require',
200
203
  cjsDefault: 'auto',
201
204
  idiomaticExports: 'safe',
202
205
  importMetaPrelude: 'auto',
203
206
  topLevelAwait: 'error',
207
+ sourceMap: false,
204
208
  cwd: undefined,
205
209
  out: undefined,
206
210
  inPlace: false
207
211
  });
208
- const transform = async (filename, options) => {
212
+ async function transform(filename, options) {
209
213
  const base = createDefaultOptions();
210
214
  const opts = options ? {
211
215
  ...base,
@@ -222,9 +226,19 @@ const transform = async (filename, options) => {
222
226
  const file = resolve(cwdBase, filename);
223
227
  const code = (await readFile(file)).toString();
224
228
  const ast = parse(filename, code);
225
- let source = await format(code, ast, opts);
229
+ let sourceCode = null;
230
+ let source;
231
+ if (opts.sourceMap) {
232
+ sourceCode = await format(code, ast, {
233
+ ...opts,
234
+ sourceMap: true
235
+ });
236
+ source = sourceCode.toString();
237
+ } else {
238
+ source = await format(code, ast, opts);
239
+ }
226
240
  if (opts.rewriteSpecifier || appendMode !== 'off' || dirIndex) {
227
- const code = await specifier.updateSrc(source, getLangFromExt(filename), spec => {
241
+ const applyRewrite = spec => {
228
242
  if (spec.type === 'TemplateLiteral' && opts.rewriteTemplateLiterals === 'static-only') {
229
243
  const node = spec.node;
230
244
  if (node.expressions.length > 0) return;
@@ -234,8 +248,14 @@ const transform = async (filename, options) => {
234
248
  const baseValue = rewritten ?? normalized ?? spec.value;
235
249
  const appended = appendExtensionIfNeeded(spec, appendMode, dirIndex, baseValue);
236
250
  return appended ?? rewritten ?? normalized ?? undefined;
237
- });
238
- source = code;
251
+ };
252
+ if (opts.sourceMap && sourceCode) {
253
+ await specifier.updateMagicString(sourceCode, code, ast, applyRewrite);
254
+ source = sourceCode.toString();
255
+ } else {
256
+ const rewritten = await specifier.updateSrc(source, getLangFromExt(filename), applyRewrite);
257
+ source = rewritten;
258
+ }
239
259
  }
240
260
  if (detectCycles !== 'off' && opts.target === 'module' && opts.transformSyntax) {
241
261
  await detectCircularRequireGraph(file, detectCycles, dirIndex || 'index.js');
@@ -244,6 +264,18 @@ const transform = async (filename, options) => {
244
264
  if (outputPath) {
245
265
  await writeFile(outputPath, source);
246
266
  }
267
+ if (opts.sourceMap && sourceCode) {
268
+ const map = sourceCode.generateMap({
269
+ hires: true,
270
+ includeContent: true,
271
+ file: outputPath ?? filename,
272
+ source: opts.filePath ?? filename
273
+ });
274
+ return {
275
+ code: source,
276
+ map
277
+ };
278
+ }
247
279
  return source;
248
- };
280
+ }
249
281
  export { transform, collectProjectDualPackageHazards };
@@ -1,4 +1,5 @@
1
- import type { ParserOptions, StringLiteral, TemplateLiteral, BinaryExpression, NewExpression, ImportDeclaration, ExportNamedDeclaration, ExportAllDeclaration, TSImportType, ImportExpression, CallExpression } from 'oxc-parser';
1
+ import MagicString from 'magic-string';
2
+ import type { ParserOptions, ParseResult, StringLiteral, TemplateLiteral, BinaryExpression, NewExpression, ImportDeclaration, ExportNamedDeclaration, ExportAllDeclaration, TSImportType, ImportExpression, CallExpression } from 'oxc-parser';
2
3
  type Spec = {
3
4
  type: 'StringLiteral' | 'TemplateLiteral' | 'BinaryExpression' | 'NewExpression';
4
5
  node: StringLiteral | TemplateLiteral | BinaryExpression | NewExpression;
@@ -11,6 +12,7 @@ type Callback = (spec: Spec) => string | void;
11
12
  declare const specifier: {
12
13
  update(path: string, callback: Callback): Promise<string>;
13
14
  updateSrc(src: string, lang: ParserOptions["lang"], callback: Callback): Promise<string>;
15
+ updateMagicString(code: MagicString, src: string, ast: ParseResult, callback: Callback): Promise<MagicString>;
14
16
  };
15
17
  export { specifier };
16
18
  export type { Spec, Callback };
package/dist/specifier.js CHANGED
@@ -13,8 +13,7 @@ const isBinaryExpression = node => {
13
13
  const isCallExpression = node => {
14
14
  return node.type === 'CallExpression' && node.callee !== undefined;
15
15
  };
16
- const formatSpecifiers = async (src, ast, cb) => {
17
- const code = new MagicString(src);
16
+ const formatSpecifiers = async (src, ast, cb, code = new MagicString(src)) => {
18
17
  const formatExpression = expression => {
19
18
  const node = isCallExpression(expression) ? expression.arguments[0] : expression.source;
20
19
  const {
@@ -228,7 +227,7 @@ const formatSpecifiers = async (src, ast, cb) => {
228
227
  }
229
228
  }
230
229
  });
231
- return code.toString();
230
+ return code;
232
231
  };
233
232
  const isValidFilename = async filename => {
234
233
  let stats;
@@ -251,12 +250,18 @@ const specifier = {
251
250
  }
252
251
  const src = (await readFile(filename)).toString();
253
252
  const ast = parseSync(filename, src);
254
- return await formatSpecifiers(src, ast, callback);
253
+ const code = await formatSpecifiers(src, ast, callback);
254
+ return code.toString();
255
255
  },
256
256
  async updateSrc(src, lang, callback) {
257
257
  const filename = lang === 'ts' ? 'file.ts' : lang === 'tsx' ? 'file.tsx' : lang === 'js' ? 'file.js' : 'file.jsx';
258
258
  const ast = parseSync(filename, src);
259
- return await formatSpecifiers(src, ast, callback);
259
+ const code = await formatSpecifiers(src, ast, callback);
260
+ return code.toString();
261
+ },
262
+ async updateMagicString(code, src, ast, callback) {
263
+ await formatSpecifiers(src, ast, callback, code);
264
+ return code;
260
265
  }
261
266
  };
262
267
  export { specifier };
package/dist/types.d.ts CHANGED
@@ -6,6 +6,8 @@ export type ModuleOptions = {
6
6
  target: 'module' | 'commonjs';
7
7
  /** Explicit source type; auto infers from file extension. */
8
8
  sourceType?: 'auto' | 'module' | 'commonjs';
9
+ /** Emit a source map alongside the transformed code. */
10
+ sourceMap?: boolean;
9
11
  /**
10
12
  * Enable syntax transforms beyond parsing.
11
13
  * - true: full CJS↔ESM lowering/raising
@@ -38,6 +40,8 @@ export type ModuleOptions = {
38
40
  detectDualPackageHazard?: 'off' | 'warn' | 'error';
39
41
  /** Scope for dual package hazard detection. */
40
42
  dualPackageHazardScope?: 'file' | 'project';
43
+ /** Packages to ignore for dual package hazard diagnostics. */
44
+ dualPackageHazardAllowlist?: string[];
41
45
  /** Source used to provide require in ESM output. */
42
46
  requireSource?: 'builtin' | 'create-require';
43
47
  /** How to rewrite nested or non-hoistable require calls. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@knighted/module",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
4
4
  "description": "Bidirectional transform for ES modules and CommonJS.",
5
5
  "type": "module",
6
6
  "main": "dist/module.js",