@knighted/module 1.4.0-rc.2 → 1.4.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
@@ -125,6 +125,7 @@ type ModuleOptions = {
125
125
  | '.mts'
126
126
  | '.cts'
127
127
  | ((value: string) => string | null | undefined)
128
+ rewriteTemplateLiterals?: 'allow' | 'static-only'
128
129
  dirFilename?: 'inject' | 'preserve' | 'error'
129
130
  importMeta?: 'preserve' | 'shim' | 'error'
130
131
  importMetaMain?: 'shim' | 'warn' | 'error'
@@ -151,15 +152,16 @@ type ModuleOptions = {
151
152
  - `appendJsExtension` (`relative-only` when targeting ESM): append `.js` to relative specifiers; never touches bare specifiers.
152
153
  - `appendDirectoryIndex` (`index.js`): when a relative specifier ends with a slash, append this index filename (set `false` to disable).
153
154
  - `appenders` precedence: `rewriteSpecifier` runs first; if it returns a string, that result is used. If it returns `undefined` or `null`, `appendJsExtension` and `appendDirectoryIndex` still run. Bare specifiers are never modified by appenders.
155
+ - `rewriteTemplateLiterals` (`allow`): when `static-only`, interpolated template literals are left untouched by specifier rewriting; string literals and non-interpolated templates still rewrite.
154
156
  - `dirFilename` (`inject`): inject `__dirname`/`__filename`, preserve existing, or throw.
155
157
  - `importMeta` (`shim`): rewrite `import.meta.*` to CommonJS equivalents.
156
158
  - `importMetaMain` (`shim`): gate `import.meta.main` with shimming/warning/error when Node support is too old.
157
159
  - `requireMainStrategy` (`import-meta-main`): use `import.meta.main` or the realpath-based `pathToFileURL(realpathSync(process.argv[1])).href` check.
158
160
  - `importMetaPrelude` (`auto`): emit a no-op `void import.meta.filename;` touch. `on` always emits; `off` never emits; `auto` emits only when helpers that reference `import.meta.*` are synthesized (e.g., `__dirname`/`__filename` in CJS→ESM, require-main shims, createRequire helpers). Useful for bundlers/transpilers that do usage-based `import.meta` polyfilling.
159
- - `detectCircularRequires` (`off`): optionally detect relative static require cycles and warn/throw.
161
+ - `detectCircularRequires` (`off`): optionally detect relative static require cycles across `.js`/`.mjs`/`.cjs`/`.ts`/`.mts`/`.cts` (realpath-normalized) and warn/throw.
160
162
  - `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.
161
163
  - `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.
162
- - `topLevelAwait` (`error`): throw, wrap, or preserve when TLA appears in CommonJS output.
164
+ - `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.
163
165
  - `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.
164
166
  - `requireSource` (`builtin`): whether `require` comes from Node or `createRequire`.
165
167
  - `cjsDefault` (`auto`): bundler-style default interop vs direct `module.exports`.
@@ -0,0 +1,2 @@
1
+ declare const builtinSpecifiers: Set<string>;
2
+ export { builtinSpecifiers };
@@ -0,0 +1,2 @@
1
+ declare const builtinSpecifiers: Set<string>;
2
+ export { builtinSpecifiers };
package/dist/cjs/cli.cjs CHANGED
@@ -9,19 +9,20 @@ var _nodeProcess = require("node:process");
9
9
  var _nodeUtil = require("node:util");
10
10
  var _promises = require("node:fs/promises");
11
11
  var _nodePath = require("node:path");
12
- var _nodeModule = require("node:module");
13
12
  var _glob = require("glob");
14
13
  var _module = require("./module.cjs");
15
14
  var _parse = require("./parse.cjs");
16
15
  var _format = require("./format.cjs");
17
16
  var _specifier = require("./specifier.cjs");
18
17
  var _lang = require("./utils/lang.cjs");
18
+ var _builtinSpecifiers = require("./utils/builtinSpecifiers.cjs");
19
19
  const defaultOptions = {
20
20
  target: 'commonjs',
21
21
  sourceType: 'auto',
22
22
  transformSyntax: true,
23
23
  liveBindings: 'strict',
24
24
  rewriteSpecifier: undefined,
25
+ rewriteTemplateLiterals: 'allow',
25
26
  appendJsExtension: undefined,
26
27
  appendDirectoryIndex: 'index.js',
27
28
  dirFilename: 'inject',
@@ -77,11 +78,6 @@ const colorize = enabled => {
77
78
  cyan: wrap(codes.cyan)
78
79
  };
79
80
  };
80
- const builtinSpecifiers = new Set(_nodeModule.builtinModules.map(mod => mod.startsWith('node:') ? mod.slice(5) : mod).flatMap(mod => {
81
- const parts = mod.split('/');
82
- const base = parts[0];
83
- return parts.length > 1 ? [mod, base] : [mod];
84
- }));
85
81
  const collapseSpecifier = value => value.replace(/['"`+)\s]|new String\(/g, '');
86
82
  const appendExtensionIfNeeded = (value, mode, dirIndex) => {
87
83
  if (mode === 'off') return;
@@ -117,7 +113,7 @@ const normalizeBuiltinSpecifier = value => {
117
113
  if (/^[a-zA-Z][a-zA-Z+.-]*:/.test(specPart) && !specPart.startsWith('node:')) return;
118
114
  const bare = specPart.startsWith('node:') ? specPart.slice(5) : specPart;
119
115
  const base = bare.split('/')[0] ?? '';
120
- if (!builtinSpecifiers.has(bare) && !builtinSpecifiers.has(base)) return;
116
+ if (!_builtinSpecifiers.builtinSpecifiers.has(bare) && !_builtinSpecifiers.builtinSpecifiers.has(base)) return;
121
117
  if (specPart.startsWith('node:')) return;
122
118
  const quote = /^['"`]/.exec(value)?.[0] ?? '';
123
119
  return quote ? `${quote}node:${value.slice(quote.length)}` : `node:${value}`;
@@ -137,6 +133,11 @@ const optionsTable = [{
137
133
  short: 'r',
138
134
  type: 'string',
139
135
  desc: 'Rewrite import specifiers (.js/.mjs/.cjs/.ts/.mts/.cts)'
136
+ }, {
137
+ long: 'rewrite-template-literals',
138
+ short: undefined,
139
+ type: 'string',
140
+ desc: 'Rewrite template literals (allow|static-only)'
140
141
  }, {
141
142
  long: 'append-js-extension',
142
143
  short: 'j',
@@ -283,6 +284,7 @@ const parseAppendDirectoryIndex = value => {
283
284
  const toModuleOptions = values => {
284
285
  const target = parseEnum(values.target, ['module', 'commonjs']) ?? defaultOptions.target;
285
286
  const transformSyntax = parseTransformSyntax(values['transform-syntax']);
287
+ const rewriteTemplateLiterals = parseEnum(values['rewrite-template-literals'], ['allow', 'static-only']) ?? defaultOptions.rewriteTemplateLiterals;
286
288
  const appendJsExtension = parseEnum(values['append-js-extension'], ['off', 'relative-only', 'all']);
287
289
  const appendDirectoryIndex = parseAppendDirectoryIndex(values['append-directory-index']);
288
290
  const opts = {
@@ -290,6 +292,7 @@ const toModuleOptions = values => {
290
292
  target,
291
293
  transformSyntax,
292
294
  rewriteSpecifier: values['rewrite-specifier'] ?? undefined,
295
+ rewriteTemplateLiterals,
293
296
  appendJsExtension: appendJsExtension,
294
297
  appendDirectoryIndex,
295
298
  detectCircularRequires: parseEnum(values['detect-circular-requires'], ['off', 'warn', 'error']) ?? defaultOptions.detectCircularRequires,
@@ -347,6 +350,10 @@ const applySpecifierUpdates = async (source, filename, opts, appendMode, dirInde
347
350
  if (!opts.rewriteSpecifier && appendMode === 'off' && !dirIndex) return source;
348
351
  const lang = (0, _lang.getLangFromExt)(filename);
349
352
  const updated = await _specifier.specifier.updateSrc(source, lang, spec => {
353
+ if (spec.type === 'TemplateLiteral' && opts.rewriteTemplateLiterals === 'static-only') {
354
+ const node = spec.node;
355
+ if (node.expressions.length > 0) return;
356
+ }
350
357
  const normalized = normalizeBuiltinSpecifier(spec.value);
351
358
  const rewritten = rewriteSpecifierValue(normalized ?? spec.value, opts.rewriteSpecifier);
352
359
  const baseValue = rewritten ?? normalized ?? spec.value;
@@ -4,7 +4,6 @@ Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
6
  exports.format = exports.dualPackageHazardDiagnostics = exports.collectDualPackageUsage = void 0;
7
- var _nodeModule = require("node:module");
8
7
  var _nodePath = require("node:path");
9
8
  var _promises = require("node:fs/promises");
10
9
  var _magicString = _interopRequireDefault(require("magic-string"));
@@ -24,14 +23,10 @@ var _interopHelpers = require("./pipeline/interopHelpers.cjs");
24
23
  var _exports = require("./utils/exports.cjs");
25
24
  var _identifiers = require("./utils/identifiers.cjs");
26
25
  var _url = require("./utils/url.cjs");
26
+ var _builtinSpecifiers = require("./utils/builtinSpecifiers.cjs");
27
27
  var _walk = require("./walk.cjs");
28
28
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
29
29
  const isRequireMainMember = (node, shadowed) => node.type === 'MemberExpression' && node.object.type === 'Identifier' && node.object.name === 'require' && !shadowed.has('require') && node.property.type === 'Identifier' && node.property.name === 'main';
30
- const builtinSpecifiers = new Set(_nodeModule.builtinModules.map(mod => mod.startsWith('node:') ? mod.slice(5) : mod).flatMap(mod => {
31
- const parts = mod.split('/');
32
- const base = parts[0];
33
- return parts.length > 1 ? [mod, base] : [mod];
34
- }));
35
30
  const stripQuery = value => value.includes('?') || value.includes('#') ? value.split(/[?#]/)[0] ?? value : value;
36
31
  const packageFromSpecifier = spec => {
37
32
  const cleaned = stripQuery(spec);
@@ -43,7 +38,7 @@ const packageFromSpecifier = spec => {
43
38
  if (cleaned.startsWith('@')) {
44
39
  if (parts.length < 2) return null;
45
40
  const pkg = `${parts[0]}/${parts[1]}`;
46
- if (builtinSpecifiers.has(pkg) || builtinSpecifiers.has(parts[1] ?? '')) return null;
41
+ if (_builtinSpecifiers.builtinSpecifiers.has(pkg) || _builtinSpecifiers.builtinSpecifiers.has(parts[1] ?? '')) return null;
47
42
  const subpath = parts.slice(2).join('/');
48
43
  return {
49
44
  pkg,
@@ -51,7 +46,7 @@ const packageFromSpecifier = spec => {
51
46
  };
52
47
  }
53
48
  const pkg = parts[0] ?? '';
54
- if (!pkg || builtinSpecifiers.has(pkg)) return null;
49
+ if (!pkg || _builtinSpecifiers.builtinSpecifiers.has(pkg)) return null;
55
50
  const subpath = parts.slice(1).join('/');
56
51
  return {
57
52
  pkg,
@@ -10,15 +10,10 @@ var _specifier = require("./specifier.cjs");
10
10
  var _parse = require("./parse.cjs");
11
11
  var _format = require("./format.cjs");
12
12
  var _lang = require("./utils/lang.cjs");
13
- var _nodeModule = require("node:module");
14
13
  var _walk = require("./walk.cjs");
15
14
  var _identifiers = require("./utils/identifiers.cjs");
15
+ var _builtinSpecifiers = require("./utils/builtinSpecifiers.cjs");
16
16
  const collapseSpecifier = value => value.replace(/['"`+)\s]|new String\(/g, '');
17
- const builtinSpecifiers = new Set(_nodeModule.builtinModules.map(mod => mod.startsWith('node:') ? mod.slice(5) : mod).flatMap(mod => {
18
- const parts = mod.split('/');
19
- const base = parts[0];
20
- return parts.length > 1 ? [mod, base] : [mod];
21
- }));
22
17
  const appendExtensionIfNeeded = (spec, mode, dirIndex, value = spec.value) => {
23
18
  if (mode === 'off') return;
24
19
  if (spec.type === 'TemplateLiteral') {
@@ -63,7 +58,7 @@ const normalizeBuiltinSpecifier = value => {
63
58
  if (/^[a-zA-Z][a-zA-Z+.-]*:/.test(specPart) && !specPart.startsWith('node:')) return;
64
59
  const bare = specPart.startsWith('node:') ? specPart.slice(5) : specPart;
65
60
  const base = bare.split('/')[0] ?? '';
66
- if (!builtinSpecifiers.has(bare) && !builtinSpecifiers.has(base)) return;
61
+ if (!_builtinSpecifiers.builtinSpecifiers.has(bare) && !_builtinSpecifiers.builtinSpecifiers.has(base)) return;
67
62
  if (specPart.startsWith('node:')) return;
68
63
  const quote = /^['"`]/.exec(value)?.[0] ?? '';
69
64
  return quote ? `${quote}node:${value.slice(quote.length)}` : `node:${value}`;
@@ -76,6 +71,7 @@ const fileExists = async candidate => {
76
71
  return false;
77
72
  }
78
73
  };
74
+ const normalizePath = async p => (0, _nodePath.resolve)(await (0, _promises.realpath)(p).catch(() => p));
79
75
  const resolveRequirePath = async (fromFile, spec, dirIndex) => {
80
76
  if (!spec.startsWith('./') && !spec.startsWith('../')) return null;
81
77
  const base = (0, _nodePath.resolve)((0, _nodePath.dirname)(fromFile), spec);
@@ -84,11 +80,11 @@ const resolveRequirePath = async (fromFile, spec, dirIndex) => {
84
80
  if (ext) {
85
81
  candidates.push(base);
86
82
  } else {
87
- candidates.push(`${base}.js`, `${base}.cjs`, `${base}.mjs`);
83
+ candidates.push(`${base}.js`, `${base}.cjs`, `${base}.mjs`, `${base}.ts`, `${base}.mts`, `${base}.cts`);
88
84
  candidates.push((0, _nodePath.join)(base, dirIndex));
89
85
  }
90
86
  for (const candidate of candidates) {
91
- if (await fileExists(candidate)) return candidate;
87
+ if (await fileExists(candidate)) return await normalizePath(candidate);
92
88
  }
93
89
  return null;
94
90
  };
@@ -118,8 +114,9 @@ const detectCircularRequireGraph = async (entryFile, mode, dirIndex) => {
118
114
  const visiting = new Set();
119
115
  const visited = new Set();
120
116
  const dfs = async (file, stack) => {
121
- if (visiting.has(file)) {
122
- const cycle = [...stack, file];
117
+ const normalized = await normalizePath(file);
118
+ if (visiting.has(normalized)) {
119
+ const cycle = [...stack, normalized];
123
120
  const msg = `Circular require detected: ${cycle.join(' -> ')}`;
124
121
  if (mode === 'error') {
125
122
  throw new Error(msg);
@@ -128,22 +125,22 @@ const detectCircularRequireGraph = async (entryFile, mode, dirIndex) => {
128
125
  console.warn(msg);
129
126
  return;
130
127
  }
131
- if (visited.has(file)) return;
132
- visiting.add(file);
133
- stack.push(file);
134
- let deps = cache.get(file);
128
+ if (visited.has(normalized)) return;
129
+ visiting.add(normalized);
130
+ stack.push(normalized);
131
+ let deps = cache.get(normalized);
135
132
  if (!deps) {
136
- deps = await collectStaticRequires(file, dirIndex);
137
- cache.set(file, deps);
133
+ deps = await collectStaticRequires(normalized, dirIndex);
134
+ cache.set(normalized, deps);
138
135
  }
139
136
  for (const dep of deps) {
140
137
  await dfs(dep, stack);
141
138
  }
142
139
  stack.pop();
143
- visiting.delete(file);
144
- visited.add(file);
140
+ visiting.delete(normalized);
141
+ visited.add(normalized);
145
142
  };
146
- await dfs(entryFile, []);
143
+ await dfs(await normalizePath(entryFile), []);
147
144
  };
148
145
  const mergeUsageMaps = (target, source) => {
149
146
  for (const [pkg, usage] of source) {
@@ -186,12 +183,13 @@ const collectProjectDualPackageHazards = async (files, opts) => {
186
183
  return byFile;
187
184
  };
188
185
  exports.collectProjectDualPackageHazards = collectProjectDualPackageHazards;
189
- const defaultOptions = {
186
+ const createDefaultOptions = () => ({
190
187
  target: 'commonjs',
191
188
  sourceType: 'auto',
192
189
  transformSyntax: true,
193
190
  liveBindings: 'strict',
194
191
  rewriteSpecifier: undefined,
192
+ rewriteTemplateLiterals: 'allow',
195
193
  appendJsExtension: undefined,
196
194
  appendDirectoryIndex: 'index.js',
197
195
  dirFilename: 'inject',
@@ -210,12 +208,16 @@ const defaultOptions = {
210
208
  cwd: undefined,
211
209
  out: undefined,
212
210
  inPlace: false
213
- };
214
- const transform = async (filename, options = defaultOptions) => {
215
- const opts = {
216
- ...defaultOptions,
211
+ });
212
+ const transform = async (filename, options) => {
213
+ const base = createDefaultOptions();
214
+ const opts = options ? {
215
+ ...base,
217
216
  ...options,
218
217
  filePath: filename
218
+ } : {
219
+ ...base,
220
+ filePath: filename
219
221
  };
220
222
  const cwdBase = opts.cwd ? (0, _nodePath.resolve)(opts.cwd) : process.cwd();
221
223
  const appendMode = options?.appendJsExtension ?? (opts.target === 'module' ? 'relative-only' : 'off');
@@ -227,6 +229,10 @@ const transform = async (filename, options = defaultOptions) => {
227
229
  let source = await (0, _format.format)(code, ast, opts);
228
230
  if (opts.rewriteSpecifier || appendMode !== 'off' || dirIndex) {
229
231
  const code = await _specifier.specifier.updateSrc(source, (0, _lang.getLangFromExt)(filename), spec => {
232
+ if (spec.type === 'TemplateLiteral' && opts.rewriteTemplateLiterals === 'static-only') {
233
+ const node = spec.node;
234
+ if (node.expressions.length > 0) return;
235
+ }
230
236
  const normalized = normalizeBuiltinSpecifier(spec.value);
231
237
  const rewritten = rewriteSpecifierValue(normalized ?? spec.value, opts.rewriteSpecifier);
232
238
  const baseValue = rewritten ?? normalized ?? spec.value;
@@ -17,6 +17,8 @@ export type ModuleOptions = {
17
17
  liveBindings?: 'strict' | 'loose' | 'off';
18
18
  /** Rewrite import specifiers (e.g. add extensions). */
19
19
  rewriteSpecifier?: RewriteSpecifier;
20
+ /** Whether to rewrite template literals that contain expressions. Default allows rewrites; set to 'static-only' to skip interpolated templates. */
21
+ rewriteTemplateLiterals?: 'allow' | 'static-only';
20
22
  /** Whether to append .js to relative imports. */
21
23
  appendJsExtension?: 'off' | 'relative-only' | 'all';
22
24
  /** Add directory index (e.g. /index.js) or disable. */
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.builtinSpecifiers = void 0;
7
+ var _nodeModule = require("node:module");
8
+ const builtinSpecifiers = exports.builtinSpecifiers = new Set(_nodeModule.builtinModules.map(mod => mod.startsWith('node:') ? mod.slice(5) : mod).flatMap(mod => {
9
+ const parts = mod.split('/');
10
+ const base = parts[0];
11
+ return parts.length > 1 ? [mod, base] : [mod];
12
+ }));
package/dist/cli.js CHANGED
@@ -3,19 +3,20 @@ import { stdin as defaultStdin, stdout as defaultStdout, stderr as defaultStderr
3
3
  import { parseArgs } from 'node:util';
4
4
  import { readFile, mkdir } from 'node:fs/promises';
5
5
  import { dirname, resolve, relative, join } from 'node:path';
6
- import { builtinModules } from 'node:module';
7
6
  import { glob } from 'glob';
8
7
  import { transform, collectProjectDualPackageHazards } from './module.js';
9
8
  import { parse } from './parse.js';
10
9
  import { format } from './format.js';
11
10
  import { specifier } from './specifier.js';
12
11
  import { getLangFromExt } from './utils/lang.js';
12
+ import { builtinSpecifiers } from './utils/builtinSpecifiers.js';
13
13
  const defaultOptions = {
14
14
  target: 'commonjs',
15
15
  sourceType: 'auto',
16
16
  transformSyntax: true,
17
17
  liveBindings: 'strict',
18
18
  rewriteSpecifier: undefined,
19
+ rewriteTemplateLiterals: 'allow',
19
20
  appendJsExtension: undefined,
20
21
  appendDirectoryIndex: 'index.js',
21
22
  dirFilename: 'inject',
@@ -71,11 +72,6 @@ const colorize = enabled => {
71
72
  cyan: wrap(codes.cyan)
72
73
  };
73
74
  };
74
- const builtinSpecifiers = new Set(builtinModules.map(mod => mod.startsWith('node:') ? mod.slice(5) : mod).flatMap(mod => {
75
- const parts = mod.split('/');
76
- const base = parts[0];
77
- return parts.length > 1 ? [mod, base] : [mod];
78
- }));
79
75
  const collapseSpecifier = value => value.replace(/['"`+)\s]|new String\(/g, '');
80
76
  const appendExtensionIfNeeded = (value, mode, dirIndex) => {
81
77
  if (mode === 'off') return;
@@ -131,6 +127,11 @@ const optionsTable = [{
131
127
  short: 'r',
132
128
  type: 'string',
133
129
  desc: 'Rewrite import specifiers (.js/.mjs/.cjs/.ts/.mts/.cts)'
130
+ }, {
131
+ long: 'rewrite-template-literals',
132
+ short: undefined,
133
+ type: 'string',
134
+ desc: 'Rewrite template literals (allow|static-only)'
134
135
  }, {
135
136
  long: 'append-js-extension',
136
137
  short: 'j',
@@ -277,6 +278,7 @@ const parseAppendDirectoryIndex = value => {
277
278
  const toModuleOptions = values => {
278
279
  const target = parseEnum(values.target, ['module', 'commonjs']) ?? defaultOptions.target;
279
280
  const transformSyntax = parseTransformSyntax(values['transform-syntax']);
281
+ const rewriteTemplateLiterals = parseEnum(values['rewrite-template-literals'], ['allow', 'static-only']) ?? defaultOptions.rewriteTemplateLiterals;
280
282
  const appendJsExtension = parseEnum(values['append-js-extension'], ['off', 'relative-only', 'all']);
281
283
  const appendDirectoryIndex = parseAppendDirectoryIndex(values['append-directory-index']);
282
284
  const opts = {
@@ -284,6 +286,7 @@ const toModuleOptions = values => {
284
286
  target,
285
287
  transformSyntax,
286
288
  rewriteSpecifier: values['rewrite-specifier'] ?? undefined,
289
+ rewriteTemplateLiterals,
287
290
  appendJsExtension: appendJsExtension,
288
291
  appendDirectoryIndex,
289
292
  detectCircularRequires: parseEnum(values['detect-circular-requires'], ['off', 'warn', 'error']) ?? defaultOptions.detectCircularRequires,
@@ -341,6 +344,10 @@ const applySpecifierUpdates = async (source, filename, opts, appendMode, dirInde
341
344
  if (!opts.rewriteSpecifier && appendMode === 'off' && !dirIndex) return source;
342
345
  const lang = getLangFromExt(filename);
343
346
  const updated = await specifier.updateSrc(source, lang, spec => {
347
+ if (spec.type === 'TemplateLiteral' && opts.rewriteTemplateLiterals === 'static-only') {
348
+ const node = spec.node;
349
+ if (node.expressions.length > 0) return;
350
+ }
344
351
  const normalized = normalizeBuiltinSpecifier(spec.value);
345
352
  const rewritten = rewriteSpecifierValue(normalized ?? spec.value, opts.rewriteSpecifier);
346
353
  const baseValue = rewritten ?? normalized ?? spec.value;
package/dist/format.js CHANGED
@@ -1,4 +1,3 @@
1
- import { builtinModules } from 'node:module';
2
1
  import { dirname, join, resolve as pathResolve } from 'node:path';
3
2
  import { readFile as fsReadFile, stat as fsStat } from 'node:fs/promises';
4
3
  import MagicString from 'magic-string';
@@ -18,13 +17,9 @@ import { interopHelper } from './pipeline/interopHelpers.js';
18
17
  import { collectCjsExports } from './utils/exports.js';
19
18
  import { collectModuleIdentifiers } from './utils/identifiers.js';
20
19
  import { isValidUrl } from './utils/url.js';
20
+ import { builtinSpecifiers } from './utils/builtinSpecifiers.js';
21
21
  import { ancestorWalk } from './walk.js';
22
22
  const isRequireMainMember = (node, shadowed) => node.type === 'MemberExpression' && node.object.type === 'Identifier' && node.object.name === 'require' && !shadowed.has('require') && node.property.type === 'Identifier' && node.property.name === 'main';
23
- const builtinSpecifiers = new Set(builtinModules.map(mod => mod.startsWith('node:') ? mod.slice(5) : mod).flatMap(mod => {
24
- const parts = mod.split('/');
25
- const base = parts[0];
26
- return parts.length > 1 ? [mod, base] : [mod];
27
- }));
28
23
  const stripQuery = value => value.includes('?') || value.includes('#') ? value.split(/[?#]/)[0] ?? value : value;
29
24
  const packageFromSpecifier = spec => {
30
25
  const cleaned = stripQuery(spec);
package/dist/module.js CHANGED
@@ -4,18 +4,13 @@ import { specifier } from './specifier.js';
4
4
  import { parse } from './parse.js';
5
5
  import { format, collectDualPackageUsage, dualPackageHazardDiagnostics } from './format.js';
6
6
  import { getLangFromExt } from './utils/lang.js';
7
- import { builtinModules } from 'node:module';
8
7
  import { resolve as pathResolve, dirname as pathDirname, extname, join } from 'node:path';
9
- import { readFile as fsReadFile, stat } from 'node:fs/promises';
8
+ import { readFile as fsReadFile, stat, realpath } from 'node:fs/promises';
10
9
  import { parse as parseModule } from './parse.js';
11
10
  import { walk } from './walk.js';
12
11
  import { collectModuleIdentifiers } from './utils/identifiers.js';
12
+ import { builtinSpecifiers } from './utils/builtinSpecifiers.js';
13
13
  const collapseSpecifier = value => value.replace(/['"`+)\s]|new String\(/g, '');
14
- const builtinSpecifiers = new Set(builtinModules.map(mod => mod.startsWith('node:') ? mod.slice(5) : mod).flatMap(mod => {
15
- const parts = mod.split('/');
16
- const base = parts[0];
17
- return parts.length > 1 ? [mod, base] : [mod];
18
- }));
19
14
  const appendExtensionIfNeeded = (spec, mode, dirIndex, value = spec.value) => {
20
15
  if (mode === 'off') return;
21
16
  if (spec.type === 'TemplateLiteral') {
@@ -73,6 +68,7 @@ const fileExists = async candidate => {
73
68
  return false;
74
69
  }
75
70
  };
71
+ const normalizePath = async p => pathResolve(await realpath(p).catch(() => p));
76
72
  const resolveRequirePath = async (fromFile, spec, dirIndex) => {
77
73
  if (!spec.startsWith('./') && !spec.startsWith('../')) return null;
78
74
  const base = pathResolve(pathDirname(fromFile), spec);
@@ -81,11 +77,11 @@ const resolveRequirePath = async (fromFile, spec, dirIndex) => {
81
77
  if (ext) {
82
78
  candidates.push(base);
83
79
  } else {
84
- candidates.push(`${base}.js`, `${base}.cjs`, `${base}.mjs`);
80
+ candidates.push(`${base}.js`, `${base}.cjs`, `${base}.mjs`, `${base}.ts`, `${base}.mts`, `${base}.cts`);
85
81
  candidates.push(join(base, dirIndex));
86
82
  }
87
83
  for (const candidate of candidates) {
88
- if (await fileExists(candidate)) return candidate;
84
+ if (await fileExists(candidate)) return await normalizePath(candidate);
89
85
  }
90
86
  return null;
91
87
  };
@@ -115,8 +111,9 @@ const detectCircularRequireGraph = async (entryFile, mode, dirIndex) => {
115
111
  const visiting = new Set();
116
112
  const visited = new Set();
117
113
  const dfs = async (file, stack) => {
118
- if (visiting.has(file)) {
119
- const cycle = [...stack, file];
114
+ const normalized = await normalizePath(file);
115
+ if (visiting.has(normalized)) {
116
+ const cycle = [...stack, normalized];
120
117
  const msg = `Circular require detected: ${cycle.join(' -> ')}`;
121
118
  if (mode === 'error') {
122
119
  throw new Error(msg);
@@ -125,22 +122,22 @@ const detectCircularRequireGraph = async (entryFile, mode, dirIndex) => {
125
122
  console.warn(msg);
126
123
  return;
127
124
  }
128
- if (visited.has(file)) return;
129
- visiting.add(file);
130
- stack.push(file);
131
- let deps = cache.get(file);
125
+ if (visited.has(normalized)) return;
126
+ visiting.add(normalized);
127
+ stack.push(normalized);
128
+ let deps = cache.get(normalized);
132
129
  if (!deps) {
133
- deps = await collectStaticRequires(file, dirIndex);
134
- cache.set(file, deps);
130
+ deps = await collectStaticRequires(normalized, dirIndex);
131
+ cache.set(normalized, deps);
135
132
  }
136
133
  for (const dep of deps) {
137
134
  await dfs(dep, stack);
138
135
  }
139
136
  stack.pop();
140
- visiting.delete(file);
141
- visited.add(file);
137
+ visiting.delete(normalized);
138
+ visited.add(normalized);
142
139
  };
143
- await dfs(entryFile, []);
140
+ await dfs(await normalizePath(entryFile), []);
144
141
  };
145
142
  const mergeUsageMaps = (target, source) => {
146
143
  for (const [pkg, usage] of source) {
@@ -182,12 +179,13 @@ const collectProjectDualPackageHazards = async (files, opts) => {
182
179
  }
183
180
  return byFile;
184
181
  };
185
- const defaultOptions = {
182
+ const createDefaultOptions = () => ({
186
183
  target: 'commonjs',
187
184
  sourceType: 'auto',
188
185
  transformSyntax: true,
189
186
  liveBindings: 'strict',
190
187
  rewriteSpecifier: undefined,
188
+ rewriteTemplateLiterals: 'allow',
191
189
  appendJsExtension: undefined,
192
190
  appendDirectoryIndex: 'index.js',
193
191
  dirFilename: 'inject',
@@ -206,12 +204,16 @@ const defaultOptions = {
206
204
  cwd: undefined,
207
205
  out: undefined,
208
206
  inPlace: false
209
- };
210
- const transform = async (filename, options = defaultOptions) => {
211
- const opts = {
212
- ...defaultOptions,
207
+ });
208
+ const transform = async (filename, options) => {
209
+ const base = createDefaultOptions();
210
+ const opts = options ? {
211
+ ...base,
213
212
  ...options,
214
213
  filePath: filename
214
+ } : {
215
+ ...base,
216
+ filePath: filename
215
217
  };
216
218
  const cwdBase = opts.cwd ? resolve(opts.cwd) : process.cwd();
217
219
  const appendMode = options?.appendJsExtension ?? (opts.target === 'module' ? 'relative-only' : 'off');
@@ -223,6 +225,10 @@ const transform = async (filename, options = defaultOptions) => {
223
225
  let source = await format(code, ast, opts);
224
226
  if (opts.rewriteSpecifier || appendMode !== 'off' || dirIndex) {
225
227
  const code = await specifier.updateSrc(source, getLangFromExt(filename), spec => {
228
+ if (spec.type === 'TemplateLiteral' && opts.rewriteTemplateLiterals === 'static-only') {
229
+ const node = spec.node;
230
+ if (node.expressions.length > 0) return;
231
+ }
226
232
  const normalized = normalizeBuiltinSpecifier(spec.value);
227
233
  const rewritten = rewriteSpecifierValue(normalized ?? spec.value, opts.rewriteSpecifier);
228
234
  const baseValue = rewritten ?? normalized ?? spec.value;
package/dist/types.d.ts CHANGED
@@ -17,6 +17,8 @@ export type ModuleOptions = {
17
17
  liveBindings?: 'strict' | 'loose' | 'off';
18
18
  /** Rewrite import specifiers (e.g. add extensions). */
19
19
  rewriteSpecifier?: RewriteSpecifier;
20
+ /** Whether to rewrite template literals that contain expressions. Default allows rewrites; set to 'static-only' to skip interpolated templates. */
21
+ rewriteTemplateLiterals?: 'allow' | 'static-only';
20
22
  /** Whether to append .js to relative imports. */
21
23
  appendJsExtension?: 'off' | 'relative-only' | 'all';
22
24
  /** Add directory index (e.g. /index.js) or disable. */
@@ -0,0 +1,2 @@
1
+ declare const builtinSpecifiers: Set<string>;
2
+ export { builtinSpecifiers };
@@ -0,0 +1,7 @@
1
+ import { builtinModules } from 'node:module';
2
+ const builtinSpecifiers = new Set(builtinModules.map(mod => mod.startsWith('node:') ? mod.slice(5) : mod).flatMap(mod => {
3
+ const parts = mod.split('/');
4
+ const base = parts[0];
5
+ return parts.length > 1 ? [mod, base] : [mod];
6
+ }));
7
+ export { builtinSpecifiers };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@knighted/module",
3
- "version": "1.4.0-rc.2",
3
+ "version": "1.4.0",
4
4
  "description": "Bidirectional transform for ES modules and CommonJS.",
5
5
  "type": "module",
6
6
  "main": "dist/module.js",