qwen-code/packages/vscode-ide-companion/scripts/prepackage.js

252 lines
7.2 KiB
JavaScript

/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
/**
* VS Code extension packaging orchestration.
*
* We bundle the CLI into the extension so users don't need a global install.
* To match the published CLI layout, we need to:
* - build root bundle (dist/cli.js + vendor/ + sandbox profiles)
* - run root prepare:package (dist/package.json + locales + README/LICENSE)
* - install production deps into root dist/ (dist/node_modules) so runtime deps
* like optional node-pty are present inside the VSIX payload.
*
* Then we generate notices and build the extension.
*/
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { spawnSync } from 'node:child_process';
import fs from 'node:fs';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const extensionRoot = path.resolve(__dirname, '..');
const repoRoot = path.resolve(extensionRoot, '..', '..');
const bundledCliDir = path.join(extensionRoot, 'dist', 'qwen-cli');
function npmBin() {
return process.platform === 'win32' ? 'npm.cmd' : 'npm';
}
function run(cmd, args, opts = {}) {
const res = spawnSync(cmd, args, {
stdio: 'inherit',
shell: process.platform === 'win32',
...opts,
});
if (res.error) {
throw res.error;
}
if (typeof res.status === 'number' && res.status !== 0) {
throw new Error(
`Command failed (${res.status}): ${cmd} ${args.map((a) => JSON.stringify(a)).join(' ')}`,
);
}
}
function parseVsceTarget(target) {
if (!target) return null;
const parts = target.split('-');
if (parts.length !== 2) return null;
const [platform, arch] = parts;
return { platform, arch };
}
function getExpectedRipgrepDirName() {
const target = parseVsceTarget(process.env.VSCODE_TARGET);
const platform = target?.platform ?? process.platform;
const arch = target?.arch ?? process.arch;
const normalizedPlatform =
platform === 'darwin' || platform === 'linux' || platform === 'win32'
? platform
: null;
const normalizedArch = arch === 'x64' || arch === 'arm64' ? arch : null;
if (!normalizedPlatform || !normalizedArch) return null;
return `${normalizedArch}-${normalizedPlatform}`;
}
function pruneBundledRipgrep() {
const isUniversalBuild = process.env.UNIVERSAL_BUILD === 'true';
if (isUniversalBuild) {
console.log('[prepackage] Universal build: keeping all ripgrep binaries');
return;
}
if (!process.env.VSCODE_TARGET) {
console.log(
'[prepackage] VSCODE_TARGET not set: keeping all ripgrep binaries',
);
return;
}
const expectedDirName = getExpectedRipgrepDirName();
if (!expectedDirName) {
console.warn(
'[prepackage] Could not resolve expected ripgrep target; keeping all binaries',
);
return;
}
const ripgrepDir = path.join(bundledCliDir, 'vendor', 'ripgrep');
if (!fs.existsSync(ripgrepDir)) {
console.log('[prepackage] No bundled ripgrep directory found; skipping');
return;
}
const entries = fs.readdirSync(ripgrepDir, { withFileTypes: true });
const removed = [];
for (const entry of entries) {
if (!entry.isDirectory()) continue;
const name = entry.name;
if (!/^(x64|arm64)-(darwin|linux|win32)$/.test(name)) continue;
if (name === expectedDirName) continue;
const fullPath = path.join(ripgrepDir, name);
fs.rmSync(fullPath, { recursive: true, force: true });
removed.push(name);
}
if (removed.length === 0) {
console.log(
`[prepackage] Ripgrep already pruned for ${expectedDirName} (no changes)`,
);
return;
}
console.log(
`[prepackage] Pruned ripgrep binaries; kept ${expectedDirName}, removed: ${removed.join(', ')}`,
);
}
function removeSelfReferenceFromNodeModules() {
if (process.platform !== 'win32') return;
const packageJsonPath = path.join(bundledCliDir, 'package.json');
if (!fs.existsSync(packageJsonPath)) return;
let packageName;
try {
const parsed = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
packageName = parsed?.name;
} catch {
return;
}
if (typeof packageName !== 'string' || packageName.length === 0) return;
// Some npm installations on Windows can create a junction in node_modules
// pointing back to the package itself. vsce/yazl can't zip that reliably.
let selfPath;
if (packageName.startsWith('@')) {
const [scope, name] = packageName.split('/');
if (!scope || !name) return;
selfPath = path.join(bundledCliDir, 'node_modules', scope, name);
} else {
selfPath = path.join(bundledCliDir, 'node_modules', packageName);
}
if (!fs.existsSync(selfPath)) return;
fs.rmSync(selfPath, { recursive: true, force: true });
console.log(
`[prepackage] Windows: removed self-reference from node_modules: ${packageName}`,
);
// Cleanup empty scope directory (cosmetic).
try {
const parentDir = path.dirname(selfPath);
if (
fs.existsSync(parentDir) &&
fs.statSync(parentDir).isDirectory() &&
fs.readdirSync(parentDir).length === 0
) {
fs.rmdirSync(parentDir);
}
} catch {
// Best-effort cleanup only.
}
}
function main() {
const npm = npmBin();
// Root bundling depends on built workspace outputs. Use the root build to
// ensure all required workspace dist/ artifacts exist.
console.log('[prepackage] Building repo...');
run(npm, ['--prefix', repoRoot, 'run', 'build'], { cwd: repoRoot });
console.log('[prepackage] Bundling root CLI...');
run(npm, ['--prefix', repoRoot, 'run', 'bundle'], { cwd: repoRoot });
console.log('[prepackage] Preparing root dist/ package metadata...');
run(npm, ['--prefix', repoRoot, 'run', 'prepare:package'], { cwd: repoRoot });
console.log('[prepackage] Preparing webui dist/ package metadata...');
run(
npm,
['--prefix', path.join(repoRoot, 'packages', 'webui'), 'run', 'build'],
{ cwd: repoRoot },
);
console.log('[prepackage] Generating notices...');
run(npm, ['run', 'generate:notices'], { cwd: extensionRoot });
console.log('[prepackage] Building extension (production)...');
run(npm, ['run', 'build:prod'], { cwd: extensionRoot });
console.log('[prepackage] Copying bundled CLI dist/ into extension...');
run(
'node',
[`${path.join(extensionRoot, 'scripts', 'copy-bundled-cli.js')}`],
{
cwd: extensionRoot,
},
);
const isUniversalBuild = process.env.UNIVERSAL_BUILD === 'true';
console.log(
'[prepackage] Installing production deps into extension dist/qwen-cli...',
);
const installArgs = [
'--prefix',
bundledCliDir,
'install',
'--omit=dev',
'--no-audit',
'--no-fund',
];
// For universal build, exclude optional dependencies (node-pty native binaries)
// This ensures the universal VSIX works on all platforms using child_process fallback
if (isUniversalBuild) {
installArgs.push('--omit=optional');
console.log(
'[prepackage] Universal build: excluding optional dependencies (node-pty)',
);
}
run(npm, installArgs, {
cwd: bundledCliDir,
env: {
...process.env,
npm_config_workspaces: 'false',
npm_config_include_workspace_root: 'false',
npm_config_link_workspace_packages: 'false',
},
});
removeSelfReferenceFromNodeModules();
pruneBundledRipgrep();
}
main();