diff --git a/src/importers/__tests__/sassTildeImporter.ts b/src/importers/__tests__/sassTildeImporter.ts index 6dac467..2a4f223 100644 --- a/src/importers/__tests__/sassTildeImporter.ts +++ b/src/importers/__tests__/sassTildeImporter.ts @@ -1,5 +1,5 @@ import { join } from 'path'; -import { sassTildeImporter } from '../sassTildeImporter'; +import { sassTildeImporter, resolveUrls } from '../sassTildeImporter'; const getAbsoluteFileUrl = (expected: string) => `file://${join(process.cwd(), expected)}`; @@ -59,6 +59,12 @@ describe('importers / sassTildeImporter', () => { }) ?.toString(), ).toBe(getAbsoluteFileUrl('node_modules/bootstrap/scss/_grid.scss')); + expect(resolveUrls('~sass-mq/mq.scss')).toContain( + 'node_modules/sass-mq/_mq.scss', + ); + expect(resolveUrls('~sass-mq/mq')).toContain( + 'node_modules/sass-mq/_mq.scss', + ); }); it('should resolve index files', () => { diff --git a/src/importers/sassTildeImporter.ts b/src/importers/sassTildeImporter.ts index f752c48..fd3c717 100644 --- a/src/importers/sassTildeImporter.ts +++ b/src/importers/sassTildeImporter.ts @@ -2,66 +2,58 @@ import path from 'path'; import fs from 'fs'; import sass from 'sass'; +const DEFAULT_EXTS = ['scss', 'sass', 'css']; + +export function resolveUrls(url: string, extensions: string[] = DEFAULT_EXTS) { + // We only care about tilde-prefixed imports that do not look like paths. + if (!url.startsWith('~') || url.startsWith('~/')) { + return []; + } + + const module_path = path.join('node_modules', url.substring(1)); + let variants = [module_path]; + + const parts = path.parse(module_path); + + // Support sass partials by including paths where the file is prefixed by an underscore. + if (!parts.base.startsWith('_')) { + const underscore_name = '_'.concat(parts.name); + const replacement = { + root: parts.root, + dir: parts.dir, + ext: parts.ext, + base: `${underscore_name}${parts.ext}`, + name: underscore_name, + }; + variants.push(path.format(replacement)); + } + + // Support index files. + variants.push(path.join(module_path, '_index')); + + // Create variants such that it has entries of the form + // node_modules/@foo/bar/baz.(scss|sass) + // for an import of the form ~@foo/bar/baz(.(scss|sass))? + if (!extensions.some((ext) => parts.ext == `.${ext}`)) { + variants = extensions.flatMap((ext) => + variants.map((variant) => `${variant}.${ext}`), + ); + } + + return variants; +} + /** * Creates a sass importer which resolves Webpack-style tilde-imports. */ export const sassTildeImporter: sass.FileImporter<'sync'> = { findFileUrl(url) { - // We only care about tilde-prefixed imports that do not look like paths. - if (!url.startsWith('~') || url.startsWith('~/')) { - return null; - } - - // Create subpathsWithExts such that it has entries of the form - // node_modules/@foo/bar/baz.(scss|sass) - // for an import of the form ~@foo/bar/baz(.(scss|sass))? - const nodeModSubpath = path.join('node_modules', url.substring(1)); - const subpathsWithExts: string[] = []; - if ( - nodeModSubpath.endsWith('.scss') || - nodeModSubpath.endsWith('.sass') || - nodeModSubpath.endsWith('.css') - ) { - subpathsWithExts.push(nodeModSubpath); - } else { - // Look for .scss first. - subpathsWithExts.push( - `${nodeModSubpath}.scss`, - `${nodeModSubpath}.sass`, - `${nodeModSubpath}.css`, - ); - } - - // Support index files. - subpathsWithExts.push( - `${nodeModSubpath}/_index.scss`, - `${nodeModSubpath}/_index.sass`, - ); - - // Support sass partials by including paths where the file is prefixed by an underscore. - const basename = path.basename(nodeModSubpath); - if (!basename.startsWith('_')) { - const partials = subpathsWithExts.map((file) => - file.replace(basename, `_${basename}`), - ); - subpathsWithExts.push(...partials); - } + const searchPaths = resolveUrls(url); - // Climbs the filesystem tree until we get to the root, looking for the first - // node_modules directory which has a matching module and filename. - let prevDir = ''; - let dir = path.dirname(url); - while (prevDir !== dir) { - const searchPaths = subpathsWithExts.map((subpathWithExt) => - path.join(dir, subpathWithExt), - ); - for (const searchPath of searchPaths) { - if (fs.existsSync(searchPath)) { - return new URL(`file://${path.resolve(searchPath)}`); - } + for (const searchPath of searchPaths) { + if (fs.existsSync(searchPath)) { + return new URL(`file://${path.resolve(searchPath)}`); } - prevDir = dir; - dir = path.dirname(dir); } // Returning null is not itself an error, it tells sass to instead try the