提交 6da01482 编写于 作者: fxy060608's avatar fxy060608

fix: preprocess css

上级 a5c3f1fb
...@@ -2686,9 +2686,9 @@ var serviceContext = (function (vue) { ...@@ -2686,9 +2686,9 @@ var serviceContext = (function (vue) {
} }
function parseRedirectInfo() { function parseRedirectInfo() {
const weexPlus = weex.requireModule('plus'); const weexPlus = weex.requireModule('plus');
const { path, query, extraData, userAction } = weexPlus.getRedirectInfo() || {}; const { path, query, extraData, userAction, fromAppid } = weexPlus.getRedirectInfo() || {};
const referrerInfo = { const referrerInfo = {
appId: '', appId: fromAppid,
extraData: {}, extraData: {},
}; };
if (extraData) { if (extraData) {
......
...@@ -28,6 +28,7 @@ import type Stylus from 'stylus' ...@@ -28,6 +28,7 @@ import type Stylus from 'stylus'
import type Less from 'less' import type Less from 'less'
import type { Alias } from 'types/alias' import type { Alias } from 'types/alias'
import { transform, formatMessages } from 'esbuild' import { transform, formatMessages } from 'esbuild'
import { preCss } from '../../../../preprocess'
// const debug = createDebugger('vite:css') // const debug = createDebugger('vite:css')
export interface CSSOptions { export interface CSSOptions {
...@@ -866,6 +867,12 @@ const sass: SassStylePreprocessor = (source, root, options, aliasResolver) => ...@@ -866,6 +867,12 @@ const sass: SassStylePreprocessor = (source, root, options, aliasResolver) =>
aliasResolver aliasResolver
) )
export function preprocessCss(content: string) {
if (content.includes('#endif')) {
return preCss(content)
}
return content
}
/** /**
* relative url() inside \@imported sass and less files must be rebased to use * relative url() inside \@imported sass and less files must be rebased to use
* root file as base. * root file as base.
...@@ -876,18 +883,22 @@ async function rebaseUrls( ...@@ -876,18 +883,22 @@ async function rebaseUrls(
alias: Alias[] alias: Alias[]
): Promise<Sass.ImporterReturnType> { ): Promise<Sass.ImporterReturnType> {
file = path.resolve(file) // ensure os-specific flashes file = path.resolve(file) // ensure os-specific flashes
// 条件编译
const contents = preprocessCss(fs.readFileSync(file, 'utf-8'))
// in the same dir, no need to rebase // in the same dir, no need to rebase
const fileDir = path.dirname(file) const fileDir = path.dirname(file)
const rootDir = path.dirname(rootFile) const rootDir = path.dirname(rootFile)
if (fileDir === rootDir) { if (fileDir === rootDir) {
return { file } return { file, contents }
} }
// no url() // no url()
const content = fs.readFileSync(file, 'utf-8') if (!cssUrlRE.test(contents)) {
if (!cssUrlRE.test(content)) { return { file, contents }
return { file }
} }
const rebased = await rewriteCssUrls(content, (url) => { const rebased = await rewriteCssUrls(contents, (url) => {
if (url.startsWith('/')) return url if (url.startsWith('/')) return url
// match alias, no need to rewrite // match alias, no need to rewrite
for (const { find } of alias) { for (const { find } of alias) {
......
import path from 'path'
import { parse as parseUrl } from 'url'
import fs, { promises as fsp } from 'fs'
import mime from 'mime/lite'
import { Plugin } from '../plugin'
import { ResolvedConfig } from '../config'
import { cleanUrl } from '../utils'
import { FS_PREFIX } from '../constants'
import { OutputOptions, PluginContext, RenderedChunk } from 'rollup'
import MagicString from 'magic-string'
import { createHash } from 'crypto'
import { normalizePath } from '../utils'
export const assetUrlRE = /__VITE_ASSET__([a-z\d]{8})__(?:\$_(.*?)__)?/g
const rawRE = /(\?|&)raw(?:&|$)/
const urlRE = /(\?|&)url(?:&|$)/
export const chunkToEmittedAssetsMap = new WeakMap<RenderedChunk, Set<string>>()
const assetCache = new WeakMap<ResolvedConfig, Map<string, string>>()
const assetHashToFilenameMap = new WeakMap<
ResolvedConfig,
Map<string, string>
>()
// save hashes of the files that has been emitted in build watch
const emittedHashMap = new WeakMap<ResolvedConfig, Set<string>>()
/**
* Also supports loading plain strings with import text from './foo.txt?raw'
*/
export function assetPlugin(config: ResolvedConfig): Plugin {
// assetHashToFilenameMap initialization in buildStart causes getAssetFilename to return undefined
assetHashToFilenameMap.set(config, new Map())
return {
name: 'vite:asset',
buildStart() {
assetCache.set(config, new Map())
emittedHashMap.set(config, new Set())
},
resolveId(id) {
if (!config.assetsInclude(cleanUrl(id))) {
return
}
// imports to absolute urls pointing to files in /public
// will fail to resolve in the main resolver. handle them here.
const publicFile = checkPublicFile(id, config)
if (publicFile) {
return id
}
},
async load(id) {
if (id.startsWith('\0')) {
// Rollup convention, this id should be handled by the
// plugin that marked it with \0
return
}
// raw requests, read from disk
if (rawRE.test(id)) {
const file = checkPublicFile(id, config) || cleanUrl(id)
// raw query, read file and return as string
return `export default ${JSON.stringify(
await fsp.readFile(file, 'utf-8')
)}`
}
if (!config.assetsInclude(cleanUrl(id)) && !urlRE.test(id)) {
return
}
id = id.replace(urlRE, '$1').replace(/[\?&]$/, '')
const url = await fileToUrl(id, config, this)
return `export default ${JSON.stringify(url)}`
},
renderChunk(code, chunk) {
let match: RegExpExecArray | null
let s: MagicString | undefined
// Urls added with JS using e.g.
// imgElement.src = "my/file.png" are using quotes
// Urls added in CSS that is imported in JS end up like
// var inlined = ".inlined{color:green;background:url(__VITE_ASSET__5aa0ddc0__)}\n";
// In both cases, the wrapping should already be fine
while ((match = assetUrlRE.exec(code))) {
s = s || (s = new MagicString(code))
const [full, hash, postfix = ''] = match
// some internal plugins may still need to emit chunks (e.g. worker) so
// fallback to this.getFileName for that.
const file = getAssetFilename(hash, config) || this.getFileName(hash)
registerAssetToChunk(chunk, file)
const outputFilepath = config.base + file + postfix
s.overwrite(match.index, match.index + full.length, outputFilepath)
}
if (s) {
return {
code: s.toString(),
map: config.build.sourcemap ? s.generateMap({ hires: true }) : null,
}
} else {
return null
}
},
generateBundle(_, bundle) {
// do not emit assets for SSR build
if (config.command === 'build' && config.build.ssr) {
for (const file in bundle) {
if (
bundle[file].type === 'asset' &&
!file.includes('ssr-manifest.json')
) {
delete bundle[file]
}
}
}
},
}
}
export function registerAssetToChunk(chunk: RenderedChunk, file: string): void {
let emitted = chunkToEmittedAssetsMap.get(chunk)
if (!emitted) {
emitted = new Set()
chunkToEmittedAssetsMap.set(chunk, emitted)
}
emitted.add(cleanUrl(file))
}
export function checkPublicFile(
url: string,
{ publicDir }: ResolvedConfig
): string | undefined {
// note if the file is in /public, the resolver would have returned it
// as-is so it's not going to be a fully resolved path.
if (!publicDir || !url.startsWith('/')) {
return
}
const publicFile = path.join(publicDir, cleanUrl(url))
if (fs.existsSync(publicFile)) {
return publicFile
} else {
return
}
}
export function fileToUrl(
id: string,
config: ResolvedConfig,
ctx: PluginContext
): string | Promise<string> {
if (config.command === 'serve') {
return fileToDevUrl(id, config)
} else {
return fileToBuiltUrl(id, config, ctx)
}
}
function fileToDevUrl(id: string, config: ResolvedConfig) {
let rtn: string
if (checkPublicFile(id, config)) {
// in public dir, keep the url as-is
rtn = id
} else if (id.startsWith(config.root)) {
// in project root, infer short public path
rtn = '/' + path.posix.relative(config.root, id)
} else {
// outside of project root, use absolute fs path
// (this is special handled by the serve static middleware
rtn = path.posix.join(FS_PREFIX + id)
}
const origin = config.server?.origin ?? ''
return origin + config.base + rtn.replace(/^\//, '')
}
export function getAssetFilename(
hash: string,
config: ResolvedConfig
): string | undefined {
return assetHashToFilenameMap.get(config)?.get(hash)
}
/**
* converts the source filepath of the asset to the output filename based on the assetFileNames option. \
* this function imitates the behavior of rollup.js. \
* https://rollupjs.org/guide/en/#outputassetfilenames
*
* @example
* ```ts
* const content = Buffer.from('text');
* const fileName = assetFileNamesToFileName(
* 'assets/[name].[hash][extname]',
* '/path/to/file.txt',
* getAssetHash(content),
* content
* )
* // fileName: 'assets/file.982d9e3e.txt'
* ```
*
* @param assetFileNames filename pattern. e.g. `'assets/[name].[hash][extname]'`
* @param file filepath of the asset
* @param contentHash hash of the asset. used for `'[hash]'` placeholder
* @param content content of the asset. passed to `assetFileNames` if `assetFileNames` is a function
* @returns output filename
*/
export function assetFileNamesToFileName(
assetFileNames: Exclude<OutputOptions['assetFileNames'], undefined>,
file: string,
contentHash: string,
content: string | Buffer
): string {
const basename = path.basename(file)
// placeholders for `assetFileNames`
// `hash` is slightly different from the rollup's one
const extname = path.extname(basename)
const ext = extname.substr(1)
const name = basename.slice(0, -extname.length)
const hash = contentHash
if (typeof assetFileNames === 'function') {
assetFileNames = assetFileNames({
name: file,
source: content,
type: 'asset',
})
if (typeof assetFileNames !== 'string') {
throw new TypeError('assetFileNames must return a string')
}
} else if (typeof assetFileNames !== 'string') {
throw new TypeError('assetFileNames must be a string or a function')
}
const fileName = assetFileNames.replace(
/\[\w+\]/g,
(placeholder: string): string => {
switch (placeholder) {
case '[ext]':
return ext
case '[extname]':
return extname
case '[hash]':
return hash
case '[name]':
return name
}
throw new Error(
`invalid placeholder ${placeholder} in assetFileNames "${assetFileNames}"`
)
}
)
return fileName
}
/**
* Register an asset to be emitted as part of the bundle (if necessary)
* and returns the resolved public URL
*/
async function fileToBuiltUrl(
id: string,
config: ResolvedConfig,
pluginContext: PluginContext,
skipPublicCheck = false
): Promise<string> {
if (!skipPublicCheck && checkPublicFile(id, config)) {
return config.base + id.slice(1)
}
const cache = assetCache.get(config)!
const cached = cache.get(id)
if (cached) {
return cached
}
const file = cleanUrl(id)
const content = await fsp.readFile(file)
let url: string
if (
config.build.lib ||
(!file.endsWith('.svg') &&
content.length < Number(config.build.assetsInlineLimit))
) {
// base64 inlined as a string
url = `data:${mime.getType(file)};base64,${content.toString('base64')}`
} else {
// emit as asset
// rollup supports `import.meta.ROLLUP_FILE_URL_*`, but it generates code
// that uses runtime url sniffing and it can be verbose when targeting
// non-module format. It also fails to cascade the asset content change
// into the chunk's hash, so we have to do our own content hashing here.
// https://bundlers.tooling.report/hashing/asset-cascade/
// https://github.com/rollup/rollup/issues/3415
const map = assetHashToFilenameMap.get(config)!
const contentHash = getAssetHash(content)
const { search, hash } = parseUrl(id)
const postfix = (search || '') + (hash || '')
const output = config.build?.rollupOptions?.output
const assetFileNames =
(output && !Array.isArray(output) ? output.assetFileNames : undefined) ??
// defaults to '<assetsDir>/[name].[hash][extname]'
// slightly different from rollup's one ('assets/[name]-[hash][extname]')
path.posix.join(config.build.assetsDir, '[name].[hash][extname]')
const fileName = assetFileNamesToFileName(
assetFileNames,
file,
contentHash,
content
)
if (!map.has(contentHash)) {
map.set(contentHash, fileName)
}
const emittedSet = emittedHashMap.get(config)!
if (!emittedSet.has(contentHash)) {
const name = normalizePath(path.relative(config.root, file))
pluginContext.emitFile({
name,
fileName,
type: 'asset',
source: content,
})
emittedSet.add(contentHash)
}
url = `__VITE_ASSET__${contentHash}__${postfix ? `$_${postfix}__` : ``}`
}
cache.set(id, url)
return url
}
export function getAssetHash(content: Buffer): string {
return createHash('sha256').update(content).digest('hex').slice(0, 8)
}
export async function urlToBuiltUrl(
url: string,
importer: string,
config: ResolvedConfig,
pluginContext: PluginContext
): Promise<string> {
if (checkPublicFile(url, config)) {
return config.base + url.slice(1)
}
const file = url.startsWith('/')
? path.join(config.root, url)
: path.join(path.dirname(importer), url)
return fileToBuiltUrl(
file,
config,
pluginContext,
// skip public check since we just did it above
true
)
}
import fs from 'fs'
import path from 'path'
import glob from 'fast-glob'
import {
// createDebugger,
isExternalUrl,
asyncReplace,
cleanUrl,
generateCodeFrame,
isDataUrl,
isObject,
normalizePath,
processSrcSet,
} from '../utils'
import { Plugin } from '../plugin'
import { ResolvedConfig } from '../config'
import postcssrc from 'postcss-load-config'
import {
NormalizedOutputOptions,
OutputChunk,
RenderedChunk,
RollupError,
SourceMap,
} from 'rollup'
import { dataToEsm } from '@rollup/pluginutils'
import chalk from 'chalk'
import { CLIENT_PUBLIC_PATH } from '../constants'
import { ResolveFn, ViteDevServer } from '../'
import {
getAssetFilename,
assetUrlRE,
registerAssetToChunk,
fileToUrl,
checkPublicFile,
} from './h5Asset'
import MagicString from 'magic-string'
import * as Postcss from 'postcss'
import type Sass from 'sass'
// We need to disable check of extraneous import which is buggy for stylus,
// and causes the CI tests fail, see: https://github.com/vitejs/vite/pull/2860
import type Stylus from 'stylus'
import type Less from 'less'
import { Alias } from 'types/alias'
import type { ModuleNode } from '../server/moduleGraph'
import { transform, formatMessages } from 'esbuild'
import { preprocessCss } from './css'
// const debug = createDebugger('vite:css')
export interface CSSOptions {
/**
* https://github.com/css-modules/postcss-modules
*/
modules?: CSSModulesOptions | false
preprocessorOptions?: Record<string, any>
postcss?:
| string
| (Postcss.ProcessOptions & {
plugins?: Postcss.Plugin[]
})
}
export interface CSSModulesOptions {
getJSON?: (
cssFileName: string,
json: Record<string, string>,
outputFileName: string
) => void
scopeBehaviour?: 'global' | 'local'
globalModulePaths?: RegExp[]
generateScopedName?:
| string
| ((name: string, filename: string, css: string) => string)
hashPrefix?: string
/**
* default: null
*/
localsConvention?:
| 'camelCase'
| 'camelCaseOnly'
| 'dashes'
| 'dashesOnly'
| null
}
const cssLangs = `\\.(css|less|sass|scss|styl|stylus|pcss|postcss)($|\\?)`
const cssLangRE = new RegExp(cssLangs)
const cssModuleRE = new RegExp(`\\.module${cssLangs}`)
const directRequestRE = /(\?|&)direct\b/
const commonjsProxyRE = /\?commonjs-proxy/
const inlineRE = /(\?|&)inline\b/
const usedRE = /(\?|&)used\b/
const enum PreprocessLang {
less = 'less',
sass = 'sass',
scss = 'scss',
styl = 'styl',
stylus = 'stylus',
}
const enum PureCssLang {
css = 'css',
}
type CssLang = keyof typeof PureCssLang | keyof typeof PreprocessLang
export const isCSSRequest = (request: string): boolean =>
cssLangRE.test(request)
export const isDirectCSSRequest = (request: string): boolean =>
cssLangRE.test(request) && directRequestRE.test(request)
export const isDirectRequest = (request: string): boolean =>
directRequestRE.test(request)
const cssModulesCache = new WeakMap<
ResolvedConfig,
Map<string, Record<string, string>>
>()
export const chunkToEmittedCssFileMap = new WeakMap<
RenderedChunk,
Set<string>
>()
export const removedPureCssFilesCache = new WeakMap<
ResolvedConfig,
Map<string, RenderedChunk>
>()
const postcssConfigCache = new WeakMap<
ResolvedConfig,
PostCSSConfigResult | null
>()
/**
* Plugin applied before user plugins
*/
export function cssPlugin(config: ResolvedConfig): Plugin {
let server: ViteDevServer
let moduleCache: Map<string, Record<string, string>>
const resolveUrl = config.createResolver({
preferRelative: true,
tryIndex: false,
extensions: [],
})
const atImportResolvers = createCSSResolvers(config)
return {
name: 'vite:css',
configureServer(_server) {
server = _server
},
buildStart() {
// Ensure a new cache for every build (i.e. rebuilding in watch mode)
moduleCache = new Map<string, Record<string, string>>()
cssModulesCache.set(config, moduleCache)
removedPureCssFilesCache.set(config, new Map<string, RenderedChunk>())
},
async transform(raw, id) {
if (!isCSSRequest(id) || commonjsProxyRE.test(id)) {
return
}
const urlReplacer: CssUrlReplacer = async (url, importer) => {
if (checkPublicFile(url, config)) {
return config.base + url.slice(1)
}
const resolved = await resolveUrl(url, importer)
if (resolved) {
return fileToUrl(resolved, config, this)
}
return url
}
const {
code: css,
modules,
deps,
} = await compileCSS(
id,
raw,
config,
urlReplacer,
atImportResolvers,
server
)
if (modules) {
moduleCache.set(id, modules)
}
// track deps for build watch mode
if (config.command === 'build' && config.build.watch && deps) {
for (const file of deps) {
this.addWatchFile(file)
}
}
// dev
if (server) {
// server only logic for handling CSS @import dependency hmr
const { moduleGraph } = server
const thisModule = moduleGraph.getModuleById(id)
if (thisModule) {
// CSS modules cannot self-accept since it exports values
const isSelfAccepting = !modules && !inlineRE.test(id)
if (deps) {
// record deps in the module graph so edits to @import css can trigger
// main import to hot update
const depModules = new Set<string | ModuleNode>()
for (const file of deps) {
depModules.add(
isCSSRequest(file)
? moduleGraph.createFileOnlyEntry(file)
: await moduleGraph.ensureEntryFromUrl(
(
await fileToUrl(file, config, this)
).replace(
(config.server?.origin ?? '') + config.base,
'/'
)
)
)
}
moduleGraph.updateModuleInfo(
thisModule,
depModules,
// The root CSS proxy module is self-accepting and should not
// have an explicit accept list
new Set(),
isSelfAccepting
)
for (const file of deps) {
this.addWatchFile(file)
}
} else {
thisModule.isSelfAccepting = isSelfAccepting
}
}
}
return {
code: css,
// TODO CSS source map
map: { mappings: '' },
}
},
}
}
/**
* Plugin applied after user plugins
*/
export function cssPostPlugin(config: ResolvedConfig): Plugin {
// styles initialization in buildStart causes a styling loss in watch
const styles: Map<string, string> = new Map<string, string>()
let pureCssChunks: Set<string>
// when there are multiple rollup outputs and extracting CSS, only emit once,
// since output formats have no effect on the generated CSS.
let outputToExtractedCSSMap: Map<NormalizedOutputOptions, string>
let hasEmitted = false
return {
name: 'vite:css-post',
buildStart() {
// Ensure new caches for every build (i.e. rebuilding in watch mode)
pureCssChunks = new Set<string>()
outputToExtractedCSSMap = new Map<NormalizedOutputOptions, string>()
hasEmitted = false
},
async transform(css, id, options) {
if (!isCSSRequest(id) || commonjsProxyRE.test(id)) {
return
}
const ssr =
typeof options === 'boolean'
? options
: (options && (options as any).ssr) === true
const inlined = inlineRE.test(id)
const modules = cssModulesCache.get(config)!.get(id)
const modulesCode =
modules && dataToEsm(modules, { namedExports: true, preferConst: true })
if (config.command === 'serve') {
if (isDirectCSSRequest(id)) {
return css
} else {
// server only
if (ssr) {
return modulesCode || `export default ${JSON.stringify(css)}`
}
if (inlined) {
return `export default ${JSON.stringify(css)}`
}
return [
`import { updateStyle, removeStyle } from ${JSON.stringify(
path.posix.join(config.base, CLIENT_PUBLIC_PATH)
)}`,
`const id = ${JSON.stringify(id)}`,
`const css = ${JSON.stringify(css)}`,
`updateStyle(id, css)`,
// css modules exports change on edit so it can't self accept
`${modulesCode || `import.meta.hot.accept()\nexport default css`}`,
`import.meta.hot.prune(() => removeStyle(id))`,
].join('\n')
}
}
// build CSS handling ----------------------------------------------------
// record css
if (!inlined) {
styles.set(id, css)
}
return {
code:
modulesCode ||
(usedRE.test(id)
? `export default ${JSON.stringify(
inlined ? await minifyCSS(css, config) : css
)}`
: `export default ''`),
map: { mappings: '' },
// avoid the css module from being tree-shaken so that we can retrieve
// it in renderChunk()
moduleSideEffects: inlined ? false : 'no-treeshake',
}
},
async renderChunk(code, chunk, opts) {
let chunkCSS = ''
let isPureCssChunk = true
const ids = Object.keys(chunk.modules)
for (const id of ids) {
if (
!isCSSRequest(id) ||
cssModuleRE.test(id) ||
commonjsProxyRE.test(id)
) {
isPureCssChunk = false
}
if (styles.has(id)) {
chunkCSS += styles.get(id)
}
}
if (!chunkCSS) {
return null
}
// resolve asset URL placeholders to their built file URLs and perform
// minification if necessary
const processChunkCSS = async (
css: string,
{
inlined,
minify,
}: {
inlined: boolean
minify: boolean
}
) => {
// replace asset url references with resolved url.
const isRelativeBase = config.base === '' || config.base.startsWith('.')
css = css.replace(assetUrlRE, (_, fileHash, postfix = '') => {
const filename = getAssetFilename(fileHash, config) + postfix
registerAssetToChunk(chunk, filename)
if (!isRelativeBase || inlined) {
// absolute base or relative base but inlined (injected as style tag into
// index.html) use the base as-is
return config.base + filename
} else {
// relative base + extracted CSS - asset file will be in the same dir
return `./${path.posix.basename(filename)}`
}
})
// only external @imports should exist at this point - and they need to
// be hoisted to the top of the CSS chunk per spec (#1845)
if (css.includes('@import')) {
css = await hoistAtImports(css)
}
if (minify && config.build.minify) {
css = await minifyCSS(css, config)
}
return css
}
if (config.build.cssCodeSplit) {
if (isPureCssChunk) {
// this is a shared CSS-only chunk that is empty.
pureCssChunks.add(chunk.fileName)
}
if (opts.format === 'es' || opts.format === 'cjs') {
chunkCSS = await processChunkCSS(chunkCSS, {
inlined: false,
minify: true,
})
// emit corresponding css file
const fileHandle = this.emitFile({
name: chunk.name + '.css',
type: 'asset',
source: chunkCSS,
})
chunkToEmittedCssFileMap.set(
chunk,
new Set([this.getFileName(fileHandle)])
)
} else if (!config.build.ssr) {
// legacy build, inline css
chunkCSS = await processChunkCSS(chunkCSS, {
inlined: true,
minify: true,
})
const style = `__vite_style__`
const injectCode =
`var ${style} = document.createElement('style');` +
`${style}.innerHTML = ${JSON.stringify(chunkCSS)};` +
`document.head.appendChild(${style});`
if (config.build.sourcemap) {
const s = new MagicString(code)
s.prepend(injectCode)
return {
code: s.toString(),
map: s.generateMap({ hires: true }),
}
} else {
return { code: injectCode + code }
}
}
} else {
// non-split extracted CSS will be minified together
chunkCSS = await processChunkCSS(chunkCSS, {
inlined: false,
minify: false,
})
outputToExtractedCSSMap.set(
opts,
(outputToExtractedCSSMap.get(opts) || '') + chunkCSS
)
}
return null
},
async generateBundle(opts, bundle) {
// remove empty css chunks and their imports
if (pureCssChunks.size) {
const emptyChunkFiles = [...pureCssChunks]
.map((file) => path.basename(file))
.join('|')
.replace(/\./g, '\\.')
const emptyChunkRE = new RegExp(
opts.format === 'es'
? `\\bimport\\s*"[^"]*(?:${emptyChunkFiles})";\n?`
: `\\brequire\\(\\s*"[^"]*(?:${emptyChunkFiles})"\\);\n?`,
'g'
)
for (const file in bundle) {
const chunk = bundle[file]
if (chunk.type === 'chunk') {
// remove pure css chunk from other chunk's imports,
// and also register the emitted CSS files under the importer
// chunks instead.
chunk.imports = chunk.imports.filter((file) => {
if (pureCssChunks.has(file)) {
const css = chunkToEmittedCssFileMap.get(
bundle[file] as OutputChunk
)
if (css) {
let existing = chunkToEmittedCssFileMap.get(chunk)
if (!existing) {
existing = new Set()
}
css.forEach((file) => existing!.add(file))
chunkToEmittedCssFileMap.set(chunk, existing)
}
return false
}
return true
})
chunk.code = chunk.code.replace(
emptyChunkRE,
// remove css import while preserving source map location
(m) => `/* empty css ${''.padEnd(m.length - 15)}*/`
)
}
}
const removedPureCssFiles = removedPureCssFilesCache.get(config)!
pureCssChunks.forEach((fileName) => {
removedPureCssFiles.set(fileName, bundle[fileName] as RenderedChunk)
delete bundle[fileName]
})
}
let extractedCss = outputToExtractedCSSMap.get(opts)
if (extractedCss && !hasEmitted) {
hasEmitted = true
// minify css
if (config.build.minify) {
extractedCss = await minifyCSS(extractedCss, config)
}
this.emitFile({
name: 'style.css',
type: 'asset',
source: extractedCss,
})
}
},
}
}
interface CSSAtImportResolvers {
css: ResolveFn
sass: ResolveFn
less: ResolveFn
}
function createCSSResolvers(config: ResolvedConfig): CSSAtImportResolvers {
let cssResolve: ResolveFn | undefined
let sassResolve: ResolveFn | undefined
let lessResolve: ResolveFn | undefined
return {
get css() {
return (
cssResolve ||
(cssResolve = config.createResolver({
extensions: ['.css'],
mainFields: ['style'],
tryIndex: false,
preferRelative: true,
}))
)
},
get sass() {
return (
sassResolve ||
(sassResolve = config.createResolver({
extensions: ['.scss', '.sass', '.css'],
mainFields: ['sass', 'style'],
tryIndex: true,
tryPrefix: '_',
preferRelative: true,
}))
)
},
get less() {
return (
lessResolve ||
(lessResolve = config.createResolver({
extensions: ['.less', '.css'],
mainFields: ['less', 'style'],
tryIndex: false,
preferRelative: true,
}))
)
},
}
}
function getCssResolversKeys(
resolvers: CSSAtImportResolvers
): Array<keyof CSSAtImportResolvers> {
return Object.keys(resolvers) as unknown as Array<keyof CSSAtImportResolvers>
}
async function compileCSS(
id: string,
code: string,
config: ResolvedConfig,
urlReplacer: CssUrlReplacer,
atImportResolvers: CSSAtImportResolvers,
server?: ViteDevServer
): Promise<{
code: string
map?: SourceMap
ast?: Postcss.Result
modules?: Record<string, string>
deps?: Set<string>
}> {
const { modules: modulesOptions, preprocessorOptions } = config.css || {}
const isModule = modulesOptions !== false && cssModuleRE.test(id)
// although at serve time it can work without processing, we do need to
// crawl them in order to register watch dependencies.
const needInlineImport = code.includes('@import')
const hasUrl = cssUrlRE.test(code) || cssImageSetRE.test(code)
const postcssConfig = await resolvePostcssConfig(config)
const lang = id.match(cssLangRE)?.[1] as CssLang | undefined
// 1. plain css that needs no processing
if (
lang === 'css' &&
!postcssConfig &&
!isModule &&
!needInlineImport &&
!hasUrl
) {
return { code }
}
let map: SourceMap | undefined
let modules: Record<string, string> | undefined
const deps = new Set<string>()
// 2. pre-processors: sass etc.
if (isPreProcessor(lang)) {
const preProcessor = preProcessors[lang]
let opts = (preprocessorOptions && preprocessorOptions[lang]) || {}
// support @import from node dependencies by default
switch (lang) {
case PreprocessLang.scss:
case PreprocessLang.sass:
opts = {
includePaths: ['node_modules'],
alias: config.resolve.alias,
...opts,
}
break
case PreprocessLang.less:
case PreprocessLang.styl:
case PreprocessLang.stylus:
opts = {
paths: ['node_modules'],
alias: config.resolve.alias,
...opts,
}
}
// important: set this for relative import resolving
opts.filename = cleanUrl(id)
const preprocessResult = await preProcessor(
code,
config.root,
opts,
atImportResolvers
)
if (preprocessResult.errors.length) {
throw preprocessResult.errors[0]
}
code = preprocessResult.code
map = preprocessResult.map as SourceMap
if (preprocessResult.deps) {
preprocessResult.deps.forEach((dep) => {
// sometimes sass registers the file itself as a dep
if (normalizePath(dep) !== normalizePath(opts.filename)) {
deps.add(dep)
}
})
}
}
// 3. postcss
const postcssOptions = (postcssConfig && postcssConfig.options) || {}
const postcssPlugins =
postcssConfig && postcssConfig.plugins ? postcssConfig.plugins.slice() : []
if (needInlineImport) {
postcssPlugins.unshift(
(await import('postcss-import')).default({
async resolve(id, basedir) {
const resolved = await atImportResolvers.css(
id,
path.join(basedir, '*')
)
if (resolved) {
return path.resolve(resolved)
}
return id
},
})
)
}
postcssPlugins.push(
UrlRewritePostcssPlugin({
replacer: urlReplacer,
}) as Postcss.Plugin
)
if (isModule) {
postcssPlugins.unshift(
(await import('postcss-modules')).default({
...modulesOptions,
getJSON(
cssFileName: string,
_modules: Record<string, string>,
outputFileName: string
) {
modules = _modules
if (modulesOptions && typeof modulesOptions.getJSON === 'function') {
modulesOptions.getJSON(cssFileName, _modules, outputFileName)
}
},
async resolve(id: string) {
for (const key of getCssResolversKeys(atImportResolvers)) {
const resolved = await atImportResolvers[key](id)
if (resolved) {
return path.resolve(resolved)
}
}
return id
},
})
)
}
if (!postcssPlugins.length) {
return {
code,
map,
}
}
// postcss is an unbundled dep and should be lazy imported
const postcssResult = await (await import('postcss'))
.default(postcssPlugins)
.process(code, {
...postcssOptions,
to: id,
from: id,
map: {
inline: false,
annotation: false,
prev: map,
},
})
// record CSS dependencies from @imports
for (const message of postcssResult.messages) {
if (message.type === 'dependency') {
deps.add(message.file as string)
} else if (message.type === 'dir-dependency') {
// https://github.com/postcss/postcss/blob/main/docs/guidelines/plugin.md#3-dependencies
const { dir, glob: globPattern = '**' } = message
const pattern =
normalizePath(path.resolve(path.dirname(id), dir)) + `/` + globPattern
const files = glob.sync(pattern, {
ignore: ['**/node_modules/**'],
})
for (let i = 0; i < files.length; i++) {
deps.add(files[i])
}
if (server) {
// register glob importers so we can trigger updates on file add/remove
if (!(id in (server as any)._globImporters)) {
;(server as any)._globImporters[id] = {
module: server.moduleGraph.getModuleById(id)!,
importGlobs: [],
}
}
;(server as any)._globImporters[id].importGlobs.push({
base: config.root,
pattern,
})
}
} else if (message.type === 'warning') {
let msg = `[vite:css] ${message.text}`
if (message.line && message.column) {
msg += `\n${generateCodeFrame(code, {
line: message.line,
column: message.column,
})}`
}
config.logger.warn(chalk.yellow(msg))
}
}
return {
ast: postcssResult,
code: postcssResult.css,
map: postcssResult.map as any,
modules,
deps,
}
}
interface PostCSSConfigResult {
options: Postcss.ProcessOptions
plugins: Postcss.Plugin[]
}
async function resolvePostcssConfig(
config: ResolvedConfig
): Promise<PostCSSConfigResult | null> {
let result = postcssConfigCache.get(config)
if (result !== undefined) {
return result
}
// inline postcss config via vite config
const inlineOptions = config.css?.postcss
if (isObject(inlineOptions)) {
const options = { ...inlineOptions }
delete options.plugins
result = {
options,
plugins: inlineOptions.plugins || [],
}
} else {
try {
const searchPath =
typeof inlineOptions === 'string' ? inlineOptions : config.root
// @ts-ignore
result = await postcssrc({}, searchPath)
} catch (e: any) {
if (!/No PostCSS Config found/.test(e.message)) {
throw e
}
result = null
}
}
postcssConfigCache.set(config, result)
return result
}
type CssUrlReplacer = (
url: string,
importer?: string
) => string | Promise<string>
// https://drafts.csswg.org/css-syntax-3/#identifier-code-point
export const cssUrlRE =
/(?<=^|[^\w\-\u0080-\uffff])url\(\s*('[^']+'|"[^"]+"|[^'")]+)\s*\)/
const cssImageSetRE = /image-set\(([^)]+)\)/
const UrlRewritePostcssPlugin: Postcss.PluginCreator<{
replacer: CssUrlReplacer
}> = (opts) => {
if (!opts) {
throw new Error('base or replace is required')
}
return {
postcssPlugin: 'vite-url-rewrite',
Once(root) {
const promises: Promise<void>[] = []
root.walkDecls((declaration) => {
const isCssUrl = cssUrlRE.test(declaration.value)
const isCssImageSet = cssImageSetRE.test(declaration.value)
if (isCssUrl || isCssImageSet) {
const replacerForDeclaration = (rawUrl: string) => {
const importer = declaration.source?.input.file
return opts.replacer(rawUrl, importer)
}
const rewriterToUse = isCssUrl ? rewriteCssUrls : rewriteCssImageSet
promises.push(
rewriterToUse(declaration.value, replacerForDeclaration).then(
(url) => {
declaration.value = url
}
)
)
}
})
if (promises.length) {
return Promise.all(promises) as any
}
},
}
}
UrlRewritePostcssPlugin.postcss = true
function rewriteCssUrls(
css: string,
replacer: CssUrlReplacer
): Promise<string> {
return asyncReplace(css, cssUrlRE, async (match) => {
const [matched, rawUrl] = match
return await doUrlReplace(rawUrl, matched, replacer)
})
}
function rewriteCssImageSet(
css: string,
replacer: CssUrlReplacer
): Promise<string> {
return asyncReplace(css, cssImageSetRE, async (match) => {
const [matched, rawUrl] = match
const url = await processSrcSet(rawUrl, ({ url }) =>
doUrlReplace(url, matched, replacer)
)
return `image-set(${url})`
})
}
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})`
}
async function minifyCSS(css: string, config: ResolvedConfig) {
const { code, warnings } = await transform(css, {
loader: 'css',
minify: true,
target: config.build.cssTarget || undefined,
})
if (warnings.length) {
const msgs = await formatMessages(warnings, { kind: 'warning' })
config.logger.warn(
chalk.yellow(`warnings when minifying css:\n${msgs.join('\n')}`)
)
}
return code
}
// #1845
// CSS @import can only appear at top of the file. We need to hoist all @import
// to top when multiple files are concatenated.
async function hoistAtImports(css: string) {
const postcss = await import('postcss')
return (await postcss.default([AtImportHoistPlugin]).process(css)).css
}
const AtImportHoistPlugin: Postcss.PluginCreator<any> = () => {
return {
postcssPlugin: 'vite-hoist-at-imports',
Once(root) {
const imports: Postcss.AtRule[] = []
root.walkAtRules((rule) => {
if (rule.name === 'import') {
// record in reverse so that can simply prepend to preserve order
imports.unshift(rule)
}
})
imports.forEach((i) => root.prepend(i))
},
}
}
AtImportHoistPlugin.postcss = true
// Preprocessor support. This logic is largely replicated from @vue/compiler-sfc
type PreprocessorAdditionalData =
| string
| ((source: string, filename: string) => string | Promise<string>)
type StylePreprocessorOptions = {
[key: string]: any
additionalData?: PreprocessorAdditionalData
filename: string
alias: Alias[]
}
type SassStylePreprocessorOptions = StylePreprocessorOptions & Sass.Options
type StylePreprocessor = (
source: string,
root: string,
options: StylePreprocessorOptions,
resolvers: CSSAtImportResolvers
) => StylePreprocessorResults | Promise<StylePreprocessorResults>
type SassStylePreprocessor = (
source: string,
root: string,
options: SassStylePreprocessorOptions,
resolvers: CSSAtImportResolvers
) => StylePreprocessorResults | Promise<StylePreprocessorResults>
export interface StylePreprocessorResults {
code: string
map?: object
errors: RollupError[]
deps: string[]
}
const loadedPreprocessors: Partial<Record<PreprocessLang, any>> = {}
function loadPreprocessor(lang: PreprocessLang.scss, root: string): typeof Sass
function loadPreprocessor(lang: PreprocessLang.sass, root: string): typeof Sass
function loadPreprocessor(lang: PreprocessLang.less, root: string): typeof Less
function loadPreprocessor(
lang: PreprocessLang.stylus,
root: string
): typeof Stylus
function loadPreprocessor(lang: PreprocessLang, root: string): any {
if (lang in loadedPreprocessors) {
return loadedPreprocessors[lang]
}
try {
// Search for the preprocessor in the root directory first, and fall back
// to the default require paths.
const fallbackPaths = require.resolve.paths?.(lang) || []
const resolved = require.resolve(lang, { paths: [root, ...fallbackPaths] })
return (loadedPreprocessors[lang] = require(resolved))
} catch (e) {
throw new Error(
`Preprocessor dependency "${lang}" not found. Did you install it?`
)
}
}
// .scss/.sass processor
const scss: SassStylePreprocessor = async (
source,
root,
options,
resolvers
) => {
const render = loadPreprocessor(PreprocessLang.sass, root).render
const internalImporter: Sass.Importer = (url, importer, done) => {
resolvers.sass(url, importer).then((resolved) => {
if (resolved) {
rebaseUrls(resolved, options.filename, options.alias)
.then((data) => done?.(data))
.catch((data) => done?.(data))
} else {
done?.(null)
}
})
}
const importer = [internalImporter]
if (options.importer) {
Array.isArray(options.importer)
? importer.push(...options.importer)
: importer.push(options.importer)
}
const finalOptions: Sass.Options = {
...options,
data: await getSource(source, options.filename, options.additionalData),
file: options.filename,
outFile: options.filename,
importer,
}
try {
const result = await new Promise<Sass.Result>((resolve, reject) => {
render(finalOptions, (err, res) => {
if (err) {
reject(err)
} else {
resolve(res)
}
})
})
const deps = result.stats.includedFiles
return {
code: result.css.toString(),
errors: [],
deps,
}
} catch (e: any) {
// normalize SASS error
e.id = e.file
e.frame = e.formatted
return { code: '', errors: [e], deps: [] }
}
}
const sass: SassStylePreprocessor = (source, root, options, aliasResolver) =>
scss(
source,
root,
{
...options,
indentedSyntax: true,
},
aliasResolver
)
/**
* 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<Sass.ImporterReturnType> {
file = path.resolve(file) // ensure os-specific flashes
// fixed by xxxxxx 条件编译
const contents = preprocessCss(fs.readFileSync(file, 'utf-8'))
// in the same dir, no need to rebase
const fileDir = path.dirname(file)
const rootDir = path.dirname(rootFile)
if (fileDir === rootDir) {
return { file, contents }
}
// no url()
if (!cssUrlRE.test(contents)) {
return { file, contents }
}
const rebased = await rewriteCssUrls(contents, (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,
}
}
// .less
const less: StylePreprocessor = async (source, root, options, resolvers) => {
const nodeLess = loadPreprocessor(PreprocessLang.less, root)
const viteResolverPlugin = createViteLessPlugin(
nodeLess,
options.filename,
options.alias,
resolvers
)
source = await getSource(source, options.filename, options.additionalData)
let result: Less.RenderOutput | undefined
try {
result = await nodeLess.render(source, {
...options,
plugins: [viteResolverPlugin, ...(options.plugins || [])],
})
} catch (e) {
const error = e as Less.RenderError
// normalize error info
const normalizedError: RollupError = new Error(error.message || error.type)
normalizedError.loc = {
file: error.filename || options.filename,
line: error.line,
column: error.column,
}
return { code: '', errors: [normalizedError], deps: [] }
}
return {
code: result.css.toString(),
deps: result.imports,
errors: [],
}
}
/**
* Less manager, lazy initialized
*/
let ViteLessManager: any
function createViteLessPlugin(
less: typeof Less,
rootFile: string,
alias: Alias[],
resolvers: CSSAtImportResolvers
): Less.Plugin {
if (!ViteLessManager) {
ViteLessManager = class ViteManager extends less.FileManager {
resolvers
rootFile
alias
constructor(
rootFile: string,
resolvers: CSSAtImportResolvers,
alias: Alias[]
) {
super()
this.rootFile = rootFile
this.resolvers = resolvers
this.alias = alias
}
override supports() {
return true
}
override supportsSync() {
return false
}
override 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: string
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],
}
}
// .styl
const styl: StylePreprocessor = async (source, root, options) => {
const nodeStylus = loadPreprocessor(PreprocessLang.stylus, root)
// Get source with preprocessor options.additionalData. Make sure a new line separator
// is added to avoid any render error, as added stylus content may not have semi-colon separators
source = await getSource(
source,
options.filename,
options.additionalData,
'\n'
)
// Get preprocessor options.imports dependencies as stylus
// does not return them with its builtin `.deps()` method
const importsDeps = (options.imports ?? []).map((dep: string) =>
path.resolve(dep)
)
try {
const ref = nodeStylus(source, options)
// if (map) ref.set('sourcemap', { inline: false, comment: false })
const result = ref.render()
// Concat imports deps with computed deps
const deps = [...ref.deps(), ...importsDeps]
return { code: result, errors: [], deps }
} catch (e: any) {
return { code: '', errors: [e], deps: [] }
}
}
function getSource(
source: string,
filename: string,
additionalData?: PreprocessorAdditionalData,
sep: string = ''
): string | Promise<string> {
if (!additionalData) return source
if (typeof additionalData === 'function') {
return additionalData(source, filename)
}
return additionalData + sep + source
}
const preProcessors = Object.freeze({
[PreprocessLang.less]: less,
[PreprocessLang.sass]: sass,
[PreprocessLang.scss]: scss,
[PreprocessLang.styl]: styl,
[PreprocessLang.stylus]: styl,
})
function isPreProcessor(lang: any): lang is PreprocessLang {
return lang && lang in preProcessors
}
import type { Plugin, ResolvedConfig } from 'vite' import type { Plugin, ResolvedConfig } from 'vite'
import { assetPlugin } from '../plugins/vitejs/plugins/asset' import { assetPlugin } from '../plugins/vitejs/plugins/asset'
import { cssPlugin, cssPostPlugin } from '../plugins/vitejs/plugins/css' import { cssPlugin, cssPostPlugin } from '../plugins/vitejs/plugins/css'
import { assetPlugin as h5AssetPlugin } from '../plugins/vitejs/plugins/h5Asset'
import {
cssPlugin as h5CssPlugin,
cssPostPlugin as h5CssPostPlugin,
} from '../plugins/vitejs/plugins/h5Css'
export type CreateUniViteFilterPlugin = ( export type CreateUniViteFilterPlugin = (
opts: UniViteFilterPluginOptions opts: UniViteFilterPluginOptions
) => Plugin ) => Plugin
...@@ -9,6 +14,18 @@ export interface UniViteFilterPluginOptions { ...@@ -9,6 +14,18 @@ export interface UniViteFilterPluginOptions {
filter: (id: string) => boolean filter: (id: string) => boolean
} }
export function injectH5AssetPlugin(config: ResolvedConfig) {
replacePlugins([h5AssetPlugin(config)], config)
}
export function injectH5CssPlugin(config: ResolvedConfig) {
replacePlugins([h5CssPlugin(config)], config)
}
export function injectH5CssPostPlugin(config: ResolvedConfig) {
replacePlugins([h5CssPostPlugin(config)], config)
}
export function injectAssetPlugin(config: ResolvedConfig) { export function injectAssetPlugin(config: ResolvedConfig) {
replacePlugins([assetPlugin(config)], config) replacePlugins([assetPlugin(config)], config)
} }
......
import type { ResolvedConfig } from 'vite' import type { ResolvedConfig } from 'vite'
import { UniVitePlugin } from '@dcloudio/uni-cli-shared' import {
injectH5AssetPlugin,
injectH5CssPlugin,
injectH5CssPostPlugin,
UniVitePlugin,
} from '@dcloudio/uni-cli-shared'
import { createHandleHotUpdate } from './handleHotUpdate' import { createHandleHotUpdate } from './handleHotUpdate'
import { createTransformIndexHtml } from './transformIndexHtml' import { createTransformIndexHtml } from './transformIndexHtml'
import { createConfigureServer } from './configureServer' import { createConfigureServer } from './configureServer'
...@@ -21,6 +26,10 @@ export function uniH5PLugin(): UniVitePlugin { ...@@ -21,6 +26,10 @@ export function uniH5PLugin(): UniVitePlugin {
configOptions.resolvedConfig = config configOptions.resolvedConfig = config
// TODO 禁止 optimizeDeps // TODO 禁止 optimizeDeps
;(config as any).cacheDir = '' ;(config as any).cacheDir = ''
injectH5AssetPlugin(config)
injectH5CssPlugin(config)
injectH5CssPostPlugin(config)
}, },
configureServer: createConfigureServer(), configureServer: createConfigureServer(),
handleHotUpdate: createHandleHotUpdate(), handleHotUpdate: createHandleHotUpdate(),
......
...@@ -10765,10 +10765,11 @@ function usePageHeadSearchInput({ ...@@ -10765,10 +10765,11 @@ function usePageHeadSearchInput({
}; };
} }
var _export_sfc = (sfc, props2) => { var _export_sfc = (sfc, props2) => {
const target = sfc.__vccOpts || sfc;
for (const [key, val] of props2) { for (const [key, val] of props2) {
sfc[key] = val; target[key] = val;
} }
return sfc; return target;
}; };
const _sfc_main = { const _sfc_main = {
name: "PageRefresh", name: "PageRefresh",
......
...@@ -21564,10 +21564,11 @@ function usePageHeadSearchInput({ ...@@ -21564,10 +21564,11 @@ function usePageHeadSearchInput({
}; };
} }
var _export_sfc = (sfc, props2) => { var _export_sfc = (sfc, props2) => {
const target = sfc.__vccOpts || sfc;
for (const [key, val] of props2) { for (const [key, val] of props2) {
sfc[key] = val; target[key] = val;
} }
return sfc; return target;
}; };
const _sfc_main = { const _sfc_main = {
name: "PageRefresh", name: "PageRefresh",
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册