Skip to content

fix sass tilde importer partials support #193

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Feb 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
98 changes: 45 additions & 53 deletions src/importers/sassTildeImporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down