Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/strip-executable-comments.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'agent-bundle': patch
---

Strip comments from generated executables. `agent-bundle build` now runs the SWC minimizer with compression, mangling, and whitespace minification off, so bundled dependency documentation no longer ships while code stays readable and `/*! … */` license headers stay inline. Set `tools.rsbuild.output.minify` to `false` to keep every comment. (#861)
5 changes: 2 additions & 3 deletions docs/framework-mode.md
Original file line number Diff line number Diff line change
Expand Up @@ -561,9 +561,8 @@ which never exists on disk: the virtual paths are predictable, so the build
refuses to compile while anything occupies that directory
(`assertGeneratedModulesRootAbsent`). That namespace hangs off the project root, the
bundler `context`, on purpose: Rspack writes module identifiers relative to
`context` into emitted bundles (the `// NAMESPACE OBJECT: ./…` comments of
concatenated modules), so a namespace under the staging root would stamp the
per-build token into the artifact.
`context` into emitted bundles (the keys of the module map), so a namespace
under the staging root would stamp the per-build token into the artifact.

`agent-bundle build` writes one composite plugin root that every selected host
installs from as-is. The root carries one `INSTALL.md` with a section per
Expand Down
12 changes: 9 additions & 3 deletions packages/agent-bundle/src/build/rslib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ export const generatedExecutableSyntax = ((): 'es2022' => {
}
return 'es2022';
})();
export const generatedExecutableLegalComments = 'inline' as const;

export interface RslibVirtualModule {
readonly name: string;
Expand Down Expand Up @@ -664,8 +663,15 @@ export const composeEntryLibConfig = (
distPath: { root: options.outputRoot },
filename: { js: entry.outputRelativePath },
filenameHash: false,
legalComments: generatedExecutableLegalComments,
minify: false,
legalComments: 'inline',
// SWC runs only to drop comments, which in bundled dependencies
// outweigh the code. `minify: false` keeps its output formatted, and
// `legalComments: 'inline'` keeps license headers.
minify: {
css: false,
js: true,
jsOptions: { minimizerOptions: { compress: false, mangle: false, minify: false } },
},
sourceMap: options.sourceMap === true ? { js: 'inline-source-map' } : false,
target: 'node',
},
Expand Down
7 changes: 6 additions & 1 deletion packages/agent-bundle/tests/build.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1597,6 +1597,7 @@ it('parses emitted bundles in full when a tools hatch could have rewritten them'
// Rspack parsed the source and can rewrite the emitted asset — here a raw
// banner that leaves the lexer satisfied but Node unable to start the module
// — so the record says `coverage.rewritable` and the walk keeps the full parse.
// The banner lands after minification, which would otherwise reject it first.
const project = await createProject();
try {
await expect(build({
Expand All @@ -1606,7 +1607,11 @@ it('parses emitted bundles in full when a tools hatch could have rewritten them'
registry: new TargetRegistry().register((await import('../src/adapters/portable.ts')).portableAdapter, { default: true }),
tools: {
rspack: (config, { rspack }) => {
config.plugins = [...(config.plugins ?? []), new rspack.BannerPlugin({ banner: 'export const broken = ;', raw: true })];
config.plugins = [...(config.plugins ?? []), new rspack.BannerPlugin({
banner: 'export const broken = ;',
raw: true,
stage: rspack.Compilation.PROCESS_ASSETS_STAGE_REPORT,
})];
},
},
})).rejects.toThrow(
Expand Down
31 changes: 19 additions & 12 deletions packages/agent-bundle/tests/self-contained-bundler-config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ import { createRsbuild } from '@rsbuild/core';
import { createRslib } from '@rslib/core';
import { expect, it } from '@rstest/core';
import { init, parse } from 'es-module-lexer/minimal';
import { execFile } from 'node:child_process';
import { mkdir, mkdtemp, readFile, readdir, symlink, writeFile } from 'node:fs/promises';
import { isBuiltin } from 'node:module';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { promisify } from 'node:util';

import { composeMcpAppsRsbuildConfig } from '../src/build/mcp-apps.ts';
import { buildWithRslib } from '../src/build/compiler.ts';
Expand All @@ -14,7 +16,6 @@ import {
compilerHostNodeFloor,
composeEntryLibConfig,
entryLibId,
generatedExecutableLegalComments,
generatedExecutableSyntax,
type RslibEntry,
} from '../src/build/rslib.ts';
Expand Down Expand Up @@ -55,10 +56,17 @@ const selfContainedProject = async (): Promise<{ readonly entry: RslibEntry; rea
await mkdir(join(root, 'views'), { recursive: true });
await writeFile(join(root, 'package.json'), '{"type":"module"}\n');
await symlink(agentBundleNodeModules, join(root, 'node_modules'), 'dir');
await writeFile(join(root, 'src', 'licensed.js'), [
'/*! licensed-probe v1.0.0 | MIT License */',
'/** Probe documentation that must not reach the artifact. */',
"export const licensed = () => 'licensed-probe';",
'',
].join('\n'));
await writeFile(join(root, 'src', 'entry.ts'), [
"import { basename } from 'node:path';",
"import { parse } from 'yaml';",
"console.log(basename('/probe/config.yaml'), parse('probe: true'));",
"import { licensed } from './licensed.js';",
"console.log(JSON.stringify([basename('/probe/config.yaml'), parse('probe: true'), licensed()]));",
'',
].join('\n'));
await writeFile(join(root, 'views', 'dashboard.ts'), "document.title = 'dashboard';\n");
Expand Down Expand Up @@ -117,15 +125,6 @@ it('derives generated-executable syntax from the compiler host floor matching en
expect(lib.syntax).toBe(generatedExecutableSyntax);
});

it('reserves inline legal comments for future minification without adding a license asset today', () => {
expect(generatedExecutableLegalComments).toBe('inline');
const root = join(tmpdir(), 'agent-bundle-legal-comments-profile');
const lib = composeEntryLibConfig(probeEntry(root), { cwd: root, meta: testMeta, outputRoot: join(root, 'dist') });
expect(lib.output?.legalComments).toBe(generatedExecutableLegalComments);
expect(lib.output?.minify).toBe(false);
expect(lib.output?.sourceMap).toBe(false);
});

it('opts generated-executable source maps in through output.sourceMap', () => {
const root = join(tmpdir(), 'agent-bundle-source-map-profile');
const entry = probeEntry(root);
Expand All @@ -140,7 +139,8 @@ it('opts generated-executable source maps in through output.sourceMap', () => {
});

it('lowers a generated executable with only Node builtins external and inlines its dependencies', async () => {
const { entry, root } = await selfContainedProject();
const { entry: probe, root } = await selfContainedProject();
const entry: RslibEntry = { ...probe, banner: '#!/usr/bin/env node' };
const inspections: RslibInspection[] = [];
try {
await buildWithRslib({ cwd: root, entries: [entry], meta: testMeta, outputRoot: join(root, 'dist') }, {
Expand Down Expand Up @@ -189,6 +189,13 @@ it('lowers a generated executable with only Node builtins external and inlines i
expect(specifiers.filter((specifier) => specifier === undefined || !isBuiltin(specifier))).toEqual([]);
expect(bundle).toContain('YAMLParseError');
expect(bundle).toMatch(/createRequire\(import\.meta\.url\)/u);
expect(bundle.startsWith('#!/usr/bin/env node\n')).toBe(true);
expect(bundle).toContain('\nvar __webpack_modules__ = {\n');
expect(bundle).toContain('/*! licensed-probe v1.0.0 | MIT License */');
expect(bundle).not.toContain('Probe documentation that must not reach the artifact.');
expect(bundle).not.toContain('Parse the input as a stream of YAML documents.');
const { stdout } = await promisify(execFile)(process.execPath, [join(root, 'dist', 'scripts', 'probe.mjs')]);
expect(JSON.parse(stdout)).toEqual(['config.yaml', { probe: true }, 'licensed-probe']);
await expect(readdir(join(root, 'dist'))).resolves.toEqual(['scripts']);
await expect(readdir(join(root, 'dist', 'scripts'))).resolves.toEqual(['probe.mjs']);
} finally {
Expand Down
5 changes: 5 additions & 0 deletions website/docs/en/reference/configuration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,11 @@ map so the map stays inside the executable, sibling `.map` files are not part of
artifact file set. A non-boolean value is `AB4707`. MCP App views keep their own default (off)
and still opt in through `tools.rsbuild.output.sourceMap`.

Generated executables keep readable code. The compiler profile runs the SWC minimizer only to
remove comments, with compression, mangling, and whitespace minification off, so bundled
dependency documentation does not ship. Legal comments, such as `/*! … */` license headers, stay
inline. Set `tools.rsbuild.output.minify` to `false` to keep every comment.

`runtime.node` is a minimum Node.js version in `major.minor[.patch]` form. It can only raise the
default floor for generated executables, never lower it, and the selected floor is recorded as
`runtime.node` in the artifact manifest. The floor itself is described in
Expand Down
4 changes: 4 additions & 0 deletions website/docs/zh/reference/configuration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,10 @@ profile 会把打包器的 `output.sourceMap` 设为内联 JS map,让 map 留
文件不属于计划中的产物文件集合。非布尔值是 `AB4707`。MCP App 视图仍保持自身的默认值(关闭),并继续
通过 `tools.rsbuild.output.sourceMap` 选择加入。

生成式可执行文件保持代码可读。编译器 profile 只用 SWC 压缩器移除注释,压缩、混淆和空白压缩均关闭,
因此被打包依赖的文档注释不会随产物发布。法律注释(例如 `/*! … */` 许可证头)以内联方式保留。将
`tools.rsbuild.output.minify` 设为 `false` 可保留全部注释。

`runtime.node` 是 `major.minor[.patch]` 形式的最低 Node.js 版本。它只能抬高生成式可执行文件的默认下限,
绝不能降低,且所选下限会作为 `runtime.node` 记录在产物清单中。这个下限本身在
[配置模型](../guide/authoring/index.mdx)中介绍。
Expand Down
Loading