diff --git a/.changeset/strip-executable-comments.md b/.changeset/strip-executable-comments.md new file mode 100644 index 000000000..a8166ad62 --- /dev/null +++ b/.changeset/strip-executable-comments.md @@ -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) diff --git a/docs/framework-mode.md b/docs/framework-mode.md index 04555fc81..84318c946 100644 --- a/docs/framework-mode.md +++ b/docs/framework-mode.md @@ -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 diff --git a/packages/agent-bundle/src/build/rslib.ts b/packages/agent-bundle/src/build/rslib.ts index 1f9ffdcb1..93de7d38c 100644 --- a/packages/agent-bundle/src/build/rslib.ts +++ b/packages/agent-bundle/src/build/rslib.ts @@ -44,7 +44,6 @@ export const generatedExecutableSyntax = ((): 'es2022' => { } return 'es2022'; })(); -export const generatedExecutableLegalComments = 'inline' as const; export interface RslibVirtualModule { readonly name: string; @@ -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', }, diff --git a/packages/agent-bundle/tests/build.test.ts b/packages/agent-bundle/tests/build.test.ts index eca652913..ed494feaf 100644 --- a/packages/agent-bundle/tests/build.test.ts +++ b/packages/agent-bundle/tests/build.test.ts @@ -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({ @@ -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( diff --git a/packages/agent-bundle/tests/self-contained-bundler-config.test.ts b/packages/agent-bundle/tests/self-contained-bundler-config.test.ts index 45bb3d3f9..aa124409f 100644 --- a/packages/agent-bundle/tests/self-contained-bundler-config.test.ts +++ b/packages/agent-bundle/tests/self-contained-bundler-config.test.ts @@ -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'; @@ -14,7 +16,6 @@ import { compilerHostNodeFloor, composeEntryLibConfig, entryLibId, - generatedExecutableLegalComments, generatedExecutableSyntax, type RslibEntry, } from '../src/build/rslib.ts'; @@ -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"); @@ -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); @@ -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') }, { @@ -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 { diff --git a/website/docs/en/reference/configuration.mdx b/website/docs/en/reference/configuration.mdx index 5f7732f50..069200985 100644 --- a/website/docs/en/reference/configuration.mdx +++ b/website/docs/en/reference/configuration.mdx @@ -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 diff --git a/website/docs/zh/reference/configuration.mdx b/website/docs/zh/reference/configuration.mdx index 681765688..744be32da 100644 --- a/website/docs/zh/reference/configuration.mdx +++ b/website/docs/zh/reference/configuration.mdx @@ -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)中介绍。