Skip to content
Open
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
62 changes: 47 additions & 15 deletions src/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,43 @@ export async function build(
return buildWithConfigs(configs, configDeps, () => build(inlineConfig))
}

async function mapWithConcurrency<T, R>(
items: T[],
limit: number,
mapper: (item: T, index: number) => Promise<R>,
): Promise<R[]> {
if (items.length === 0) return []
const results: R[] = Array.from({ length: items.length })
let nextIndex = 0
const worker = async () => {
while (nextIndex < items.length) {
const index = nextIndex++
results[index] = await mapper(items[index], index)
}
}
await Promise.all(
Array.from({ length: Math.min(limit, items.length) }, () => worker()),
)
return results
}

function resolveMaxParallel(configs: ResolvedConfig[]): number | undefined {
const configured = configs
.map((config) => config.maxParallel)
.filter((value): value is number => value != null)

if (configured.length === 0) return undefined

const resolved = Math.min(...configured)
if (new Set(configured).size > 1) {
globalLogger.warn(
`Conflicting \`maxParallel\` values detected (${configured.join(', ')}). Using the smallest value: ${resolved}.`,
)
}

return resolved
}

/**
* Build with `ResolvedConfigs`.
*
Expand Down Expand Up @@ -84,21 +121,16 @@ export async function buildWithConfigs(
}

globalLogger.info('Build start')
const bundles = await Promise.all(
configs.map((options) => {
const isDualFormat = options.pkg
? configChunksByPkg[options.pkg.packageJsonPath].formats.size > 1
: true
return buildSingle(
options,
configDeps,
isDualFormat,
clean,
restart,
done,
)
}),
)
const maxParallel = resolveMaxParallel(configs)
const buildConfig = (options: ResolvedConfig) => {
const isDualFormat = options.pkg
? configChunksByPkg[options.pkg.packageJsonPath].formats.size > 1
: true
return buildSingle(options, configDeps, isDualFormat, clean, restart, done)
}
const bundles = maxParallel
? await mapWithConcurrency(configs, maxParallel, buildConfig)
: await Promise.all(configs.map(buildConfig))

const firstDevtoolsConfig = configs.find(
(config) => config.devtools && config.devtools.ui,
Expand Down
4 changes: 4 additions & 0 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,10 @@ cli
.option('--root <dir>', 'Root directory of input files')
.option('--exe', 'Bundle as executable')
.option('-W, --workspace [dir]', 'Enable workspace mode')
.option(
'--max-parallel <number>',
'Maximum number of config builds to run in parallel',
)
.option(
'-F, --filter <pattern>',
'Filter configs (cwd or name), e.g. /pkg-name$/ or pkg-name',
Expand Down
12 changes: 12 additions & 0 deletions src/config/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,12 @@ export async function resolveUserConfig(
devtools = false,
write = true,
exe = false,
maxParallel: maxParallelRaw,
} = userConfig

const maxParallel =
maxParallelRaw == null ? undefined : Number(maxParallelRaw)

const pkg = await readPackageJson(cwd)
if (workspace) {
name ||= pkg?.name
Expand All @@ -130,6 +134,13 @@ export async function resolveUserConfig(
logger.warn('`bundle` option is deprecated. Use `unbundle` instead.')
}

if (
maxParallel != null &&
(!Number.isInteger(maxParallel) || maxParallel < 1)
) {
throw new TypeError('`maxParallel` must be a positive integer.')
}

if (removeNodeProtocol) {
if (nodeProtocol)
throw new TypeError(
Expand Down Expand Up @@ -322,6 +333,7 @@ export async function resolveUserConfig(
hash,
ignoreWatch,
logger,
maxParallel,
name,
nameLabel,
nodeProtocol,
Expand Down
14 changes: 14 additions & 0 deletions src/config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -685,6 +685,19 @@ export interface UserConfig {
* This allows you to build multiple packages in a monorepo.
*/
workspace?: Workspace | Arrayable<string> | true

/**
* Limit how many config builds can run at the same time.
*
* This is especially useful in workspace mode where each package can start
* expensive child processes (e.g. `tsgo` via `dts.tsgo: true`). Without a
* limit, all configs start building concurrently, which can spawn many
* subprocesses and exhaust system resources.
*
* When set to a positive integer, at most that many builds run in parallel.
* When omitted or `undefined`, all builds run concurrently (existing behavior).
*/
maxParallel?: number
}

export interface InlineConfig extends UserConfig {
Expand Down Expand Up @@ -748,6 +761,7 @@ export type ResolvedConfig = Overwrite<
| 'footer'
| 'checks'
| 'css'
| 'maxParallel'
>,
{
/**
Expand Down
50 changes: 50 additions & 0 deletions tests/issues.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -322,4 +322,54 @@ export const loadDynamic = () => import('./dynamic')`,
expect(fileMap['index.mjs']).toContain('polyfill-uuid')
expect(fileMap['index.mjs']).not.toMatch(/from ['"]crypto['"]/)
})

test('#929', async (context) => {
let inFlight = 0
let peak = 0

await testBuild({
context,
snapshot: false,
files: {
'package.json': JSON.stringify({
name: 'issue-929',
private: true,
}),
'packages/a/package.json': JSON.stringify({
name: 'a',
version: '1.0.0',
}),
'packages/a/index.ts': 'export const a = 1',
'packages/b/package.json': JSON.stringify({
name: 'b',
version: '1.0.0',
}),
'packages/b/index.ts': 'export const b = 1',
'packages/c/package.json': JSON.stringify({
name: 'c',
version: '1.0.0',
}),
'packages/c/index.ts': 'export const c = 1',
'packages/d/package.json': JSON.stringify({
name: 'd',
version: '1.0.0',
}),
'packages/d/index.ts': 'export const d = 1',
},
options: {
workspace: 'packages/*',
maxParallel: 2,
hooks: {
'build:prepare': async () => {
inFlight++
peak = Math.max(peak, inFlight)
await new Promise((resolve) => setTimeout(resolve, 50))
inFlight--
},
},
},
})

expect(peak).toBeLessThanOrEqual(2)
})
})
Loading