Skip to content

Commit 3efaa44

Browse files
committed
feat: fall back to module resolution when relative imports fail
1 parent 11fcf2d commit 3efaa44

File tree

2 files changed

+43
-24
lines changed

2 files changed

+43
-24
lines changed

src/loaders/sass/importer.ts

Lines changed: 41 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,60 @@
11
import path from "path";
2-
import { packageFilterBuilder, resolveAsync, resolveSync } from "../../utils/resolve";
3-
import { getUrlOfPartial, isModule, normalizeUrl } from "../../utils/url";
2+
import { isAbsolutePath, isRelativePath } from "../../utils/path";
3+
import { packageFilterBuilder, resolveAsync, ResolveOpts, resolveSync } from "../../utils/resolve";
4+
import { getUrlOfPartial, hasModuleSpecifier, normalizeUrl } from "../../utils/url";
45

56
const extensions = [".scss", ".sass", ".css"];
67
const conditions = ["sass", "style"];
78

8-
export const importer: sass.Importer = (url, importer, done): void => {
9-
const finalize = (id: string): void => done({ file: id.replace(/\.css$/i, "") });
10-
const next = (): void => done(null);
9+
/**
10+
* The exact behavior of importers defined here differ slightly between dart-sass and node-sass:
11+
* https://github.com/sass/dart-sass/issues/574
12+
*
13+
* In short, dart-sass specifies that the *correct* behavior is to only call importers when a
14+
* stylesheet fails to resolve via relative path. Since these importers below are implementation-
15+
* agnostic, the first attempt to resolve a file by a relative is unneeded in dart-sass and can be
16+
* removed once support for node-sass is fully deprecated.
17+
*/
18+
function importerImpl<T extends (ids: string[], userOpts: ResolveOpts) => unknown>(
19+
url: string,
20+
importer: string,
21+
resolve: T,
22+
): ReturnType<T> {
23+
const candidates: string[] = [];
24+
if (hasModuleSpecifier(url)) {
25+
const moduleUrl = normalizeUrl(url);
26+
// Give precedence to importing a partial
27+
candidates.push(getUrlOfPartial(moduleUrl), moduleUrl);
28+
} else {
29+
const relativeUrl = normalizeUrl(url);
30+
candidates.push(getUrlOfPartial(relativeUrl), relativeUrl);
1131

12-
if (!isModule(url)) return next();
13-
const moduleUrl = normalizeUrl(url);
14-
const partialUrl = getUrlOfPartial(moduleUrl);
32+
// fall back to module imports
33+
if (!isAbsolutePath(url) && !isRelativePath(url)) {
34+
const moduleUrl = normalizeUrl(`~${url}`);
35+
candidates.push(getUrlOfPartial(moduleUrl), moduleUrl);
36+
}
37+
}
1538
const options = {
1639
caller: "Sass importer",
1740
basedirs: [path.dirname(importer)],
1841
extensions,
1942
packageFilter: packageFilterBuilder({ conditions }),
2043
};
21-
// Give precedence to importing a partial
22-
resolveAsync([partialUrl, moduleUrl], options).then(finalize).catch(next);
23-
};
44+
return resolve(candidates, options) as ReturnType<T>;
45+
}
2446

2547
const finalize = (id: string): sass.Data => ({ file: id.replace(/\.css$/i, "") });
48+
49+
export const importer: sass.Importer = (url, importer, done): void => {
50+
void importerImpl(url, importer, resolveAsync)
51+
.then(id => done(finalize(id)))
52+
.catch(() => done(null));
53+
};
54+
2655
export const importerSync: sass.Importer = (url, importer): sass.Data => {
27-
if (!isModule(url)) return null;
28-
const moduleUrl = normalizeUrl(url);
29-
const partialUrl = getUrlOfPartial(moduleUrl);
30-
const options = {
31-
caller: "Sass importer",
32-
basedirs: [path.dirname(importer)],
33-
extensions,
34-
packageFilter: packageFilterBuilder({ conditions }),
35-
};
36-
// Give precedence to importing a partial
3756
try {
38-
return finalize(resolveSync([partialUrl, moduleUrl], options));
57+
return finalize(importerImpl(url, importer, resolveSync));
3958
} catch {
4059
return null;
4160
}

src/utils/url.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
import path from "path";
22
import { isAbsolutePath, isRelativePath, normalizePath } from "./path";
33

4-
export const isModule = (url: string): boolean => /^~[\d@A-Za-z]/.test(url);
4+
export const hasModuleSpecifier = (url: string): boolean => /^~[\d@A-Za-z]/.test(url);
55

66
export function getUrlOfPartial(url: string): string {
77
const { dir, base } = path.parse(url);
88
return dir ? `${normalizePath(dir)}/_${base}` : `_${base}`;
99
}
1010

1111
export function normalizeUrl(url: string): string {
12-
if (isModule(url)) return normalizePath(url.slice(1));
12+
if (hasModuleSpecifier(url)) return normalizePath(url.slice(1));
1313
if (isAbsolutePath(url) || isRelativePath(url)) return normalizePath(url);
1414
return `./${normalizePath(url)}`;
1515
}

0 commit comments

Comments
 (0)