feat(release): ship esbuild bundle in npm package

The npm registry publish path ships unbundled dist/ plus full
node_modules/ (44,835 files), causing ~1-2 minute cold starts on
Windows due to Defender real-time scanning. The GitHub registry
path already ships a single-file bundle with zero dependencies.

Apply the same bundle transformation to the npm publish path:
- Add scripts/prepare-npm-release.js that rewrites the CLI
  package.json to ship bundle/ instead of dist/, removes all
  dependencies, and adds optionalDependencies for native modules
  (node-pty, keytar) so platform-specific features keep working.
- Add a "Prepare bundled CLI for npm release" step in the publish
  action, conditioned on non-GitHub registry URLs.

Result: npm package drops from 44,835 files to ~129 files (6.8 MB).

Closes #19169
This commit is contained in:
Gen Zhang
2026-02-15 19:53:22 +00:00
committed by Yuna Seol
parent be12371380
commit 5cc2c27e7b
2 changed files with 91 additions and 0 deletions
@@ -181,6 +181,13 @@ runs:
--workspace="${{ inputs.a2a-package-name }}" \
--save-exact
- name: '📦 Prepare bundled CLI for npm release'
if: "inputs.npm-registry-url != 'https://npm.pkg.github.com/'"
working-directory: '${{ inputs.working-directory }}'
shell: 'bash'
run: |
node ${{ github.workspace }}/scripts/prepare-npm-release.js
- name: 'Get CLI Token'
uses: './.github/actions/npm-auth-token'
id: 'cli-token'
+84
View File
@@ -0,0 +1,84 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import fs from 'node:fs';
import path from 'node:path';
const rootDir = process.cwd();
function readJson(filePath) {
return JSON.parse(fs.readFileSync(path.resolve(rootDir, filePath), 'utf-8'));
}
function writeJson(filePath, data) {
fs.writeFileSync(
path.resolve(rootDir, filePath),
JSON.stringify(data, null, 2),
);
}
// Copy bundle directory into packages/cli
const sourceBundleDir = path.resolve(rootDir, 'bundle');
const destBundleDir = path.resolve(rootDir, 'packages/cli/bundle');
if (fs.existsSync(sourceBundleDir)) {
fs.rmSync(destBundleDir, { recursive: true, force: true });
fs.cpSync(sourceBundleDir, destBundleDir, { recursive: true });
console.log('Copied bundle/ directory to packages/cli/');
} else {
console.error(
'Error: bundle/ directory not found at project root. Please run `npm run bundle` first.',
);
process.exit(1);
}
// Read native module versions from root package.json
const rootPkg = readJson('package.json');
const rootOptional = rootPkg.optionalDependencies || {};
const nativeModules = [
'@lydell/node-pty',
'@lydell/node-pty-darwin-arm64',
'@lydell/node-pty-darwin-x64',
'@lydell/node-pty-linux-x64',
'@lydell/node-pty-win32-arm64',
'@lydell/node-pty-win32-x64',
'keytar',
'node-pty',
];
const optionalDependencies = {};
for (const mod of nativeModules) {
if (rootOptional[mod]) {
optionalDependencies[mod] = rootOptional[mod];
}
}
// Update @google/gemini-cli package.json for bundled npm release
const cliPkgPath = 'packages/cli/package.json';
const cliPkg = readJson(cliPkgPath);
cliPkg.files = ['bundle/'];
cliPkg.bin = {
gemini: 'bundle/gemini.js',
};
delete cliPkg.dependencies;
delete cliPkg.devDependencies;
delete cliPkg.scripts;
delete cliPkg.main;
delete cliPkg.config;
cliPkg.optionalDependencies = optionalDependencies;
writeJson(cliPkgPath, cliPkg);
console.log('Updated packages/cli/package.json for bundled npm release.');
console.log(
'optionalDependencies:',
JSON.stringify(optionalDependencies, null, 2),
);
console.log('Successfully prepared packages for npm release.');