Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions turbopack/crates/turbopack-node/js/src/transforms/dispatch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/**
* Unified entry for Node.js transform workers.
*
* Both PostCSS and webpack-loader transforms are evaluated in the same worker
* pool. Using a single config-independent entry module means a single
* `get_evaluate_pool` cache key on the Rust side, so all JS transforms share one
* pool (and one set of subprocesses/threads) instead of spawning a separate pool
* per PostCSS config.
*
* Each request carries a leading `kind` argument selecting the transform; the
* remaining arguments are forwarded unchanged to that transform's `default`
* export. There is intentionally no `init`: the webpack-loader transform never
* had one, and the PostCSS transform now initializes its config sessions lazily
* per request (keyed by config file), so nothing needs to run once per worker.
*/

import type { Channel as Ipc } from '../types'
import postcssTransform from './postcss'
import webpackTransform from './webpack-loaders'

export default function dispatch(
ipc: Ipc<any, any>,
kind: 'postcss' | 'webpack',
...args: any[]
) {
switch (kind) {
case 'postcss':
return (postcssTransform as any)(ipc, ...args)
case 'webpack':
return (webpackTransform as any)(ipc, ...args)
default:
throw new Error(`Unknown transform kind: ${kind}`)
}
}
81 changes: 69 additions & 12 deletions turbopack/crates/turbopack-node/js/src/transforms/postcss.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,53 @@
declare const __turbopack_external_require__: (
id: string,
thunk: () => any,
esm?: boolean
) => any
// Async external import (maps to the runtime's `import(id)` helper). Used to
// load the emitted config bundle from disk by file:// URL.
declare const __turbopack_external_import__: (id: string) => Promise<any>

import type { Processor } from 'postcss'
import { pathToFileURL } from 'node:url'

// @ts-ignore
import postcss from '@vercel/turbopack/postcss'
// @ts-ignore
import importedConfig from 'CONFIG'
import { getReadEnvVariables, toPath, type TransformIpc } from './transforms'

let processor: Processor | undefined
/**
* Cache of initialized PostCSS processors ("config sessions"), keyed by the
* original (project-relative) config file path.
*
* Each entry also records the `bundledPath` it was built from. The bundled path
* is content-addressed on the Rust side, so when a config file changes its
* bundled path changes too: a request for the same original path but a different
* bundled path replaces the session in place (the stale processor is dropped and
* GC'd). This keeps the map bounded by the set of distinct config files — an app
* that keeps adding *new* configs will grow it, which is acceptable — while
* editing an existing config never grows it.
*
* Workers are pulled from an idle queue without affinity to a config, so any
* worker must be able to lazily load any config it is handed; that is exactly
* what this map does on first sight of a key.
*/
const sessions = new Map<
string,
{ bundledPath: string; processor: Promise<Processor> }
>()

export const init = async (ipc: TransformIpc) => {
let config = importedConfig
async function loadConfig(bundledPath: string): Promise<Processor> {
// The config has been emitted to disk as a standalone bundle by Turbopack.
// `bundledPath` is an absolute disk path (the bundle lives under the output
// root, which may be a different filesystem than the project). Absolute paths
// don't work with ESM imports on Windows
// (https://github.com/nodejs/node/issues/31710), so convert to a file:// URL.
const configUrl = pathToFileURL(bundledPath).toString()
const mod = await __turbopack_external_import__(configUrl)
// Every config kind is emitted by `config_loader_source` as a wrapper module
// exporting an async `loadPostcssConfig()` that returns the resolved config
// (unwrapping `default` on the Rust side). The named export may live on `mod`
// or, depending on CJS↔ESM interop, on `mod.default`.
const loadPostcssConfig =
mod.loadPostcssConfig ?? mod.default?.loadPostcssConfig
if (typeof loadPostcssConfig !== 'function') {
throw new Error('PostCSS config bundle did not export loadPostcssConfig')
}
let config = await loadPostcssConfig()
if (typeof config === 'function') {
config = await config({ env: 'development' })
}
Expand Down Expand Up @@ -58,16 +90,41 @@ export const init = async (ipc: TransformIpc) => {
return plugin
})

processor = postcss(loadedPlugins)
return postcss(loadedPlugins)
}

function getProcessor(
originalConfigPath: string,
bundledConfigPath: string
): Promise<Processor> {
const cached = sessions.get(originalConfigPath)
if (cached && cached.bundledPath === bundledConfigPath) {
return cached.processor
}
const processor = loadConfig(bundledConfigPath)
sessions.set(originalConfigPath, {
bundledPath: bundledConfigPath,
processor,
})
// Evict on failure so a later request can retry instead of caching a rejection.
processor.catch(() => {
if (sessions.get(originalConfigPath)?.processor === processor) {
sessions.delete(originalConfigPath)
}
})
return processor
}

export default async function transform(
ipc: TransformIpc,
cssContent: string,
name: string,
originalConfigPath: string,
bundledConfigPath: string,
sourceMap: boolean
) {
const { css, map, messages } = await processor!.process(cssContent, {
const processor = await getProcessor(originalConfigPath, bundledConfigPath)
const { css, map, messages } = await processor.process(cssContent, {
from: name,
to: name,
map: sourceMap
Expand Down
Loading
Loading