You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

175 lines
4.7 KiB

7 months ago
import path from 'path';
import fs from 'fs';
import { Alias, normalizePath, ResolvedConfig } from 'vite';
import less from 'less';
export type ResolveFn = (
id: string,
importer?: string,
aliasOnly?: boolean
) => Promise<string | undefined>;
type CssUrlReplacer = (url: string, importer?: string) => string | Promise<string>;
export const externalRE = /^(https?:)?\/\//;
export const isExternalUrl = (url: string) => externalRE.test(url);
export const dataUrlRE = /^\s*data:/i;
export const isDataUrl = (url: string) => dataUrlRE.test(url);
const cssUrlRE = /url\(\s*('[^']+'|"[^"]+"|[^'")]+)\s*\)/;
let ViteLessManager: any;
function createViteLessPlugin(
rootFile: string,
alias: Alias[],
resolvers: { less: ResolveFn }
): Less.Plugin {
if (!ViteLessManager) {
ViteLessManager = class ViteManager extends less.FileManager {
resolvers;
rootFile;
alias;
constructor(rootFile: string, resolvers: ResolveFn, alias: Alias[]) {
super();
this.rootFile = rootFile;
this.resolvers = resolvers;
this.alias = alias;
}
supports() {
return true;
}
supportsSync() {
return false;
}
async loadFile(
filename: string,
dir: string,
opts: any,
env: any
): Promise<Less.FileLoadResult> {
const resolved = await this.resolvers.less(filename, path.join(dir, '*'));
if (resolved) {
const result = await rebaseUrls(resolved, this.rootFile, this.alias);
let contents;
if (result && 'contents' in result) {
contents = result.contents;
} else {
contents = fs.readFileSync(resolved, 'utf-8');
}
return {
filename: path.resolve(resolved),
contents,
};
} else {
return super.loadFile(filename, dir, opts, env);
}
}
};
}
return {
install(_, pluginManager) {
pluginManager.addFileManager(new ViteLessManager(rootFile, resolvers, alias));
},
minVersion: [3, 0, 0],
};
}
export function lessPlugin(id, config: ResolvedConfig) {
const resolvers = createCSSResolvers(config);
return createViteLessPlugin(id, config.resolve.alias, resolvers);
}
function createCSSResolvers(config: ResolvedConfig): { less: ResolveFn } {
let lessResolve: ResolveFn | undefined;
return {
get less() {
return (
lessResolve ||
(lessResolve = config.createResolver({
extensions: ['.less', '.css'],
mainFields: ['less', 'style'],
tryIndex: false,
preferRelative: true,
}))
);
},
};
}
/**
* relative url() inside \@imported sass and less files must be rebased to use
* root file as base.
*/
async function rebaseUrls(file: string, rootFile: string, alias: Alias[]): Promise<any> {
file = path.resolve(file); // ensure os-specific flashes
// in the same dir, no need to rebase
const fileDir = path.dirname(file);
const rootDir = path.dirname(rootFile);
if (fileDir === rootDir) {
return { file };
}
// no url()
const content = fs.readFileSync(file, 'utf-8');
if (!cssUrlRE.test(content)) {
return { file };
}
const rebased = await rewriteCssUrls(content, (url) => {
if (url.startsWith('/')) return url;
// match alias, no need to rewrite
for (const { find } of alias) {
const matches = typeof find === 'string' ? url.startsWith(find) : find.test(url);
if (matches) {
return url;
}
}
const absolute = path.resolve(fileDir, url);
const relative = path.relative(rootDir, absolute);
return normalizePath(relative);
});
return {
file,
contents: rebased,
};
}
function rewriteCssUrls(css: string, replacer: CssUrlReplacer): Promise<string> {
return asyncReplace(css, cssUrlRE, async (match) => {
const [matched, rawUrl] = match;
return await doUrlReplace(rawUrl, matched, replacer);
});
}
export async function asyncReplace(
input: string,
re: RegExp,
replacer: (match: RegExpExecArray) => string | Promise<string>
) {
let match: RegExpExecArray | null;
let remaining = input;
let rewritten = '';
while ((match = re.exec(remaining))) {
rewritten += remaining.slice(0, match.index);
rewritten += await replacer(match);
remaining = remaining.slice(match.index + match[0].length);
}
rewritten += remaining;
return rewritten;
}
async function doUrlReplace(rawUrl: string, matched: string, replacer: CssUrlReplacer) {
let wrap = '';
const first = rawUrl[0];
if (first === `"` || first === `'`) {
wrap = first;
rawUrl = rawUrl.slice(1, -1);
}
if (isExternalUrl(rawUrl) || isDataUrl(rawUrl) || rawUrl.startsWith('#')) {
return matched;
}
return `url(${wrap}${await replacer(rawUrl)}${wrap})`;
}