Skip to content

Commit

Permalink
refactor importer and add tests
Browse files Browse the repository at this point in the history
  • Loading branch information
dennispg committed Feb 3, 2023
1 parent 0482bd5 commit 6bc9ae0
Show file tree
Hide file tree
Showing 2 changed files with 51 additions and 58 deletions.
8 changes: 7 additions & 1 deletion src/importers/__tests__/sassTildeImporter.ts
Original file line number Diff line number Diff line change
@@ -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)}`;
Expand Down Expand Up @@ -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', () => {
Expand Down
101 changes: 44 additions & 57 deletions src/importers/sassTildeImporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,70 +2,57 @@ 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));
}

variants.push(path.join(module_path, '_index'));

// 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))?
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) => {
const parts = path.parse(file);
const replacement = '_'.concat(parts.name);
parts.base = parts.base.replace(parts.name, replacement);
parts.name = replacement;
return path.format(parts);
});
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
Expand Down

0 comments on commit 6bc9ae0

Please sign in to comment.