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 ) => externalRE . test ( url ) ; export const dataUrlRE = /^\s*data:/i ; export const isDataUrl = ( url ) => dataUrlRE . test ( url ) ; const cssUrlRE = /url\(\s*('[^']+'|"[^"]+"|[^'")]+)\s*\)/ ; let ViteLessManager ; function createViteLessPlugin ( rootFile , alias , resolvers ) { if ( ! ViteLessManager ) { ViteLessManager = class ViteManager extends less . FileManager { resolvers ; rootFile ; alias ; constructor ( rootFile , resolvers , alias ) { super ( ) ; this . rootFile = rootFile ; this . resolvers = resolvers ; this . alias = alias ; } supports ( ) { return true ; } supportsSync ( ) { return false ; } async loadFile ( filename , dir , opts , env ) { 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 ) { const resolvers = createCSSResolvers ( config ) ; return createViteLessPlugin ( id , config . resolve . alias , resolvers ) ; } function createCSSResolvers ( config ) { let lessResolve ; 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 , rootFile , alias ) { file = path . resolve ( file ) ; const fileDir = path . dirname ( file ) ; const rootDir = path . dirname ( rootFile ) ; if ( fileDir === rootDir ) { return { file } ; } const content = fs . readFileSync ( file , 'utf-8' ) ; if ( ! cssUrlRE . test ( content ) ) { return { file } ; } const rebased = await rewriteCssUrls ( content , ( url ) => { if ( url . startsWith ( '/' ) ) return url ; 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 , replacer ) { return asyncReplace ( css , cssUrlRE , async ( match ) => { const [ matched , rawUrl ] = match ; return await doUrlReplace ( rawUrl , matched , replacer ) ; } ) ; } export async function asyncReplace ( input , re , replacer ) { let match ; 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 , matched , replacer ) { 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 })` ; }