Skip to content

Commit e3473e6

Browse files
perf(build): one Rslib instance per target for every agent-host surface (xref row 12) (#503)
* perf(build): one Rslib instance per target for every agent-host surface Plan each target's outputs as at most two stages (src/build/target-stages.ts): the optional browser MCP Apps stage first, only for targets with App routes, then every agent-host surface — routed CLI bin, bundled scripts, hook wrappers, MCP entries and their react-server Flight workers — lowered together through one Rslib instance (one Rsbuild environment per output, one Rspack multi-compiler) instead of one sequential instance per surface. Surfaces are plan/finish pairs that keep their own authored-source evidence exclusions and results. Bundled-output evidence now asks Rspack stats only for what it reads (assets, chunk ids, the complete module list with nameForCondition/identifier/moduleType and concatenated modules) and switches off per-module reasons, export usage, optimization bailouts, depth, module traces and errors, which dominated the post-compile cost once every surface reports through one stats object. Artifact and package-build trees of examples/audiobook-curator and examples/host-test are byte-identical to the baseline (modulo the pre-existing staged-directory token comment); manifest sourceInputs match. * fix(build): derive Rslib lib ids from artifact destinations, not entry names Surfaces sharing one run may legitimately reuse an entry name (a script authored as hooks-flight emits scripts/hooks-flight.mjs beside the hook surface's standalone worker hooks/hooks-flight.mjs); keying the lib id on the destination the planner already asserts unique keeps them apart. Ids are visible only in inspect --bundler and stats; emitted bytes are unchanged (artifact trees re-diffed against main ec65738).
1 parent 4c911b0 commit e3473e6

13 files changed

Lines changed: 730 additions & 224 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'agent-bundle': patch
3+
---
4+
5+
Compile every agent-host surface of a target — the routed CLI bin, bundled scripts, hook wrappers, MCP stdio entries, and their react-server Flight workers — through one Rslib instance per target instead of one instance per surface, with the optional browser MCP Apps stage ordered first only for targets that declare App routes; `agent-bundle build`, `dev`, and `prepack` emit byte-identical artifacts with the same manifest source inputs while spending less time in bundler setup and stats collection (#503)

‎docs/framework-mode.md‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -485,6 +485,32 @@ Hook tool selectors a host cannot map still fail at plan time
485485

486486
## Distribution
487487

488+
### How a target compiles
489+
490+
`agent-bundle build` plans every target before it compiles anything, then
491+
lowers each target's outputs in at most two stages into one staged root that
492+
is published atomically once the artifact validates
493+
(`src/build/target-stages.ts`):
494+
495+
1. **MCP Apps** — the browser environment, compiled through the workspace
496+
`@rsbuild/core`. This stage exists only for a target whose project
497+
declares App routes and always runs first: the MCP entries embed its
498+
emitted HTML.
499+
2. **Agent-host surfaces** — the routed CLI bin, bundled scripts, hook
500+
wrappers, MCP stdio entries, and each surface's react-server Flight worker.
501+
All of them lower together through **one Rslib instance per target**: one
502+
Rsbuild environment per output, compiled by one Rspack multi-compiler.
503+
A host surface reaches its Flight worker by file name at run time, never
504+
through a build-time manifest, so nothing orders the two within the stage;
505+
each surface keeps its own authored-source evidence for the manifest.
506+
507+
Every synthesized bundler config — both stages plus the `dist/` package build
508+
— composes the same way: the framework profile, then the consumer's
509+
`tools.rsbuild` fragment, then the `tools.rspack` hatch, then the framework
510+
invariant layer that no hatch value can override
511+
(`src/build/compose-layers.ts`; see the `tools` section of the configuration
512+
reference). `agent-bundle inspect --bundler` prints the result.
513+
488514
`agent-bundle build` makes each target directory independently distributable.
489515
Every target includes `INSTALL.md` generated with its real plugin and
490516
marketplace names. Claude and Codex bundles include local marketplace manifests

‎packages/agent-bundle/src/build/build.ts‎

Lines changed: 82 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -11,25 +11,27 @@ import { pathTokens, type AgentBundleToolsConfig, type NormalizedPlugin } from '
1111
import { assertInside, isInsideOrEqual } from '../core/paths.ts';
1212
import { agentSkillsSchemaRevision } from '../schemas/agent-skills/contract.ts';
1313
import {
14-
compileEntries,
15-
compileHooks,
16-
compileMcpEntries,
1714
planCompiledEntries,
1815
planCompiledHooks,
1916
planCompiledMcpEntries,
17+
planHooksSurface,
18+
planMcpEntriesSurface,
19+
planScriptsSurface,
2020
type CompiledEntry,
2121
type CompiledHookEntry,
2222
type CompiledMcpEntry,
2323
} from './entries.ts';
2424
import {
2525
cliBinCollisionDiagnostics,
26-
compileCliBins,
26+
planCliBinsSurface,
2727
planCompiledCliBins,
2828
targetHostsCliBin,
2929
type CompiledCliBin,
3030
} from './cli-bins.ts';
3131
import { projectMeta } from './meta.ts';
3232
import { compileMcpApps, planCompiledMcpApps, type CompiledMcpApp } from './mcp-apps.ts';
33+
import { compileRslibSurfaces, settledRslibSurface } from './rslib.ts';
34+
import { planTargetStages } from './target-stages.ts';
3335
import {
3436
assertUniqueArtifactDestinations,
3537
artifactHookIndexName,
@@ -388,73 +390,84 @@ export const build = async (options: BuildOptions): Promise<BuildResult> => {
388390
// One identity feeds every compiled surface, exactly the identity the
389391
// manifest, `inspect`, and dev status report (issue #237).
390392
const meta = projectMeta(options.model.metadata);
393+
const plugin = { name: options.model.metadata.name, version: options.model.metadata.version };
391394
for (const target of stagedTargets) {
392-
// MCP Apps compile first: their Rsbuild pass asserts the target root
393-
// holds nothing but its own HTML, so every other surface follows it.
394-
const targetMcpApps = await compileMcpApps(options.model.mcpApps ?? [], {
395-
cwd: options.projectRoot,
396-
meta,
397-
outDir: target.root,
398-
target: target.name,
399-
...tools,
400-
});
401-
compiledMcpApps.push(...targetMcpApps);
402-
await emitPlanEntries({ entries: target.entries, root: target.root });
403-
if (target.cliBin) {
404-
compiledCliBins.push(...(await compileCliBins(options.model, {
405-
cwd: options.projectRoot,
406-
meta,
407-
outDir: target.root,
408-
target: target.name,
409-
...tools,
410-
})));
395+
let targetMcpApps: readonly CompiledMcpApp[] = Object.freeze([]);
396+
for (const stage of planTargetStages(target)) {
397+
switch (stage.kind) {
398+
case 'mcp-apps':
399+
// The optional browser stage, always first: the MCP entries
400+
// embed its HTML, and its Rsbuild pass asserts the target root
401+
// holds nothing but that HTML.
402+
targetMcpApps = await compileMcpApps(options.model.mcpApps ?? [], {
403+
cwd: options.projectRoot,
404+
meta,
405+
outDir: target.root,
406+
target: target.name,
407+
...tools,
408+
});
409+
compiledMcpApps.push(...targetMcpApps);
410+
break;
411+
case 'node-surfaces': {
412+
await emitPlanEntries({ entries: target.entries, root: target.root });
413+
const noticeDelivery = options.registry.noticeDelivery(target.name);
414+
// Every agent-host surface of the target lowers through one Rslib
415+
// instance; each surface keeps its own evidence and result.
416+
const [cliBins, scripts, hooks, mcpEntries] = await compileRslibSurfaces(
417+
{ cwd: options.projectRoot, meta, outputRoot: target.root, ...tools },
418+
[
419+
target.cliBin
420+
? planCliBinsSurface(options.model, { outDir: target.root, target: target.name })
421+
: settledRslibSurface<readonly CompiledCliBin[]>(Object.freeze([])),
422+
await planScriptsSurface(
423+
options.model.scripts.filter((script) => script.targets.includes(target.name)),
424+
{
425+
cwd: options.projectRoot,
426+
layouts: options.model.layouts ?? [],
427+
outDir: target.root,
428+
...noticePolicy,
429+
providers: options.model.providers ?? [],
430+
...(options.model.state === undefined ? {} : { state: options.model.state }),
431+
},
432+
),
433+
planHooksSurface(target.hookEntries, {
434+
artifactEpoch: options.projectContext.revision,
435+
...(noticeDelivery === undefined ? {} : { noticeDelivery }),
436+
...noticePolicy,
437+
outDir: target.root,
438+
plugin,
439+
providers: options.model.providers ?? [],
440+
...(options.model.state === undefined ? {} : { state: options.model.state }),
441+
}),
442+
await planMcpEntriesSurface(options.model.mcpServers, {
443+
apps: targetMcpApps,
444+
artifactEpoch: options.projectContext.revision,
445+
eventHooks: target.hookEntries
446+
.filter((entry) => entry.hook.eventRoute !== undefined)
447+
.map((entry) => entry.hook),
448+
layouts: options.model.layouts ?? [],
449+
...(noticeDelivery === undefined ? {} : { noticeDelivery }),
450+
...noticePolicy,
451+
outDir: target.root,
452+
plugin,
453+
providers: options.model.providers ?? [],
454+
...(options.model.state === undefined ? {} : { state: options.model.state }),
455+
target: target.name,
456+
}),
457+
],
458+
);
459+
compiledCliBins.push(...cliBins);
460+
compiledEntries.push(...scripts);
461+
compiledHooks.push(...hooks);
462+
compiledMcpEntries.push(...mcpEntries);
463+
break;
464+
}
465+
default: {
466+
const exhaustive: never = stage;
467+
throw new Error(`Unknown target compile stage ${JSON.stringify(exhaustive)}.`);
468+
}
469+
}
411470
}
412-
compiledEntries.push(
413-
...(await compileEntries(
414-
options.model.scripts.filter((script) => script.targets.includes(target.name)),
415-
{
416-
cwd: options.projectRoot,
417-
layouts: options.model.layouts ?? [],
418-
meta,
419-
outDir: target.root,
420-
...noticePolicy,
421-
providers: options.model.providers ?? [],
422-
...(options.model.state === undefined ? {} : { state: options.model.state }),
423-
...tools,
424-
},
425-
)),
426-
);
427-
const noticeDelivery = options.registry.noticeDelivery(target.name);
428-
compiledHooks.push(...(await compileHooks(target.hookEntries, {
429-
artifactEpoch: options.projectContext.revision,
430-
cwd: options.projectRoot,
431-
meta,
432-
...(noticeDelivery === undefined ? {} : { noticeDelivery }),
433-
...noticePolicy,
434-
outDir: target.root,
435-
plugin: { name: options.model.metadata.name, version: options.model.metadata.version },
436-
providers: options.model.providers ?? [],
437-
...(options.model.state === undefined ? {} : { state: options.model.state }),
438-
...tools,
439-
})));
440-
compiledMcpEntries.push(...(await compileMcpEntries(options.model.mcpServers, {
441-
apps: targetMcpApps,
442-
artifactEpoch: options.projectContext.revision,
443-
cwd: options.projectRoot,
444-
eventHooks: target.hookEntries
445-
.filter((entry) => entry.hook.eventRoute !== undefined)
446-
.map((entry) => entry.hook),
447-
layouts: options.model.layouts ?? [],
448-
meta,
449-
...(noticeDelivery === undefined ? {} : { noticeDelivery }),
450-
...noticePolicy,
451-
outDir: target.root,
452-
plugin: { name: options.model.metadata.name, version: options.model.metadata.version },
453-
providers: options.model.providers ?? [],
454-
...(options.model.state === undefined ? {} : { state: options.model.state }),
455-
target: target.name,
456-
...tools,
457-
})));
458471
}
459472
const publishedCompiledEntries = deepFreeze(compiledEntries.map((entry) =>
460473
({

‎packages/agent-bundle/src/build/cli-bins.ts‎

Lines changed: 35 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,7 @@ import { cliBinCapability } from '../adapters/capability-state.ts';
44
import type { TargetRegistry } from '../adapters/registry.ts';
55
import { routedCliBinLayout, type TargetArtifactEntry } from '../adapters/types.ts';
66
import type { Diagnostic } from '../core/diagnostics.ts';
7-
import type { AgentBundleToolsConfig, NormalizedBinEntry, NormalizedPlugin } from '../core/types.ts';
8-
import type { AgentBundleMeta } from '../meta.ts';
7+
import type { NormalizedBinEntry, NormalizedPlugin } from '../core/types.ts';
98
import { resolveArtifactDestination } from './emit.ts';
109
import { runtimeIgnoredRoot, type CompiledEntry } from './entries.ts';
1110
import {
@@ -14,7 +13,7 @@ import {
1413
generatedCliBinEntrySource,
1514
generatedRenderedRouteWorkerSource,
1615
} from './entry-shell.ts';
17-
import { buildWithRslib, type RslibEntry } from './rslib.ts';
16+
import type { RslibEntry, RslibSurfacePlan } from './rslib.ts';
1817

1918
/**
2019
* The artifact-hosted routed CLI (#387). A generated-mode `src/cli/**`
@@ -159,48 +158,44 @@ export const cliBinRslibEntries = (
159158
return entries;
160159
});
161160

162-
export const compileCliBins = async (
161+
/**
162+
* Plans the routed CLI bin of one hosting target (#387) as a surface of the
163+
* target's shared Rslib run: the executable plus, for rendered commands, its
164+
* react-server Flight worker.
165+
*/
166+
export const planCliBinsSurface = (
163167
model: NormalizedPlugin,
164-
options: {
165-
readonly cwd: string;
166-
readonly meta: AgentBundleMeta;
167-
readonly outDir: string;
168-
readonly target: string;
169-
readonly tools?: AgentBundleToolsConfig;
170-
},
171-
): Promise<readonly CompiledCliBin[]> => {
168+
options: { readonly outDir: string; readonly target: string },
169+
): RslibSurfacePlan<readonly CompiledCliBin[]> => {
172170
const planned = planCompiledCliBins(model, options);
173-
if (planned.length === 0) return Object.freeze([]);
174-
const evidence = await buildWithRslib({
175-
cwd: options.cwd,
176-
entries: cliBinRslibEntries(planned, model),
171+
return {
172+
entries: planned.length === 0 ? [] : cliBinRslibEntries(planned, model),
173+
finish: async (evidence) => {
174+
const evidenceByPath = new Map(evidence.map((entry) => [entry.path, entry.sourceInputs]));
175+
const bundledInputs = (path: string, label: string): readonly string[] => {
176+
const inputs = evidenceByPath.get(path);
177+
if (inputs === undefined) throw new Error(`Missing bundled routed CLI ${label} evidence for ${JSON.stringify(path)}.`);
178+
return inputs;
179+
};
180+
return Object.freeze(planned.map((entry): CompiledCliBin => Object.freeze({
181+
id: entry.id,
182+
name: entry.name,
183+
output: entry.output,
184+
outputKind: entry.outputKind,
185+
source: entry.source,
186+
sourceInputs: bundledInputs(cliBinArtifactPath(entry.name), 'executable'),
187+
target: entry.target,
188+
...(entry.workerOutput === undefined
189+
? {}
190+
: {
191+
workerOutput: entry.workerOutput,
192+
workerSourceInputs: bundledInputs(cliBinWorkerArtifactPath(entry.name), 'worker'),
193+
}),
194+
})));
195+
},
177196
ignoredSourcePaths: [runtimeIgnoredRoot(cliEntryRuntimePath())],
178197
logLevel: 'error',
179-
meta: options.meta,
180-
outputRoot: options.outDir,
181-
...(options.tools === undefined ? {} : { tools: options.tools }),
182-
});
183-
const evidenceByPath = new Map(evidence.map((entry) => [entry.path, entry.sourceInputs]));
184-
const bundledInputs = (path: string, label: string): readonly string[] => {
185-
const inputs = evidenceByPath.get(path);
186-
if (inputs === undefined) throw new Error(`Missing bundled routed CLI ${label} evidence for ${JSON.stringify(path)}.`);
187-
return inputs;
188198
};
189-
return Object.freeze(planned.map((entry): CompiledCliBin => Object.freeze({
190-
id: entry.id,
191-
name: entry.name,
192-
output: entry.output,
193-
outputKind: entry.outputKind,
194-
source: entry.source,
195-
sourceInputs: bundledInputs(cliBinArtifactPath(entry.name), 'executable'),
196-
target: entry.target,
197-
...(entry.workerOutput === undefined
198-
? {}
199-
: {
200-
workerOutput: entry.workerOutput,
201-
workerSourceInputs: bundledInputs(cliBinWorkerArtifactPath(entry.name), 'worker'),
202-
}),
203-
})));
204199
};
205200

206201
/**

0 commit comments

Comments
 (0)