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鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: allow parallel worker builds when worker plugins is a function #14113

Closed
wants to merge 14 commits into from
Closed
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 5 additions & 1 deletion docs/config/worker-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,14 @@ Output format for worker bundle.

## worker.plugins

- **Type:** [`(Plugin | Plugin[])[]`](./shared-options#plugins)
- **Type:** [`(() => (Plugin | Plugin[])[]) | (Plugin | Plugin[])[]`](./shared-options#plugins)

Vite plugins that apply to worker bundle. Note that [config.plugins](./shared-options#plugins) only applies to workers in dev, it should be configured here instead for build.

Providing a function that returns an array of plugins is preferred, so we can safely run parallel builds for workers. The function should be side effect free and return the same array of plugins every time.

When providing an array of plugins, the worker builds will run sequentially and workers nested inside other workers may not build.

## worker.rollupOptions

- **Type:** [`RollupOptions`](https://rollupjs.org/configuration-options/)
Expand Down
33 changes: 33 additions & 0 deletions packages/vite/src/node/__tests__/config.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -335,3 +335,36 @@ describe('resolveConfig', () => {
expect(results2.clearScreen).toBe(false)
})
})

describe('worker config', () => {
const userPlugin = (): PluginOption => {
return {
name: 'vite-plugin-worker-user-plugin',
}
}

test('resolves to default plugins function', async () => {
const results = await resolveConfig({}, 'build')
expect(typeof results.worker.plugins).toBe('function')
})

test('resolves to plugins function when user defined function given', async () => {
const config: InlineConfig = {
worker: {
plugins: () => [userPlugin()],
},
}
const results = await resolveConfig(config, 'build')
expect(typeof results.worker.plugins).toBe('function')
})

test('resolves to a plugins array when user defined array given', async () => {
const config: InlineConfig = {
worker: {
plugins: [userPlugin()],
},
}
const results = await resolveConfig(config, 'build')
expect(Array.isArray(results.worker.plugins)).toBe(true)
})
})
64 changes: 44 additions & 20 deletions packages/vite/src/node/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,7 @@ export interface UserConfig {
/**
* Vite plugins that apply to worker bundle
*/
plugins?: PluginOption[]
plugins?: PluginOption[] | (() => PluginOption[])
/**
* Rollup options to build worker bundle
*/
Expand Down Expand Up @@ -332,7 +332,7 @@ export interface LegacyOptions {

export interface ResolveWorkerOptions extends PluginHookUtils {
format: 'es' | 'iife'
plugins: Plugin[]
plugins: Plugin[] | (() => Promise<Plugin[]>)
rollupOptions: RollupOptions
}

Expand Down Expand Up @@ -454,12 +454,6 @@ export async function resolveConfig(
return p.apply === command
}
}
// Some plugins that aren't intended to work in the bundling of workers (doing post-processing at build time for example).
// And Plugins may also have cached that could be corrupted by being used in these extra rollup calls.
// So we need to separate the worker plugin from the plugin that vite needs to run.
const rawWorkerUserPlugins = (
(await asyncFlatten(config.worker?.plugins || [])) as Plugin[]
).filter(filterPlugin)

// resolve plugins
const rawUserPlugins = (
Expand Down Expand Up @@ -653,18 +647,39 @@ export async function resolveConfig(

const BASE_URL = resolvedBase

// Some plugins that aren't intended to work in the bundling of workers (doing post-processing at build time for example).
// And Plugins may also have cached that could be corrupted by being used in these extra rollup calls.
// So we need to separate the worker plugin from the plugin that vite needs to run.
const rawWorkerUserPlugins = config.worker?.plugins
const supportsUserPluginsFunction =
typeof rawWorkerUserPlugins === 'function' || !rawWorkerUserPlugins
const getSortedWorkerUserPlugins = async (
userPlugins?: PluginOption[] | (() => PluginOption[]),
) => {
const plugins =
typeof userPlugins === 'function' ? userPlugins() : userPlugins
const pluginsFlattened = (
(await asyncFlatten(plugins || [])) as Plugin[]
).filter(filterPlugin)
return sortUserPlugins(pluginsFlattened)
}

// resolve worker
let workerConfig = mergeConfig({}, config)
const [workerPrePlugins, workerNormalPlugins, workerPostPlugins] =
sortUserPlugins(rawWorkerUserPlugins)
await getSortedWorkerUserPlugins(rawWorkerUserPlugins)

// run config hooks
const workerUserPlugins = [
const workerUserPluginsForHooks = [
...workerPrePlugins,
...workerNormalPlugins,
...workerPostPlugins,
]
workerConfig = await runConfigHook(workerConfig, workerUserPlugins, configEnv)
workerConfig = await runConfigHook(
workerConfig,
workerUserPluginsForHooks,
configEnv,
)
const resolvedWorkerOptions: ResolveWorkerOptions = {
format: workerConfig.worker?.format || 'iife',
plugins: [],
Expand Down Expand Up @@ -754,15 +769,17 @@ export async function resolveConfig(
isWorker: true,
mainConfig: resolved,
}
resolvedConfig.worker.plugins = await resolvePlugins(
workerResolved,
workerPrePlugins,
workerNormalPlugins,
workerPostPlugins,
)

const getResolvedWorkerPlugins = async () => {
return resolvePlugins(
workerResolved,
...(await getSortedWorkerUserPlugins(rawWorkerUserPlugins)),
)
}

Object.assign(
resolvedConfig.worker,
createPluginHookUtils(resolvedConfig.worker.plugins),
createPluginHookUtils(await getResolvedWorkerPlugins()),
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Might need to add some comments. The assumption is that the user worker.plugins function would be side effect free. This allows us to pre-process relevant hook/config updates but still allow fresh plugin instantiation per worker build.

Does that make sense? 馃槄

)

// call configResolved hooks
Expand All @@ -775,6 +792,10 @@ export async function resolveConfig(
.map((hook) => hook(workerResolved)),
])

resolvedConfig.worker.plugins = supportsUserPluginsFunction
? () => getResolvedWorkerPlugins()
: await getResolvedWorkerPlugins()

// validate config

if (middlewareMode === 'ssr') {
Expand Down Expand Up @@ -812,7 +833,10 @@ export async function resolveConfig(
plugins: resolved.plugins.map((p) => p.name),
worker: {
...resolved.worker,
plugins: resolved.worker.plugins.map((p) => p.name),
plugins: (typeof resolved.worker.plugins === 'function'
? await resolved.worker.plugins()
: resolved.worker.plugins
).map((p) => p.name),
},
})

Expand Down Expand Up @@ -856,7 +880,7 @@ assetFileNames isn't equal for every build.rollupOptions.output. A single patter
) {
resolved.logger.warn(
colors.yellow(`
(!) Experimental legacy.buildSsrCjsExternalHeuristics and ssr.format: 'cjs' are going to be removed in Vite 5.
(!) Experimental legacy.buildSsrCjsExternalHeuristics and ssr.format: 'cjs' are going to be removed in Vite 5.
Find more information and give feedback at https://github.com/vitejs/vite/discussions/13816.
`),
)
Expand Down
21 changes: 14 additions & 7 deletions packages/vite/src/node/plugins/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,29 +42,36 @@ function saveEmitWorkerAsset(
}

// Ensure that only one rollup build is called at the same time to avoid
// leaking state in plugins between worker builds.
// TODO: Review if we can parallelize the bundling of workers.
// leaking state in plugins between worker builds when running sequentially.
const workerConfigSemaphore = new WeakMap<
ResolvedConfig,
Promise<OutputChunk>
>()
export async function bundleWorkerEntry(
export async function handleBundleWorkerEntry(
config: ResolvedConfig,
id: string,
query: Record<string, string> | null,
): Promise<OutputChunk> {
// When plugins are a function we can assume that plugins are instantiated separately
// and have a separate state. This means that we can run the worker builds in parallel.
const canRunParallel = typeof config.worker.plugins === 'function'

if (canRunParallel) {
return bundleWorkerEntry(config, id, query)
}

const processing = workerConfigSemaphore.get(config)
if (processing) {
await processing
return bundleWorkerEntry(config, id, query)
}
const promise = serialBundleWorkerEntry(config, id, query)
const promise = bundleWorkerEntry(config, id, query)
workerConfigSemaphore.set(config, promise)
promise.then(() => workerConfigSemaphore.delete(config))
return promise
}

async function serialBundleWorkerEntry(
async function bundleWorkerEntry(
config: ResolvedConfig,
id: string,
query: Record<string, string> | null,
Expand All @@ -75,7 +82,7 @@ async function serialBundleWorkerEntry(
const bundle = await rollup({
...rollupOptions,
input: cleanUrl(id),
plugins,
plugins: typeof plugins === 'function' ? await plugins() : plugins,
onwarn(warning, warn) {
onRollupWarning(warning, warn, config)
},
Expand Down Expand Up @@ -173,7 +180,7 @@ export async function workerFileToUrl(
const workerMap = workerCache.get(config.mainConfig || config)!
let fileName = workerMap.bundle.get(id)
if (!fileName) {
const outputChunk = await bundleWorkerEntry(config, id, query)
const outputChunk = await handleBundleWorkerEntry(config, id, query)
fileName = outputChunk.fileName
saveEmitWorkerAsset(config, {
fileName,
Expand Down
2 changes: 1 addition & 1 deletion playground/worker/__tests__/es/es-worker.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,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(28)
expect(files.length).toBe(29)
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
17 changes: 17 additions & 0 deletions playground/worker/__tests__/parallel/parallel-worker-build.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { describe, test } from 'vitest'
import { isBuild, page, untilUpdated } from '~utils'

describe.runIf(isBuild)('build', () => {
test('expect the plugin state to be unpolluted and match across worker builds', async () => {
await untilUpdated(
() => page.textContent('.nested-worker-plugin-state'),
'"data":1',
true,
)
await untilUpdated(
() => page.textContent('.sub-worker-plugin-state'),
'"data":1',
true,
)
})
})
18 changes: 18 additions & 0 deletions playground/worker/__tests__/serial/serial-worker-build.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { describe, test } from 'vitest'
import { isBuild, page, untilUpdated } from '~utils'

describe.runIf(isBuild)('build', () => {
test('expect the plugin state to be polluted and not match across worker builds', async () => {
await untilUpdated(
() => page.textContent('.nested-worker-plugin-state'),
'"data":1',
true,
)
// The plugin state is polluted and should have a different data value from the sub worker
await untilUpdated(
() => page.textContent('.sub-worker-plugin-state'),
'"data":2',
true,
)
})
})
12 changes: 12 additions & 0 deletions playground/worker/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,18 @@ <h2 class="format-iife">format iife:</h2>
</p>
<code class="nested-worker-constructor"></code>

<p>
import NestedWorker from './worker-nested-worker?worker' - plugin-state
<span class="classname">.nested-worker-plugin-state</span>
</p>
<code class="nested-worker-plugin-state"></code>

<p>
import SubWorker from './sub-worker?worker' - plugin-state
<span class="classname">.sub-worker-plugin-state</span>
</p>
<code class="sub-worker-plugin-state"></code>

<p>
new Worker(new URL('./classic-worker.js', import.meta.url))
<span class="classname">.classic-worker</span>
Expand Down
1 change: 1 addition & 0 deletions playground/worker/modules/test-state.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const state = 0
6 changes: 5 additions & 1 deletion playground/worker/sub-worker.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { state } from './modules/test-state.js'

self.postMessage({ type: 'plugin-state-sub', data: state })

self.onmessage = (event) => {
if (event.data === 'ping') {
self.postMessage(`pong ${self.location.href}`)
self.postMessage({ type: 'module', data: `pong ${self.location.href}` })
}
}

Expand Down
35 changes: 35 additions & 0 deletions playground/worker/vite.config-parallel.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { defineConfig } from 'vite'
import workerPluginTestPlugin from './worker-plugin-test-plugin'
import workerPluginStatePlugin from './worker-plugin-state-plugin'

export default defineConfig({
base: '/serial/',
resolve: {
alias: {
'@': __dirname,
},
},
worker: {
format: 'iife',
plugins: () => [workerPluginTestPlugin(), workerPluginStatePlugin()],
rollupOptions: {
output: {
assetFileNames: 'assets/worker_asset-[name].[ext]',
chunkFileNames: 'assets/worker_chunk-[name].js',
entryFileNames: 'assets/worker_entry-[name].js',
},
},
},
build: {
outDir: 'dist/parallel',
rollupOptions: {
output: {
assetFileNames: 'assets/[name].[ext]',
chunkFileNames: 'assets/[name].js',
entryFileNames: 'assets/[name].js',
},
},
},
plugins: [workerPluginTestPlugin()],
cacheDir: 'node_modules/.vite-es',
})
35 changes: 35 additions & 0 deletions playground/worker/vite.config-serial.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { defineConfig } from 'vite'
import workerPluginTestPlugin from './worker-plugin-test-plugin'
import workerPluginStatePlugin from './worker-plugin-state-plugin'

export default defineConfig({
base: '/serial/',
resolve: {
alias: {
'@': __dirname,
},
},
worker: {
format: 'iife',
plugins: [workerPluginTestPlugin(), workerPluginStatePlugin()],
rollupOptions: {
output: {
assetFileNames: 'assets/worker_asset-[name].[ext]',
chunkFileNames: 'assets/worker_chunk-[name].js',
entryFileNames: 'assets/worker_entry-[name].js',
},
},
},
build: {
outDir: 'dist/serial',
rollupOptions: {
output: {
assetFileNames: 'assets/[name].[ext]',
chunkFileNames: 'assets/[name].js',
entryFileNames: 'assets/[name].js',
},
},
},
plugins: [workerPluginTestPlugin()],
cacheDir: 'node_modules/.vite-es',
})