sass-loader 16.0.7 → 17.0.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 +22 -49
- package/dist/cjs/index.js +78 -0
- package/dist/{options.json → cjs/options.json} +3 -3
- package/dist/cjs/package.json +1 -0
- package/dist/{utils.js → cjs/utils.js} +251 -327
- package/dist/esm/index.js +71 -0
- package/dist/esm/options.json +64 -0
- package/dist/esm/utils.js +647 -0
- package/package.json +55 -66
- package/types/index.d.ts +19 -0
- package/types/utils.d.ts +294 -0
- package/dist/cjs.js +0 -4
- package/dist/index.js +0 -103
|
@@ -0,0 +1,647 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import url from "node:url";
|
|
3
|
+
|
|
4
|
+
// eslint-disable-next-line jsdoc/reject-any-type
|
|
5
|
+
/** @typedef {any} EXPECTED_ANY */
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @typedef {object} SourceLocation
|
|
9
|
+
* @property {number} line line number
|
|
10
|
+
* @property {number} column column number
|
|
11
|
+
* @property {number} offset character offset
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @typedef {object} SourceSpan
|
|
16
|
+
* @property {SourceLocation} start start location
|
|
17
|
+
* @property {SourceLocation} end end location
|
|
18
|
+
* @property {URL=} url canonical URL of the file
|
|
19
|
+
* @property {string} text covered text
|
|
20
|
+
* @property {string=} context surrounding context
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/** @typedef {{ deprecation?: boolean, span?: SourceSpan, stack?: string }} LoggerWarnOptions */
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @typedef {object} Logger
|
|
27
|
+
* @property {((message: string, options: LoggerWarnOptions) => void)=} warn warn handler
|
|
28
|
+
* @property {((message: string, options: { span: SourceSpan }) => void)=} debug debug handler
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @typedef {object} CompileResult
|
|
33
|
+
* @property {Buffer | string} css css output
|
|
34
|
+
* @property {RawSourceMap=} sourceMap source map
|
|
35
|
+
* @property {URL[]} loadedUrls loaded URLs
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @typedef {object} Importer
|
|
40
|
+
* @property {(originalUrl: string, context: { containingUrl: URL | null, fromImport: boolean }) => Promise<URL | null>} canonicalize canonicalize
|
|
41
|
+
* @property {(canonicalUrl: URL) => Promise<{ contents: string, syntax: "scss" | "indented" | "css", sourceMapUrl?: URL } | null>} load load
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
/** @typedef {"expanded" | "compressed"} OutputStyle */
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @typedef {object} KnownSassOptions
|
|
48
|
+
* @property {"scss" | "indented" | "css"=} syntax syntax
|
|
49
|
+
* @property {URL=} url url
|
|
50
|
+
* @property {"expanded" | "compressed"=} style style
|
|
51
|
+
* @property {string[]=} loadPaths load paths
|
|
52
|
+
* @property {boolean=} sourceMap source map
|
|
53
|
+
* @property {boolean=} sourceMapIncludeSources source map include sources
|
|
54
|
+
* @property {Importer[]=} importers importers
|
|
55
|
+
* @property {Logger=} logger logger
|
|
56
|
+
*/
|
|
57
|
+
|
|
58
|
+
/** @typedef {KnownSassOptions & Record<string, EXPECTED_ANY>} SassOptions */
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* @typedef {object} AsyncCompiler
|
|
62
|
+
* @property {(source: string, options?: SassOptions) => Promise<CompileResult>} compileStringAsync compile a string
|
|
63
|
+
* @property {() => Promise<void>} dispose dispose the compiler
|
|
64
|
+
*/
|
|
65
|
+
|
|
66
|
+
/** @typedef {{ info: string; compileStringAsync(source: string, options?: SassOptions): Promise<CompileResult>; initAsyncCompiler?(): Promise<AsyncCompiler> }} SassImplementation */
|
|
67
|
+
|
|
68
|
+
/** @typedef {"auto" | "modern" | "modern-compiler"} ApiType */
|
|
69
|
+
|
|
70
|
+
/** @typedef {import("webpack").LoaderContext<LoaderOptions>} LoaderContext */
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* @typedef {object} LoaderOptions
|
|
74
|
+
* @property {SassImplementation=} implementation SaSS implementation
|
|
75
|
+
* @property {SassOptions | ((loaderContext: LoaderContext) => SassOptions)=} sassOptions SaSS options
|
|
76
|
+
* @property {boolean=} sourceMap true if source map is enabled, otherwise false
|
|
77
|
+
* @property {string | ((content: string, loaderContext: LoaderContext) => string)=} additionalData prepends Sass/SCSS code before the actual entry file
|
|
78
|
+
* @property {boolean=} webpackImporter true if webpack importer is enabled, otherwise false
|
|
79
|
+
* @property {ApiType=} api API type
|
|
80
|
+
* @property {boolean=} warnRuleAsWarning true if treats the `@warn` rule as a webpack warning, otherwise false
|
|
81
|
+
*/
|
|
82
|
+
|
|
83
|
+
/** @typedef {(context: string, request: string, fromImport?: boolean) => Promise<string>} Resolver */
|
|
84
|
+
/** @typedef {{ resolve: (context: string, request: string) => Promise<string>, context: string, possibleRequests: string[] }[]} ResolutionMap */
|
|
85
|
+
/** @typedef {{ version: number, sources: string[], names: string[], sourceRoot?: string, sourcesContent?: string[], mappings: string, file: string, debugId?: string, ignoreList?: number[] }} RawSourceMap */
|
|
86
|
+
/** @typedef {Error & { formatted?: string, span?: { url?: URL, start: { line: number, column: number }, context?: string } }} SassError */
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Convert a string `implementation` option into something the ECMAScript
|
|
90
|
+
* `import()` expression actually accepts. Bare package specifiers and
|
|
91
|
+
* `file:` URLs are passed through unchanged; absolute filesystem paths
|
|
92
|
+
* (including Windows paths like `C:\\...`) are converted to `file:` URLs
|
|
93
|
+
* — dynamic `import()` rejects those otherwise.
|
|
94
|
+
* @param {string} specifier import specifier
|
|
95
|
+
* @returns {string} a valid dynamic import specifier
|
|
96
|
+
*/
|
|
97
|
+
function normalizeImportSpecifier(specifier) {
|
|
98
|
+
if (specifier.startsWith("file:")) {
|
|
99
|
+
return specifier;
|
|
100
|
+
}
|
|
101
|
+
if (path.isAbsolute(specifier)) {
|
|
102
|
+
return url.pathToFileURL(specifier).href;
|
|
103
|
+
}
|
|
104
|
+
return specifier;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* This function is not Webpack-specific and can be used by tools wishing to mimic `sass-loader`'s behaviour, so its signature should not be changed.
|
|
109
|
+
* @param {SassImplementation | string | undefined} implementation sass implementation
|
|
110
|
+
* @returns {Promise<SassImplementation>} resolved sass implementation
|
|
111
|
+
*/
|
|
112
|
+
async function getSassImplementation(implementation) {
|
|
113
|
+
/** @type {SassImplementation} */
|
|
114
|
+
let resolvedImplementation;
|
|
115
|
+
if (!implementation) {
|
|
116
|
+
try {
|
|
117
|
+
resolvedImplementation = /** @type {SassImplementation} */
|
|
118
|
+
await import("sass-embedded");
|
|
119
|
+
} catch (err) {
|
|
120
|
+
// Only fall back to `sass` when `sass-embedded` is not installed.
|
|
121
|
+
// Any other failure (e.g. a broken install or a side-effect throw at
|
|
122
|
+
// module-load time) should surface so the user can diagnose it
|
|
123
|
+
// instead of being silently masked by the `sass` fallback.
|
|
124
|
+
const {
|
|
125
|
+
code
|
|
126
|
+
} = /** @type {NodeJS.ErrnoException} */err;
|
|
127
|
+
if (code !== "ERR_MODULE_NOT_FOUND" && code !== "MODULE_NOT_FOUND") {
|
|
128
|
+
throw err;
|
|
129
|
+
}
|
|
130
|
+
resolvedImplementation = /** @type {SassImplementation} */
|
|
131
|
+
await import("sass");
|
|
132
|
+
}
|
|
133
|
+
} else if (typeof implementation === "string") {
|
|
134
|
+
resolvedImplementation = /** @type {SassImplementation} */
|
|
135
|
+
await import(normalizeImportSpecifier(implementation));
|
|
136
|
+
} else {
|
|
137
|
+
resolvedImplementation = implementation;
|
|
138
|
+
}
|
|
139
|
+
const {
|
|
140
|
+
info
|
|
141
|
+
} = resolvedImplementation;
|
|
142
|
+
if (!info) {
|
|
143
|
+
throw new Error("Unknown Sass implementation.");
|
|
144
|
+
}
|
|
145
|
+
const infoParts = info.split("\t");
|
|
146
|
+
if (infoParts.length < 2) {
|
|
147
|
+
throw new Error(`Unknown Sass implementation "${info}".`);
|
|
148
|
+
}
|
|
149
|
+
const [implementationName] = infoParts;
|
|
150
|
+
if (implementationName === "dart-sass") {
|
|
151
|
+
return resolvedImplementation;
|
|
152
|
+
} else if (implementationName === "sass-embedded") {
|
|
153
|
+
return resolvedImplementation;
|
|
154
|
+
}
|
|
155
|
+
throw new Error(`Unknown Sass implementation "${implementationName}".`);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* @param {LoaderContext} loaderContext loader context
|
|
160
|
+
* @returns {boolean} true when mode is production, otherwise false
|
|
161
|
+
*/
|
|
162
|
+
function isProductionLikeMode(loaderContext) {
|
|
163
|
+
return loaderContext.mode === "production" || !loaderContext.mode;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Derives the sass options from the loader context and normalizes its values with sane defaults.
|
|
168
|
+
* @param {LoaderContext} loaderContext loader context
|
|
169
|
+
* @param {LoaderOptions} loaderOptions loader options
|
|
170
|
+
* @param {string} content content
|
|
171
|
+
* @param {boolean} useSourceMap true when need to generate source maps, otherwise false
|
|
172
|
+
* @returns {Promise<Required<KnownSassOptions> & { data: string }>} sass options
|
|
173
|
+
*/
|
|
174
|
+
async function getSassOptions(loaderContext, loaderOptions, content, useSourceMap) {
|
|
175
|
+
/** @type {SassOptions} */
|
|
176
|
+
const options = loaderOptions.sassOptions ? typeof loaderOptions.sassOptions === "function" ? loaderOptions.sassOptions(loaderContext) || {} : loaderOptions.sassOptions : {};
|
|
177
|
+
/** @type {KnownSassOptions & { data: string }} */
|
|
178
|
+
const sassOptions = {
|
|
179
|
+
...options,
|
|
180
|
+
data: loaderOptions.additionalData ? typeof loaderOptions.additionalData === "function" ? await loaderOptions.additionalData(content, loaderContext) : `${loaderOptions.additionalData}\n${content}` : content
|
|
181
|
+
};
|
|
182
|
+
if (!sassOptions.logger) {
|
|
183
|
+
const needEmitWarning = loaderOptions.warnRuleAsWarning !== false;
|
|
184
|
+
const logger = loaderContext.getLogger("sass-loader");
|
|
185
|
+
/**
|
|
186
|
+
* @param {SourceSpan} span span
|
|
187
|
+
* @returns {string} formatted span
|
|
188
|
+
*/
|
|
189
|
+
const formatSpan = span => `Warning on line ${span.start.line}, column ${span.start.column} of ${span.url || "-"}:${span.start.line}:${span.start.column}:\n`;
|
|
190
|
+
/**
|
|
191
|
+
* @param {SourceSpan} span span
|
|
192
|
+
* @returns {string} formatted debug span
|
|
193
|
+
*/
|
|
194
|
+
const formatDebugSpan = span => `[debug:${span.start.line}:${span.start.column}] `;
|
|
195
|
+
sassOptions.logger = {
|
|
196
|
+
/**
|
|
197
|
+
* @param {string} message message
|
|
198
|
+
* @param {{ span: SourceSpan }} loggerOptions logger options
|
|
199
|
+
* @returns {void}
|
|
200
|
+
*/
|
|
201
|
+
debug(message, loggerOptions) {
|
|
202
|
+
let builtMessage = "";
|
|
203
|
+
if (loggerOptions.span) {
|
|
204
|
+
builtMessage = formatDebugSpan(loggerOptions.span);
|
|
205
|
+
}
|
|
206
|
+
builtMessage += message;
|
|
207
|
+
logger.debug(builtMessage);
|
|
208
|
+
},
|
|
209
|
+
/**
|
|
210
|
+
* @param {string} message message
|
|
211
|
+
* @param {LoggerWarnOptions} loggerOptions logger options
|
|
212
|
+
* @returns {void}
|
|
213
|
+
*/
|
|
214
|
+
warn(message, loggerOptions) {
|
|
215
|
+
let builtMessage = "";
|
|
216
|
+
if (loggerOptions.deprecation) {
|
|
217
|
+
builtMessage += "Deprecation ";
|
|
218
|
+
}
|
|
219
|
+
if (loggerOptions.span) {
|
|
220
|
+
builtMessage += formatSpan(loggerOptions.span);
|
|
221
|
+
}
|
|
222
|
+
builtMessage += message;
|
|
223
|
+
if (loggerOptions.span && loggerOptions.span.context) {
|
|
224
|
+
builtMessage += `\n\n${loggerOptions.span.start.line} | ${loggerOptions.span.context}`;
|
|
225
|
+
}
|
|
226
|
+
if (loggerOptions.stack && loggerOptions.stack !== "null") {
|
|
227
|
+
builtMessage += `\n\n${loggerOptions.stack}`;
|
|
228
|
+
}
|
|
229
|
+
if (needEmitWarning) {
|
|
230
|
+
const warning = new Error(builtMessage);
|
|
231
|
+
warning.name = "SassWarning";
|
|
232
|
+
warning.stack = undefined;
|
|
233
|
+
loaderContext.emitWarning(warning);
|
|
234
|
+
} else {
|
|
235
|
+
logger.warn(builtMessage);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
const {
|
|
241
|
+
resourcePath
|
|
242
|
+
} = loaderContext;
|
|
243
|
+
sassOptions.url = url.pathToFileURL(resourcePath);
|
|
244
|
+
|
|
245
|
+
// opt.outputStyle
|
|
246
|
+
if (!sassOptions.style && isProductionLikeMode(loaderContext)) {
|
|
247
|
+
sassOptions.style = "compressed";
|
|
248
|
+
}
|
|
249
|
+
if (useSourceMap) {
|
|
250
|
+
sassOptions.sourceMap = true;
|
|
251
|
+
sassOptions.sourceMapIncludeSources = true;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// If we are compiling sass and indentedSyntax isn't set, automatically set it.
|
|
255
|
+
if (typeof sassOptions.syntax === "undefined") {
|
|
256
|
+
const ext = path.extname(resourcePath);
|
|
257
|
+
if (ext && ext.toLowerCase() === ".scss") {
|
|
258
|
+
sassOptions.syntax = "scss";
|
|
259
|
+
} else if (ext && ext.toLowerCase() === ".sass") {
|
|
260
|
+
sassOptions.syntax = "indented";
|
|
261
|
+
} else if (ext && ext.toLowerCase() === ".css") {
|
|
262
|
+
sassOptions.syntax = "css";
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
sassOptions.loadPaths = [
|
|
266
|
+
// We use `loadPaths` in context for resolver, so it should be always absolute
|
|
267
|
+
...(sassOptions.loadPaths ? [...sassOptions.loadPaths] : []).map(includePath => path.isAbsolute(includePath) ? includePath : path.join(process.cwd(), includePath)), ...(process.env.SASS_PATH ? process.env.SASS_PATH.split(process.platform === "win32" ? ";" : ":") : [])];
|
|
268
|
+
sassOptions.importers = sassOptions.importers ? Array.isArray(sassOptions.importers) ? [...sassOptions.importers] : [sassOptions.importers] : [];
|
|
269
|
+
return /** @type {Required<KnownSassOptions> & { data: string }} */sassOptions;
|
|
270
|
+
}
|
|
271
|
+
const MODULE_REQUEST_REGEX = /^[^?]*~/;
|
|
272
|
+
|
|
273
|
+
// Examples:
|
|
274
|
+
// - ~package
|
|
275
|
+
// - ~package/
|
|
276
|
+
// - ~@org
|
|
277
|
+
// - ~@org/
|
|
278
|
+
// - ~@org/package
|
|
279
|
+
// - ~@org/package/
|
|
280
|
+
const IS_MODULE_IMPORT = /^~([^/]+|[^/]+\/|@[^/]+[/][^/]+|@[^/]+\/?|@[^/]+[/][^/]+\/)$/;
|
|
281
|
+
const IS_PKG_SCHEME = /^pkg:/i;
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* When `sass` tries to resolve an import, it uses a special algorithm.
|
|
285
|
+
* Since the `sass-loader` uses webpack to resolve the modules, we need to simulate that algorithm.
|
|
286
|
+
* This function returns an array of import paths to try.
|
|
287
|
+
* The last entry in the array is always the original url to enable straight-forward webpack.config aliases.
|
|
288
|
+
*
|
|
289
|
+
* We don't need emulate `dart-sass` "It's not clear which file to import." errors (when "file.ext" and "_file.ext" files are present simultaneously in the same directory).
|
|
290
|
+
* This reduces performance and `dart-sass` always do it on own side.
|
|
291
|
+
* @param {string} url url
|
|
292
|
+
* @param {boolean} forWebpackResolver true when for webpack resolver, otherwise false
|
|
293
|
+
* @param {boolean} fromImport true when from `@import`, otherwise false
|
|
294
|
+
* @returns {string[]} possible requests
|
|
295
|
+
*/
|
|
296
|
+
function getPossibleRequests(url, forWebpackResolver = false, fromImport = false) {
|
|
297
|
+
let request = url;
|
|
298
|
+
|
|
299
|
+
// In case there is module request, send this to webpack resolver
|
|
300
|
+
if (forWebpackResolver) {
|
|
301
|
+
if (MODULE_REQUEST_REGEX.test(url)) {
|
|
302
|
+
request = request.replace(MODULE_REQUEST_REGEX, "");
|
|
303
|
+
}
|
|
304
|
+
if (IS_PKG_SCHEME.test(url)) {
|
|
305
|
+
request = `${request.slice(4)}`;
|
|
306
|
+
return [...new Set([request, url])];
|
|
307
|
+
}
|
|
308
|
+
if (IS_MODULE_IMPORT.test(url) || IS_PKG_SCHEME.test(url)) {
|
|
309
|
+
request = request[request.length - 1] === "/" ? request : `${request}/`;
|
|
310
|
+
return [...new Set([request, url])];
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Keep in mind: ext can also be something like '.datepicker' when the true extension is omitted and the filename contains a dot.
|
|
315
|
+
// @see https://github.com/webpack/sass/issues/167
|
|
316
|
+
const extension = path.extname(request).toLowerCase();
|
|
317
|
+
|
|
318
|
+
// Because @import is also defined in CSS, Sass needs a way of compiling plain CSS @imports without trying to import the files at compile time.
|
|
319
|
+
// To accomplish this, and to ensure SCSS is as much of a superset of CSS as possible, Sass will compile any @imports with the following characteristics to plain CSS imports:
|
|
320
|
+
// - imports where the URL ends with .css.
|
|
321
|
+
// - imports where the URL begins http:// or https://.
|
|
322
|
+
// - imports where the URL is written as a url().
|
|
323
|
+
// - imports that have media queries.
|
|
324
|
+
//
|
|
325
|
+
// sass outputs as is `@import "style.css"`, but `@use "style.css"` should include CSS content
|
|
326
|
+
if (extension === ".css") {
|
|
327
|
+
return fromImport ? [] : [url];
|
|
328
|
+
}
|
|
329
|
+
const dirname = path.dirname(request).replaceAll("\\", "/");
|
|
330
|
+
const normalizedDirname = dirname === "." ? "" : `${dirname}/`;
|
|
331
|
+
const basename = path.basename(request);
|
|
332
|
+
const basenameWithoutExtension = path.basename(request, extension);
|
|
333
|
+
return [...new Set([...[fromImport ? [`${normalizedDirname}_${basenameWithoutExtension}.import${extension}`, `${normalizedDirname}${basenameWithoutExtension}.import${extension}`] : []].flat(), `${normalizedDirname}_${basename}`, `${normalizedDirname}${basename}`, ...(forWebpackResolver ? [url] : [])])];
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* @param {(context: string, request: string, callback: (error: Error | null, result: string) => void) => void} callbackResolve callback resolve
|
|
338
|
+
* @returns {(context: string, request: string) => Promise<string>} promise resolve
|
|
339
|
+
*/
|
|
340
|
+
function promiseResolve(callbackResolve) {
|
|
341
|
+
return (context, request) => new Promise((resolve, reject) => {
|
|
342
|
+
callbackResolve(context, request, (error, result) => {
|
|
343
|
+
if (error) {
|
|
344
|
+
reject(error);
|
|
345
|
+
} else {
|
|
346
|
+
resolve(result);
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* @param {ResolutionMap} resolutionMap resolution map
|
|
354
|
+
* @returns {Promise<string>} resolved value
|
|
355
|
+
*/
|
|
356
|
+
async function startResolving(resolutionMap) {
|
|
357
|
+
if (resolutionMap.length === 0) {
|
|
358
|
+
throw new Error("Next");
|
|
359
|
+
}
|
|
360
|
+
const [{
|
|
361
|
+
possibleRequests
|
|
362
|
+
}] = resolutionMap;
|
|
363
|
+
if (possibleRequests.length === 0) {
|
|
364
|
+
throw new Error("Next");
|
|
365
|
+
}
|
|
366
|
+
const [{
|
|
367
|
+
resolve,
|
|
368
|
+
context
|
|
369
|
+
}] = resolutionMap;
|
|
370
|
+
try {
|
|
371
|
+
return await resolve(context, possibleRequests[0]);
|
|
372
|
+
} catch {
|
|
373
|
+
const [, ...tailResult] = possibleRequests;
|
|
374
|
+
if (tailResult.length === 0) {
|
|
375
|
+
const [, ...tailResolutionMap] = resolutionMap;
|
|
376
|
+
return startResolving(tailResolutionMap);
|
|
377
|
+
}
|
|
378
|
+
resolutionMap[0].possibleRequests = tailResult;
|
|
379
|
+
return startResolving(resolutionMap);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// `[drive_letter]:\` + `\\[server]\[sharename]\`
|
|
384
|
+
const IS_NATIVE_WIN32_PATH = /^[a-z]:[/\\]|^\\\\/i;
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Create the resolve function used in the custom Sass importer.
|
|
388
|
+
* Can be used by external tools to mimic how `sass-loader` works, for example
|
|
389
|
+
* in a Jest transform. Such usages will want to wrap `resolve.create` from
|
|
390
|
+
* [`enhanced-resolve`]{@link https://github.com/webpack/enhanced-resolve} to
|
|
391
|
+
* pass as the `resolverFactory` argument.
|
|
392
|
+
* @param {LoaderContext["getResolve"]} resolverFactory a factory function for creating a Webpack resolver.
|
|
393
|
+
* @returns {Resolver} webpack resolver
|
|
394
|
+
*/
|
|
395
|
+
function getWebpackResolver(resolverFactory) {
|
|
396
|
+
// We only have one difference with the built-in sass resolution logic and out resolution logic:
|
|
397
|
+
// First, we look at the files starting with `_`, then without `_` (i.e. `_name.sass`, `_name.scss`, `_name.css`, `name.sass`, `name.scss`, `name.css`),
|
|
398
|
+
// although `sass` look together by extensions (i.e. `_name.sass`/`name.sass`/`_name.scss`/`name.scss`/`_name.css`/`name.css`).
|
|
399
|
+
// It shouldn't be a problem because `sass` throw errors:
|
|
400
|
+
// - on having `_name.sass` and `name.sass` (extension can be `sass`, `scss` or `css`) in the same directory
|
|
401
|
+
// - on having `_name.sass` and `_name.scss` in the same directory
|
|
402
|
+
//
|
|
403
|
+
// Also `sass` prefer `sass`/`scss` over `css`.
|
|
404
|
+
const webpackModuleResolve = promiseResolve(resolverFactory({
|
|
405
|
+
dependencyType: "sass",
|
|
406
|
+
conditionNames: ["sass", "style", "..."],
|
|
407
|
+
mainFields: ["sass", "style", "main", "..."],
|
|
408
|
+
mainFiles: ["_index", "index", "..."],
|
|
409
|
+
extensions: [".sass", ".scss", ".css"],
|
|
410
|
+
restrictions: [/\.((sa|sc|c)ss)$/i],
|
|
411
|
+
preferRelative: true
|
|
412
|
+
}));
|
|
413
|
+
const webpackImportResolve = promiseResolve(resolverFactory({
|
|
414
|
+
dependencyType: "sass",
|
|
415
|
+
conditionNames: ["sass", "style", "..."],
|
|
416
|
+
mainFields: ["sass", "style", "main", "..."],
|
|
417
|
+
mainFiles: ["_index.import", "_index", "index.import", "index", "..."],
|
|
418
|
+
extensions: [".sass", ".scss", ".css"],
|
|
419
|
+
restrictions: [/\.((sa|sc|c)ss)$/i],
|
|
420
|
+
preferRelative: true
|
|
421
|
+
}));
|
|
422
|
+
return (context, request, fromImport) => {
|
|
423
|
+
const originalRequest = request;
|
|
424
|
+
const isFileScheme = originalRequest.slice(0, 5).toLowerCase() === "file:";
|
|
425
|
+
if (isFileScheme) {
|
|
426
|
+
try {
|
|
427
|
+
request = url.fileURLToPath(originalRequest);
|
|
428
|
+
} catch {
|
|
429
|
+
request = request.slice(7);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/** @type {ResolutionMap} */
|
|
434
|
+
let resolutionMap = [];
|
|
435
|
+
const webpackPossibleRequests = getPossibleRequests(request, true, fromImport);
|
|
436
|
+
resolutionMap = [...resolutionMap, {
|
|
437
|
+
resolve: fromImport ? webpackImportResolve : webpackModuleResolve,
|
|
438
|
+
context: path.dirname(context),
|
|
439
|
+
possibleRequests: webpackPossibleRequests
|
|
440
|
+
}];
|
|
441
|
+
return startResolving(resolutionMap);
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* @param {LoaderContext} loaderContext loader context
|
|
447
|
+
* @returns {Importer} the modern webpack importer
|
|
448
|
+
*/
|
|
449
|
+
function getModernWebpackImporter(loaderContext) {
|
|
450
|
+
const resolve = getWebpackResolver(loaderContext.getResolve);
|
|
451
|
+
return {
|
|
452
|
+
/**
|
|
453
|
+
* @param {string} originalUrl original url
|
|
454
|
+
* @param {{ fromImport: boolean, containingUrl: URL | null }} context context
|
|
455
|
+
* @returns {Promise<URL | null>} canonicalized URL
|
|
456
|
+
*/
|
|
457
|
+
async canonicalize(originalUrl, context) {
|
|
458
|
+
const {
|
|
459
|
+
fromImport
|
|
460
|
+
} = context;
|
|
461
|
+
const prev = context.containingUrl ? url.fileURLToPath(context.containingUrl.toString()) : loaderContext.resourcePath;
|
|
462
|
+
let result;
|
|
463
|
+
try {
|
|
464
|
+
result = await resolve(prev, originalUrl, fromImport);
|
|
465
|
+
} catch {
|
|
466
|
+
// If no stylesheets are found, the importer should return null.
|
|
467
|
+
return null;
|
|
468
|
+
}
|
|
469
|
+
loaderContext.addDependency(path.normalize(result));
|
|
470
|
+
return url.pathToFileURL(result);
|
|
471
|
+
},
|
|
472
|
+
/**
|
|
473
|
+
* @param {URL} canonicalUrl canonical url
|
|
474
|
+
* @returns {Promise<{ contents: string, syntax: "scss" | "indented" | "css", sourceMapUrl: URL } | null>} load result
|
|
475
|
+
*/
|
|
476
|
+
async load(canonicalUrl) {
|
|
477
|
+
const ext = path.extname(canonicalUrl.pathname);
|
|
478
|
+
|
|
479
|
+
/** @type {"scss" | "indented" | "css"} */
|
|
480
|
+
let syntax;
|
|
481
|
+
if (ext && ext.toLowerCase() === ".scss") {
|
|
482
|
+
syntax = "scss";
|
|
483
|
+
} else if (ext && ext.toLowerCase() === ".sass") {
|
|
484
|
+
syntax = "indented";
|
|
485
|
+
} else if (ext && ext.toLowerCase() === ".css") {
|
|
486
|
+
syntax = "css";
|
|
487
|
+
} else {
|
|
488
|
+
// Fallback to default value
|
|
489
|
+
syntax = "scss";
|
|
490
|
+
}
|
|
491
|
+
try {
|
|
492
|
+
const contents = /** @type {string} */
|
|
493
|
+
await new Promise((resolve, reject) => {
|
|
494
|
+
// Old version of `enhanced-resolve` supports only path as a string
|
|
495
|
+
// TODO simplify in the next major release and pass URL
|
|
496
|
+
const canonicalPath = url.fileURLToPath(canonicalUrl);
|
|
497
|
+
loaderContext.fs.readFile(canonicalPath,
|
|
498
|
+
/**
|
|
499
|
+
* @param {NodeJS.ErrnoException | null} err error
|
|
500
|
+
* @param {Buffer | undefined} content content
|
|
501
|
+
* @returns {void}
|
|
502
|
+
*/
|
|
503
|
+
(err, content) => {
|
|
504
|
+
if (err || !content) {
|
|
505
|
+
reject(err);
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
resolve(content.toString("utf8"));
|
|
509
|
+
});
|
|
510
|
+
});
|
|
511
|
+
return {
|
|
512
|
+
contents,
|
|
513
|
+
syntax,
|
|
514
|
+
sourceMapUrl: canonicalUrl
|
|
515
|
+
};
|
|
516
|
+
} catch {
|
|
517
|
+
return null;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/** @type {WeakMap<import("webpack").Compiler, AsyncCompiler>} */
|
|
524
|
+
const sassModernCompilers = new WeakMap();
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* Verifies that the implementation and version of Sass is supported by this loader.
|
|
528
|
+
* @template {SassImplementation} T
|
|
529
|
+
* @param {LoaderContext} loaderContext loader context
|
|
530
|
+
* @param {T} implementation sass implementation
|
|
531
|
+
* @param {ApiType=} apiType api type
|
|
532
|
+
* @returns {(sassOptions: SassOptions & { data: string }) => Promise<CompileResult>} compile function
|
|
533
|
+
*/
|
|
534
|
+
function getCompileFn(loaderContext, implementation, apiType = "auto") {
|
|
535
|
+
const {
|
|
536
|
+
initAsyncCompiler
|
|
537
|
+
} = implementation;
|
|
538
|
+
if (apiType === "modern-compiler" || apiType === "auto" && typeof initAsyncCompiler === "function") {
|
|
539
|
+
return async (/** @type {SassOptions & { data: string }} */sassOptions) => {
|
|
540
|
+
const webpackCompiler = /** @type {LoaderContext & { _compiler?: import("webpack").Compiler }} */
|
|
541
|
+
loaderContext._compiler;
|
|
542
|
+
const {
|
|
543
|
+
data,
|
|
544
|
+
...rest
|
|
545
|
+
} = sassOptions;
|
|
546
|
+
|
|
547
|
+
// Some people can run the loader in a multi-threading way;
|
|
548
|
+
// there is no webpack compiler object in such case.
|
|
549
|
+
if (webpackCompiler && initAsyncCompiler) {
|
|
550
|
+
if (!sassModernCompilers.has(webpackCompiler)) {
|
|
551
|
+
// Create a long-running compiler process that can be reused
|
|
552
|
+
// for compiling individual files.
|
|
553
|
+
const compiler = await initAsyncCompiler();
|
|
554
|
+
|
|
555
|
+
// Check again because awaiting the initialization function
|
|
556
|
+
// introduces a race condition.
|
|
557
|
+
if (!sassModernCompilers.has(webpackCompiler)) {
|
|
558
|
+
sassModernCompilers.set(webpackCompiler, compiler);
|
|
559
|
+
webpackCompiler.hooks.shutdown.tap("sass-loader", () => {
|
|
560
|
+
compiler.dispose();
|
|
561
|
+
});
|
|
562
|
+
} else {
|
|
563
|
+
compiler.dispose();
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
return /** @type {AsyncCompiler} */sassModernCompilers.get(webpackCompiler).compileStringAsync(/** @type {string} */data, rest);
|
|
567
|
+
}
|
|
568
|
+
return implementation.compileStringAsync(/** @type {string} */data, rest);
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
return (/** @type {SassOptions & { data: string }} */sassOptions) => {
|
|
572
|
+
const {
|
|
573
|
+
data,
|
|
574
|
+
...rest
|
|
575
|
+
} = sassOptions;
|
|
576
|
+
return implementation.compileStringAsync(/** @type {string} */data, rest);
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
const ABSOLUTE_SCHEME = /^[A-Za-z0-9+\-.]+:/;
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* @param {string} source source
|
|
583
|
+
* @returns {"absolute" | "scheme-relative" | "path-absolute" | "path-relative"} a type of URL
|
|
584
|
+
*/
|
|
585
|
+
function getURLType(source) {
|
|
586
|
+
if (source[0] === "/") {
|
|
587
|
+
if (source[1] === "/") {
|
|
588
|
+
return "scheme-relative";
|
|
589
|
+
}
|
|
590
|
+
return "path-absolute";
|
|
591
|
+
}
|
|
592
|
+
if (IS_NATIVE_WIN32_PATH.test(source)) {
|
|
593
|
+
return "path-absolute";
|
|
594
|
+
}
|
|
595
|
+
return ABSOLUTE_SCHEME.test(source) ? "absolute" : "path-relative";
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* @param {RawSourceMap} map source map
|
|
600
|
+
* @param {string} rootContext root context
|
|
601
|
+
* @returns {RawSourceMap} normalized source map
|
|
602
|
+
*/
|
|
603
|
+
function normalizeSourceMap(map, rootContext) {
|
|
604
|
+
const newMap = map;
|
|
605
|
+
|
|
606
|
+
// result.map.file is an optional property that provides the output filename.
|
|
607
|
+
// Since we don't know the final filename in the webpack build chain yet, it makes no sense to have it.
|
|
608
|
+
|
|
609
|
+
if (typeof newMap.file !== "undefined") {
|
|
610
|
+
// @ts-expect-error need to fix on webpack side
|
|
611
|
+
delete newMap.file;
|
|
612
|
+
}
|
|
613
|
+
newMap.sourceRoot = "";
|
|
614
|
+
|
|
615
|
+
// sass returns POSIX paths, that's why we need to transform them back to native paths.
|
|
616
|
+
// This fixes an error on windows where the source-map module cannot resolve the source maps.
|
|
617
|
+
// @see https://github.com/webpack/sass-loader/issues/366#issuecomment-279460722
|
|
618
|
+
|
|
619
|
+
newMap.sources = newMap.sources.map((/** @type {string} */source) => {
|
|
620
|
+
const sourceType = getURLType(source);
|
|
621
|
+
|
|
622
|
+
// Do no touch `scheme-relative`, `path-absolute` and `absolute` types (except `file:`)
|
|
623
|
+
if (sourceType === "absolute" && /^file:/i.test(source)) {
|
|
624
|
+
return url.fileURLToPath(source);
|
|
625
|
+
} else if (sourceType === "path-relative") {
|
|
626
|
+
return path.resolve(rootContext, path.normalize(source));
|
|
627
|
+
}
|
|
628
|
+
return source;
|
|
629
|
+
});
|
|
630
|
+
return newMap;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/**
|
|
634
|
+
* @param {Error | SassError} error the original sass error
|
|
635
|
+
* @returns {Error} a new error
|
|
636
|
+
*/
|
|
637
|
+
function errorFactory(error) {
|
|
638
|
+
const sassError = /** @type {SassError} */error;
|
|
639
|
+
const message = sassError.formatted ? sassError.formatted.replace(/^(.+)?Error: /, "") : (error.message || error.toString()).replace(/^(.+)?Error: /, "");
|
|
640
|
+
const obj = new Error(message, {
|
|
641
|
+
cause: error
|
|
642
|
+
});
|
|
643
|
+
obj.name = error.name;
|
|
644
|
+
obj.stack = undefined;
|
|
645
|
+
return obj;
|
|
646
|
+
}
|
|
647
|
+
export { errorFactory, getCompileFn, getModernWebpackImporter, getSassImplementation, getSassOptions, getWebpackResolver, normalizeSourceMap };
|