Skip to content

Commit 566ff3d

Browse files
committed
fix: register non-JS modules so we can report violations
1 parent 99cfa1f commit 566ff3d

File tree

2 files changed

+47
-21
lines changed

2 files changed

+47
-21
lines changed

src/index.ts

Lines changed: 25 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -515,9 +515,12 @@ export const ImpoundPlugin = createUnplugin<ImpoundOptions>((globalOptions) => {
515515
// shared transform logic for module graph building and flushing pending violations.
516516
async function traceTransform(code: string, id: string, getCombinedSourcemap?: () => unknown): Promise<void> {
517517
await init
518+
let importMap = new Map<string, ImportLocation>()
519+
let originalCode: string | undefined
520+
let sourceMap: unknown
521+
518522
try {
519523
const [imports] = parse(code, id)
520-
const importMap = new Map<string, ImportLocation>()
521524
for (const imp of imports) {
522525
if (imp.n) {
523526
const { line, column } = offsetToLineColumn(code, imp.s)
@@ -531,8 +534,6 @@ export const ImpoundPlugin = createUnplugin<ImpoundOptions>((globalOptions) => {
531534
}
532535

533536
// extract the combined source map for original-source snippets.
534-
let originalCode: string | undefined
535-
let sourceMap: unknown
536537
if (getCombinedSourcemap) {
537538
try {
538539
const map = getCombinedSourcemap() as { mappings?: string, sourcesContent?: (string | null)[] } | undefined
@@ -548,28 +549,31 @@ export const ImpoundPlugin = createUnplugin<ImpoundOptions>((globalOptions) => {
548549
// getCombinedSourcemap may throw — fall back to transformed code
549550
}
550551
}
551-
552-
const graphEntry: ModuleGraphEntry = { code, originalCode, sourceMap, imports: importMap }
553-
moduleGraph.set(id, graphEntry)
554-
// Also store under normalized key forms so enrichAndReport can find it
555-
// when the importer path format differs (e.g. with/without query string)
556-
/* v8 ignore start -- defensive normalization for framework-specific virtual module IDs */
557-
const bareId = id.split('?')[0]!
558-
if (bareId !== id)
559-
moduleGraph.set(bareId, graphEntry)
560-
if (isAbsolute(id) && globalOptions.cwd) {
561-
const relId = relative(globalOptions.cwd, id)
562-
moduleGraph.set(relId, graphEntry)
563-
const relBareId = relId.split('?')[0]!
564-
if (relBareId !== relId)
565-
moduleGraph.set(relBareId, graphEntry)
566-
}
567-
/* v8 ignore stop */
568552
}
569553
catch {
570-
// If parsing fails (e.g. non-JS asset), just skip
554+
// If parsing fails (e.g. non-JS asset like a raw Vue SFC), use empty imports.
555+
// We still register the module in the graph so that resolveId can find
556+
// the importer and report violations immediately instead of deferring them.
557+
importMap = new Map()
571558
}
572559

560+
const graphEntry: ModuleGraphEntry = { code, originalCode, sourceMap, imports: importMap }
561+
moduleGraph.set(id, graphEntry)
562+
// Also store under normalized key forms so enrichAndReport can find it
563+
// when the importer path format differs (e.g. with/without query string)
564+
/* v8 ignore start -- defensive normalization for framework-specific virtual module IDs */
565+
const bareId = id.split('?')[0]!
566+
if (bareId !== id)
567+
moduleGraph.set(bareId, graphEntry)
568+
if (isAbsolute(id) && globalOptions.cwd) {
569+
const relId = relative(globalOptions.cwd, id)
570+
moduleGraph.set(relId, graphEntry)
571+
const relBareId = relId.split('?')[0]!
572+
if (relBareId !== relId)
573+
moduleGraph.set(relBareId, graphEntry)
574+
}
575+
/* v8 ignore stop */
576+
573577
// Flush any violations that were waiting for this module's transform.
574578
// Check multiple key forms since resolveId may use relative paths while
575579
// transform receives absolute paths (or vice versa with query strings).

test/index.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -934,6 +934,28 @@ describe('trace mode (deferred violations)', () => {
934934
// Should not throw — just skip the dynamic import entry
935935
})
936936

937+
it('registers module in graph even when parsing fails (e.g. Vue SFC)', async () => {
938+
// When es-module-lexer can't parse a file (like a raw Vue SFC), the module should
939+
// still be registered in the graph so that resolveId can report violations immediately.
940+
const plugins = ImpoundPlugin.rollup({ trace: true, patterns: [['secret', 'Not allowed']] })
941+
const pluginArray = Array.isArray(plugins) ? plugins : [plugins]
942+
const impoundPlugin = pluginArray.find(p => p.name === 'impound')!
943+
const tracePlugin = pluginArray.find(p => p.name === 'impound:trace')!
944+
945+
const errors: string[] = []
946+
const context = { error: (msg: string) => errors.push(msg) }
947+
948+
// Transform with unparseable SFC content — parse will fail
949+
await (tracePlugin as any).transform('<script setup>\nimport secret from "secret"\n</script>', 'app.vue')
950+
951+
// resolveId should find the importer in the graph and report immediately
952+
await (impoundPlugin as any).resolveId.call(context, 'secret', 'app.vue')
953+
expect(errors).toHaveLength(1)
954+
expect(errors[0]).toContain('Not allowed')
955+
// No Code: section since parsing failed and import locations are empty
956+
expect(errors[0]).not.toContain('Code:')
957+
})
958+
937959
it('tracks resolved imports across multiple resolveIds from same importer', async () => {
938960
const plugins = ImpoundPlugin.rollup({ trace: true, patterns: [['secret', 'Not allowed']] })
939961
const pluginArray = Array.isArray(plugins) ? plugins : [plugins]

0 commit comments

Comments
 (0)