提交 0a764d80 编写于 作者: fxy060608's avatar fxy060608

refactor(h5): preprocess scss

上级 1297730a
......@@ -867,7 +867,7 @@ const sass: SassStylePreprocessor = (source, root, options, aliasResolver) =>
aliasResolver
)
export function preprocessCss(content: string) {
function preprocessCss(content: string) {
if (content.includes('#endif')) {
return preCss(content)
}
......
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 type { Plugin, ResolvedConfig } from 'vite'
import { assetPlugin } from '../plugins/vitejs/plugins/asset'
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 = (
opts: UniViteFilterPluginOptions
) => Plugin
......@@ -14,18 +10,6 @@ export interface UniViteFilterPluginOptions {
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) {
replacePlugins([assetPlugin(config)], config)
}
......
import type { ResolvedConfig } from 'vite'
import {
injectH5AssetPlugin,
injectH5CssPlugin,
injectH5CssPostPlugin,
UniVitePlugin,
} from '@dcloudio/uni-cli-shared'
import fs from 'fs'
import path from 'path'
import { preCss, UniVitePlugin } from '@dcloudio/uni-cli-shared'
import { createHandleHotUpdate } from './handleHotUpdate'
import { createTransformIndexHtml } from './transformIndexHtml'
import { createConfigureServer } from './configureServer'
import { createUni } from './uni'
import { createConfig } from './config'
import { isString } from '@vue/shared'
export function uniH5PLugin(): UniVitePlugin {
const configOptions: {
......@@ -18,6 +16,7 @@ export function uniH5PLugin(): UniVitePlugin {
} = {
resolvedConfig: null,
}
rewriteReadFileSync()
return {
name: 'vite:uni-h5',
uni: createUni(),
......@@ -26,13 +25,28 @@ export function uniH5PLugin(): UniVitePlugin {
configOptions.resolvedConfig = config
// TODO 禁止 optimizeDeps
;(config as any).cacheDir = ''
injectH5AssetPlugin(config)
injectH5CssPlugin(config)
injectH5CssPostPlugin(config)
},
configureServer: createConfigureServer(),
handleHotUpdate: createHandleHotUpdate(),
transformIndexHtml: createTransformIndexHtml(),
}
}
/**
* 重写 readFileSync
* 目前主要解决 scss 文件被 @import 的条件编译
*/
function rewriteReadFileSync() {
const { readFileSync } = fs
fs.readFileSync = ((filepath, options) => {
const content = readFileSync(filepath, options)
if (
isString(filepath) &&
isString(content) &&
path.extname(filepath) === '.scss' &&
content.includes('#endif')
) {
return preCss(content)
}
return content
}) as typeof fs['readFileSync']
}
......@@ -46,6 +46,7 @@
"@dcloudio/uni-cli-shared": "3.0.0-alpha-3030020211124001",
"@types/google.maps": "^3.45.6",
"acorn-loose": "^8.2.1",
"acorn-walk": "^8.2.0",
"vue": "3.2.22"
}
}
'use strict';
var version = "3.0.0-alpha-3021320211123002";
var version = "3.0.0-alpha-3030020211124001";
const STAT_VERSION = version;
const STAT_URL = 'https://tongji.dcloud.io/uni/stat';
......
var version = "3.0.0-alpha-3021320211123002";
var version = "3.0.0-alpha-3030020211124001";
const STAT_VERSION = version;
const STAT_URL = 'https://tongji.dcloud.io/uni/stat';
......
......@@ -468,6 +468,7 @@ importers:
'@vue/server-renderer': 3.2.22
'@vue/shared': 3.2.22
acorn-loose: ^8.2.1
acorn-walk: ^8.2.0
localstorage-polyfill: ^1.0.1
safe-area-insets: ^1.4.1
vue: 3.2.22
......@@ -489,6 +490,7 @@ importers:
'@dcloudio/uni-cli-shared': link:../uni-cli-shared
'@types/google.maps': 3.47.0
acorn-loose: 8.2.1
acorn-walk: 8.2.0
vue: 3.2.22
packages/uni-h5-vite:
......@@ -3201,6 +3203,11 @@ packages:
engines: {node: '>=0.4.0'}
dev: true
/acorn-walk/8.2.0:
resolution: {integrity: sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==}
engines: {node: '>=0.4.0'}
dev: true
/acorn/5.7.4:
resolution: {integrity: sha512-1D++VG7BhrtvQpNbBzovKNc1FLGGEE/oGe7b9xJm/RFHMBeUaUGpluV9RLjZa47YFdPcDAenEYuq9pQPcMdLJg==}
engines: {node: '>=0.4.0'}
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册