diff --git a/packages/mako/package.json b/packages/mako/package.json index 01d4fa07d..ca179a28f 100644 --- a/packages/mako/package.json +++ b/packages/mako/package.json @@ -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", @@ -90,4 +91,4 @@ "access": "public" }, "repository": "git@github.com:umijs/mako.git" -} \ No newline at end of file +} diff --git a/packages/mako/src/lessLoader/plugin.ts b/packages/mako/src/lessLoader/plugin.ts new file mode 100644 index 000000000..da373cf52 --- /dev/null +++ b/packages/mako/src/lessLoader/plugin.ts @@ -0,0 +1,164 @@ +import path from 'path'; +import EnhancedResolve, { type ResolveFunctionAsync } from 'enhanced-resolve'; + +const trailingSlash = /[/\\]$/; + +const IS_SPECIAL_MODULE_IMPORT = /^~[^/]+$/; + +// `[drive_letter]:\` + `\\[server]\[share_name]\` +const IS_NATIVE_WIN32_PATH = /^[a-z]:[/\\]|^\\\\/i; + +// Examples: +// - ~package +// - ~package/ +// - ~@org +// - ~@org/ +// - ~@org/package +// - ~@org/package/ +const IS_MODULE_IMPORT = + /^~([^/]+|[^/]+\/|@[^/]+[/][^/]+|@[^/]+\/?|@[^/]+[/][^/]+\/)$/; +const MODULE_REQUEST_REGEX = /^[^?]*~/; + +export function createLessPlugin(less: LessStatic): Less.Plugin { + const resolve = EnhancedResolve.create({ + conditionNames: ['less', 'style', '...'], + mainFields: ['less', 'style', 'main', '...'], + mainFiles: ['index', '...'], + extensions: ['.less', '.css'], + preferRelative: true, + }); + + class FileManager extends less.FileManager { + supports(filename: string) { + if (filename[0] === '/' || IS_NATIVE_WIN32_PATH.test(filename)) { + return true; + } + + if (this.isPathAbsolute(filename)) { + return false; + } + + return true; + } + + supportsSync() { + return false; + } + + async resolveFilename(filename: string, currentDirectory: string) { + // Less is giving us trailing slashes, but the context should have no trailing slash + const context = currentDirectory.replace(trailingSlash, ''); + + let request = filename; + + // A `~` makes the url an module + if (MODULE_REQUEST_REGEX.test(filename)) { + request = request.replace(MODULE_REQUEST_REGEX, ''); + } + + if (IS_MODULE_IMPORT.test(filename)) { + request = request[request.length - 1] === '/' ? request : `${request}/`; + } + + return this.resolveRequests(context, [...new Set([request, filename])]); + } + + async resolveRequests( + context: string, + possibleRequests: string[], + ): Promise { + if (possibleRequests.length === 0) { + return Promise.reject(); + } + + let result; + + try { + result = await asyncResolve(context, possibleRequests[0], resolve); + } catch (error) { + const [, ...tailPossibleRequests] = possibleRequests; + + if (tailPossibleRequests.length === 0) { + throw error; + } + + result = await this.resolveRequests(context, tailPossibleRequests); + } + + return result; + } + + async loadFile( + filename: string, + currentDirectory: string, + options: Less.LoadFileOptions, + environment: Less.Environment, + ) { + let result; + + try { + if (IS_SPECIAL_MODULE_IMPORT.test(filename)) { + const error = new Error() as any; + error.type = 'Next'; + throw error; + } + + result = await super.loadFile( + filename, + currentDirectory, + options, + environment, + ); + } catch (error: any) { + if (error.type !== 'File' && error.type !== 'Next') { + return Promise.reject(error); + } + + try { + result = await this.resolveFilename(filename, currentDirectory); + } catch (_error) { + return Promise.reject(error); + } + + // FIXME: need to add dependency + // addDependency(result); + + return super.loadFile(result, currentDirectory, options, environment); + } + + // @ts-ignore + const absoluteFilename = path.isAbsolute(result.filename) + ? result.filename + : path.resolve('.', result.filename); + + // FIXME: need to add dependency + // addDependency(path.normalize(absoluteFilename)); + + return result; + } + } + + return { + install(_lessInstance, pluginManager) { + pluginManager.addFileManager(new FileManager()); + }, + minVersion: [3, 0, 0], + }; +} + +function asyncResolve( + context: string, + path: string, + resolve: ResolveFunctionAsync, +): Promise { + return new Promise((res, rej) => { + resolve(context, path, (err, result) => { + if (err) { + rej(err); + return; + } + + res(result as string); + }); + }); +} diff --git a/packages/mako/src/lessLoader/render.ts b/packages/mako/src/lessLoader/render.ts index 25a1c58ab..c8269b454 100644 --- a/packages/mako/src/lessLoader/render.ts +++ b/packages/mako/src/lessLoader/render.ts @@ -1,6 +1,7 @@ import fs from 'fs'; import less from 'less'; import { LessLoaderOpts } from '.'; +import { createLessPlugin } from './plugin'; module.exports = async function render(param: { filename: string; @@ -19,6 +20,8 @@ module.exports = async function render(param: { } }); + pluginInstances?.unshift(createLessPlugin(less)); + const result = await less .render(input, { filename: param.filename, diff --git a/packages/mako/src/sassLoader/importer.ts b/packages/mako/src/sassLoader/importer.ts new file mode 100644 index 000000000..b1d23c27e --- /dev/null +++ b/packages/mako/src/sassLoader/importer.ts @@ -0,0 +1,237 @@ +import fs from 'fs'; +import path from 'path'; +import url from 'url'; +import EnhancedResolve, { type ResolveFunctionAsync } from 'enhanced-resolve'; +import type { Importer, ImporterResult } from 'sass'; + +export function createImporter( + filename: string, + implementation: any, +): Importer { + return { + async canonicalize(originalUrl, context) { + const { fromImport } = context; + const prev = context.containingUrl + ? url.fileURLToPath(context.containingUrl.toString()) + : filename; + + const resolver = getResolver( + typeof implementation?.compileStringAsync !== 'undefined', + ); + try { + const result = await resolver(prev, originalUrl, fromImport); + + // FIXME: need to add dependency + // addDependency(path.normalize(result)); + + 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; + } + }, + }; +} + +function getResolver(isModernSass: boolean) { + const importResolve = EnhancedResolve.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 = EnhancedResolve.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 possibleRequests = getPossibleRequests(request, fromImport); + + resolutionMap = resolutionMap.concat({ + resolve: fromImport ? importResolve : moduleResolve, + context: path.dirname(context), + possibleRequests: possibleRequests, + }); + + 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) { + 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 { + return new Promise((res, rej) => { + resolve(context, path, (err, result) => { + if (err) { + rej(err); + return; + } + + res(result as string); + }); + }); +} diff --git a/packages/mako/src/sassLoader/render.ts b/packages/mako/src/sassLoader/render.ts index c8962b90f..d95d35b44 100644 --- a/packages/mako/src/sassLoader/render.ts +++ b/packages/mako/src/sassLoader/render.ts @@ -1,10 +1,11 @@ import { type Options } from 'sass'; +import { createImporter } from './importer'; 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) { @@ -12,8 +13,13 @@ async function render(param: { '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(createImporter(param.filename, sass)); + const result = await sass - .compileAsync(param.filename, { style: 'compressed', ...param.opts }) + .compileAsync(param.filename, options) .catch((err: any) => { throw new Error(err.toString()); }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ffc945928..51bc0a995 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -551,6 +551,9 @@ importers: chalk: specifier: ^4.1.2 version: 4.1.2 + enhanced-resolve: + specifier: ^5.18.1 + version: 5.18.1 less: specifier: ^4.2.0 version: 4.2.0 @@ -6659,22 +6662,12 @@ packages: - typescript dev: true - /@umijs/mako-darwin-arm64@0.11.3: - resolution: {integrity: sha512-BEBz8sejc4wP7MEs5dtZCyW7AxqgQQlNzceSfm4ulCeGYxqHO8OdtFk4ZwxIaue7ODYyIqUVd5LqIiqP6ZIsmA==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [darwin] - requiresBuild: true - dev: true - optional: true - /@umijs/mako-darwin-arm64@0.11.4: resolution: {integrity: sha512-wPFKpgaZuzfiS9SwMtvrNxw+fneZsjS6D3bXCaK9sz/ROxl4THk4FdfstLIaxIBYFC6gEtwrddPgDMl2PVXWjA==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] requiresBuild: true - dev: false optional: true /@umijs/mako-darwin-arm64@0.7.4: @@ -6686,22 +6679,12 @@ packages: dev: true optional: true - /@umijs/mako-darwin-x64@0.11.3: - resolution: {integrity: sha512-G2VX1Ue47NYpQhWIbYyuYe3j3UzGLh7Vw5PYuc/R8e8ruVeJ7vKLk6FKaWrQIcGUWSsffcTy+IsvRzcE5unu6g==} - engines: {node: '>= 10'} - cpu: [x64] - os: [darwin] - requiresBuild: true - dev: true - optional: true - /@umijs/mako-darwin-x64@0.11.4: resolution: {integrity: sha512-mrVeKuj6SVSLoONec/UgGWbJh9TOUMnBoyp0GVk3M3Z3JhOea5wvtr7IGpvC3R5TFlgqE2nUF1C50DGKF47jmg==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] requiresBuild: true - dev: false optional: true /@umijs/mako-darwin-x64@0.7.4: @@ -6713,31 +6696,12 @@ packages: dev: true optional: true - /@umijs/mako-linux-arm64-gnu@0.11.3: - resolution: {integrity: sha512-9tkjnNhEvA61CB0gVUgM1FK31Iamrx5qU1KtZRPslJ5U5wXxkHBoOJEO77rRdZw3Oq1fqNM+lrjejpuPQPsAAw==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: true - optional: true - /@umijs/mako-linux-arm64-gnu@0.11.4: resolution: {integrity: sha512-eXxXi/VsEh6tqJZPuffYzCv6PUmzm1yLhTPWXPPatpVknKcPZsu3qJMUJhmmdd0PkCWFL7mz9JaLx9vIrZYUdg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] requiresBuild: true - dev: false - optional: true - - /@umijs/mako-linux-arm64-musl@0.11.3: - resolution: {integrity: sha512-e+mSdAgLm2VcbmQsElIbfkDs9B/HzsKOynW0hwPFZVyXH85fcIhEFca3rIN0cplXeJw8R1qEcKEDkkBmz2Dx/g==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: true optional: true /@umijs/mako-linux-arm64-musl@0.11.4: @@ -6746,16 +6710,6 @@ packages: cpu: [arm64] os: [linux] requiresBuild: true - dev: false - optional: true - - /@umijs/mako-linux-x64-gnu@0.11.3: - resolution: {integrity: sha512-h4dGVyjFgdwkqsaQZdsc0TvRWz8vY79L6+uU7MVWLZ2iPQXz4hMvxx5qmQ4Y5Or4WKslSDCIFICeG5h81Atspw==} - engines: {node: '>= 10'} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: true optional: true /@umijs/mako-linux-x64-gnu@0.11.4: @@ -6764,7 +6718,6 @@ packages: cpu: [x64] os: [linux] requiresBuild: true - dev: false optional: true /@umijs/mako-linux-x64-gnu@0.7.4: @@ -6776,22 +6729,12 @@ packages: dev: true optional: true - /@umijs/mako-linux-x64-musl@0.11.3: - resolution: {integrity: sha512-mdfV1IYOvDFSEMPwf6hvW9fnPpXGs46pkaoE3Fuj9B8704WqQReutsIO4aBojwhk+6lBvRf0F7Uff2qItwcNAQ==} - engines: {node: '>= 10'} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: true - optional: true - /@umijs/mako-linux-x64-musl@0.11.4: resolution: {integrity: sha512-SXY8OKEnGCl7edlyPDAK+mcjCiJ3J9UGqxwZHTFexk02ulKAY93RcSb7Pkq2sWsh0RD6N/xJO0fF4jPrJ6ruJQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] requiresBuild: true - dev: false optional: true /@umijs/mako-linux-x64-musl@0.7.4: @@ -6803,31 +6746,12 @@ packages: dev: true optional: true - /@umijs/mako-win32-ia32-msvc@0.11.3: - resolution: {integrity: sha512-J6mAiIfMjRT9vGRZN1W3TKK1lB5GAh7EPx5imcNkKQ+i9Z883BUzrGQCHzVGv7ooXNCpuWp5VgWME80LIwvm5w==} - engines: {node: '>= 10'} - cpu: [ia32] - os: [win32] - requiresBuild: true - dev: true - optional: true - /@umijs/mako-win32-ia32-msvc@0.11.4: resolution: {integrity: sha512-SkRiup3UF1yNrmIMrAgT5BLzruv0xwDVeM0zoZeNCsN/P3BzP92QTYgXbOex9fZLFvldqS5MCscLnh9STCKDLw==} engines: {node: '>= 10'} cpu: [ia32] os: [win32] requiresBuild: true - dev: false - optional: true - - /@umijs/mako-win32-x64-msvc@0.11.3: - resolution: {integrity: sha512-zSPKj4kBjyl492IDnkFOKMbI3pDLYoRGUkZmx+3eZ7ynmQWWsm/XFcDQRluPIqbVEDGZJwT6F3r24cBXM2Om8w==} - engines: {node: '>= 10'} - cpu: [x64] - os: [win32] - requiresBuild: true - dev: true optional: true /@umijs/mako-win32-x64-msvc@0.11.4: @@ -6836,11 +6760,10 @@ packages: cpu: [x64] os: [win32] requiresBuild: true - dev: false optional: true - /@umijs/mako@0.11.3: - resolution: {integrity: sha512-fXG6nf1EDECwKbIcqmx72CVtsgtE71mp+GfqN5oPtKXLJjAUOEZyvvyIpZTrtt/CJXYN+nT+aplsbQlT0b2zmQ==} + /@umijs/mako@0.11.4: + resolution: {integrity: sha512-XF+8w267Qm2biYQ4PoPBhsilWBCbeXtsU7Pp0wSvg/mK102admFWKzsbbFiW4dw2yJwdV8Qvq12tEz+g2DboEg==} engines: {node: '>= 16'} hasBin: true dependencies: @@ -6858,14 +6781,14 @@ packages: semver: 7.6.2 yargs-parser: 21.1.1 optionalDependencies: - '@umijs/mako-darwin-arm64': 0.11.3 - '@umijs/mako-darwin-x64': 0.11.3 - '@umijs/mako-linux-arm64-gnu': 0.11.3 - '@umijs/mako-linux-arm64-musl': 0.11.3 - '@umijs/mako-linux-x64-gnu': 0.11.3 - '@umijs/mako-linux-x64-musl': 0.11.3 - '@umijs/mako-win32-ia32-msvc': 0.11.3 - '@umijs/mako-win32-x64-msvc': 0.11.3 + '@umijs/mako-darwin-arm64': 0.11.4 + '@umijs/mako-darwin-x64': 0.11.4 + '@umijs/mako-linux-arm64-gnu': 0.11.4 + '@umijs/mako-linux-arm64-musl': 0.11.4 + '@umijs/mako-linux-x64-gnu': 0.11.4 + '@umijs/mako-linux-x64-musl': 0.11.4 + '@umijs/mako-win32-ia32-msvc': 0.11.4 + '@umijs/mako-win32-x64-msvc': 0.11.4 transitivePeerDependencies: - supports-color dev: true @@ -7155,7 +7078,7 @@ packages: '@google/generative-ai': 0.21.0 '@types/yargs-parser': 21.0.3 '@umijs/clack-prompts': 0.0.4 - '@umijs/mako': 0.11.3 + '@umijs/mako': 0.11.4 git-repo-info: 2.1.1 picocolors: 1.1.1 tsx: 4.19.2 @@ -9596,20 +9519,20 @@ packages: dependencies: once: 1.4.0 - /enhanced-resolve@5.15.0: - resolution: {integrity: sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==} + /enhanced-resolve@5.17.0: + resolution: {integrity: sha512-dwDPwZL0dmye8Txp2gzFmA6sxALaSvdRDjPH0viLcKrtlOL3tw62nWWweVD1SdILDTJrbrL6tdWVN58Wo6U3eA==} engines: {node: '>=10.13.0'} dependencies: graceful-fs: 4.2.11 tapable: 2.2.1 + dev: true - /enhanced-resolve@5.17.0: - resolution: {integrity: sha512-dwDPwZL0dmye8Txp2gzFmA6sxALaSvdRDjPH0viLcKrtlOL3tw62nWWweVD1SdILDTJrbrL6tdWVN58Wo6U3eA==} + /enhanced-resolve@5.18.1: + resolution: {integrity: sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==} engines: {node: '>=10.13.0'} dependencies: graceful-fs: 4.2.11 tapable: 2.2.1 - dev: true /enhanced-resolve@5.9.3: resolution: {integrity: sha512-Bq9VSor+kjvW3f9/MiiR4eE3XYgOl7/rS8lnSxbRbF3kS0B2r+Y9w5krBWxZgDxASVZbdYrn5wT4j/Wb0J9qow==} @@ -12189,7 +12112,7 @@ packages: /less-plugin-resolve@1.0.2: resolution: {integrity: sha512-e1AHq0XNTU8S3d9JCc8CFYajoUBr0EK3pcuLT5PogyBBeE0knzZJL105kKKSZWfq2lQLq3/uEDrMK3JPq+fHaA==} dependencies: - enhanced-resolve: 5.15.0 + enhanced-resolve: 5.18.1 /less@4.1.3: resolution: {integrity: sha512-w16Xk/Ta9Hhyei0Gpz9m7VS8F28nieJaL/VyShID7cYvP6IL5oHeL6p4TXSDJqZE/lNv0oJ2pGVjJsRkfwm5FA==} @@ -18560,7 +18483,7 @@ packages: acorn-import-assertions: 1.9.0(acorn@8.8.2) browserslist: 4.21.7 chrome-trace-event: 1.0.3 - enhanced-resolve: 5.15.0 + enhanced-resolve: 5.17.0 es-module-lexer: 1.2.1 eslint-scope: 5.1.1 events: 3.3.0