sass-loader 14.1.0 → 14.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -657,16 +657,16 @@ module.exports = {
657
657
  Type:
658
658
 
659
659
  ```ts
660
- type api = "legacy" | "modern";
660
+ type api = "legacy" | "modern" | "modern-compiler";
661
661
  ```
662
662
 
663
663
  Default: `"legacy"`
664
664
 
665
- Allows you to switch between `legacy` and `modern` API. You can find more information [here](https://sass-lang.com/documentation/js-api).
665
+ Allows you to switch between `legacy` and `modern` API. You can find more information [here](https://sass-lang.com/documentation/js-api). The `modern-compiler` option enables the modern API with support for [Shared Resources](https://github.com/sass/sass/blob/main/accepted/shared-resources.d.ts.md).
666
666
 
667
- > **Warning**
667
+ > **Note**
668
668
  >
669
- > "modern" API is experimental, so some features may not work (known: built-in `importer` is not working and files with errors is not watching on initial run), you can follow this [here](https://github.com/webpack-contrib/sass-loader/issues/774).
669
+ > Using `modern-compiler` and `sass-embedded` together significantly improve performance and decrease built time. We strongly recommend their use. We will enable them by default in a future major release.
670
670
 
671
671
  > **Warning**
672
672
  >
package/dist/index.js CHANGED
@@ -29,26 +29,28 @@ async function loader(content) {
29
29
  const sassOptions = await (0, _utils.getSassOptions)(this, options, content, implementation, useSourceMap);
30
30
  const shouldUseWebpackImporter = typeof options.webpackImporter === "boolean" ? options.webpackImporter : true;
31
31
  if (shouldUseWebpackImporter) {
32
- const isModernAPI = options.api === "modern";
32
+ const isModernAPI = options.api === "modern" || options.api === "modern-compiler";
33
33
  if (!isModernAPI) {
34
34
  const {
35
35
  includePaths
36
36
  } = sassOptions;
37
37
  sassOptions.importer.push((0, _utils.getWebpackImporter)(this, implementation, includePaths));
38
38
  } else {
39
- sassOptions.importers.push((0, _utils.getModernWebpackImporter)(this, implementation));
39
+ sassOptions.importers.push(
40
+ // No need to pass `loadPaths`, because modern API handle them itself
41
+ (0, _utils.getModernWebpackImporter)(this, implementation, []));
40
42
  }
41
43
  }
42
44
  let compile;
43
45
  try {
44
- compile = (0, _utils.getCompileFn)(implementation, options);
46
+ compile = (0, _utils.getCompileFn)(this, implementation, options);
45
47
  } catch (error) {
46
48
  callback(error);
47
49
  return;
48
50
  }
49
51
  let result;
50
52
  try {
51
- result = await compile(sassOptions, options);
53
+ result = await compile(sassOptions);
52
54
  } catch (error) {
53
55
  // There are situations when the `file`/`span.url` property do not exist
54
56
  // Modern API
package/dist/options.json CHANGED
@@ -17,7 +17,7 @@
17
17
  "api": {
18
18
  "description": "Switch between old and modern API for `sass` (`Dart Sass`) and `Sass Embedded` implementations.",
19
19
  "link": "https://github.com/webpack-contrib/sass-loader#sassoptions",
20
- "enum": ["legacy", "modern"]
20
+ "enum": ["legacy", "modern", "modern-compiler"]
21
21
  },
22
22
  "sassOptions": {
23
23
  "description": "Options for `node-sass` or `sass` (`Dart Sass`) implementation.",
package/dist/utils.js CHANGED
@@ -145,7 +145,7 @@ async function getSassOptions(loaderContext, loaderOptions, content, implementat
145
145
  }
146
146
  };
147
147
  }
148
- const isModernAPI = loaderOptions.api === "modern";
148
+ const isModernAPI = loaderOptions.api === "modern" || loaderOptions.api === "modern-compiler";
149
149
  const {
150
150
  resourcePath
151
151
  } = loaderContext;
@@ -171,6 +171,9 @@ async function getSassOptions(loaderContext, loaderOptions, content, implementat
171
171
  sassOptions.syntax = "css";
172
172
  }
173
173
  }
174
+ sassOptions.loadPaths = [].concat(
175
+ // We use `loadPaths` in context for resolver, so it should be always absolute
176
+ (sassOptions.loadPaths ? sassOptions.loadPaths.slice() : []).map(includePath => _path.default.isAbsolute(includePath) ? includePath : _path.default.join(process.cwd(), includePath))).concat(process.env.SASS_PATH ? process.env.SASS_PATH.split(process.platform === "win32" ? ";" : ":") : []);
174
177
  sassOptions.importers = sassOptions.importers ? Array.isArray(sassOptions.importers) ? sassOptions.importers.slice() : [sassOptions.importers] : [];
175
178
  } else {
176
179
  sassOptions.file = resourcePath;
@@ -228,6 +231,7 @@ const MODULE_REQUEST_REGEX = /^[^?]*~/;
228
231
  // - ~@org/package
229
232
  // - ~@org/package/
230
233
  const IS_MODULE_IMPORT = /^~([^/]+|[^/]+\/|@[^/]+[/][^/]+|@[^/]+\/?|@[^/]+[/][^/]+\/)$/;
234
+ const IS_PKG_SCHEME = /^pkg:/i;
231
235
 
232
236
  /**
233
237
  * When `sass`/`node-sass` tries to resolve an import, it uses a special algorithm.
@@ -253,7 +257,11 @@ url, forWebpackResolver = false, fromImport = false) {
253
257
  if (MODULE_REQUEST_REGEX.test(url)) {
254
258
  request = request.replace(MODULE_REQUEST_REGEX, "");
255
259
  }
256
- if (IS_MODULE_IMPORT.test(url)) {
260
+ if (IS_PKG_SCHEME.test(url)) {
261
+ request = `${request.slice(4)}`;
262
+ return [...new Set([request, url])];
263
+ }
264
+ if (IS_MODULE_IMPORT.test(url) || IS_PKG_SCHEME.test(url)) {
257
265
  request = request[request.length - 1] === "/" ? request : `${request}/`;
258
266
  return [...new Set([request, url])];
259
267
  }
@@ -341,7 +349,7 @@ const IS_NATIVE_WIN32_PATH = /^[a-z]:[/\\]|^\\\\/i;
341
349
  * @throws If a compatible Sass implementation cannot be found.
342
350
  */
343
351
  function getWebpackResolver(resolverFactory, implementation, includePaths = []) {
344
- const isDartSass = implementation && implementation.info.includes("dart-sass");
352
+ const isModernSass = implementation && (implementation.info.includes("dart-sass") || implementation.info.includes("sass-embedded"));
345
353
  // We only have one difference with the built-in sass resolution logic and out resolution logic:
346
354
  // First, we look at the files starting with `_`, then without `_` (i.e. `_name.sass`, `_name.scss`, `_name.css`, `name.sass`, `name.scss`, `name.css`),
347
355
  // although `sass` look together by extensions (i.e. `_name.sass`/`name.sass`/`_name.scss`/`name.scss`/`_name.css`/`name.css`).
@@ -398,7 +406,7 @@ function getWebpackResolver(resolverFactory, implementation, includePaths = [])
398
406
  // See https://github.com/webpack/webpack/issues/12340
399
407
  // Because `node-sass` calls our importer before `1. Filesystem imports relative to the base file.`
400
408
  // custom importer may not return `{ file: '/path/to/name.ext' }` and therefore our `context` will be relative
401
- if (!isDartSass && !_path.default.isAbsolute(context)) {
409
+ if (!isModernSass && !_path.default.isAbsolute(context)) {
402
410
  return Promise.reject();
403
411
  }
404
412
  const originalRequest = request;
@@ -416,6 +424,8 @@ function getWebpackResolver(resolverFactory, implementation, includePaths = [])
416
424
  const needEmulateSassResolver =
417
425
  // `sass` doesn't support module import
418
426
  !IS_SPECIAL_MODULE_IMPORT.test(request) &&
427
+ // don't handle `pkg:` scheme
428
+ !IS_PKG_SCHEME.test(request) &&
419
429
  // We need improve absolute paths handling.
420
430
  // Absolute paths should be resolved:
421
431
  // - Server-relative URLs - `<context>/path/to/file.ext` (where `<context>` is root context)
@@ -434,7 +444,7 @@ function getWebpackResolver(resolverFactory, implementation, includePaths = [])
434
444
  const sassPossibleRequests = getPossibleRequests(request, false, fromImport);
435
445
 
436
446
  // `node-sass` calls our importer before `1. Filesystem imports relative to the base file.`, so we need emulate this too
437
- if (!isDartSass) {
447
+ if (!isModernSass) {
438
448
  resolutionMap = resolutionMap.concat({
439
449
  resolve: fromImport ? sassImportResolve : sassModuleResolve,
440
450
  context: _path.default.dirname(context),
@@ -461,13 +471,57 @@ function getWebpackResolver(resolverFactory, implementation, includePaths = [])
461
471
  };
462
472
  }
463
473
  const MATCH_CSS = /\.css$/i;
464
- function getModernWebpackImporter() {
474
+ function getModernWebpackImporter(loaderContext, implementation, loadPaths) {
475
+ const resolve = getWebpackResolver(loaderContext.getResolve, implementation, loadPaths);
465
476
  return {
466
- async canonicalize() {
467
- return null;
477
+ async canonicalize(originalUrl, context) {
478
+ const {
479
+ fromImport
480
+ } = context;
481
+ const prev = context.containingUrl ? _url.default.fileURLToPath(context.containingUrl.toString()) : loaderContext.resourcePath;
482
+ let result;
483
+ try {
484
+ result = await resolve(prev, originalUrl, fromImport);
485
+ } catch (err) {
486
+ // If no stylesheets are found, the importer should return null.
487
+ return null;
488
+ }
489
+ loaderContext.addDependency(_path.default.normalize(result));
490
+ return _url.default.pathToFileURL(result);
468
491
  },
469
- load() {
470
- // TODO implement
492
+ async load(canonicalUrl) {
493
+ const ext = _path.default.extname(canonicalUrl.pathname);
494
+ let syntax;
495
+ if (ext && ext.toLowerCase() === ".scss") {
496
+ syntax = "scss";
497
+ } else if (ext && ext.toLowerCase() === ".sass") {
498
+ syntax = "indented";
499
+ } else if (ext && ext.toLowerCase() === ".css") {
500
+ syntax = "css";
501
+ } else {
502
+ // Fallback to default value
503
+ syntax = "scss";
504
+ }
505
+ try {
506
+ const contents = await new Promise((resolve, reject) => {
507
+ // Old version of `enhanced-resolve` supports only path as a string
508
+ // TODO simplify in the next major release and pass URL
509
+ const canonicalPath = _url.default.fileURLToPath(canonicalUrl);
510
+ loaderContext.fs.readFile(canonicalPath, "utf8", (err, content) => {
511
+ if (err) {
512
+ reject(err);
513
+ return;
514
+ }
515
+ resolve(content);
516
+ });
517
+ });
518
+ return {
519
+ contents,
520
+ syntax
521
+ };
522
+ } catch (err) {
523
+ return null;
524
+ }
471
525
  }
472
526
  };
473
527
  }
@@ -497,15 +551,17 @@ function getWebpackImporter(loaderContext, implementation, includePaths) {
497
551
  };
498
552
  }
499
553
  let nodeSassJobQueue = null;
554
+ const sassModernCompilers = new WeakMap();
500
555
 
501
556
  /**
502
557
  * Verifies that the implementation and version of Sass is supported by this loader.
503
558
  *
559
+ * @param {Object} loaderContext
504
560
  * @param {Object} implementation
505
561
  * @param {Object} options
506
562
  * @returns {Function}
507
563
  */
508
- function getCompileFn(implementation, options) {
564
+ function getCompileFn(loaderContext, implementation, options) {
509
565
  const isNewSass = implementation.info.includes("dart-sass") || implementation.info.includes("sass-embedded");
510
566
  if (isNewSass) {
511
567
  if (options.api === "modern") {
@@ -517,6 +573,37 @@ function getCompileFn(implementation, options) {
517
573
  return implementation.compileStringAsync(data, rest);
518
574
  };
519
575
  }
576
+ if (options.api === "modern-compiler") {
577
+ return async sassOptions => {
578
+ // eslint-disable-next-line no-underscore-dangle
579
+ const webpackCompiler = loaderContext._compiler;
580
+ const {
581
+ data,
582
+ ...rest
583
+ } = sassOptions;
584
+
585
+ // Some people can run the loader in a multi-threading way;
586
+ // there is no webpack compiler object in such case.
587
+ if (webpackCompiler) {
588
+ if (!sassModernCompilers.has(implementation)) {
589
+ // Create a long-running compiler process that can be reused
590
+ // for compiling individual files.
591
+ const compiler = await implementation.initAsyncCompiler();
592
+
593
+ // Check again because awaiting the initialization function
594
+ // introduces a race condition.
595
+ if (!sassModernCompilers.has(webpackCompiler)) {
596
+ sassModernCompilers.set(webpackCompiler, compiler);
597
+ webpackCompiler.hooks.shutdown.tap("sass-loader", () => {
598
+ compiler.dispose();
599
+ });
600
+ }
601
+ }
602
+ return sassModernCompilers.get(webpackCompiler).compileStringAsync(data, rest);
603
+ }
604
+ return implementation.compileStringAsync(data, rest);
605
+ };
606
+ }
520
607
  return sassOptions => new Promise((resolve, reject) => {
521
608
  implementation.render(sassOptions, (error, result) => {
522
609
  if (error) {
@@ -527,7 +614,7 @@ function getCompileFn(implementation, options) {
527
614
  });
528
615
  });
529
616
  }
530
- if (options.api === "modern") {
617
+ if (options.api === "modern" || options.api === "modern-compiler") {
531
618
  throw new Error("Modern API is not supported for 'node-sass'");
532
619
  }
533
620
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sass-loader",
3
- "version": "14.1.0",
3
+ "version": "14.2.0",
4
4
  "description": "Sass loader for webpack",
5
5
  "license": "MIT",
6
6
  "repository": "webpack-contrib/sass-loader",
@@ -35,7 +35,7 @@
35
35
  "test:coverage": "npm run test:only -- --collectCoverageFrom=\"src/**/*.js\" --coverage",
36
36
  "pretest": "npm run lint",
37
37
  "test": "npm run test:coverage",
38
- "prepare": "husky install && npm run build",
38
+ "prepare": "husky && npm run build",
39
39
  "release": "standard-version"
40
40
  },
41
41
  "files": [
@@ -70,8 +70,8 @@
70
70
  },
71
71
  "devDependencies": {
72
72
  "@babel/cli": "^7.23.4",
73
- "@babel/core": "^7.23.7",
74
- "@babel/preset-env": "^7.23.8",
73
+ "@babel/core": "^7.24.0",
74
+ "@babel/preset-env": "^7.24.0",
75
75
  "@commitlint/cli": "^18.4.4",
76
76
  "@commitlint/config-conventional": "^18.4.4",
77
77
  "@webpack-contrib/eslint-config-webpack": "^3.0.0",
@@ -80,32 +80,34 @@
80
80
  "bootstrap-v4": "npm:bootstrap@^4.5.3",
81
81
  "bootstrap-v5": "npm:bootstrap@^5.0.1",
82
82
  "cross-env": "^7.0.3",
83
- "cspell": "^8.3.2",
83
+ "cspell": "^8.6.0",
84
84
  "css-loader": "^6.9.0",
85
85
  "del": "^6.1.1",
86
86
  "del-cli": "^5.1.0",
87
- "enhanced-resolve": "^5.15.0",
88
- "eslint": "^8.46.0",
87
+ "enhanced-resolve": "^5.15.1",
88
+ "eslint": "^8.57.0",
89
89
  "eslint-config-prettier": "^9.1.0",
90
90
  "eslint-plugin-import": "^2.28.0",
91
91
  "file-loader": "^6.2.0",
92
92
  "foundation-sites": "^6.7.5",
93
- "husky": "^8.0.3",
93
+ "husky": "^9.0.11",
94
94
  "jest": "^29.6.2",
95
95
  "jest-environment-node-single-context": "^29.1.0",
96
96
  "lint-staged": "^15.2.0",
97
97
  "material-components-web": "^9.0.0",
98
- "memfs": "^4.6.0",
98
+ "memfs": "^4.7.7",
99
99
  "node-sass": "^9.0.0",
100
100
  "node-sass-glob-importer": "^5.3.2",
101
101
  "npm-run-all": "^4.1.5",
102
102
  "prettier": "^3.2.2",
103
- "sass": "^1.69.7",
104
- "sass-embedded": "^1.69.7",
103
+ "sass": "^1.71.1",
104
+ "sass-embedded": "^1.71.1",
105
105
  "semver": "^7.5.4",
106
106
  "standard-version": "^9.3.1",
107
107
  "style-loader": "^3.3.4",
108
- "webpack": "^5.88.2"
108
+ "webpack": "^5.88.2",
109
+ "webpack-cli": "^5.1.4",
110
+ "webpack-dev-server": "^5.0.4"
109
111
  },
110
112
  "keywords": [
111
113
  "sass",