diff --git a/cli/engine/node/file-system.mjs b/cli/engine/node/file-system.mjs index eee17ea4f..6a74fa353 100644 --- a/cli/engine/node/file-system.mjs +++ b/cli/engine/node/file-system.mjs @@ -31,6 +31,12 @@ const SCANNABLE_EXTENSIONS = new Set([ const HTML_EXTENSIONS = new Set(['.html', '.htm']); +const IMPORT_SPECIFIER_PATTERNS = [ + /import\s+(?:[\s\S]*?from\s+)?['"]([^'"]+)['"]/g, + /@import\s+(?:url\(\s*)?['"]?([^'");\s]+)['"]?\s*\)?/g, + /@(?:use|forward)\s+['"]([^'"]+)['"]/g, +]; + function walkDir(dir) { const files = []; let entries; @@ -75,26 +81,11 @@ function buildImportGraph(files) { const dir = path.dirname(file); const imports = new Set(); - // ES imports: import ... from '...' and import '...' - const esRe = /import\s+(?:[\s\S]*?from\s+)?['"]([^'"]+)['"]/g; - let m; - while ((m = esRe.exec(content)) !== null) { - const resolved = resolveImport(m[1], dir, fileSet); - if (resolved) imports.add(resolved); - } - - // CSS @import - const cssRe = /@import\s+(?:url\(\s*)?['"]?([^'");\s]+)['"]?\s*\)?/g; - while ((m = cssRe.exec(content)) !== null) { - const resolved = resolveImport(m[1], dir, fileSet); - if (resolved) imports.add(resolved); - } - - // SCSS @use / @forward - const scssRe = /@(?:use|forward)\s+['"]([^'"]+)['"]/g; - while ((m = scssRe.exec(content)) !== null) { - const resolved = resolveImport(m[1], dir, fileSet); - if (resolved) imports.add(resolved); + for (const pattern of IMPORT_SPECIFIER_PATTERNS) { + for (const match of content.matchAll(pattern)) { + const resolved = resolveImport(match[1], dir, fileSet); + if (resolved) imports.add(resolved); + } } graph.set(file, imports); diff --git a/tests/detect-antipatterns.test.js b/tests/detect-antipatterns.test.js index 85ae0c2e9..a9e8ebe4b 100644 --- a/tests/detect-antipatterns.test.js +++ b/tests/detect-antipatterns.test.js @@ -2797,6 +2797,20 @@ describe('buildImportGraph', () => { expect(themeImports.has(path.join(MF, 'variables.sass'))).toBe(true); }); + test('resolves Sass @use and @forward', async () => { + await withStaticFixture({ + 'theme.scss': "@use './variables';\n@forward './tokens';\n", + 'variables.scss': '$primary: rebeccapurple;\n', + 'tokens.scss': '$spacing: 1rem;\n', + }, ({ dir }) => { + const theme = path.join(dir, 'theme.scss'); + const variables = path.join(dir, 'variables.scss'); + const tokens = path.join(dir, 'tokens.scss'); + const graph = buildImportGraph([theme, variables, tokens]); + expect(graph.get(theme)).toEqual(new Set([variables, tokens])); + }); + }); + test('ignores bare/node_modules imports', () => { const graph = buildImportGraph([ path.join(MF, 'App.tsx'),