@knighted/duel 4.0.0-rc.2 → 4.0.0-rc.4

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
@@ -117,7 +117,7 @@ Project scope is helpful in monorepos or hoisted installs where hazards surface
117
117
 
118
118
  ## Options
119
119
 
120
- The available options are limited, because you should define most of them inside your project's `tsconfig.json` file.
120
+ These are the CLI options `duel` supports to work alongside your project's `tsconfig.json` settings.
121
121
 
122
122
  - `--project, -p` The path to the project's configuration file. Defaults to `tsconfig.json`.
123
123
  - `--pkg-dir, -k` The directory to start looking for a package.json file. Defaults to `--project` dir.
package/dist/cjs/duel.cjs CHANGED
@@ -428,8 +428,8 @@ const duel = async (args) => {
428
428
  rel = altRel;
429
429
  }
430
430
  else {
431
- (0, util_js_1.logWarn)(`Skipping copy for ${file} outside of project root ${projectRoot}`);
432
- continue;
431
+ (0, util_js_1.logError)(`Referenced config or source is outside the project root and cannot be patched: ${file}. Move it inside ${projectRoot} (or its parent for project references) so Duel can create an isolated shadow build.`);
432
+ process.exit(1);
433
433
  }
434
434
  }
435
435
  const dest = (0, node_path_1.join)(subDir, rel);
@@ -464,14 +464,7 @@ const duel = async (args) => {
464
464
  if (configFile === configPath)
465
465
  continue;
466
466
  const dest = (0, node_path_1.join)(subDir, (0, node_path_1.relative)(projectRoot, configFile));
467
- let parsed = null;
468
- try {
469
- parsed = (0, get_tsconfig_1.parseTsconfig)(dest);
470
- }
471
- catch (err) {
472
- (0, util_js_1.logWarn)(`Skipping referenced tsconfig at ${dest} (parse failed): ${err?.message ?? err}`);
473
- continue;
474
- }
467
+ const parsed = (0, get_tsconfig_1.parseTsconfig)(dest);
475
468
  const cfg = parsed?.tsconfig ?? parsed;
476
469
  if (!cfg || typeof cfg !== 'object')
477
470
  continue;
@@ -1,14 +1,69 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  exports.rewriteSpecifiersAndExtensions = void 0;
4
7
  const promises_1 = require("node:fs/promises");
5
8
  const node_fs_1 = require("node:fs");
6
9
  const node_path_1 = require("node:path");
10
+ const magic_string_1 = __importDefault(require("magic-string"));
11
+ const trace_mapping_1 = require("@jridgewell/trace-mapping");
12
+ const gen_mapping_1 = require("@jridgewell/gen-mapping");
7
13
  const module_1 = require("@knighted/module");
8
- /**
9
- * Rewrites specifiers and file extensions for dual builds.
10
- * Currently mirrors existing behavior and provides hooks for future validation.
11
- */
14
+ const loadMapIfExists = async (path) => {
15
+ try {
16
+ const raw = await (0, promises_1.readFile)(path, 'utf8');
17
+ return JSON.parse(raw);
18
+ }
19
+ catch {
20
+ return null;
21
+ }
22
+ };
23
+ const updateSourceMappingUrl = (content, mapFile) => {
24
+ const comment = `//# sourceMappingURL=${mapFile}`;
25
+ if (/\/\/# sourceMappingURL=/.test(content)) {
26
+ return content.replace(/\/\/# sourceMappingURL=.*/g, comment);
27
+ }
28
+ return `${content}\n${comment}\n`;
29
+ };
30
+ const composeSourceMaps = (rewriteMap, baseMap) => {
31
+ const outer = new trace_mapping_1.TraceMap(rewriteMap);
32
+ const inner = new trace_mapping_1.TraceMap(baseMap);
33
+ const file = rewriteMap.file ?? baseMap.file;
34
+ const gen = new gen_mapping_1.GenMapping({ file });
35
+ (0, trace_mapping_1.eachMapping)(outer, mapping => {
36
+ if (mapping.originalLine == null || mapping.originalColumn == null)
37
+ return;
38
+ const traced = (0, trace_mapping_1.originalPositionFor)(inner, {
39
+ line: mapping.originalLine,
40
+ column: mapping.originalColumn,
41
+ });
42
+ if (traced.line == null || traced.column == null || traced.source == null)
43
+ return;
44
+ (0, gen_mapping_1.addMapping)(gen, {
45
+ generated: { line: mapping.generatedLine, column: mapping.generatedColumn },
46
+ original: { line: traced.line, column: traced.column },
47
+ source: traced.source,
48
+ name: traced.name ?? mapping.name ?? null,
49
+ });
50
+ });
51
+ const sourcesContent = new Map();
52
+ const baseSources = Array.isArray(baseMap.sources) ? baseMap.sources : [];
53
+ const baseContents = Array.isArray(baseMap.sourcesContent) ? baseMap.sourcesContent : [];
54
+ baseSources.forEach((source, idx) => {
55
+ const content = baseContents[idx];
56
+ if (content != null) {
57
+ sourcesContent.set(source, content);
58
+ }
59
+ });
60
+ for (const [source, content] of sourcesContent.entries()) {
61
+ (0, gen_mapping_1.setSourceContent)(gen, source, content);
62
+ }
63
+ const composed = (0, gen_mapping_1.toDecodedMap)(gen);
64
+ composed.file = file;
65
+ return composed;
66
+ };
12
67
  const rewriteSpecifiersAndExtensions = async (filenames, options = {}) => {
13
68
  const { target, ext, syntaxMode, rewritePolicy = 'safe', validateSpecifiers = false, onRewrite = () => { }, onWarn = () => { }, } = options;
14
69
  const rewrites = [];
@@ -20,13 +75,55 @@ const rewriteSpecifiersAndExtensions = async (filenames, options = {}) => {
20
75
  : filename.replace(/\.js$/, ext);
21
76
  if (isDts) {
22
77
  const source = await (0, promises_1.readFile)(filename, 'utf8');
23
- const rewritten = source.replace(/(?<=["'`])(\.\.?(?:\/[\w.-]+)*)\.js(?=["'`])/g, `$1${ext}`);
24
- if (rewritten !== source) {
78
+ const code = new magic_string_1.default(source);
79
+ let mutated = false;
80
+ for (const match of source.matchAll(/(?<=['"`])(\.{1,2}(?:\/[\w.-]+)*)\.js(?=['"`])/g)) {
81
+ if (match.index == null)
82
+ continue;
83
+ const start = match.index;
84
+ const end = start + match[0].length;
85
+ code.overwrite(start, end, `${match[1]}${ext}`);
86
+ mutated = true;
87
+ }
88
+ const existingMapPath = `${filename}.map`;
89
+ const existingMap = await loadMapIfExists(existingMapPath);
90
+ if (mutated) {
25
91
  rewrites.push({ file: filename, kind: 'dts' });
26
92
  }
27
- await (0, promises_1.writeFile)(outFilename, rewritten);
93
+ const outMapPath = `${outFilename}.map`;
94
+ const mapFile = (0, node_path_1.basename)(outMapPath);
95
+ let output = code.toString();
96
+ let nextMap = null;
97
+ /*
98
+ * If an upstream map exists, carry it forward when we rename the file,
99
+ * and compose when the content was mutated. If no upstream map exists,
100
+ * do not emit a new one.
101
+ */
102
+ if (existingMap) {
103
+ if (mutated) {
104
+ const rewriteMap = code.generateMap({
105
+ hires: true,
106
+ includeContent: true,
107
+ file: outFilename,
108
+ source: filename,
109
+ });
110
+ nextMap = composeSourceMaps(rewriteMap, existingMap);
111
+ }
112
+ else {
113
+ nextMap = { ...existingMap };
114
+ }
115
+ }
116
+ if (nextMap) {
117
+ nextMap.file = (0, node_path_1.basename)(outFilename);
118
+ output = updateSourceMappingUrl(output, mapFile);
119
+ await (0, promises_1.writeFile)(outMapPath, JSON.stringify(nextMap));
120
+ }
121
+ await (0, promises_1.writeFile)(outFilename, output);
28
122
  if (outFilename !== filename) {
29
123
  await (0, promises_1.rm)(filename, { force: true });
124
+ if (existingMap) {
125
+ await (0, promises_1.rm)(existingMapPath, { force: true });
126
+ }
30
127
  }
31
128
  continue;
32
129
  }
@@ -82,11 +179,36 @@ const rewriteSpecifiersAndExtensions = async (filenames, options = {}) => {
82
179
  target,
83
180
  rewriteSpecifier,
84
181
  transformSyntax: syntaxMode,
182
+ sourceMap: true,
85
183
  ...(outFilename === filename ? { inPlace: true } : { out: outFilename }),
86
184
  };
87
- await (0, module_1.transform)(filename, writeOptions);
185
+ const result = await (0, module_1.transform)(filename, writeOptions);
186
+ const nextCode = result?.code ?? result;
187
+ const rewriteMap = result?.map ?? null;
188
+ const existingMapPath = `${filename}.map`;
189
+ const existingMap = await loadMapIfExists(existingMapPath);
190
+ const outMapPath = `${outFilename}.map`;
191
+ let output = typeof nextCode === 'string' ? nextCode : String(nextCode);
192
+ let nextMap = null;
193
+ /*
194
+ * Compose the rewrite map with the upstream map when present; if the
195
+ * input had no map, we do not emit a new one.
196
+ */
197
+ if (rewriteMap && existingMap) {
198
+ nextMap = composeSourceMaps(rewriteMap, existingMap);
199
+ }
200
+ if (nextMap) {
201
+ const mapFile = (0, node_path_1.basename)(outMapPath);
202
+ nextMap.file = (0, node_path_1.basename)(outFilename);
203
+ output = updateSourceMappingUrl(output, mapFile);
204
+ await (0, promises_1.writeFile)(outMapPath, JSON.stringify(nextMap));
205
+ }
206
+ await (0, promises_1.writeFile)(outFilename, output);
88
207
  if (outFilename !== filename) {
89
208
  await (0, promises_1.rm)(filename, { force: true });
209
+ if (existingMap) {
210
+ await (0, promises_1.rm)(existingMapPath, { force: true });
211
+ }
90
212
  }
91
213
  rewrites.push({ file: filename, kind: 'source' });
92
214
  onRewrite(filename, outFilename);
@@ -1,7 +1,3 @@
1
- /**
2
- * Rewrites specifiers and file extensions for dual builds.
3
- * Currently mirrors existing behavior and provides hooks for future validation.
4
- */
5
1
  export function rewriteSpecifiersAndExtensions(filenames: any, options?: {}): Promise<{
6
2
  rewrites: {
7
3
  file: any;
package/dist/esm/duel.js CHANGED
@@ -425,8 +425,8 @@ const duel = async (args) => {
425
425
  rel = altRel;
426
426
  }
427
427
  else {
428
- logWarn(`Skipping copy for ${file} outside of project root ${projectRoot}`);
429
- continue;
428
+ logError(`Referenced config or source is outside the project root and cannot be patched: ${file}. Move it inside ${projectRoot} (or its parent for project references) so Duel can create an isolated shadow build.`);
429
+ process.exit(1);
430
430
  }
431
431
  }
432
432
  const dest = join(subDir, rel);
@@ -461,14 +461,7 @@ const duel = async (args) => {
461
461
  if (configFile === configPath)
462
462
  continue;
463
463
  const dest = join(subDir, relative(projectRoot, configFile));
464
- let parsed = null;
465
- try {
466
- parsed = parseTsconfig(dest);
467
- }
468
- catch (err) {
469
- logWarn(`Skipping referenced tsconfig at ${dest} (parse failed): ${err?.message ?? err}`);
470
- continue;
471
- }
464
+ const parsed = parseTsconfig(dest);
472
465
  const cfg = parsed?.tsconfig ?? parsed;
473
466
  if (!cfg || typeof cfg !== 'object')
474
467
  continue;
@@ -1,7 +1,3 @@
1
- /**
2
- * Rewrites specifiers and file extensions for dual builds.
3
- * Currently mirrors existing behavior and provides hooks for future validation.
4
- */
5
1
  export function rewriteSpecifiersAndExtensions(filenames: any, options?: {}): Promise<{
6
2
  rewrites: {
7
3
  file: any;
@@ -1,11 +1,63 @@
1
1
  import { readFile, writeFile, rm } from 'node:fs/promises';
2
2
  import { accessSync } from 'node:fs';
3
- import { dirname, resolve } from 'node:path';
3
+ import { basename, dirname, resolve } from 'node:path';
4
+ import MagicString from 'magic-string';
5
+ import { TraceMap, eachMapping, originalPositionFor } from '@jridgewell/trace-mapping';
6
+ import { GenMapping, addMapping, setSourceContent, toDecodedMap, } from '@jridgewell/gen-mapping';
4
7
  import { transform } from '@knighted/module';
5
- /**
6
- * Rewrites specifiers and file extensions for dual builds.
7
- * Currently mirrors existing behavior and provides hooks for future validation.
8
- */
8
+ const loadMapIfExists = async (path) => {
9
+ try {
10
+ const raw = await readFile(path, 'utf8');
11
+ return JSON.parse(raw);
12
+ }
13
+ catch {
14
+ return null;
15
+ }
16
+ };
17
+ const updateSourceMappingUrl = (content, mapFile) => {
18
+ const comment = `//# sourceMappingURL=${mapFile}`;
19
+ if (/\/\/# sourceMappingURL=/.test(content)) {
20
+ return content.replace(/\/\/# sourceMappingURL=.*/g, comment);
21
+ }
22
+ return `${content}\n${comment}\n`;
23
+ };
24
+ const composeSourceMaps = (rewriteMap, baseMap) => {
25
+ const outer = new TraceMap(rewriteMap);
26
+ const inner = new TraceMap(baseMap);
27
+ const file = rewriteMap.file ?? baseMap.file;
28
+ const gen = new GenMapping({ file });
29
+ eachMapping(outer, mapping => {
30
+ if (mapping.originalLine == null || mapping.originalColumn == null)
31
+ return;
32
+ const traced = originalPositionFor(inner, {
33
+ line: mapping.originalLine,
34
+ column: mapping.originalColumn,
35
+ });
36
+ if (traced.line == null || traced.column == null || traced.source == null)
37
+ return;
38
+ addMapping(gen, {
39
+ generated: { line: mapping.generatedLine, column: mapping.generatedColumn },
40
+ original: { line: traced.line, column: traced.column },
41
+ source: traced.source,
42
+ name: traced.name ?? mapping.name ?? null,
43
+ });
44
+ });
45
+ const sourcesContent = new Map();
46
+ const baseSources = Array.isArray(baseMap.sources) ? baseMap.sources : [];
47
+ const baseContents = Array.isArray(baseMap.sourcesContent) ? baseMap.sourcesContent : [];
48
+ baseSources.forEach((source, idx) => {
49
+ const content = baseContents[idx];
50
+ if (content != null) {
51
+ sourcesContent.set(source, content);
52
+ }
53
+ });
54
+ for (const [source, content] of sourcesContent.entries()) {
55
+ setSourceContent(gen, source, content);
56
+ }
57
+ const composed = toDecodedMap(gen);
58
+ composed.file = file;
59
+ return composed;
60
+ };
9
61
  const rewriteSpecifiersAndExtensions = async (filenames, options = {}) => {
10
62
  const { target, ext, syntaxMode, rewritePolicy = 'safe', validateSpecifiers = false, onRewrite = () => { }, onWarn = () => { }, } = options;
11
63
  const rewrites = [];
@@ -17,13 +69,55 @@ const rewriteSpecifiersAndExtensions = async (filenames, options = {}) => {
17
69
  : filename.replace(/\.js$/, ext);
18
70
  if (isDts) {
19
71
  const source = await readFile(filename, 'utf8');
20
- const rewritten = source.replace(/(?<=["'`])(\.\.?(?:\/[\w.-]+)*)\.js(?=["'`])/g, `$1${ext}`);
21
- if (rewritten !== source) {
72
+ const code = new MagicString(source);
73
+ let mutated = false;
74
+ for (const match of source.matchAll(/(?<=['"`])(\.{1,2}(?:\/[\w.-]+)*)\.js(?=['"`])/g)) {
75
+ if (match.index == null)
76
+ continue;
77
+ const start = match.index;
78
+ const end = start + match[0].length;
79
+ code.overwrite(start, end, `${match[1]}${ext}`);
80
+ mutated = true;
81
+ }
82
+ const existingMapPath = `${filename}.map`;
83
+ const existingMap = await loadMapIfExists(existingMapPath);
84
+ if (mutated) {
22
85
  rewrites.push({ file: filename, kind: 'dts' });
23
86
  }
24
- await writeFile(outFilename, rewritten);
87
+ const outMapPath = `${outFilename}.map`;
88
+ const mapFile = basename(outMapPath);
89
+ let output = code.toString();
90
+ let nextMap = null;
91
+ /*
92
+ * If an upstream map exists, carry it forward when we rename the file,
93
+ * and compose when the content was mutated. If no upstream map exists,
94
+ * do not emit a new one.
95
+ */
96
+ if (existingMap) {
97
+ if (mutated) {
98
+ const rewriteMap = code.generateMap({
99
+ hires: true,
100
+ includeContent: true,
101
+ file: outFilename,
102
+ source: filename,
103
+ });
104
+ nextMap = composeSourceMaps(rewriteMap, existingMap);
105
+ }
106
+ else {
107
+ nextMap = { ...existingMap };
108
+ }
109
+ }
110
+ if (nextMap) {
111
+ nextMap.file = basename(outFilename);
112
+ output = updateSourceMappingUrl(output, mapFile);
113
+ await writeFile(outMapPath, JSON.stringify(nextMap));
114
+ }
115
+ await writeFile(outFilename, output);
25
116
  if (outFilename !== filename) {
26
117
  await rm(filename, { force: true });
118
+ if (existingMap) {
119
+ await rm(existingMapPath, { force: true });
120
+ }
27
121
  }
28
122
  continue;
29
123
  }
@@ -79,11 +173,36 @@ const rewriteSpecifiersAndExtensions = async (filenames, options = {}) => {
79
173
  target,
80
174
  rewriteSpecifier,
81
175
  transformSyntax: syntaxMode,
176
+ sourceMap: true,
82
177
  ...(outFilename === filename ? { inPlace: true } : { out: outFilename }),
83
178
  };
84
- await transform(filename, writeOptions);
179
+ const result = await transform(filename, writeOptions);
180
+ const nextCode = result?.code ?? result;
181
+ const rewriteMap = result?.map ?? null;
182
+ const existingMapPath = `${filename}.map`;
183
+ const existingMap = await loadMapIfExists(existingMapPath);
184
+ const outMapPath = `${outFilename}.map`;
185
+ let output = typeof nextCode === 'string' ? nextCode : String(nextCode);
186
+ let nextMap = null;
187
+ /*
188
+ * Compose the rewrite map with the upstream map when present; if the
189
+ * input had no map, we do not emit a new one.
190
+ */
191
+ if (rewriteMap && existingMap) {
192
+ nextMap = composeSourceMaps(rewriteMap, existingMap);
193
+ }
194
+ if (nextMap) {
195
+ const mapFile = basename(outMapPath);
196
+ nextMap.file = basename(outFilename);
197
+ output = updateSourceMappingUrl(output, mapFile);
198
+ await writeFile(outMapPath, JSON.stringify(nextMap));
199
+ }
200
+ await writeFile(outFilename, output);
85
201
  if (outFilename !== filename) {
86
202
  await rm(filename, { force: true });
203
+ if (existingMap) {
204
+ await rm(existingMapPath, { force: true });
205
+ }
87
206
  }
88
207
  rewrites.push({ file: filename, kind: 'source' });
89
208
  onRewrite(filename, outFilename);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@knighted/duel",
3
- "version": "4.0.0-rc.2",
3
+ "version": "4.0.0-rc.4",
4
4
  "description": "TypeScript dual packages.",
5
5
  "type": "module",
6
6
  "main": "dist/esm/duel.js",
@@ -87,10 +87,13 @@
87
87
  "vite": "^7.2.4"
88
88
  },
89
89
  "dependencies": {
90
- "@knighted/module": "^1.4.0",
90
+ "@jridgewell/gen-mapping": "^0.3.13",
91
+ "@jridgewell/trace-mapping": "^0.3.31",
92
+ "@knighted/module": "^1.5.0-rc.0",
91
93
  "find-up": "^8.0.0",
92
94
  "get-tsconfig": "^4.13.0",
93
95
  "glob": "^13.0.0",
96
+ "magic-string": "^0.30.21",
94
97
  "read-package-up": "^12.0.0"
95
98
  },
96
99
  "lint-staged": {