mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-04-28 11:41:04 +00:00
252 lines
7.2 KiB
JavaScript
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();
|