Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: worker emit fileName with config #7804

Merged
merged 12 commits into from May 12, 2022
9 changes: 8 additions & 1 deletion packages/vite/src/node/config.ts
Expand Up @@ -252,6 +252,8 @@ export type ResolvedConfig = Readonly<
command: 'build' | 'serve'
mode: string
isWorker: boolean
/** @internal */
mainConfig: ResolvedConfig | null
isProduction: boolean
env: Record<string, any>
resolve: ResolveOptions & {
Expand Down Expand Up @@ -482,6 +484,7 @@ export async function resolveConfig(
command,
mode,
isWorker: false,
mainConfig: null,
isProduction,
plugins: userPlugins,
server,
Expand Down Expand Up @@ -513,7 +516,11 @@ export async function resolveConfig(
// flat config.worker.plugin
const [workerPrePlugins, workerNormalPlugins, workerPostPlugins] =
sortUserPlugins(config.worker?.plugins as Plugin[])
const workerResolved: ResolvedConfig = { ...resolved, isWorker: true }
const workerResolved: ResolvedConfig = {
...resolved,
isWorker: true,
mainConfig: resolved
}
resolved.worker.plugins = await resolvePlugins(
workerResolved,
workerPrePlugins,
Expand Down
189 changes: 79 additions & 110 deletions packages/vite/src/node/plugins/worker.ts
@@ -1,85 +1,40 @@
import path from 'path'
import type Rollup from 'rollup'
import type { EmittedFile, TransformPluginContext } from 'rollup'
import type { EmittedAsset, OutputChunk, TransformPluginContext } from 'rollup'
import type { ResolvedConfig } from '../config'
import type { Plugin } from '../plugin'
import { cleanUrl, getHash, injectQuery, parseRequest } from '../utils'
import { cleanUrl, injectQuery, parseRequest } from '../utils'
import { ENV_PUBLIC_PATH } from '../constants'
import { onRollupWarning } from '../build'
import { fileToUrl } from './asset'

interface WorkerCache {
// save worker bundle emitted files avoid overwrites the same file.
// <chunk_filename, hash>
assets: Map<string, string>
chunks: Map<string, string>
// save worker all emit chunk avoid rollup make the same asset unique.
assets: Map<string, EmittedAsset>

// worker bundle don't deps on any more worker runtime info an id only had an result.
// save worker bundled file id to avoid repeated execution of bundles
// <filename, hash>
// <input_filename, hash>
bundle: Map<string, string>
// nested worker bundle context don't had file what emitted by outside bundle
// save the hash to id to rewrite truth id.
// <hash, id>
emitted: Map<string, string>
}

const WorkerFileId = 'worker_file'
const workerCache = new WeakMap<ResolvedConfig, WorkerCache>()

function emitWorkerFile(
ctx: Rollup.TransformPluginContext,
function saveEmitWorkerAsset(
config: ResolvedConfig,
asset: EmittedFile,
type: 'assets' | 'chunks'
): string {
asset: EmittedAsset
): void {
const fileName = asset.fileName!
const workerMap = workerCache.get(config)!

if (workerMap[type].has(fileName)) {
return workerMap[type].get(fileName)!
}
const hash = ctx.emitFile(asset)
workerMap[type].set(fileName, hash)
workerMap.emitted.set(hash, fileName)
return hash
}

function emitWorkerAssets(
ctx: Rollup.TransformPluginContext,
config: ResolvedConfig,
asset: EmittedFile
) {
const { format } = config.worker
return emitWorkerFile(
ctx,
config,
asset,
format === 'es' ? 'chunks' : 'assets'
)
}

function emitWorkerSourcemap(
ctx: Rollup.TransformPluginContext,
config: ResolvedConfig,
asset: EmittedFile
) {
return emitWorkerFile(ctx, config, asset, 'assets')
}

function emitWorkerChunks(
ctx: Rollup.TransformPluginContext,
config: ResolvedConfig,
asset: EmittedFile
) {
return emitWorkerFile(ctx, config, asset, 'chunks')
const workerMap = workerCache.get(config.mainConfig || config)!
workerMap.assets.set(fileName, asset)
}

export async function bundleWorkerEntry(
ctx: Rollup.TransformPluginContext,
ctx: TransformPluginContext,
config: ResolvedConfig,
id: string,
query: Record<string, string> | null
): Promise<Buffer> {
): Promise<OutputChunk> {
// bundle the file as entry to support imports
const { rollup } = await import('rollup')
const { plugins, rollupOptions, format } = config.worker
Expand All @@ -92,24 +47,40 @@ export async function bundleWorkerEntry(
},
preserveEntrySignatures: false
})
let chunk: Rollup.OutputChunk
let chunk: OutputChunk
try {
const workerOutputConfig = config.worker.rollupOptions.output
const workerConfig = workerOutputConfig
? Array.isArray(workerOutputConfig)
? workerOutputConfig[0] || {}
: workerOutputConfig
: {}
Comment on lines +53 to +57
Copy link
Member Author

@poyoho poyoho May 13, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh I miss one point.

Array.isArray(workerOutputConfig)
        ? workerOutputConfig[0] || {}
        : workerOutputConfig

It is always using the first item. Can we limit the type just accept object? @patak-dev

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I saw this and it makes sense in this context. We could warn if there are two output options maybe

const {
output: [outputChunk, ...outputChunks]
} = await bundle.generate({
entryFileNames: path.posix.join(
config.build.assetsDir,
'[name].[hash].js'
),
chunkFileNames: path.posix.join(
config.build.assetsDir,
'[name].[hash].js'
),
assetFileNames: path.posix.join(
config.build.assetsDir,
'[name].[hash].[ext]'
),
...workerConfig,
format,
sourcemap: config.build.sourcemap
})
chunk = outputChunk
outputChunks.forEach((outputChunk) => {
if (outputChunk.type === 'asset') {
emitWorkerAssets(ctx, config, outputChunk)
saveEmitWorkerAsset(config, outputChunk)
} else if (outputChunk.type === 'chunk') {
emitWorkerChunks(ctx, config, {
fileName: path.posix.join(
config.build.assetsDir,
outputChunk.fileName
),
saveEmitWorkerAsset(config, {
fileName: outputChunk.fileName,
source: outputChunk.code,
type: 'asset'
})
Expand All @@ -118,36 +89,32 @@ export async function bundleWorkerEntry(
} finally {
await bundle.close()
}
return emitSourcemapForWorkerEntry(ctx, config, id, query, chunk)
return emitSourcemapForWorkerEntry(ctx, config, query, chunk)
}

function emitSourcemapForWorkerEntry(
context: TransformPluginContext,
ctx: TransformPluginContext,
config: ResolvedConfig,
id: string,
query: Record<string, string> | null,
chunk: Rollup.OutputChunk
): Buffer {
let { code, map: sourcemap } = chunk
chunk: OutputChunk
): OutputChunk {
const { map: sourcemap } = chunk

if (sourcemap) {
if (config.build.sourcemap === 'inline') {
// Manually add the sourcemap to the code if configured for inline sourcemaps.
// TODO: Remove when https://github.com/rollup/rollup/issues/3913 is resolved
// Currently seems that it won't be resolved until Rollup 3
const dataUrl = sourcemap.toUrl()
code += `//# sourceMappingURL=${dataUrl}`
chunk.code += `//# sourceMappingURL=${dataUrl}`
} else if (
config.build.sourcemap === 'hidden' ||
config.build.sourcemap === true
) {
const basename = path.parse(cleanUrl(id)).name
const data = sourcemap.toString()
const content = Buffer.from(data)
const contentHash = getHash(content)
const fileName = `${basename}.${contentHash}.js.map`
const filePath = path.posix.join(config.build.assetsDir, fileName)
emitWorkerSourcemap(context, config, {
fileName: filePath,
const mapFileName = chunk.fileName + '.map'
saveEmitWorkerAsset(config, {
fileName: mapFileName,
type: 'asset',
source: data
})
Expand All @@ -161,57 +128,50 @@ function emitSourcemapForWorkerEntry(
// non-inline web workers can use a relative path
const sourceMapUrl =
query?.inline != null
? path.posix.join(config.base, filePath)
: fileName
code += `//# sourceMappingURL=${sourceMapUrl}`
? mapFileName
: path.relative(config.build.assetsDir, mapFileName)
chunk.code += `//# sourceMappingURL=${sourceMapUrl}`
}
}
}

return Buffer.from(code)
return chunk
}

export async function workerFileToUrl(
ctx: Rollup.TransformPluginContext,
ctx: TransformPluginContext,
config: ResolvedConfig,
id: string,
query: Record<string, string> | null
): Promise<string> {
const workerMap = workerCache.get(config)!

let hash = workerMap.bundle.get(id)
if (hash) {
// rewrite truth id, no need to replace by asset plugin
return config.base + workerMap.emitted.get(hash)!
const workerMap = workerCache.get(config.mainConfig || config)!
let fileName = workerMap.bundle.get(id)
if (!fileName) {
const outputChunk = await bundleWorkerEntry(ctx, config, id, query)
fileName = outputChunk.fileName
saveEmitWorkerAsset(config, {
fileName,
source: outputChunk.code,
type: 'asset'
})
workerMap.bundle.set(id, fileName)
}
const code = await bundleWorkerEntry(ctx, config, id, query)
const basename = path.parse(cleanUrl(id)).name
const contentHash = getHash(code)
const fileName = path.posix.join(
config.build.assetsDir,
`${basename}.${contentHash}.js`
)
hash = emitWorkerAssets(ctx, config, {
fileName,
type: 'asset',
source: code
})
workerMap.bundle.set(id, hash)
return `__VITE_ASSET__${hash}__`
return config.base + fileName
}

export function webWorkerPlugin(config: ResolvedConfig): Plugin {
const isBuild = config.command === 'build'

const isWorker = config.isWorker
return {
name: 'vite:worker',

buildStart() {
if (isWorker) {
return
}
workerCache.set(config, {
assets: new Map(),
chunks: new Map(),
bundle: new Map(),
emitted: new Map()
bundle: new Map()
})
},

Expand Down Expand Up @@ -244,12 +204,14 @@ export function webWorkerPlugin(config: ResolvedConfig): Plugin {
let url: string
if (isBuild) {
if (query.inline != null) {
const code = await bundleWorkerEntry(this, config, id, query)
const chunk = await bundleWorkerEntry(this, config, id, query)
const { format } = config.worker
const workerOptions = format === 'es' ? '{type: "module"}' : '{}'
// inline as blob data url
return {
code: `const encodedJs = "${code.toString('base64')}";
code: `const encodedJs = "${Buffer.from(chunk.code).toString(
'base64'
)}";
const blob = typeof window !== "undefined" && window.Blob && new Blob([atob(encodedJs)], { type: "text/javascript;charset=utf-8" });
export default function WorkerWrapper() {
const objURL = blob && (window.URL || window.webkitURL).createObjectURL(blob);
Expand Down Expand Up @@ -289,6 +251,13 @@ export function webWorkerPlugin(config: ResolvedConfig): Plugin {
if (config.isWorker && code.includes('import.meta.url')) {
return code.replace('import.meta.url', 'self.location.href')
}
if (!isWorker) {
const workerMap = workerCache.get(config)!
workerMap.assets.forEach((asset) => {
this.emitFile(asset)
workerMap.assets.delete(asset.fileName!)
})
}
}
}
}
8 changes: 5 additions & 3 deletions playground/worker/__tests__/es/es-worker.spec.ts
Expand Up @@ -52,8 +52,10 @@ test.each([[true], [false]])('shared worker', async (doTick) => {
})

test('worker emitted and import.meta.url in nested worker (serve)', async () => {
expect(await page.textContent('.nested-worker')).toMatch('/worker-nested')
expect(await page.textContent('.nested-worker-module')).toMatch('/sub-worker')
expect(await page.textContent('.nested-worker')).toMatch(
'worker-nested-worker'
)
expect(await page.textContent('.nested-worker-module')).toMatch('sub-worker')
expect(await page.textContent('.nested-worker-constructor')).toMatch(
'"type":"constructor"'
)
Expand All @@ -64,7 +66,7 @@ describe.runIf(isBuild)('build', () => {
test('inlined code generation', async () => {
const assetsDir = path.resolve(testDir, 'dist/es/assets')
const files = fs.readdirSync(assetsDir)
expect(files.length).toBe(26)
expect(files.length).toBe(25)
const index = files.find((f) => f.includes('main-module'))
const content = fs.readFileSync(path.resolve(assetsDir, index), 'utf-8')
const worker = files.find((f) => f.includes('my-worker'))
Expand Down
18 changes: 16 additions & 2 deletions playground/worker/vite.config-es.js
Expand Up @@ -7,10 +7,24 @@ module.exports = vite.defineConfig({
enforce: 'pre',
worker: {
format: 'es',
plugins: [vueJsx()]
plugins: [vueJsx()],
rollupOptions: {
output: {
assetFileNames: 'assets/worker_asset.[name].[ext]',
chunkFileNames: 'assets/worker_chunk.[name].js',
entryFileNames: 'assets/worker_entry.[name].js'
}
}
},
build: {
outDir: 'dist/es'
outDir: 'dist/es',
rollupOptions: {
output: {
assetFileNames: 'assets/[name].[ext]',
chunkFileNames: 'assets/[name].js',
entryFileNames: 'assets/[name].js'
}
}
},
plugins: [
{
Expand Down