Skip to content

Commit

Permalink
fix: 支持webpack中sass-loader的路径解析
Browse files Browse the repository at this point in the history
  • Loading branch information
YufJi committed Feb 26, 2025
1 parent 32d723d commit 3c77380
Show file tree
Hide file tree
Showing 3 changed files with 257 additions and 102 deletions.
3 changes: 2 additions & 1 deletion packages/mako/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"@swc/helpers": "0.5.1",
"@types/resolve": "^1.20.6",
"chalk": "^4.1.2",
"enhanced-resolve": "^5.18.1",
"less": "^4.2.0",
"less-plugin-resolve": "^1.0.2",
"lodash": "^4.17.21",
Expand Down Expand Up @@ -90,4 +91,4 @@
"access": "public"
},
"repository": "[email protected]:umijs/mako.git"
}
}
237 changes: 234 additions & 3 deletions packages/mako/src/sassLoader/render.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,254 @@
import { type Options } from 'sass';
import fs from 'fs';
import path from 'path';
import url from 'url';
import resolve, { type ResolveFunctionAsync } from 'enhanced-resolve';
import { type ImporterResult, type Options } from 'sass';

async function render(param: {
filename: string;
opts: Options<'async'> & { resources: string[] };
}): Promise<{ content: string; type: 'css' }> {
let sass;
let sass: any;
try {
sass = require('sass');
} catch (err) {
throw new Error(
'The "sass" package is not installed. Please run "npm install sass" to install it.',
);
}

const options = { style: 'compressed', ...param.opts };
options.importers = options.importers || [];
options.importers.push({
async canonicalize(originalUrl, context) {
const { fromImport } = context;
const prev = context.containingUrl
? url.fileURLToPath(context.containingUrl.toString())
: param.filename;

const resolver = getResolver(sass?.compileStringAsync);
try {
const result = await resolver(prev, originalUrl, fromImport);
return url.pathToFileURL(result) as URL;
} catch (err) {
return null;
}
},
async load(canonicalUrl) {
const ext = path.extname(canonicalUrl.pathname);

let syntax;

if (ext && ext.toLowerCase() === '.scss') {
syntax = 'scss';
} else if (ext && ext.toLowerCase() === '.sass') {
syntax = 'indented';
} else if (ext && ext.toLowerCase() === '.css') {
syntax = 'css';
} else {
// Fallback to default value
syntax = 'scss';
}

try {
const contents = await new Promise((resolve, reject) => {
const canonicalPath = url.fileURLToPath(canonicalUrl);

fs.readFile(
canonicalPath,
{
encoding: 'utf8',
},
(err, content) => {
if (err) {
reject(err);
return;
}

resolve(content);
},
);
});

return {
contents,
syntax,
sourceMapUrl: canonicalUrl,
} as ImporterResult;
} catch (err) {
return null;
}
},
});

const result = await sass
.compileAsync(param.filename, { style: 'compressed', ...param.opts })
.compileAsync(param.filename, options)
.catch((err: any) => {
throw new Error(err.toString());
});
return { content: result.css, type: 'css' };
}

export { render };

function getResolver(compileStringAsync: any) {
const isModernSass = typeof compileStringAsync !== 'undefined';

const importResolve = resolve.create({
conditionNames: ['sass', 'style', '...'],
mainFields: ['sass', 'style', 'main', '...'],
mainFiles: ['_index.import', '_index', 'index.import', 'index', '...'],
extensions: ['.sass', '.scss', '.css'],
restrictions: [/\.((sa|sc|c)ss)$/i],
preferRelative: true,
});
const moduleResolve = resolve.create({
conditionNames: ['sass', 'style', '...'],
mainFields: ['sass', 'style', 'main', '...'],
mainFiles: ['_index', 'index', '...'],
extensions: ['.sass', '.scss', '.css'],
restrictions: [/\.((sa|sc|c)ss)$/i],
preferRelative: true,
});

return (context: string, request: string, fromImport: boolean) => {
if (!isModernSass && !path.isAbsolute(context)) {
return Promise.reject();
}

const originalRequest = request;
const isFileScheme = originalRequest.slice(0, 5).toLowerCase() === 'file:';

if (isFileScheme) {
try {
request = url.fileURLToPath(originalRequest);
} catch (error) {
request = request.slice(7);
}
}

let resolutionMap: any[] = [];

const webpackPossibleRequests = getPossibleRequests(request, fromImport);

resolutionMap = resolutionMap.concat({
resolve: fromImport ? importResolve : moduleResolve,
context: path.dirname(context),
possibleRequests: webpackPossibleRequests,
});

return startResolving(resolutionMap);
};
}

const MODULE_REQUEST_REGEX = /^[^?]*~/;

// Examples:
// - ~package
// - ~package/
// - ~@org
// - ~@org/
// - ~@org/package
// - ~@org/package/
const IS_MODULE_IMPORT =
/^~([^/]+|[^/]+\/|@[^/]+[/][^/]+|@[^/]+\/?|@[^/]+[/][^/]+\/)$/;

const IS_PKG_SCHEME = /^pkg:/i;

function getPossibleRequests(url: string, fromImport: boolean) {
console.log('getPossibleRequests', url);
let request = url;

if (MODULE_REQUEST_REGEX.test(url)) {
request = request.replace(MODULE_REQUEST_REGEX, '');
}

if (IS_PKG_SCHEME.test(url)) {
request = `${request.slice(4)}`;

return [...new Set([request, url])];
}

if (IS_MODULE_IMPORT.test(url) || IS_PKG_SCHEME.test(url)) {
request = request[request.length - 1] === '/' ? request : `${request}/`;

return [...new Set([request, url])];
}

const extension = path.extname(request).toLowerCase();

if (extension === '.css') {
return fromImport ? [] : [url];
}

const dirname = path.dirname(request);
const normalizedDirname = dirname === '.' ? '' : `${dirname}/`;
const basename = path.basename(request);
const basenameWithoutExtension = path.basename(request, extension);

return [
...new Set(
([] as any[])
.concat(
fromImport
? [
`${normalizedDirname}_${basenameWithoutExtension}.import${extension}`,
`${normalizedDirname}${basenameWithoutExtension}.import${extension}`,
]
: [],
)
.concat([
`${normalizedDirname}_${basename}`,
`${normalizedDirname}${basename}`,
])
.concat([url]),
),
];
}

async function startResolving(resolutionMap: any[]) {
if (resolutionMap.length === 0) {
return Promise.reject();
}

const [{ possibleRequests }] = resolutionMap;

if (possibleRequests.length === 0) {
return Promise.reject();
}

const [{ resolve, context }] = resolutionMap;

try {
return await asyncResolve(context, possibleRequests[0], resolve);
} catch (_ignoreError) {
const [, ...tailResult] = possibleRequests;

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

return startResolving(tailResolutionMap);
}

resolutionMap[0].possibleRequests = tailResult;

return startResolving(resolutionMap);
}
}

function asyncResolve(
context: string,
path: string,
resolve: ResolveFunctionAsync,
): Promise<string> {
return new Promise((res, rej) => {
resolve(context, path, (err, result) => {
if (err) {
rej(err);
return;
}

res(result as string);
});
});
}
Loading

0 comments on commit 3c77380

Please sign in to comment.