push
This commit is contained in:
174
build/plugin/theme/preprocessor/less/index.ts
Normal file
174
build/plugin/theme/preprocessor/less/index.ts
Normal file
@@ -0,0 +1,174 @@
|
||||
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})`;
|
||||
}
|
||||
Reference in New Issue
Block a user