Skip to content

Commit

Permalink
refactor: extract LoaderContext from resolver used in Sass importer
Browse files Browse the repository at this point in the history
  • Loading branch information
vvanpo committed Aug 2, 2020
1 parent c6d56e4 commit 5e7bd57
Showing 1 changed file with 95 additions and 64 deletions.
159 changes: 95 additions & 64 deletions src/utils.js
Expand Up @@ -211,22 +211,19 @@ const isModuleImport = /^~([^/]+|[^/]+\/|@[^/]+[/][^/]+|@[^/]+\/?|@[^/]+[/][^/]+
*
* @param {string} url
* @param {boolean} forWebpackResolver
* @param {Object} loaderContext
* @param {string} rootContext
* @returns {Array<string>}
*/
export default function getPossibleRequests(
loaderContext,
function getPossibleRequests(
// eslint-disable-next-line no-shadow
url,
forWebpackResolver = false
forWebpackResolver = false,
rootContext = false
) {
const request = urlToRequest(
url,
// Maybe it is server-relative URLs
forWebpackResolver && url.charAt(0) === '/'
? loaderContext.rootContext
: // eslint-disable-next-line no-undefined
undefined
forWebpackResolver && rootContext
);

// In case there is module request, send this to webpack resolver
Expand Down Expand Up @@ -262,75 +259,83 @@ export default function getPossibleRequests(
];
}

const matchCss = /\.css$/i;
function promiseResolve(callbackResolve) {
return (context, request) =>
new Promise((resolve, reject) => {
callbackResolve(context, request, (err, result) => {
if (err) reject(err);
else resolve(result);
});
});
}

const isSpecialModuleImport = /^~[^/]+$/;
// `[drive_letter]:\` + `\\[server]\[sharename]\`
const isNativeWin32Path = /^[a-zA-Z]:[/\\]|^\\\\/i;

function getWebpackImporter(loaderContext, implementation, includePaths) {
function getWebpackResolver(
implementation,
resolverFactory,
includePaths = [],
rootContext = false
) {
function startResolving(resolutionMap) {
if (resolutionMap.length === 0) {
return Promise.reject();
}

const [{ resolve, context, possibleRequests }] = resolutionMap;

return resolve(context, possibleRequests[0])
.then((result) => {
// Add the result as dependency.
// Although we're also using stats.includedFiles, this might come in handy when an error occurs.
// In this case, we don't get stats.includedFiles from node-sass/sass.
loaderContext.addDependency(path.normalize(result));
return resolve(context, possibleRequests[0]).catch(() => {
const [, ...tailResult] = possibleRequests;

// By removing the CSS file extension, we trigger node-sass to include the CSS file instead of just linking it.
return { file: result.replace(matchCss, '') };
})
.catch(() => {
const [, ...tailResult] = possibleRequests;
if (tailResult.length === 0) {
const [, ...tailResolutionMap] = resolutionMap;

if (tailResult.length === 0) {
const [, ...tailResolutionMap] = resolutionMap;

return startResolving(tailResolutionMap);
}
return startResolving(tailResolutionMap);
}

// eslint-disable-next-line no-param-reassign
resolutionMap[0].possibleRequests = tailResult;
// eslint-disable-next-line no-param-reassign
resolutionMap[0].possibleRequests = tailResult;

return startResolving(resolutionMap);
});
return startResolving(resolutionMap);
});
}

const sassResolve = loaderContext.getResolve({
alias: [],
aliasFields: [],
conditionNames: [],
descriptionFiles: [],
extensions: ['.sass', '.scss', '.css'],
exportsFields: [],
mainFields: [],
mainFiles: ['_index', 'index'],
modules: [],
restrictions: [/\.((sa|sc|c)ss)$/i],
});
const webpackResolve = loaderContext.getResolve({
conditionNames: ['sass', 'style'],
mainFields: ['sass', 'style', 'main', '...'],
mainFiles: ['_index', 'index', '...'],
extensions: ['.sass', '.scss', '.css'],
restrictions: [/\.((sa|sc|c)ss)$/i],
});

return (originalUrl, prev, done) => {
let request = originalUrl;
const sassResolve = promiseResolve(
resolverFactory({
alias: [],
aliasFields: [],
conditionNames: [],
descriptionFiles: [],
extensions: ['.sass', '.scss', '.css'],
exportsFields: [],
mainFields: [],
mainFiles: ['_index', 'index'],
modules: [],
restrictions: [/\.((sa|sc|c)ss)$/i],
})
);
const webpackResolve = promiseResolve(
resolverFactory({
conditionNames: ['sass', 'style'],
mainFields: ['sass', 'style', 'main', '...'],
mainFiles: ['_index', 'index', '...'],
extensions: ['.sass', '.scss', '.css'],
restrictions: [/\.((sa|sc|c)ss)$/i],
})
);

const isFileScheme = originalUrl.slice(0, 5).toLowerCase() === 'file:';
return (context, request) => {
const originalRequest = request;
const isFileScheme = originalRequest.slice(0, 5).toLowerCase() === 'file:';

if (isFileScheme) {
try {
// eslint-disable-next-line no-param-reassign
request = url.fileURLToPath(originalUrl);
request = url.fileURLToPath(originalRequest);
} catch (ignoreError) {
// eslint-disable-next-line no-param-reassign
request = request.slice(7);
}
}
Expand All @@ -345,8 +350,8 @@ function getWebpackImporter(loaderContext, implementation, includePaths) {
// - Server-relative URLs - `<context>/path/to/file.ext` (where `<context>` is root context)
// - Absolute path - `/full/path/to/file.ext` or `C:\\full\path\to\file.ext`
!isFileScheme &&
!originalUrl.startsWith('/') &&
!isNativeWin32Path.test(originalUrl);
!originalRequest.startsWith('/') &&
!isNativeWin32Path.test(originalRequest);

if (includePaths.length > 0 && needEmulateSassResolver) {
// The order of import precedence is as follows:
Expand All @@ -358,19 +363,20 @@ function getWebpackImporter(loaderContext, implementation, includePaths) {
// 5. Filesystem imports relative to a `SASS_PATH` path.
//
// Because `sass`/`node-sass` run custom importers before `3`, `4` and `5` points, we need to emulate this behavior to avoid wrong resolution.
const sassPossibleRequests = getPossibleRequests(loaderContext, request);
const sassPossibleRequests = getPossibleRequests(request);
const isDartSass = implementation.info.includes('dart-sass');

// `node-sass` calls our importer before `1. Filesystem imports relative to the base file.`, so we need emulate this too
if (!isDartSass) {
resolutionMap = resolutionMap.concat({
resolve: sassResolve,
context: path.dirname(prev),
context: path.dirname(context),
possibleRequests: sassPossibleRequests,
});
}

resolutionMap = resolutionMap.concat(
// eslint-disable-next-line no-shadow
includePaths.map((context) => ({
resolve: sassResolve,
context,
Expand All @@ -380,21 +386,45 @@ function getWebpackImporter(loaderContext, implementation, includePaths) {
}

const webpackPossibleRequests = getPossibleRequests(
loaderContext,
request,
true
true,
rootContext
);

resolutionMap = resolutionMap.concat({
resolve: webpackResolve,
context: path.dirname(prev),
context: path.dirname(context),
possibleRequests: webpackPossibleRequests,
});

startResolving(resolutionMap)
return startResolving(resolutionMap);
};
}

const matchCss = /\.css$/i;

function getWebpackImporter(loaderContext, implementation, includePaths) {
const resolve = getWebpackResolver(
implementation,
loaderContext.getResolve,
includePaths,
loaderContext.rootContext
);

return (originalUrl, prev, done) => {
resolve(prev, originalUrl)
.then((result) => {
// Add the result as dependency.
// Although we're also using stats.includedFiles, this might come in handy when an error occurs.
// In this case, we don't get stats.includedFiles from node-sass/sass.
loaderContext.addDependency(path.normalize(result));
// By removing the CSS file extension, we trigger node-sass to include the CSS file instead of just linking it.
done({ file: result.replace(matchCss, '') });
})
// Catch all resolving errors, return the original file and pass responsibility back to other custom importers
.catch(() => ({ file: originalUrl }))
.then(done);
.catch(() => {
done({ file: originalUrl });
});
};
}

Expand Down Expand Up @@ -431,6 +461,7 @@ function getRenderFunctionFromSassImplementation(implementation) {
export {
getSassImplementation,
getSassOptions,
getWebpackResolver,
getWebpackImporter,
getRenderFunctionFromSassImplementation,
};

0 comments on commit 5e7bd57

Please sign in to comment.