Skip to content

Commit

Permalink
feat: add support output when build env
Browse files Browse the repository at this point in the history
  • Loading branch information
pengzhanbo committed Jan 11, 2023
1 parent 59c5812 commit 1e1c71e
Show file tree
Hide file tree
Showing 4 changed files with 180 additions and 63 deletions.
2 changes: 1 addition & 1 deletion example/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@ import { defineConfig } from 'vite'
import imagePlaceholder from '../src/index'

export default defineConfig(() => ({
plugins: [imagePlaceholder({ inline: true })],
plugins: [imagePlaceholder()],
}))
35 changes: 35 additions & 0 deletions src/bufferToFile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { createHash } from 'node:crypto'
import fs, { promises as fsp } from 'node:fs'
import path from 'node:path'
import type { ImageType, OutputFile, OutputFilename } from './types'

const RE_HTTP = /^https?:\/\//

const isHTTP = (url: string): boolean => RE_HTTP.test(url)

export async function bufferToFile(
buffer: Buffer,
type: ImageType,
outDir: string,
assetsDir: string,
filename?: OutputFilename,
) {
const hash = createHash('sha256')
hash.update(buffer)
const hex = hash.digest('hex').slice(0, 12)

const _filename = path.join(assetsDir, `${hex}.${type}`)
const file: OutputFile = { basename: hex, assetsDir, ext: type }

const pathname = filename ? filename(_filename, file) : _filename

const _http = isHTTP(pathname)
const output = path.join(outDir, _http ? _filename : pathname)

const dirname = path.dirname(output)

fs.mkdirSync(dirname, { recursive: true })
await fsp.writeFile(output, new Uint8Array(buffer))

return _http ? pathname : path.join('/', _filename)
}
180 changes: 118 additions & 62 deletions src/plugin.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import path from 'node:path'
import MagicString from 'magic-string'
import type { Plugin, ResolvedConfig } from 'vite'
import { isCSSRequest } from 'vite'
import { bufferToFile } from './bufferToFile'
import { contentCache } from './cache'
import { DEFAULT_PREFIX } from './constants'
import { generatePathRules } from './pathRules'
Expand Down Expand Up @@ -30,6 +32,25 @@ const parseOptions = (
return options as Required<ImagePlaceholderOptions>
}

const parseOutput = (
output: Required<ImagePlaceholderOptions>['output'],
config: ResolvedConfig,
) => {
const { outDir, assetsDir } = config.build
let assets
let filename
const out = path.join(config.root, outDir)
if (output === true) {
assets = assetsDir
} else if (typeof output === 'string') {
assets = output.replace(/^\/+/, '')
} else {
assets = (output!.dir || assetsDir).replace(/^\/+/, '')
filename = output.filename
}
return { assetsDir: assets, outDir: out, filename }
}

const bufferToBase64 = (image: ImageCacheItem) => {
const base64 = image.buffer.toString('base64')
const content = `data:${getMimeType(image.type)};base64,${base64}`
Expand Down Expand Up @@ -70,8 +91,14 @@ function placeholderImporterPlugin(
const RE_VIRTUAL = /^\0virtual:\s*/
const moduleId = `virtual:${opts.prefix.slice(1)}`
const resolveVirtualModuleId = `\0${moduleId}`
let config: ResolvedConfig
let isBuild: boolean
return {
name: 'vite-plugin-image-placeholder-importer',
configResolved(_config) {
config = _config
isBuild = config.command === 'build'
},
resolveId(id) {
if (id.startsWith(moduleId)) {
return `\0${id}`
Expand All @@ -85,7 +112,22 @@ function placeholderImporterPlugin(
}
const image = await pathToImage(url, pathRules, opts)
if (image) {
const content = bufferToBase64(image)
let content: string
if (isBuild && opts.output) {
const { outDir, assetsDir, filename } = parseOutput(
opts.output,
config,
)
content = await bufferToFile(
image.buffer,
image.type,
outDir,
assetsDir,
filename,
)
} else {
content = bufferToBase64(image)
}
contentCache.set(url, content)
return `export default '${content}'`
}
Expand All @@ -94,7 +136,7 @@ function placeholderImporterPlugin(
}
}

function placeholderInlinePlugin(
function placeholderTransformPlugin(
options: ImagePlaceholderOptions = {},
): Plugin {
const opts = parseOptions(options)
Expand All @@ -109,17 +151,19 @@ function placeholderInlinePlugin(
let isBuild = false
let config: ResolvedConfig
return {
name: 'vite-plugin-image-placeholder-inline',
config(_, { command }) {
isBuild = command === 'build'
},
name: 'vite-plugin-image-placeholder-transform',
configResolved(_config) {
config = _config
isBuild = config.command === 'build'
},
async transform(code, id) {
if (isBuild && !opts.inline) {
// 构建时如果未配置 inline 和 output, 则不转换,直接跳过
if (isBuild && !opts.inline && !opts.output) {
return
}
// 开发环境不对css转换,因为CSS可以直接通过 GET请求获取资源,
// 跳过html资源,在transformIndexHtml中转换,开发环境时也是通过 GET请求获取,
// 优化性能,跳过非js资源和 assets 资源
if (
(!isBuild && isCSSRequest(id)) ||
isHTMLRequest(id) ||
Expand All @@ -129,68 +173,80 @@ function placeholderInlinePlugin(
) {
return
}
const s = new MagicString(code)
let hasReplaced = false
let match
// eslint-disable-next-line no-cond-assign
while ((match = RE_PATTERN.exec(code))) {
const url = match[4] || match[3] || match[2] || match[1]
const dynamic = match[0].includes('(') ? ['("', '")'] : ['"', '"']
const start = match.index
const end = start + match[0].length
if (contentCache.has(url)) {
hasReplaced = true
s.update(
start,
end,
`${dynamic[0]}${contentCache.get(url)}${dynamic[1]}`,
)
} else {
const image = await pathToImage(url, pathRules, opts)
if (image) {
hasReplaced = true
const content = bufferToBase64(image)
contentCache.set(url, content)
s.update(start, end, `${dynamic[0]}${content}${dynamic[1]}`)
}
}
}
if (!hasReplaced) {
return null
}
return {
code: s.toString(),
}
const result = await transformPlaceholder(
code,
RE_PATTERN,
pathRules,
opts,
config,
)

return result ? { code: result } : null
},
async transformIndexHtml(html) {
if (!isBuild) return html
if (!opts.inline) return html
const s = new MagicString(html)
let match
// eslint-disable-next-line no-cond-assign
while ((match = RE_PATTERN.exec(html))) {
const url = match[4] || match[3] || match[2] || match[1]
const dynamic = match[0].includes('(') ? ['("', '")'] : ['"', '"']
const start = match.index
const end = start + match[0].length
if (contentCache.has(url)) {
s.update(
start,
end,
`${dynamic[0]}${contentCache.get(url)}${dynamic[1]}`,
if (!opts.inline && !opts.output) return html
const result = await transformPlaceholder(
html,
RE_PATTERN,
pathRules,
opts,
config,
)

return result || html
},
}
}

async function transformPlaceholder(
code: string,
pattern: RegExp,
rules: string[],
opts: Required<ImagePlaceholderOptions>,
config: ResolvedConfig,
) {
const s = new MagicString(code)
let hasReplaced = false
let match
// eslint-disable-next-line no-cond-assign
while ((match = pattern.exec(code))) {
const url = match[4] || match[3] || match[2] || match[1]
const dynamic = match[0].startsWith('(') ? ['("', '")'] : ['"', '"']
const start = match.index
const end = start + match[0].length
if (contentCache.has(url)) {
hasReplaced = true
s.update(start, end, `${dynamic[0]}${contentCache.get(url)}${dynamic[1]}`)
} else {
const image = await pathToImage(url, rules, opts)
if (image) {
hasReplaced = true
let content: string
if (opts.output) {
const { outDir, assetsDir, filename } = parseOutput(
opts.output,
config,
)
content = await bufferToFile(
image.buffer,
image.type,
outDir,
assetsDir,
filename,
)
} else {
const image = await pathToImage(url, pathRules, opts)
if (image) {
const content = bufferToBase64(image)
contentCache.set(url, content)
s.update(start, end, `${dynamic[0]}${content}${dynamic[1]}`)
}
content = bufferToBase64(image)
}
contentCache.set(url, content)
s.update(start, end, `${dynamic[0]}${content}${dynamic[1]}`)
}
return s.toString()
},
}
}
if (!hasReplaced) {
return null
}
return s.toString()
}

export function imagePlaceholderPlugin(
Expand All @@ -199,6 +255,6 @@ export function imagePlaceholderPlugin(
return [
placeholderServerPlugin(options),
placeholderImporterPlugin(options),
placeholderInlinePlugin(options),
placeholderTransformPlugin(options),
]
}
26 changes: 26 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,32 @@ export interface ImagePlaceholderOptions {
* @default false
*/
inline?: boolean

/**
* 生产构建时,输出图片资源到构建目录中
*
* 如果取值为 true,默认根据 vite output 配置,输出到 dist/assets
*
* @default true
*/
output?:
| true
| string
| {
dir?: string
/**
* 重写 filename,有时候图片资源需要发布到CDN,可以在这里修改文件名称
*/
filename?: OutputFilename
}
}

export type OutputFilename = (filename: string, file: OutputFile) => string

export interface OutputFile {
basename: string
assetsDir: string
ext: string
}

export type ImageType =
Expand Down

0 comments on commit 1e1c71e

Please sign in to comment.