Skip to content

Commit e0fde1a

Browse files
evilebottnawijhnns
authored andcommitted
feat: Refactor resolving and simplify webpack config aliases (#479)
- Make it easier to alias modules via the webpack config - Make importsToResolve algorithm more readable BREAKING CHANGE: This slightly changes the resolving algorithm. Should not break in normal usage, but might break in complex configurations.
1 parent 6439cef commit e0fde1a

File tree

11 files changed

+73
-49
lines changed

11 files changed

+73
-49
lines changed

lib/importsToResolve.js

Lines changed: 38 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,60 @@
11
"use strict";
22

33
const path = require("path");
4+
const utils = require("loader-utils");
45

5-
// libsass uses this precedence when importing files without extension
6-
const extPrecedence = [".scss", ".sass", ".css"];
6+
const matchModuleImport = /^~([^\/]+|@[^\/]+[\/][^\/]+)$/g;
77

88
/**
99
* When libsass tries to resolve an import, it uses a special algorithm.
1010
* Since the sass-loader uses webpack to resolve the modules, we need to simulate that algorithm. This function
11-
* returns an array of import paths to try.
11+
* returns an array of import paths to try. The first entry in the array is always the original url
12+
* to enable straight-forward webpack.config aliases.
1213
*
13-
* @param {string} request
14+
* @param {string} url
1415
* @returns {Array<string>}
1516
*/
16-
function importsToResolve(request) {
17+
function importsToResolve(url) {
18+
const request = utils.urlToRequest(url);
19+
// Keep in mind: ext can also be something like '.datepicker' when the true extension is omitted and the filename contains a dot.
20+
// @see https://github.com/webpack-contrib/sass-loader/issues/167
21+
const ext = path.extname(request);
22+
23+
if (matchModuleImport.test(url)) {
24+
return [url, request];
25+
}
26+
1727
// libsass' import algorithm works like this:
18-
// In case there is no file extension...
19-
// - Prefer modules starting with '_'.
20-
// - File extension precedence: .scss, .sass, .css.
28+
2129
// In case there is a file extension...
2230
// - If the file is a CSS-file, do not include it all, but just link it via @import url().
2331
// - The exact file name must match (no auto-resolving of '_'-modules).
32+
if (ext === ".css") {
33+
return [];
34+
}
35+
if (ext === ".scss" || ext === ".sass") {
36+
return [url, request];
37+
}
2438

25-
// Keep in mind: ext can also be something like '.datepicker' when the true extension is omitted and the filename contains a dot.
26-
// @see https://github.com/webpack-contrib/sass-loader/issues/167
27-
const ext = path.extname(request);
39+
// In case there is no file extension...
40+
// - Prefer modules starting with '_'.
41+
// - File extension precedence: .scss, .sass, .css.
2842
const basename = path.basename(request);
29-
const dirname = path.dirname(request);
30-
const startsWithUnderscore = basename.charAt(0) === "_";
31-
const hasCssExt = ext === ".css";
32-
const hasSassExt = ext === ".scss" || ext === ".sass";
33-
34-
// a module import is an identifier like 'bootstrap-sass'
35-
// We also need to check for dirname since it might also be a deep import like 'bootstrap-sass/something'
36-
let isModuleImport = request.charAt(0) !== "." && dirname === ".";
37-
38-
if (dirname.charAt(0) === "@") {
39-
// Check whether it is a deep import from scoped npm package
40-
// (i.e. @pkg/foo/file), if so, process import as file import;
41-
// otherwise, if we import from root npm scoped package (i.e. @pkg/foo)
42-
// process import as a module import.
43-
isModuleImport = !(dirname.indexOf("/") > -1);
43+
44+
if (basename.charAt(0) === "_") {
45+
return [
46+
url,
47+
`${ request }.scss`, `${ request }.sass`, `${ request }.css`
48+
];
4449
}
4550

46-
return (isModuleImport && [request]) || // Do not modify module imports
47-
(hasCssExt && []) || // Do not import css files
48-
(hasSassExt && [request]) || // Do not modify imports with explicit extensions
49-
(startsWithUnderscore ? [] : extPrecedence) // Do not add underscore imports if there is already an underscore
50-
.map(ext => "_" + basename + ext)
51-
.concat(
52-
extPrecedence.map(ext => basename + ext)
53-
).map(
54-
file => dirname + "/" + file // No path.sep required here, because imports inside SASS are usually with /
55-
);
51+
const dirname = path.dirname(request);
52+
53+
return [
54+
url,
55+
`${ dirname }/_${ basename }.scss`, `${ dirname }/_${ basename }.sass`, `${ dirname }/_${ basename }.css`,
56+
`${ request }.scss`, `${ request }.sass`, `${ request }.css`
57+
];
5658
}
5759

5860
module.exports = importsToResolve;

lib/webpackImporter.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
*/
1818

1919
const path = require("path");
20-
const utils = require("loader-utils");
2120
const tail = require("lodash.tail");
2221
const importsToResolve = require("./importsToResolve");
2322

@@ -63,7 +62,7 @@ function webpackImporter(resourcePath, resolve, addNormalizedDependency) {
6362
return (url, prev, done) => {
6463
startResolving(
6564
dirContextFrom(prev),
66-
importsToResolve(utils.urlToRequest(url))
65+
importsToResolve(url)
6766
) // Catch all resolving errors, return the original file and pass responsibility back to other custom importers
6867
.catch(() => ({ file: url }))
6968
.then(done);

test/index.test.js

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,14 @@ const loaderContextMock = {
2626
};
2727

2828
Object.defineProperty(loaderContextMock, "options", {
29-
set() {},
29+
set() { },
3030
get() {
3131
throw new Error("webpack options are not allowed to be accessed anymore.");
3232
}
3333
});
3434

3535
syntaxStyles.forEach(ext => {
36-
function execTest(testId, options) {
36+
function execTest(testId, loaderOptions, webpackOptions) {
3737
return new Promise((resolve, reject) => {
3838
const baseConfig = merge({
3939
entry: path.join(__dirname, ext, testId + "." + ext),
@@ -45,11 +45,11 @@ syntaxStyles.forEach(ext => {
4545
test: new RegExp(`\\.${ ext }$`),
4646
use: [
4747
{ loader: "raw-loader" },
48-
{ loader: pathToSassLoader, options }
48+
{ loader: pathToSassLoader, options: loaderOptions }
4949
]
5050
}]
5151
}
52-
});
52+
}, webpackOptions);
5353

5454
runWebpack(baseConfig, (err) => err ? reject(err) : resolve());
5555
}).then(() => {
@@ -79,6 +79,13 @@ syntaxStyles.forEach(ext => {
7979
it("should not resolve CSS imports", () => execTest("import-css"));
8080
it("should compile bootstrap-sass without errors", () => execTest("bootstrap-sass"));
8181
it("should correctly import scoped npm packages", () => execTest("import-from-npm-org-pkg"));
82+
it("should resolve aliases", () => execTest("import-alias", {}, {
83+
resolve: {
84+
alias: {
85+
"path-to-alias": path.join(__dirname, ext, "alias." + ext)
86+
}
87+
}
88+
}));
8289
});
8390
describe("custom importers", () => {
8491
it("should use custom importer", () => execTest("custom-importer", {
@@ -170,9 +177,11 @@ describe("sass-loader", () => {
170177
test: /\.scss$/,
171178
use: [
172179
{ loader: testLoader.filename },
173-
{ loader: pathToSassLoader, options: {
174-
sourceMap: true
175-
} }
180+
{
181+
loader: pathToSassLoader, options: {
182+
sourceMap: true
183+
}
184+
}
176185
]
177186
}]
178187
}
@@ -196,7 +205,7 @@ describe("sass-loader", () => {
196205
sourceMap.should.not.have.property("file");
197206
sourceMap.should.have.property("sourceRoot", fakeCwd);
198207
// This number needs to be updated if imports.scss or any dependency of that changes
199-
sourceMap.sources.should.have.length(8);
208+
sourceMap.sources.should.have.length(9);
200209
sourceMap.sources.forEach(sourcePath =>
201210
fs.existsSync(path.resolve(sourceMap.sourceRoot, sourcePath))
202211
);

test/sass/alias.sass

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
a
2+
color: red
3+

test/sass/import-alias.sass

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
@import path-to-alias

test/sass/import-css.sass

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Special behavior of node-sass/libsass with CSS-files
22
// 1. CSS-files are not included, but linked with @import url(path/to/css)
3-
@import ../node_modules/css/some-css-module.css
3+
@import ~css/some-css-module.css
44
// 2. It does not matter whether the CSS-file exists or not, the file is just linked
55
@import ./does/not/exist.css
66
// 3. When the .css extension is missing, the file is included just like scss
7-
@import ../node_modules/css/some-css-module
7+
@import ~css/some-css-module

test/sass/imports.sass

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
@import another/module
33
/* @import another/underscore */
44
@import another/underscore
5+
/* @import another/_underscore */
6+
@import another/_underscore
57
/* @import ~sass/underscore */
68
@import ~sass/underscore
79
// Import a module with a dot in its name

test/scss/alias.scss

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
a {
2+
color: red;
3+
}

test/scss/import-alias.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
@import 'path-to-alias';

test/scss/imports.scss

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
@import "another/module";
33
/* @import "another/underscore"; */
44
@import "another/underscore";
5+
/* @import "another/underscore"; */
6+
@import "another/_underscore";
57
/* @import "~scss/underscore"; */
68
@import "~scss/underscore";
79
// Import a module with a dot in its name

test/tools/createSpec.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ function createSpec(ext) {
1515
const testNodeModules = path.relative(basePath, path.join(testFolder, "node_modules")) + path.sep;
1616
const pathToBootstrap = path.relative(basePath, path.resolve(testFolder, "..", "node_modules", "bootstrap-sass"));
1717
const pathToScopedNpmPkg = path.relative(basePath, path.resolve(testFolder, "node_modules", "@org", "pkg", "./index.scss"));
18+
const pathToFooAlias = path.relative(basePath, path.resolve(testFolder, ext, "./alias." + ext));
1819

1920
fs.readdirSync(path.join(testFolder, ext))
2021
.filter((file) => {
@@ -32,7 +33,8 @@ function createSpec(ext) {
3233
url = url
3334
.replace(/^~bootstrap-sass/, pathToBootstrap)
3435
.replace(/^~@org\/pkg/, pathToScopedNpmPkg)
35-
.replace(/^~/, testNodeModules);
36+
.replace(/^~/, testNodeModules)
37+
.replace(/^path-to-alias/, pathToFooAlias);
3638
}
3739
return {
3840
file: url

0 commit comments

Comments
 (0)