Skip to content

Commit b4e3840

Browse files
fix(build): strip comments from generated executables (#861)
* fix(build): strip comments from generated executables * chore(changeset): link #861 * test(build): inject the broken banner after minification
1 parent b990c7e commit b4e3840

7 files changed

Lines changed: 50 additions & 19 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+
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)

‎docs/framework-mode.md‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -561,9 +561,8 @@ which never exists on disk: the virtual paths are predictable, so the build
561561
refuses to compile while anything occupies that directory
562562
(`assertGeneratedModulesRootAbsent`). That namespace hangs off the project root, the
563563
bundler `context`, on purpose: Rspack writes module identifiers relative to
564-
`context` into emitted bundles (the `// NAMESPACE OBJECT: ./…` comments of
565-
concatenated modules), so a namespace under the staging root would stamp the
566-
per-build token into the artifact.
564+
`context` into emitted bundles (the keys of the module map), so a namespace
565+
under the staging root would stamp the per-build token into the artifact.
567566

568567
`agent-bundle build` writes one composite plugin root that every selected host
569568
installs from as-is. The root carries one `INSTALL.md` with a section per

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

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@ export const generatedExecutableSyntax = ((): 'es2022' => {
4444
}
4545
return 'es2022';
4646
})();
47-
export const generatedExecutableLegalComments = 'inline' as const;
4847

4948
export interface RslibVirtualModule {
5049
readonly name: string;
@@ -664,8 +663,15 @@ export const composeEntryLibConfig = (
664663
distPath: { root: options.outputRoot },
665664
filename: { js: entry.outputRelativePath },
666665
filenameHash: false,
667-
legalComments: generatedExecutableLegalComments,
668-
minify: false,
666+
legalComments: 'inline',
667+
// SWC runs only to drop comments, which in bundled dependencies
668+
// outweigh the code. `minify: false` keeps its output formatted, and
669+
// `legalComments: 'inline'` keeps license headers.
670+
minify: {
671+
css: false,
672+
js: true,
673+
jsOptions: { minimizerOptions: { compress: false, mangle: false, minify: false } },
674+
},
669675
sourceMap: options.sourceMap === true ? { js: 'inline-source-map' } : false,
670676
target: 'node',
671677
},

‎packages/agent-bundle/tests/build.test.ts‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1597,6 +1597,7 @@ it('parses emitted bundles in full when a tools hatch could have rewritten them'
15971597
// Rspack parsed the source and can rewrite the emitted asset — here a raw
15981598
// banner that leaves the lexer satisfied but Node unable to start the module
15991599
// — so the record says `coverage.rewritable` and the walk keeps the full parse.
1600+
// The banner lands after minification, which would otherwise reject it first.
16001601
const project = await createProject();
16011602
try {
16021603
await expect(build({
@@ -1606,7 +1607,11 @@ it('parses emitted bundles in full when a tools hatch could have rewritten them'
16061607
registry: new TargetRegistry().register((await import('../src/adapters/portable.ts')).portableAdapter, { default: true }),
16071608
tools: {
16081609
rspack: (config, { rspack }) => {
1609-
config.plugins = [...(config.plugins ?? []), new rspack.BannerPlugin({ banner: 'export const broken = ;', raw: true })];
1610+
config.plugins = [...(config.plugins ?? []), new rspack.BannerPlugin({
1611+
banner: 'export const broken = ;',
1612+
raw: true,
1613+
stage: rspack.Compilation.PROCESS_ASSETS_STAGE_REPORT,
1614+
})];
16101615
},
16111616
},
16121617
})).rejects.toThrow(

‎packages/agent-bundle/tests/self-contained-bundler-config.test.ts‎

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@ import { createRsbuild } from '@rsbuild/core';
22
import { createRslib } from '@rslib/core';
33
import { expect, it } from '@rstest/core';
44
import { init, parse } from 'es-module-lexer/minimal';
5+
import { execFile } from 'node:child_process';
56
import { mkdir, mkdtemp, readFile, readdir, symlink, writeFile } from 'node:fs/promises';
67
import { isBuiltin } from 'node:module';
78
import { tmpdir } from 'node:os';
89
import { join } from 'node:path';
10+
import { promisify } from 'node:util';
911

1012
import { composeMcpAppsRsbuildConfig } from '../src/build/mcp-apps.ts';
1113
import { buildWithRslib } from '../src/build/compiler.ts';
@@ -14,7 +16,6 @@ import {
1416
compilerHostNodeFloor,
1517
composeEntryLibConfig,
1618
entryLibId,
17-
generatedExecutableLegalComments,
1819
generatedExecutableSyntax,
1920
type RslibEntry,
2021
} from '../src/build/rslib.ts';
@@ -55,10 +56,17 @@ const selfContainedProject = async (): Promise<{ readonly entry: RslibEntry; rea
5556
await mkdir(join(root, 'views'), { recursive: true });
5657
await writeFile(join(root, 'package.json'), '{"type":"module"}\n');
5758
await symlink(agentBundleNodeModules, join(root, 'node_modules'), 'dir');
59+
await writeFile(join(root, 'src', 'licensed.js'), [
60+
'/*! licensed-probe v1.0.0 | MIT License */',
61+
'/** Probe documentation that must not reach the artifact. */',
62+
"export const licensed = () => 'licensed-probe';",
63+
'',
64+
].join('\n'));
5865
await writeFile(join(root, 'src', 'entry.ts'), [
5966
"import { basename } from 'node:path';",
6067
"import { parse } from 'yaml';",
61-
"console.log(basename('/probe/config.yaml'), parse('probe: true'));",
68+
"import { licensed } from './licensed.js';",
69+
"console.log(JSON.stringify([basename('/probe/config.yaml'), parse('probe: true'), licensed()]));",
6270
'',
6371
].join('\n'));
6472
await writeFile(join(root, 'views', 'dashboard.ts'), "document.title = 'dashboard';\n");
@@ -117,15 +125,6 @@ it('derives generated-executable syntax from the compiler host floor matching en
117125
expect(lib.syntax).toBe(generatedExecutableSyntax);
118126
});
119127

120-
it('reserves inline legal comments for future minification without adding a license asset today', () => {
121-
expect(generatedExecutableLegalComments).toBe('inline');
122-
const root = join(tmpdir(), 'agent-bundle-legal-comments-profile');
123-
const lib = composeEntryLibConfig(probeEntry(root), { cwd: root, meta: testMeta, outputRoot: join(root, 'dist') });
124-
expect(lib.output?.legalComments).toBe(generatedExecutableLegalComments);
125-
expect(lib.output?.minify).toBe(false);
126-
expect(lib.output?.sourceMap).toBe(false);
127-
});
128-
129128
it('opts generated-executable source maps in through output.sourceMap', () => {
130129
const root = join(tmpdir(), 'agent-bundle-source-map-profile');
131130
const entry = probeEntry(root);
@@ -140,7 +139,8 @@ it('opts generated-executable source maps in through output.sourceMap', () => {
140139
});
141140

142141
it('lowers a generated executable with only Node builtins external and inlines its dependencies', async () => {
143-
const { entry, root } = await selfContainedProject();
142+
const { entry: probe, root } = await selfContainedProject();
143+
const entry: RslibEntry = { ...probe, banner: '#!/usr/bin/env node' };
144144
const inspections: RslibInspection[] = [];
145145
try {
146146
await buildWithRslib({ cwd: root, entries: [entry], meta: testMeta, outputRoot: join(root, 'dist') }, {
@@ -189,6 +189,13 @@ it('lowers a generated executable with only Node builtins external and inlines i
189189
expect(specifiers.filter((specifier) => specifier === undefined || !isBuiltin(specifier))).toEqual([]);
190190
expect(bundle).toContain('YAMLParseError');
191191
expect(bundle).toMatch(/createRequire\(import\.meta\.url\)/u);
192+
expect(bundle.startsWith('#!/usr/bin/env node\n')).toBe(true);
193+
expect(bundle).toContain('\nvar __webpack_modules__ = {\n');
194+
expect(bundle).toContain('/*! licensed-probe v1.0.0 | MIT License */');
195+
expect(bundle).not.toContain('Probe documentation that must not reach the artifact.');
196+
expect(bundle).not.toContain('Parse the input as a stream of YAML documents.');
197+
const { stdout } = await promisify(execFile)(process.execPath, [join(root, 'dist', 'scripts', 'probe.mjs')]);
198+
expect(JSON.parse(stdout)).toEqual(['config.yaml', { probe: true }, 'licensed-probe']);
192199
await expect(readdir(join(root, 'dist'))).resolves.toEqual(['scripts']);
193200
await expect(readdir(join(root, 'dist', 'scripts'))).resolves.toEqual(['probe.mjs']);
194201
} finally {

‎website/docs/en/reference/configuration.mdx‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,11 @@ map so the map stays inside the executable, sibling `.map` files are not part of
177177
artifact file set. A non-boolean value is `AB4707`. MCP App views keep their own default (off)
178178
and still opt in through `tools.rsbuild.output.sourceMap`.
179179

180+
Generated executables keep readable code. The compiler profile runs the SWC minimizer only to
181+
remove comments, with compression, mangling, and whitespace minification off, so bundled
182+
dependency documentation does not ship. Legal comments, such as `/*! … */` license headers, stay
183+
inline. Set `tools.rsbuild.output.minify` to `false` to keep every comment.
184+
180185
`runtime.node` is a minimum Node.js version in `major.minor[.patch]` form. It can only raise the
181186
default floor for generated executables, never lower it, and the selected floor is recorded as
182187
`runtime.node` in the artifact manifest. The floor itself is described in

‎website/docs/zh/reference/configuration.mdx‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,10 @@ profile 会把打包器的 `output.sourceMap` 设为内联 JS map,让 map 留
154154
文件不属于计划中的产物文件集合。非布尔值是 `AB4707`。MCP App 视图仍保持自身的默认值(关闭),并继续
155155
通过 `tools.rsbuild.output.sourceMap` 选择加入。
156156

157+
生成式可执行文件保持代码可读。编译器 profile 只用 SWC 压缩器移除注释,压缩、混淆和空白压缩均关闭,
158+
因此被打包依赖的文档注释不会随产物发布。法律注释(例如 `/*! … */` 许可证头)以内联方式保留。将
159+
`tools.rsbuild.output.minify` 设为 `false` 可保留全部注释。
160+
157161
`runtime.node` 是 `major.minor[.patch]` 形式的最低 Node.js 版本。它只能抬高生成式可执行文件的默认下限,
158162
绝不能降低,且所选下限会作为 `runtime.node` 记录在产物清单中。这个下限本身在
159163
[配置模型](../guide/authoring/index.mdx)中介绍。

0 commit comments

Comments
 (0)