feat: static asset handling

This commit is contained in:
alex8088 2023-01-04 22:53:22 +08:00
parent f7b4146c56
commit 336b4292eb
5 changed files with 193 additions and 2 deletions

18
node.d.ts vendored
View File

@ -3,3 +3,21 @@ declare module '*?nodeWorker' {
import { Worker, WorkerOptions } from 'node:worker_threads'
export default function (options: WorkerOptions): Worker
}
// node asset
declare module '*?asset' {
const src: string
export default src
}
declare module '*?asset&asarUnpack' {
const src: string
export default src
}
// native node module
declare module '*.node' {
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
const node: any
export default node
}

View File

@ -16,6 +16,7 @@ import {
import { build } from 'esbuild'
import { electronMainVitePlugin, electronPreloadVitePlugin, electronRendererVitePlugin } from './plugins/electron'
import assetPlugin from './plugins/asset'
import workerPlugin from './plugins/worker'
import { isObject, dynamicImport } from './utils'
@ -130,7 +131,7 @@ export async function resolveConfig(
resetOutDir(mainViteConfig, outDir, 'main')
}
mergePlugins(mainViteConfig, [...electronMainVitePlugin({ root }), workerPlugin()])
mergePlugins(mainViteConfig, [...electronMainVitePlugin({ root }), assetPlugin(), workerPlugin()])
loadResult.config.main = mainViteConfig
loadResult.config.main.configFile = false
@ -142,7 +143,7 @@ export async function resolveConfig(
if (outDir) {
resetOutDir(preloadViteConfig, outDir, 'preload')
}
mergePlugins(preloadViteConfig, electronPreloadVitePlugin({ root }))
mergePlugins(preloadViteConfig, [...electronPreloadVitePlugin({ root }), assetPlugin()])
loadResult.config.preload = preloadViteConfig
loadResult.config.preload.configFile = false

143
src/plugins/asset.ts Normal file
View File

@ -0,0 +1,143 @@
import path from 'node:path'
import fs from 'node:fs/promises'
import type { SourceMapInput } from 'rollup'
import { type Plugin, normalizePath } from 'vite'
import MagicString from 'magic-string'
import { cleanUrl, parseRequest, getHash, toRelativePath } from '../utils'
interface AssetResolved {
type: 'asset' | 'native'
file: string
query: Record<string, string> | null
}
function resolveAsset(id: string): AssetResolved | null {
const file = cleanUrl(id)
const query = parseRequest(id)
if (query && typeof query.asset === 'string') {
return {
type: 'asset',
file,
query
}
}
if (file.endsWith('.node')) {
return {
type: 'native',
file,
query
}
}
return null
}
const nodeAssetRE = /__VITE_NODE_ASSET__([a-z\d]{8})__/g
const nodePublicAssetRE = /__VITE_NODE_PUBLIC_ASSET__([a-z\d]{8})__/g
export default function assetPlugin(): Plugin {
let sourcemap: boolean | 'inline' | 'hidden' = false
let publicDir = ''
let outDir = ''
const publicAssetPathCache = new Map<string, string>()
const assetCache = new Map<string, string>()
return {
name: 'vite:node-asset',
apply: 'build',
enforce: 'pre',
configResolved(config): void {
sourcemap = config.build.sourcemap
publicDir = normalizePath(config.publicDir)
outDir = normalizePath(config.build.outDir)
},
async load(id): Promise<string | void> {
const assetResolved = resolveAsset(id)
if (!assetResolved) {
return
}
let referenceId: string
const file = assetResolved.file
if (publicDir && file.startsWith(publicDir)) {
const hash = getHash(file)
if (!publicAssetPathCache.get(hash)) {
publicAssetPathCache.set(hash, file)
}
referenceId = `__VITE_NODE_PUBLIC_ASSET__${hash}__`
} else {
const cached = assetCache.get(file)
if (cached) {
referenceId = cached
} else {
const source = await fs.readFile(file)
const hash = this.emitFile({
type: 'asset',
name: path.basename(file),
source
})
referenceId = `__VITE_NODE_ASSET__${hash}__`
assetCache.set(file, referenceId)
}
}
if (assetResolved.type === 'asset') {
if (assetResolved.query && typeof assetResolved.query.asarUnpack === 'string') {
return `
import { join } from 'path'
export default join(__dirname, ${referenceId}).replace('app.asar', 'app.asar.unpacked')`
} else {
return `
import { join } from 'path'
export default join(__dirname, ${referenceId})`
}
}
if (assetResolved.type === 'native') {
return `export default require(${referenceId})`
}
},
renderChunk(code, chunk): { code: string; map: SourceMapInput } | null {
let match: RegExpExecArray | null
let s: MagicString | undefined
nodeAssetRE.lastIndex = 0
if (code.match(nodeAssetRE)) {
while ((match = nodeAssetRE.exec(code))) {
s ||= new MagicString(code)
const [full, hash] = match
const filename = this.getFileName(hash)
const outputFilepath = toRelativePath(filename, chunk.fileName)
const replacement = JSON.stringify(outputFilepath)
s.overwrite(match.index, match.index + full.length, replacement, {
contentOnly: true
})
}
}
nodePublicAssetRE.lastIndex = 0
if (code.match(nodePublicAssetRE)) {
while ((match = nodePublicAssetRE.exec(code))) {
s ||= new MagicString(code)
const [full, hash] = match
const filename = publicAssetPathCache.get(hash)!
const outputFilepath = toRelativePath(filename, normalizePath(path.join(outDir, chunk.fileName)))
const replacement = JSON.stringify(outputFilepath)
s.overwrite(match.index, match.index + full.length, replacement, {
contentOnly: true
})
}
}
if (s) {
return {
code: s.toString(),
map: sourcemap ? s.generateMap({ hires: true }) : null
}
} else {
return null
}
}
}
}

View File

@ -75,6 +75,11 @@ export function electronMainVitePlugin(options?: ElectronPluginOptions): Plugin[
defaultConfig.build.rollupOptions.output['format'] = 'cjs'
}
defaultConfig.build.rollupOptions.output['assetFileNames'] = path.posix.join(
build.assetsDir || defaultConfig.build.assetsDir,
'[name]-[hash].[ext]'
)
const buildConfig = mergeConfig(defaultConfig.build, build)
config.build = buildConfig
@ -82,6 +87,10 @@ export function electronMainVitePlugin(options?: ElectronPluginOptions): Plugin[
config.define = { ...processEnvDefine(), ...config.define }
config.envPrefix = config.envPrefix || 'MAIN_VITE_'
config.publicDir = config.publicDir || 'resources'
// do not copy public dir
config.build.copyPublicDir = false
}
},
{
@ -166,6 +175,11 @@ export function electronPreloadVitePlugin(options?: ElectronPluginOptions): Plug
defaultConfig.build.rollupOptions.output['format'] = 'cjs'
}
defaultConfig.build.rollupOptions.output['assetFileNames'] = path.posix.join(
build.assetsDir || defaultConfig.build.assetsDir,
'[name]-[hash].[ext]'
)
const buildConfig = mergeConfig(defaultConfig.build, build)
config.build = buildConfig
@ -173,6 +187,10 @@ export function electronPreloadVitePlugin(options?: ElectronPluginOptions): Plug
config.define = { ...processEnvDefine(), ...config.define }
config.envPrefix = config.envPrefix || 'PRELOAD_VITE_'
config.publicDir = config.publicDir || 'resources'
// do not copy public dir
config.build.copyPublicDir = false
}
},
{

View File

@ -1,4 +1,6 @@
import { URL, URLSearchParams } from 'node:url'
import path from 'node:path'
import { createHash } from 'node:crypto'
import { loadEnv as viteLoadEnv } from 'vite'
export function isObject(value: unknown): value is Record<string, unknown> {
@ -26,6 +28,15 @@ export function parseRequest(id: string): Record<string, string> | null {
return Object.fromEntries(new URLSearchParams(search))
}
export function getHash(text: Buffer | string): string {
return createHash('sha256').update(text).digest('hex').substring(0, 8)
}
export function toRelativePath(filename: string, importer: string): string {
const relPath = path.posix.relative(path.dirname(importer), filename)
return relPath.startsWith('.') ? relPath : `./${relPath}`
}
/**
* Load `.env` files within the `envDir`(default: `process.cwd()`).
* By default, only env variables prefixed with `MAIN_VITE_`, `PRELOAD_VITE_` and