@knighted/module 1.4.0-rc.0 → 1.4.0-rc.1
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 +3 -1
- package/dist/cjs/cli.cjs +21 -4
- package/dist/cjs/format.cjs +87 -38
- package/dist/cjs/format.d.cts +25 -3
- package/dist/cjs/module.cjs +44 -1
- package/dist/cjs/module.d.cts +3 -2
- package/dist/cjs/types.d.cts +2 -0
- package/dist/cli.js +22 -5
- package/dist/format.d.ts +25 -3
- package/dist/format.js +85 -38
- package/dist/module.d.ts +3 -2
- package/dist/module.js +44 -2
- package/dist/types.d.ts +2 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -131,6 +131,7 @@ type ModuleOptions = {
|
|
|
131
131
|
requireMainStrategy?: 'import-meta-main' | 'realpath'
|
|
132
132
|
detectCircularRequires?: 'off' | 'warn' | 'error'
|
|
133
133
|
detectDualPackageHazard?: 'off' | 'warn' | 'error'
|
|
134
|
+
dualPackageHazardScope?: 'file' | 'project'
|
|
134
135
|
requireSource?: 'builtin' | 'create-require'
|
|
135
136
|
importMetaPrelude?: 'off' | 'auto' | 'on'
|
|
136
137
|
cjsDefault?: 'module-exports' | 'auto' | 'none'
|
|
@@ -156,7 +157,8 @@ type ModuleOptions = {
|
|
|
156
157
|
- `requireMainStrategy` (`import-meta-main`): use `import.meta.main` or the realpath-based `pathToFileURL(realpathSync(process.argv[1])).href` check.
|
|
157
158
|
- `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.
|
|
158
159
|
- `detectCircularRequires` (`off`): optionally detect relative static require cycles and warn/throw.
|
|
159
|
-
- `detectDualPackageHazard` (`warn`): flag when
|
|
160
|
+
- `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
|
+
- `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.
|
|
160
162
|
- `topLevelAwait` (`error`): throw, wrap, or preserve when TLA appears in CommonJS output.
|
|
161
163
|
- `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.
|
|
162
164
|
- `requireSource` (`builtin`): whether `require` comes from Node or `createRequire`.
|
package/dist/cjs/cli.cjs
CHANGED
|
@@ -30,6 +30,7 @@ const defaultOptions = {
|
|
|
30
30
|
requireMainStrategy: 'import-meta-main',
|
|
31
31
|
detectCircularRequires: 'off',
|
|
32
32
|
detectDualPackageHazard: 'warn',
|
|
33
|
+
dualPackageHazardScope: 'file',
|
|
33
34
|
requireSource: 'builtin',
|
|
34
35
|
nestedRequireStrategy: 'create-require',
|
|
35
36
|
cjsDefault: 'auto',
|
|
@@ -156,6 +157,11 @@ const optionsTable = [{
|
|
|
156
157
|
short: 'H',
|
|
157
158
|
type: 'string',
|
|
158
159
|
desc: 'Warn/error on mixed import/require of dual packages (off|warn|error)'
|
|
160
|
+
}, {
|
|
161
|
+
long: 'dual-package-hazard-scope',
|
|
162
|
+
short: undefined,
|
|
163
|
+
type: 'string',
|
|
164
|
+
desc: 'Scope for dual package hazard detection (file|project)'
|
|
159
165
|
}, {
|
|
160
166
|
long: 'top-level-await',
|
|
161
167
|
short: 'a',
|
|
@@ -249,10 +255,10 @@ const optionsTable = [{
|
|
|
249
255
|
}];
|
|
250
256
|
const buildHelp = enableColor => {
|
|
251
257
|
const c = colorize(enableColor);
|
|
252
|
-
const maxFlagLength = Math.max(...optionsTable.map(opt => ` -${opt.short}, --${opt.long}`.length));
|
|
258
|
+
const maxFlagLength = Math.max(...optionsTable.map(opt => opt.short ? ` -${opt.short}, --${opt.long}`.length : ` --${opt.long}`.length));
|
|
253
259
|
const lines = [`${c.bold('Usage:')} dub [options] <files...>`, '', 'Examples:', ' dub -t module src/index.cjs --out-dir dist', ' dub -t commonjs src/**/*.mjs -p', ' cat input.cjs | dub -t module --stdin-filename input.cjs', '', 'Options:'];
|
|
254
260
|
for (const opt of optionsTable) {
|
|
255
|
-
const flag = ` -${opt.short}, --${opt.long}`;
|
|
261
|
+
const flag = opt.short ? ` -${opt.short}, --${opt.long}` : ` --${opt.long}`;
|
|
256
262
|
const pad = ' '.repeat(Math.max(2, maxFlagLength - flag.length + 2));
|
|
257
263
|
lines.push(`${c.bold(flag)}${pad}${opt.desc}`);
|
|
258
264
|
}
|
|
@@ -288,6 +294,7 @@ const toModuleOptions = values => {
|
|
|
288
294
|
appendDirectoryIndex,
|
|
289
295
|
detectCircularRequires: parseEnum(values['detect-circular-requires'], ['off', 'warn', 'error']) ?? defaultOptions.detectCircularRequires,
|
|
290
296
|
detectDualPackageHazard: parseEnum(values['detect-dual-package-hazard'], ['off', 'warn', 'error']) ?? defaultOptions.detectDualPackageHazard,
|
|
297
|
+
dualPackageHazardScope: parseEnum(values['dual-package-hazard-scope'], ['file', 'project']) ?? defaultOptions.dualPackageHazardScope,
|
|
291
298
|
topLevelAwait: parseEnum(values['top-level-await'], ['error', 'wrap', 'preserve']) ?? defaultOptions.topLevelAwait,
|
|
292
299
|
cjsDefault: parseEnum(values['cjs-default'], ['module-exports', 'auto', 'none']) ?? defaultOptions.cjsDefault,
|
|
293
300
|
idiomaticExports: parseEnum(values['idiomatic-exports'], ['off', 'safe', 'aggressive']) ?? defaultOptions.idiomaticExports,
|
|
@@ -374,6 +381,9 @@ const summarizeDiagnostics = diags => {
|
|
|
374
381
|
const runFiles = async (files, moduleOpts, io, flags) => {
|
|
375
382
|
const results = [];
|
|
376
383
|
const logger = makeLogger(io.stdout, io.stderr);
|
|
384
|
+
const hazardScope = moduleOpts.dualPackageHazardScope ?? 'file';
|
|
385
|
+
const hazardMode = moduleOpts.detectDualPackageHazard ?? 'warn';
|
|
386
|
+
const projectHazards = hazardScope === 'project' && hazardMode !== 'off' ? await (0, _module.collectProjectDualPackageHazards)(files, moduleOpts) : null;
|
|
377
387
|
for (const file of files) {
|
|
378
388
|
const diagnostics = [];
|
|
379
389
|
const original = await (0, _promises.readFile)(file, 'utf8');
|
|
@@ -383,7 +393,8 @@ const runFiles = async (files, moduleOpts, io, flags) => {
|
|
|
383
393
|
diagnostics: diag => diagnostics.push(diag),
|
|
384
394
|
out: undefined,
|
|
385
395
|
inPlace: false,
|
|
386
|
-
filePath: file
|
|
396
|
+
filePath: file,
|
|
397
|
+
detectDualPackageHazard: hazardScope === 'project' ? 'off' : moduleOpts.detectDualPackageHazard
|
|
387
398
|
};
|
|
388
399
|
let writeTarget;
|
|
389
400
|
if (!flags.dryRun && !flags.list) {
|
|
@@ -405,6 +416,10 @@ const runFiles = async (files, moduleOpts, io, flags) => {
|
|
|
405
416
|
}
|
|
406
417
|
const output = await (0, _module.transform)(file, perFileOpts);
|
|
407
418
|
const changed = output !== original;
|
|
419
|
+
if (projectHazards) {
|
|
420
|
+
const extras = projectHazards.get(file);
|
|
421
|
+
if (extras?.length) diagnostics.push(...extras);
|
|
422
|
+
}
|
|
408
423
|
if (flags.list && changed) {
|
|
409
424
|
logger.info(file);
|
|
410
425
|
}
|
|
@@ -454,7 +469,9 @@ const runCli = async ({
|
|
|
454
469
|
allowPositionals: true,
|
|
455
470
|
options: Object.fromEntries(optionsTable.map(opt => [opt.long, {
|
|
456
471
|
type: opt.type,
|
|
457
|
-
|
|
472
|
+
...(opt.short ? {
|
|
473
|
+
short: opt.short
|
|
474
|
+
} : {})
|
|
458
475
|
}]))
|
|
459
476
|
});
|
|
460
477
|
const logger = makeLogger(stdout, stderr);
|
package/dist/cjs/format.cjs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", {
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
|
-
exports.format = void 0;
|
|
6
|
+
exports.format = exports.dualPackageHazardDiagnostics = exports.collectDualPackageUsage = void 0;
|
|
7
7
|
var _nodeModule = require("node:module");
|
|
8
8
|
var _nodePath = require("node:path");
|
|
9
9
|
var _promises = require("node:fs/promises");
|
|
@@ -153,98 +153,147 @@ const describeDualPackage = pkgJson => {
|
|
|
153
153
|
requireTarget
|
|
154
154
|
};
|
|
155
155
|
};
|
|
156
|
-
const
|
|
157
|
-
const {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
hazardLevel,
|
|
161
|
-
filePath,
|
|
162
|
-
cwd,
|
|
163
|
-
diagOnce
|
|
164
|
-
} = params;
|
|
165
|
-
const usages = new Map();
|
|
166
|
-
const manifestCache = new Map();
|
|
167
|
-
const record = (pkg, kind, spec, subpath, loc) => {
|
|
168
|
-
const existing = usages.get(pkg) ?? {
|
|
169
|
-
imports: [],
|
|
170
|
-
requires: []
|
|
171
|
-
};
|
|
172
|
-
const bucket = kind === 'import' ? existing.imports : existing.requires;
|
|
173
|
-
bucket.push({
|
|
174
|
-
spec,
|
|
175
|
-
subpath,
|
|
176
|
-
loc
|
|
177
|
-
});
|
|
178
|
-
usages.set(pkg, existing);
|
|
156
|
+
const recordUsage = (usages, pkg, kind, spec, subpath, loc, filePath) => {
|
|
157
|
+
const existing = usages.get(pkg) ?? {
|
|
158
|
+
imports: [],
|
|
159
|
+
requires: []
|
|
179
160
|
};
|
|
161
|
+
const bucket = kind === 'import' ? existing.imports : existing.requires;
|
|
162
|
+
bucket.push({
|
|
163
|
+
spec,
|
|
164
|
+
subpath,
|
|
165
|
+
loc,
|
|
166
|
+
filePath
|
|
167
|
+
});
|
|
168
|
+
usages.set(pkg, existing);
|
|
169
|
+
};
|
|
170
|
+
const collectDualPackageUsage = async (program, shadowedBindings, filePath) => {
|
|
171
|
+
const usages = new Map();
|
|
180
172
|
await (0, _walk.ancestorWalk)(program, {
|
|
181
173
|
enter(node) {
|
|
182
174
|
if (node.type === 'ImportDeclaration' && node.source.type === 'Literal' && typeof node.source.value === 'string') {
|
|
183
175
|
const pkg = packageFromSpecifier(node.source.value);
|
|
184
|
-
if (pkg)
|
|
176
|
+
if (pkg) recordUsage(usages, pkg.pkg, 'import', node.source.value, pkg.subpath, {
|
|
185
177
|
start: node.source.start,
|
|
186
178
|
end: node.source.end
|
|
187
|
-
});
|
|
179
|
+
}, filePath);
|
|
188
180
|
}
|
|
189
181
|
if (node.type === 'ExportNamedDeclaration' && node.source && node.source.type === 'Literal' && typeof node.source.value === 'string') {
|
|
190
182
|
const pkg = packageFromSpecifier(node.source.value);
|
|
191
|
-
if (pkg)
|
|
183
|
+
if (pkg) recordUsage(usages, pkg.pkg, 'import', node.source.value, pkg.subpath, {
|
|
192
184
|
start: node.source.start,
|
|
193
185
|
end: node.source.end
|
|
194
|
-
});
|
|
186
|
+
}, filePath);
|
|
195
187
|
}
|
|
196
188
|
if (node.type === 'ExportAllDeclaration' && node.source.type === 'Literal' && typeof node.source.value === 'string') {
|
|
197
189
|
const pkg = packageFromSpecifier(node.source.value);
|
|
198
|
-
if (pkg)
|
|
190
|
+
if (pkg) recordUsage(usages, pkg.pkg, 'import', node.source.value, pkg.subpath, {
|
|
199
191
|
start: node.source.start,
|
|
200
192
|
end: node.source.end
|
|
201
|
-
});
|
|
193
|
+
}, filePath);
|
|
202
194
|
}
|
|
203
195
|
if (node.type === 'ImportExpression' && node.source.type === 'Literal' && typeof node.source.value === 'string') {
|
|
204
196
|
const pkg = packageFromSpecifier(node.source.value);
|
|
205
|
-
if (pkg)
|
|
197
|
+
if (pkg) recordUsage(usages, pkg.pkg, 'import', node.source.value, pkg.subpath, {
|
|
206
198
|
start: node.source.start,
|
|
207
199
|
end: node.source.end
|
|
208
|
-
});
|
|
200
|
+
}, filePath);
|
|
209
201
|
}
|
|
210
202
|
if (node.type === 'CallExpression' && (0, _lowerCjsRequireToImports.isStaticRequire)(node, shadowedBindings)) {
|
|
211
203
|
const arg = node.arguments[0];
|
|
212
204
|
if (arg?.type === 'Literal' && typeof arg.value === 'string') {
|
|
213
205
|
const pkg = packageFromSpecifier(arg.value);
|
|
214
|
-
if (pkg)
|
|
206
|
+
if (pkg) recordUsage(usages, pkg.pkg, 'require', arg.value, pkg.subpath, {
|
|
215
207
|
start: arg.start,
|
|
216
208
|
end: arg.end
|
|
217
|
-
});
|
|
209
|
+
}, filePath);
|
|
218
210
|
}
|
|
219
211
|
}
|
|
220
212
|
}
|
|
221
213
|
});
|
|
214
|
+
return usages;
|
|
215
|
+
};
|
|
216
|
+
exports.collectDualPackageUsage = collectDualPackageUsage;
|
|
217
|
+
const dualPackageHazardDiagnostics = async params => {
|
|
218
|
+
const {
|
|
219
|
+
usages,
|
|
220
|
+
hazardLevel,
|
|
221
|
+
filePath,
|
|
222
|
+
cwd
|
|
223
|
+
} = params;
|
|
224
|
+
const manifestCache = params.manifestCache ?? new Map();
|
|
225
|
+
const diags = [];
|
|
222
226
|
for (const [pkg, usage] of usages) {
|
|
223
227
|
const hasImport = usage.imports.length > 0;
|
|
224
228
|
const hasRequire = usage.requires.length > 0;
|
|
225
229
|
const combined = [...usage.imports, ...usage.requires];
|
|
226
230
|
const hasRoot = combined.some(entry => !entry.subpath);
|
|
227
231
|
const hasSubpath = combined.some(entry => Boolean(entry.subpath));
|
|
232
|
+
const origin = usage.imports[0] ?? usage.requires[0];
|
|
233
|
+
const diagFile = origin?.filePath ?? filePath;
|
|
228
234
|
if (hasImport && hasRequire) {
|
|
229
235
|
const importSpecs = usage.imports.map(u => u.subpath ? `${pkg}/${u.subpath}` : pkg);
|
|
230
236
|
const requireSpecs = usage.requires.map(u => u.subpath ? `${pkg}/${u.subpath}` : pkg);
|
|
231
|
-
|
|
237
|
+
diags.push({
|
|
238
|
+
level: hazardLevel,
|
|
239
|
+
code: 'dual-package-mixed-specifiers',
|
|
240
|
+
message: `Package '${pkg}' is loaded via import (${importSpecs.join(', ')}) and require (${requireSpecs.join(', ')}); conditional exports can instantiate it twice.`,
|
|
241
|
+
filePath: diagFile,
|
|
242
|
+
loc: origin?.loc
|
|
243
|
+
});
|
|
232
244
|
}
|
|
233
245
|
if (hasRoot && hasSubpath) {
|
|
234
246
|
const subpaths = combined.filter(entry => entry.subpath).map(entry => `${pkg}/${entry.subpath}`);
|
|
235
|
-
|
|
247
|
+
const originSubpath = combined.find(entry => entry.subpath) ?? combined[0];
|
|
248
|
+
diags.push({
|
|
249
|
+
level: hazardLevel,
|
|
250
|
+
code: 'dual-package-subpath',
|
|
251
|
+
message: `Package '${pkg}' is referenced via root specifier '${pkg}' and subpath(s) ${subpaths.join(', ')}; mixing them loads separate module instances.`,
|
|
252
|
+
filePath: originSubpath?.filePath ?? filePath,
|
|
253
|
+
loc: originSubpath?.loc
|
|
254
|
+
});
|
|
236
255
|
}
|
|
237
256
|
if (hasImport && hasRequire) {
|
|
238
|
-
const manifest = await readPackageManifest(pkg,
|
|
257
|
+
const manifest = await readPackageManifest(pkg, diagFile, cwd, manifestCache);
|
|
239
258
|
if (manifest) {
|
|
240
259
|
const meta = describeDualPackage(manifest);
|
|
241
260
|
if (meta.hasHazardSignals) {
|
|
242
261
|
const detail = meta.details.length ? ` (${meta.details.join('; ')})` : '';
|
|
243
|
-
|
|
262
|
+
diags.push({
|
|
263
|
+
level: hazardLevel,
|
|
264
|
+
code: 'dual-package-conditional-exports',
|
|
265
|
+
message: `Package '${pkg}' exposes different entry points for import vs require${detail}. Mixed usage can produce distinct instances.`,
|
|
266
|
+
filePath: diagFile,
|
|
267
|
+
loc: origin?.loc
|
|
268
|
+
});
|
|
244
269
|
}
|
|
245
270
|
}
|
|
246
271
|
}
|
|
247
272
|
}
|
|
273
|
+
return diags;
|
|
274
|
+
};
|
|
275
|
+
exports.dualPackageHazardDiagnostics = dualPackageHazardDiagnostics;
|
|
276
|
+
const detectDualPackageHazards = async params => {
|
|
277
|
+
const {
|
|
278
|
+
program,
|
|
279
|
+
shadowedBindings,
|
|
280
|
+
hazardLevel,
|
|
281
|
+
filePath,
|
|
282
|
+
cwd,
|
|
283
|
+
diagOnce
|
|
284
|
+
} = params;
|
|
285
|
+
const manifestCache = new Map();
|
|
286
|
+
const usages = await collectDualPackageUsage(program, shadowedBindings, filePath);
|
|
287
|
+
const diags = await dualPackageHazardDiagnostics({
|
|
288
|
+
usages,
|
|
289
|
+
hazardLevel,
|
|
290
|
+
filePath,
|
|
291
|
+
cwd,
|
|
292
|
+
manifestCache
|
|
293
|
+
});
|
|
294
|
+
for (const diag of diags) {
|
|
295
|
+
diagOnce(diag.level, diag.code, diag.message, diag.loc);
|
|
296
|
+
}
|
|
248
297
|
};
|
|
249
298
|
|
|
250
299
|
/**
|
package/dist/cjs/format.d.cts
CHANGED
|
@@ -1,9 +1,31 @@
|
|
|
1
|
-
import type { ParseResult } from 'oxc-parser';
|
|
2
|
-
import type { FormatterOptions } from './types.cjs';
|
|
1
|
+
import type { Node, ParseResult } from 'oxc-parser';
|
|
2
|
+
import type { Diagnostic, FormatterOptions } from './types.cjs';
|
|
3
|
+
type HazardLevel = 'warning' | 'error';
|
|
4
|
+
export type PackageUse = {
|
|
5
|
+
spec: string;
|
|
6
|
+
subpath: string;
|
|
7
|
+
loc?: {
|
|
8
|
+
start: number;
|
|
9
|
+
end: number;
|
|
10
|
+
};
|
|
11
|
+
filePath?: string;
|
|
12
|
+
};
|
|
13
|
+
export type PackageUsage = {
|
|
14
|
+
imports: PackageUse[];
|
|
15
|
+
requires: PackageUse[];
|
|
16
|
+
};
|
|
17
|
+
declare const collectDualPackageUsage: (program: Node, shadowedBindings: Set<string>, filePath?: string) => Promise<Map<string, PackageUsage>>;
|
|
18
|
+
declare const dualPackageHazardDiagnostics: (params: {
|
|
19
|
+
usages: Map<string, PackageUsage>;
|
|
20
|
+
hazardLevel: HazardLevel;
|
|
21
|
+
filePath?: string;
|
|
22
|
+
cwd?: string;
|
|
23
|
+
manifestCache?: Map<string, any | null>;
|
|
24
|
+
}) => Promise<Diagnostic[]>;
|
|
3
25
|
/**
|
|
4
26
|
* Node added support for import.meta.main.
|
|
5
27
|
* Added in: v24.2.0, v22.18.0
|
|
6
28
|
* @see https://nodejs.org/api/esm.html#importmetamain
|
|
7
29
|
*/
|
|
8
30
|
declare const format: (src: string, ast: ParseResult, opts: FormatterOptions) => Promise<string>;
|
|
9
|
-
export { format };
|
|
31
|
+
export { format, collectDualPackageUsage, dualPackageHazardDiagnostics };
|
package/dist/cjs/module.cjs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", {
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
|
-
exports.transform = void 0;
|
|
6
|
+
exports.transform = exports.collectProjectDualPackageHazards = void 0;
|
|
7
7
|
var _nodePath = require("node:path");
|
|
8
8
|
var _promises = require("node:fs/promises");
|
|
9
9
|
var _specifier = require("./specifier.cjs");
|
|
@@ -12,6 +12,7 @@ var _format = require("./format.cjs");
|
|
|
12
12
|
var _lang = require("./utils/lang.cjs");
|
|
13
13
|
var _nodeModule = require("node:module");
|
|
14
14
|
var _walk = require("./walk.cjs");
|
|
15
|
+
var _identifiers = require("./utils/identifiers.cjs");
|
|
15
16
|
const collapseSpecifier = value => value.replace(/['"`+)\s]|new String\(/g, '');
|
|
16
17
|
const builtinSpecifiers = new Set(_nodeModule.builtinModules.map(mod => mod.startsWith('node:') ? mod.slice(5) : mod).flatMap(mod => {
|
|
17
18
|
const parts = mod.split('/');
|
|
@@ -144,6 +145,47 @@ const detectCircularRequireGraph = async (entryFile, mode, dirIndex) => {
|
|
|
144
145
|
};
|
|
145
146
|
await dfs(entryFile, []);
|
|
146
147
|
};
|
|
148
|
+
const mergeUsageMaps = (target, source) => {
|
|
149
|
+
for (const [pkg, usage] of source) {
|
|
150
|
+
const existing = target.get(pkg) ?? {
|
|
151
|
+
imports: [],
|
|
152
|
+
requires: []
|
|
153
|
+
};
|
|
154
|
+
existing.imports.push(...usage.imports);
|
|
155
|
+
existing.requires.push(...usage.requires);
|
|
156
|
+
target.set(pkg, existing);
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
const collectProjectDualPackageHazards = async (files, opts) => {
|
|
160
|
+
const hazardMode = opts.detectDualPackageHazard ?? 'warn';
|
|
161
|
+
if (hazardMode === 'off') return new Map();
|
|
162
|
+
const hazardLevel = hazardMode === 'error' ? 'error' : 'warning';
|
|
163
|
+
const usages = new Map();
|
|
164
|
+
const manifestCache = new Map();
|
|
165
|
+
for (const file of files) {
|
|
166
|
+
const code = await (0, _promises.readFile)(file, 'utf8');
|
|
167
|
+
const ast = (0, _parse.parse)(file, code);
|
|
168
|
+
const moduleIdentifiers = await (0, _identifiers.collectModuleIdentifiers)(ast.program);
|
|
169
|
+
const shadowedBindings = new Set([...moduleIdentifiers.entries()].filter(([, meta]) => meta.declare.length > 0).map(([name]) => name));
|
|
170
|
+
const perFileUsage = await (0, _format.collectDualPackageUsage)(ast.program, shadowedBindings, file);
|
|
171
|
+
mergeUsageMaps(usages, perFileUsage);
|
|
172
|
+
}
|
|
173
|
+
const diags = await (0, _format.dualPackageHazardDiagnostics)({
|
|
174
|
+
usages,
|
|
175
|
+
hazardLevel,
|
|
176
|
+
cwd: opts.cwd,
|
|
177
|
+
manifestCache
|
|
178
|
+
});
|
|
179
|
+
const byFile = new Map();
|
|
180
|
+
for (const diag of diags) {
|
|
181
|
+
const key = diag.filePath ?? files[0];
|
|
182
|
+
const existing = byFile.get(key) ?? [];
|
|
183
|
+
existing.push(diag);
|
|
184
|
+
byFile.set(key, existing);
|
|
185
|
+
}
|
|
186
|
+
return byFile;
|
|
187
|
+
};
|
|
188
|
+
exports.collectProjectDualPackageHazards = collectProjectDualPackageHazards;
|
|
147
189
|
const defaultOptions = {
|
|
148
190
|
target: 'commonjs',
|
|
149
191
|
sourceType: 'auto',
|
|
@@ -158,6 +200,7 @@ const defaultOptions = {
|
|
|
158
200
|
requireMainStrategy: 'import-meta-main',
|
|
159
201
|
detectCircularRequires: 'off',
|
|
160
202
|
detectDualPackageHazard: 'warn',
|
|
203
|
+
dualPackageHazardScope: 'file',
|
|
161
204
|
requireSource: 'builtin',
|
|
162
205
|
nestedRequireStrategy: 'create-require',
|
|
163
206
|
cjsDefault: 'auto',
|
package/dist/cjs/module.d.cts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
-
import type { ModuleOptions } from './types.cjs';
|
|
1
|
+
import type { ModuleOptions, Diagnostic } from './types.cjs';
|
|
2
|
+
declare const collectProjectDualPackageHazards: (files: string[], opts: ModuleOptions) => Promise<Map<string, Diagnostic[]>>;
|
|
2
3
|
declare const transform: (filename: string, options?: ModuleOptions) => Promise<string>;
|
|
3
|
-
export { transform };
|
|
4
|
+
export { transform, collectProjectDualPackageHazards };
|
package/dist/cjs/types.d.cts
CHANGED
|
@@ -34,6 +34,8 @@ export type ModuleOptions = {
|
|
|
34
34
|
detectCircularRequires?: 'off' | 'warn' | 'error';
|
|
35
35
|
/** Detect divergent import/require usage of the same dual package (default warn). */
|
|
36
36
|
detectDualPackageHazard?: 'off' | 'warn' | 'error';
|
|
37
|
+
/** Scope for dual package hazard detection. */
|
|
38
|
+
dualPackageHazardScope?: 'file' | 'project';
|
|
37
39
|
/** Source used to provide require in ESM output. */
|
|
38
40
|
requireSource?: 'builtin' | 'create-require';
|
|
39
41
|
/** How to rewrite nested or non-hoistable require calls. */
|
package/dist/cli.js
CHANGED
|
@@ -5,7 +5,7 @@ import { readFile, mkdir } from 'node:fs/promises';
|
|
|
5
5
|
import { dirname, resolve, relative, join } from 'node:path';
|
|
6
6
|
import { builtinModules } from 'node:module';
|
|
7
7
|
import { glob } from 'glob';
|
|
8
|
-
import { transform } from './module.js';
|
|
8
|
+
import { transform, collectProjectDualPackageHazards } from './module.js';
|
|
9
9
|
import { parse } from './parse.js';
|
|
10
10
|
import { format } from './format.js';
|
|
11
11
|
import { specifier } from './specifier.js';
|
|
@@ -24,6 +24,7 @@ const defaultOptions = {
|
|
|
24
24
|
requireMainStrategy: 'import-meta-main',
|
|
25
25
|
detectCircularRequires: 'off',
|
|
26
26
|
detectDualPackageHazard: 'warn',
|
|
27
|
+
dualPackageHazardScope: 'file',
|
|
27
28
|
requireSource: 'builtin',
|
|
28
29
|
nestedRequireStrategy: 'create-require',
|
|
29
30
|
cjsDefault: 'auto',
|
|
@@ -150,6 +151,11 @@ const optionsTable = [{
|
|
|
150
151
|
short: 'H',
|
|
151
152
|
type: 'string',
|
|
152
153
|
desc: 'Warn/error on mixed import/require of dual packages (off|warn|error)'
|
|
154
|
+
}, {
|
|
155
|
+
long: 'dual-package-hazard-scope',
|
|
156
|
+
short: undefined,
|
|
157
|
+
type: 'string',
|
|
158
|
+
desc: 'Scope for dual package hazard detection (file|project)'
|
|
153
159
|
}, {
|
|
154
160
|
long: 'top-level-await',
|
|
155
161
|
short: 'a',
|
|
@@ -243,10 +249,10 @@ const optionsTable = [{
|
|
|
243
249
|
}];
|
|
244
250
|
const buildHelp = enableColor => {
|
|
245
251
|
const c = colorize(enableColor);
|
|
246
|
-
const maxFlagLength = Math.max(...optionsTable.map(opt => ` -${opt.short}, --${opt.long}`.length));
|
|
252
|
+
const maxFlagLength = Math.max(...optionsTable.map(opt => opt.short ? ` -${opt.short}, --${opt.long}`.length : ` --${opt.long}`.length));
|
|
247
253
|
const lines = [`${c.bold('Usage:')} dub [options] <files...>`, '', 'Examples:', ' dub -t module src/index.cjs --out-dir dist', ' dub -t commonjs src/**/*.mjs -p', ' cat input.cjs | dub -t module --stdin-filename input.cjs', '', 'Options:'];
|
|
248
254
|
for (const opt of optionsTable) {
|
|
249
|
-
const flag = ` -${opt.short}, --${opt.long}`;
|
|
255
|
+
const flag = opt.short ? ` -${opt.short}, --${opt.long}` : ` --${opt.long}`;
|
|
250
256
|
const pad = ' '.repeat(Math.max(2, maxFlagLength - flag.length + 2));
|
|
251
257
|
lines.push(`${c.bold(flag)}${pad}${opt.desc}`);
|
|
252
258
|
}
|
|
@@ -282,6 +288,7 @@ const toModuleOptions = values => {
|
|
|
282
288
|
appendDirectoryIndex,
|
|
283
289
|
detectCircularRequires: parseEnum(values['detect-circular-requires'], ['off', 'warn', 'error']) ?? defaultOptions.detectCircularRequires,
|
|
284
290
|
detectDualPackageHazard: parseEnum(values['detect-dual-package-hazard'], ['off', 'warn', 'error']) ?? defaultOptions.detectDualPackageHazard,
|
|
291
|
+
dualPackageHazardScope: parseEnum(values['dual-package-hazard-scope'], ['file', 'project']) ?? defaultOptions.dualPackageHazardScope,
|
|
285
292
|
topLevelAwait: parseEnum(values['top-level-await'], ['error', 'wrap', 'preserve']) ?? defaultOptions.topLevelAwait,
|
|
286
293
|
cjsDefault: parseEnum(values['cjs-default'], ['module-exports', 'auto', 'none']) ?? defaultOptions.cjsDefault,
|
|
287
294
|
idiomaticExports: parseEnum(values['idiomatic-exports'], ['off', 'safe', 'aggressive']) ?? defaultOptions.idiomaticExports,
|
|
@@ -368,6 +375,9 @@ const summarizeDiagnostics = diags => {
|
|
|
368
375
|
const runFiles = async (files, moduleOpts, io, flags) => {
|
|
369
376
|
const results = [];
|
|
370
377
|
const logger = makeLogger(io.stdout, io.stderr);
|
|
378
|
+
const hazardScope = moduleOpts.dualPackageHazardScope ?? 'file';
|
|
379
|
+
const hazardMode = moduleOpts.detectDualPackageHazard ?? 'warn';
|
|
380
|
+
const projectHazards = hazardScope === 'project' && hazardMode !== 'off' ? await collectProjectDualPackageHazards(files, moduleOpts) : null;
|
|
371
381
|
for (const file of files) {
|
|
372
382
|
const diagnostics = [];
|
|
373
383
|
const original = await readFile(file, 'utf8');
|
|
@@ -377,7 +387,8 @@ const runFiles = async (files, moduleOpts, io, flags) => {
|
|
|
377
387
|
diagnostics: diag => diagnostics.push(diag),
|
|
378
388
|
out: undefined,
|
|
379
389
|
inPlace: false,
|
|
380
|
-
filePath: file
|
|
390
|
+
filePath: file,
|
|
391
|
+
detectDualPackageHazard: hazardScope === 'project' ? 'off' : moduleOpts.detectDualPackageHazard
|
|
381
392
|
};
|
|
382
393
|
let writeTarget;
|
|
383
394
|
if (!flags.dryRun && !flags.list) {
|
|
@@ -399,6 +410,10 @@ const runFiles = async (files, moduleOpts, io, flags) => {
|
|
|
399
410
|
}
|
|
400
411
|
const output = await transform(file, perFileOpts);
|
|
401
412
|
const changed = output !== original;
|
|
413
|
+
if (projectHazards) {
|
|
414
|
+
const extras = projectHazards.get(file);
|
|
415
|
+
if (extras?.length) diagnostics.push(...extras);
|
|
416
|
+
}
|
|
402
417
|
if (flags.list && changed) {
|
|
403
418
|
logger.info(file);
|
|
404
419
|
}
|
|
@@ -448,7 +463,9 @@ const runCli = async ({
|
|
|
448
463
|
allowPositionals: true,
|
|
449
464
|
options: Object.fromEntries(optionsTable.map(opt => [opt.long, {
|
|
450
465
|
type: opt.type,
|
|
451
|
-
|
|
466
|
+
...(opt.short ? {
|
|
467
|
+
short: opt.short
|
|
468
|
+
} : {})
|
|
452
469
|
}]))
|
|
453
470
|
});
|
|
454
471
|
const logger = makeLogger(stdout, stderr);
|
package/dist/format.d.ts
CHANGED
|
@@ -1,9 +1,31 @@
|
|
|
1
|
-
import type { ParseResult } from 'oxc-parser';
|
|
2
|
-
import type { FormatterOptions } from './types.js';
|
|
1
|
+
import type { Node, ParseResult } from 'oxc-parser';
|
|
2
|
+
import type { Diagnostic, FormatterOptions } from './types.js';
|
|
3
|
+
type HazardLevel = 'warning' | 'error';
|
|
4
|
+
export type PackageUse = {
|
|
5
|
+
spec: string;
|
|
6
|
+
subpath: string;
|
|
7
|
+
loc?: {
|
|
8
|
+
start: number;
|
|
9
|
+
end: number;
|
|
10
|
+
};
|
|
11
|
+
filePath?: string;
|
|
12
|
+
};
|
|
13
|
+
export type PackageUsage = {
|
|
14
|
+
imports: PackageUse[];
|
|
15
|
+
requires: PackageUse[];
|
|
16
|
+
};
|
|
17
|
+
declare const collectDualPackageUsage: (program: Node, shadowedBindings: Set<string>, filePath?: string) => Promise<Map<string, PackageUsage>>;
|
|
18
|
+
declare const dualPackageHazardDiagnostics: (params: {
|
|
19
|
+
usages: Map<string, PackageUsage>;
|
|
20
|
+
hazardLevel: HazardLevel;
|
|
21
|
+
filePath?: string;
|
|
22
|
+
cwd?: string;
|
|
23
|
+
manifestCache?: Map<string, any | null>;
|
|
24
|
+
}) => Promise<Diagnostic[]>;
|
|
3
25
|
/**
|
|
4
26
|
* Node added support for import.meta.main.
|
|
5
27
|
* Added in: v24.2.0, v22.18.0
|
|
6
28
|
* @see https://nodejs.org/api/esm.html#importmetamain
|
|
7
29
|
*/
|
|
8
30
|
declare const format: (src: string, ast: ParseResult, opts: FormatterOptions) => Promise<string>;
|
|
9
|
-
export { format };
|
|
31
|
+
export { format, collectDualPackageUsage, dualPackageHazardDiagnostics };
|
package/dist/format.js
CHANGED
|
@@ -146,98 +146,145 @@ const describeDualPackage = pkgJson => {
|
|
|
146
146
|
requireTarget
|
|
147
147
|
};
|
|
148
148
|
};
|
|
149
|
-
const
|
|
150
|
-
const {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
hazardLevel,
|
|
154
|
-
filePath,
|
|
155
|
-
cwd,
|
|
156
|
-
diagOnce
|
|
157
|
-
} = params;
|
|
158
|
-
const usages = new Map();
|
|
159
|
-
const manifestCache = new Map();
|
|
160
|
-
const record = (pkg, kind, spec, subpath, loc) => {
|
|
161
|
-
const existing = usages.get(pkg) ?? {
|
|
162
|
-
imports: [],
|
|
163
|
-
requires: []
|
|
164
|
-
};
|
|
165
|
-
const bucket = kind === 'import' ? existing.imports : existing.requires;
|
|
166
|
-
bucket.push({
|
|
167
|
-
spec,
|
|
168
|
-
subpath,
|
|
169
|
-
loc
|
|
170
|
-
});
|
|
171
|
-
usages.set(pkg, existing);
|
|
149
|
+
const recordUsage = (usages, pkg, kind, spec, subpath, loc, filePath) => {
|
|
150
|
+
const existing = usages.get(pkg) ?? {
|
|
151
|
+
imports: [],
|
|
152
|
+
requires: []
|
|
172
153
|
};
|
|
154
|
+
const bucket = kind === 'import' ? existing.imports : existing.requires;
|
|
155
|
+
bucket.push({
|
|
156
|
+
spec,
|
|
157
|
+
subpath,
|
|
158
|
+
loc,
|
|
159
|
+
filePath
|
|
160
|
+
});
|
|
161
|
+
usages.set(pkg, existing);
|
|
162
|
+
};
|
|
163
|
+
const collectDualPackageUsage = async (program, shadowedBindings, filePath) => {
|
|
164
|
+
const usages = new Map();
|
|
173
165
|
await ancestorWalk(program, {
|
|
174
166
|
enter(node) {
|
|
175
167
|
if (node.type === 'ImportDeclaration' && node.source.type === 'Literal' && typeof node.source.value === 'string') {
|
|
176
168
|
const pkg = packageFromSpecifier(node.source.value);
|
|
177
|
-
if (pkg)
|
|
169
|
+
if (pkg) recordUsage(usages, pkg.pkg, 'import', node.source.value, pkg.subpath, {
|
|
178
170
|
start: node.source.start,
|
|
179
171
|
end: node.source.end
|
|
180
|
-
});
|
|
172
|
+
}, filePath);
|
|
181
173
|
}
|
|
182
174
|
if (node.type === 'ExportNamedDeclaration' && node.source && node.source.type === 'Literal' && typeof node.source.value === 'string') {
|
|
183
175
|
const pkg = packageFromSpecifier(node.source.value);
|
|
184
|
-
if (pkg)
|
|
176
|
+
if (pkg) recordUsage(usages, pkg.pkg, 'import', node.source.value, pkg.subpath, {
|
|
185
177
|
start: node.source.start,
|
|
186
178
|
end: node.source.end
|
|
187
|
-
});
|
|
179
|
+
}, filePath);
|
|
188
180
|
}
|
|
189
181
|
if (node.type === 'ExportAllDeclaration' && node.source.type === 'Literal' && typeof node.source.value === 'string') {
|
|
190
182
|
const pkg = packageFromSpecifier(node.source.value);
|
|
191
|
-
if (pkg)
|
|
183
|
+
if (pkg) recordUsage(usages, pkg.pkg, 'import', node.source.value, pkg.subpath, {
|
|
192
184
|
start: node.source.start,
|
|
193
185
|
end: node.source.end
|
|
194
|
-
});
|
|
186
|
+
}, filePath);
|
|
195
187
|
}
|
|
196
188
|
if (node.type === 'ImportExpression' && node.source.type === 'Literal' && typeof node.source.value === 'string') {
|
|
197
189
|
const pkg = packageFromSpecifier(node.source.value);
|
|
198
|
-
if (pkg)
|
|
190
|
+
if (pkg) recordUsage(usages, pkg.pkg, 'import', node.source.value, pkg.subpath, {
|
|
199
191
|
start: node.source.start,
|
|
200
192
|
end: node.source.end
|
|
201
|
-
});
|
|
193
|
+
}, filePath);
|
|
202
194
|
}
|
|
203
195
|
if (node.type === 'CallExpression' && isStaticRequire(node, shadowedBindings)) {
|
|
204
196
|
const arg = node.arguments[0];
|
|
205
197
|
if (arg?.type === 'Literal' && typeof arg.value === 'string') {
|
|
206
198
|
const pkg = packageFromSpecifier(arg.value);
|
|
207
|
-
if (pkg)
|
|
199
|
+
if (pkg) recordUsage(usages, pkg.pkg, 'require', arg.value, pkg.subpath, {
|
|
208
200
|
start: arg.start,
|
|
209
201
|
end: arg.end
|
|
210
|
-
});
|
|
202
|
+
}, filePath);
|
|
211
203
|
}
|
|
212
204
|
}
|
|
213
205
|
}
|
|
214
206
|
});
|
|
207
|
+
return usages;
|
|
208
|
+
};
|
|
209
|
+
const dualPackageHazardDiagnostics = async params => {
|
|
210
|
+
const {
|
|
211
|
+
usages,
|
|
212
|
+
hazardLevel,
|
|
213
|
+
filePath,
|
|
214
|
+
cwd
|
|
215
|
+
} = params;
|
|
216
|
+
const manifestCache = params.manifestCache ?? new Map();
|
|
217
|
+
const diags = [];
|
|
215
218
|
for (const [pkg, usage] of usages) {
|
|
216
219
|
const hasImport = usage.imports.length > 0;
|
|
217
220
|
const hasRequire = usage.requires.length > 0;
|
|
218
221
|
const combined = [...usage.imports, ...usage.requires];
|
|
219
222
|
const hasRoot = combined.some(entry => !entry.subpath);
|
|
220
223
|
const hasSubpath = combined.some(entry => Boolean(entry.subpath));
|
|
224
|
+
const origin = usage.imports[0] ?? usage.requires[0];
|
|
225
|
+
const diagFile = origin?.filePath ?? filePath;
|
|
221
226
|
if (hasImport && hasRequire) {
|
|
222
227
|
const importSpecs = usage.imports.map(u => u.subpath ? `${pkg}/${u.subpath}` : pkg);
|
|
223
228
|
const requireSpecs = usage.requires.map(u => u.subpath ? `${pkg}/${u.subpath}` : pkg);
|
|
224
|
-
|
|
229
|
+
diags.push({
|
|
230
|
+
level: hazardLevel,
|
|
231
|
+
code: 'dual-package-mixed-specifiers',
|
|
232
|
+
message: `Package '${pkg}' is loaded via import (${importSpecs.join(', ')}) and require (${requireSpecs.join(', ')}); conditional exports can instantiate it twice.`,
|
|
233
|
+
filePath: diagFile,
|
|
234
|
+
loc: origin?.loc
|
|
235
|
+
});
|
|
225
236
|
}
|
|
226
237
|
if (hasRoot && hasSubpath) {
|
|
227
238
|
const subpaths = combined.filter(entry => entry.subpath).map(entry => `${pkg}/${entry.subpath}`);
|
|
228
|
-
|
|
239
|
+
const originSubpath = combined.find(entry => entry.subpath) ?? combined[0];
|
|
240
|
+
diags.push({
|
|
241
|
+
level: hazardLevel,
|
|
242
|
+
code: 'dual-package-subpath',
|
|
243
|
+
message: `Package '${pkg}' is referenced via root specifier '${pkg}' and subpath(s) ${subpaths.join(', ')}; mixing them loads separate module instances.`,
|
|
244
|
+
filePath: originSubpath?.filePath ?? filePath,
|
|
245
|
+
loc: originSubpath?.loc
|
|
246
|
+
});
|
|
229
247
|
}
|
|
230
248
|
if (hasImport && hasRequire) {
|
|
231
|
-
const manifest = await readPackageManifest(pkg,
|
|
249
|
+
const manifest = await readPackageManifest(pkg, diagFile, cwd, manifestCache);
|
|
232
250
|
if (manifest) {
|
|
233
251
|
const meta = describeDualPackage(manifest);
|
|
234
252
|
if (meta.hasHazardSignals) {
|
|
235
253
|
const detail = meta.details.length ? ` (${meta.details.join('; ')})` : '';
|
|
236
|
-
|
|
254
|
+
diags.push({
|
|
255
|
+
level: hazardLevel,
|
|
256
|
+
code: 'dual-package-conditional-exports',
|
|
257
|
+
message: `Package '${pkg}' exposes different entry points for import vs require${detail}. Mixed usage can produce distinct instances.`,
|
|
258
|
+
filePath: diagFile,
|
|
259
|
+
loc: origin?.loc
|
|
260
|
+
});
|
|
237
261
|
}
|
|
238
262
|
}
|
|
239
263
|
}
|
|
240
264
|
}
|
|
265
|
+
return diags;
|
|
266
|
+
};
|
|
267
|
+
const detectDualPackageHazards = async params => {
|
|
268
|
+
const {
|
|
269
|
+
program,
|
|
270
|
+
shadowedBindings,
|
|
271
|
+
hazardLevel,
|
|
272
|
+
filePath,
|
|
273
|
+
cwd,
|
|
274
|
+
diagOnce
|
|
275
|
+
} = params;
|
|
276
|
+
const manifestCache = new Map();
|
|
277
|
+
const usages = await collectDualPackageUsage(program, shadowedBindings, filePath);
|
|
278
|
+
const diags = await dualPackageHazardDiagnostics({
|
|
279
|
+
usages,
|
|
280
|
+
hazardLevel,
|
|
281
|
+
filePath,
|
|
282
|
+
cwd,
|
|
283
|
+
manifestCache
|
|
284
|
+
});
|
|
285
|
+
for (const diag of diags) {
|
|
286
|
+
diagOnce(diag.level, diag.code, diag.message, diag.loc);
|
|
287
|
+
}
|
|
241
288
|
};
|
|
242
289
|
|
|
243
290
|
/**
|
|
@@ -481,4 +528,4 @@ const format = async (src, ast, opts) => {
|
|
|
481
528
|
}
|
|
482
529
|
return code.toString();
|
|
483
530
|
};
|
|
484
|
-
export { format };
|
|
531
|
+
export { format, collectDualPackageUsage, dualPackageHazardDiagnostics };
|
package/dist/module.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
-
import type { ModuleOptions } from './types.js';
|
|
1
|
+
import type { ModuleOptions, Diagnostic } from './types.js';
|
|
2
|
+
declare const collectProjectDualPackageHazards: (files: string[], opts: ModuleOptions) => Promise<Map<string, Diagnostic[]>>;
|
|
2
3
|
declare const transform: (filename: string, options?: ModuleOptions) => Promise<string>;
|
|
3
|
-
export { transform };
|
|
4
|
+
export { transform, collectProjectDualPackageHazards };
|
package/dist/module.js
CHANGED
|
@@ -2,13 +2,14 @@ import { resolve } from 'node:path';
|
|
|
2
2
|
import { readFile, writeFile } from 'node:fs/promises';
|
|
3
3
|
import { specifier } from './specifier.js';
|
|
4
4
|
import { parse } from './parse.js';
|
|
5
|
-
import { format } from './format.js';
|
|
5
|
+
import { format, collectDualPackageUsage, dualPackageHazardDiagnostics } from './format.js';
|
|
6
6
|
import { getLangFromExt } from './utils/lang.js';
|
|
7
7
|
import { builtinModules } from 'node:module';
|
|
8
8
|
import { resolve as pathResolve, dirname as pathDirname, extname, join } from 'node:path';
|
|
9
9
|
import { readFile as fsReadFile, stat } from 'node:fs/promises';
|
|
10
10
|
import { parse as parseModule } from './parse.js';
|
|
11
11
|
import { walk } from './walk.js';
|
|
12
|
+
import { collectModuleIdentifiers } from './utils/identifiers.js';
|
|
12
13
|
const collapseSpecifier = value => value.replace(/['"`+)\s]|new String\(/g, '');
|
|
13
14
|
const builtinSpecifiers = new Set(builtinModules.map(mod => mod.startsWith('node:') ? mod.slice(5) : mod).flatMap(mod => {
|
|
14
15
|
const parts = mod.split('/');
|
|
@@ -141,6 +142,46 @@ const detectCircularRequireGraph = async (entryFile, mode, dirIndex) => {
|
|
|
141
142
|
};
|
|
142
143
|
await dfs(entryFile, []);
|
|
143
144
|
};
|
|
145
|
+
const mergeUsageMaps = (target, source) => {
|
|
146
|
+
for (const [pkg, usage] of source) {
|
|
147
|
+
const existing = target.get(pkg) ?? {
|
|
148
|
+
imports: [],
|
|
149
|
+
requires: []
|
|
150
|
+
};
|
|
151
|
+
existing.imports.push(...usage.imports);
|
|
152
|
+
existing.requires.push(...usage.requires);
|
|
153
|
+
target.set(pkg, existing);
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
const collectProjectDualPackageHazards = async (files, opts) => {
|
|
157
|
+
const hazardMode = opts.detectDualPackageHazard ?? 'warn';
|
|
158
|
+
if (hazardMode === 'off') return new Map();
|
|
159
|
+
const hazardLevel = hazardMode === 'error' ? 'error' : 'warning';
|
|
160
|
+
const usages = new Map();
|
|
161
|
+
const manifestCache = new Map();
|
|
162
|
+
for (const file of files) {
|
|
163
|
+
const code = await readFile(file, 'utf8');
|
|
164
|
+
const ast = parseModule(file, code);
|
|
165
|
+
const moduleIdentifiers = await collectModuleIdentifiers(ast.program);
|
|
166
|
+
const shadowedBindings = new Set([...moduleIdentifiers.entries()].filter(([, meta]) => meta.declare.length > 0).map(([name]) => name));
|
|
167
|
+
const perFileUsage = await collectDualPackageUsage(ast.program, shadowedBindings, file);
|
|
168
|
+
mergeUsageMaps(usages, perFileUsage);
|
|
169
|
+
}
|
|
170
|
+
const diags = await dualPackageHazardDiagnostics({
|
|
171
|
+
usages,
|
|
172
|
+
hazardLevel,
|
|
173
|
+
cwd: opts.cwd,
|
|
174
|
+
manifestCache
|
|
175
|
+
});
|
|
176
|
+
const byFile = new Map();
|
|
177
|
+
for (const diag of diags) {
|
|
178
|
+
const key = diag.filePath ?? files[0];
|
|
179
|
+
const existing = byFile.get(key) ?? [];
|
|
180
|
+
existing.push(diag);
|
|
181
|
+
byFile.set(key, existing);
|
|
182
|
+
}
|
|
183
|
+
return byFile;
|
|
184
|
+
};
|
|
144
185
|
const defaultOptions = {
|
|
145
186
|
target: 'commonjs',
|
|
146
187
|
sourceType: 'auto',
|
|
@@ -155,6 +196,7 @@ const defaultOptions = {
|
|
|
155
196
|
requireMainStrategy: 'import-meta-main',
|
|
156
197
|
detectCircularRequires: 'off',
|
|
157
198
|
detectDualPackageHazard: 'warn',
|
|
199
|
+
dualPackageHazardScope: 'file',
|
|
158
200
|
requireSource: 'builtin',
|
|
159
201
|
nestedRequireStrategy: 'create-require',
|
|
160
202
|
cjsDefault: 'auto',
|
|
@@ -198,4 +240,4 @@ const transform = async (filename, options = defaultOptions) => {
|
|
|
198
240
|
}
|
|
199
241
|
return source;
|
|
200
242
|
};
|
|
201
|
-
export { transform };
|
|
243
|
+
export { transform, collectProjectDualPackageHazards };
|
package/dist/types.d.ts
CHANGED
|
@@ -34,6 +34,8 @@ export type ModuleOptions = {
|
|
|
34
34
|
detectCircularRequires?: 'off' | 'warn' | 'error';
|
|
35
35
|
/** Detect divergent import/require usage of the same dual package (default warn). */
|
|
36
36
|
detectDualPackageHazard?: 'off' | 'warn' | 'error';
|
|
37
|
+
/** Scope for dual package hazard detection. */
|
|
38
|
+
dualPackageHazardScope?: 'file' | 'project';
|
|
37
39
|
/** Source used to provide require in ESM output. */
|
|
38
40
|
requireSource?: 'builtin' | 'create-require';
|
|
39
41
|
/** How to rewrite nested or non-hoistable require calls. */
|