From 0eb1a3f7533f6a8fe1b74306b425885ef4b32c3d Mon Sep 17 00:00:00 2001 From: Mopsgamer <79159094+Mopsgamer@users.noreply.github.com> Date: Mon, 24 Nov 2025 20:49:37 +0100 Subject: [PATCH 01/12] impr: package managers handling --- package-lock.json | 10 +- src/main.ts | 17 ++- src/managers/exec.ts | 41 +++++++ src/managers/manager.ts | 30 ++++++ src/managers/none.ts | 13 +++ src/managers/npm.ts | 31 ++++++ src/managers/yarn.ts | 152 ++++++++++++++++++++++++++ src/npm.ts | 230 ---------------------------------------- src/package.ts | 62 +++++++---- src/publish.ts | 10 ++ 10 files changed, 341 insertions(+), 255 deletions(-) create mode 100644 src/managers/exec.ts create mode 100644 src/managers/manager.ts create mode 100644 src/managers/none.ts create mode 100644 src/managers/npm.ts create mode 100644 src/managers/yarn.ts delete mode 100644 src/npm.ts diff --git a/package-lock.json b/package-lock.json index 0fe9543a..55ed5a4d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1040,7 +1040,8 @@ "version": "16.18.96", "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.96.tgz", "integrity": "sha512-84iSqGXoO+Ha16j8pRZ/L90vDMKX04QTYMTfYeE1WrjWaZXuchBehGUZEpNgx7JnmlrIHdnABmpjrQjhCnNldQ==", - "dev": true + "dev": true, + "peer": true }, "node_modules/@types/normalize-package-data": { "version": "2.4.4", @@ -4631,6 +4632,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.4.tgz", "integrity": "sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==", "dev": true, + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -5755,7 +5757,8 @@ "version": "16.18.96", "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.96.tgz", "integrity": "sha512-84iSqGXoO+Ha16j8pRZ/L90vDMKX04QTYMTfYeE1WrjWaZXuchBehGUZEpNgx7JnmlrIHdnABmpjrQjhCnNldQ==", - "dev": true + "dev": true, + "peer": true }, "@types/normalize-package-data": { "version": "2.4.4", @@ -8275,7 +8278,8 @@ "version": "4.8.4", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.4.tgz", "integrity": "sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==", - "dev": true + "dev": true, + "peer": true }, "uc.micro": { "version": "2.1.0", diff --git a/src/main.ts b/src/main.ts index 68d14e3a..e816ecc4 100644 --- a/src/main.ts +++ b/src/main.ts @@ -5,10 +5,11 @@ import { publish, unpublish } from './publish'; import { show } from './show'; import { search } from './search'; import { listPublishers, deletePublisher, loginPublisher, logoutPublisher, verifyPat } from './store'; -import { getLatestVersion } from './npm'; import { CancellationToken, log } from './util'; import * as semver from 'semver'; import { isatty } from 'tty'; +import { pmNPM } from './managers/npm'; +import { Managers } from './managers/manager'; const pkg = require('../package.json'); @@ -41,7 +42,7 @@ function main(task: Promise): void { const token = new CancellationToken(); if (isatty(1)) { - getLatestVersion(pkg.name, token) + pmNPM.pmFetchLatestVersion(pkg.name, token) .then(version => (latestVersion = version)) .catch(_ => { /* noop */ @@ -58,6 +59,7 @@ function main(task: Promise): void { } const ValidTargets = [...Targets].join(', '); +const ValidManagers = [...Managers].join(', '); module.exports = function (argv: string[]): void { const program = new Command(); @@ -68,6 +70,7 @@ module.exports = function (argv: string[]): void { .command('ls') .description('Lists all the files that will be published/packaged') .option('--tree', 'Prints the files in a tree format', false) + .option('--packageManager ', `Specify package manager to use. Valid managers: ${ValidManagers}`, undefined) .option('--yarn', 'Use yarn instead of npm (default inferred from presence of yarn.lock or .yarnrc)') .option('--no-yarn', 'Use npm instead of yarn (default inferred from absence of yarn.lock or .yarnrc)') .option( @@ -82,8 +85,8 @@ module.exports = function (argv: string[]): void { .option('--no-dependencies', 'Disable dependency detection via npm or yarn', undefined) .option('--readme-path ', 'Path to README file (defaults to README.md)') .option('--follow-symlinks', 'Recurse into symlinked directories instead of treating them as files') - .action(({ tree, yarn, packagedDependencies, ignoreFile, dependencies, readmePath, followSymlinks }) => - main(ls({ tree, useYarn: yarn, packagedDependencies, ignoreFile, dependencies, readmePath, followSymlinks })) + .action(({ tree, packageManager, yarn, packagedDependencies, ignoreFile, dependencies, readmePath, followSymlinks }) => + main(ls({ tree, packageManager, useYarn: yarn, packagedDependencies, ignoreFile, dependencies, readmePath, followSymlinks })) ); program @@ -112,6 +115,7 @@ module.exports = function (argv: string[]): void { .option('--no-rewrite-relative-links', 'Skip rewriting relative links.') .option('--baseContentUrl ', 'Prepend all relative links in README.md with the specified URL.') .option('--baseImagesUrl ', 'Prepend all relative image links in README.md with the specified URL.') + .option('--packageManager ', `Specify package manager to use. Valid managers: ${ValidManagers}`, undefined) .option('--yarn', 'Use yarn instead of npm (default inferred from presence of yarn.lock or .yarnrc)') .option('--no-yarn', 'Use npm instead of yarn (default inferred from absence of yarn.lock or .yarnrc)') .option('--ignoreFile ', 'Indicate alternative .vscodeignore') @@ -147,6 +151,7 @@ module.exports = function (argv: string[]): void { rewriteRelativeLinks, baseContentUrl, baseImagesUrl, + packageManager, yarn, ignoreFile, gitHubIssueLinking, @@ -180,6 +185,7 @@ module.exports = function (argv: string[]): void { rewriteRelativeLinks, baseContentUrl, baseImagesUrl, + packageManager, useYarn: yarn, ignoreFile, gitHubIssueLinking, @@ -233,6 +239,7 @@ module.exports = function (argv: string[]): void { ) .option('--baseContentUrl ', 'Prepend all relative links in README.md with the specified URL.') .option('--baseImagesUrl ', 'Prepend all relative image links in README.md with the specified URL.') + .option('--packageManager ', `Specify package manager to use. Valid managers: ${ValidManagers}`, undefined) .option('--yarn', 'Use yarn instead of npm (default inferred from presence of yarn.lock or .yarnrc)') .option('--no-yarn', 'Use npm instead of yarn (default inferred from absence of yarn.lock or .yarnrc)') .option('--no-verify', 'Allow all proposed APIs (deprecated: use --allow-all-proposed-apis instead)') @@ -274,6 +281,7 @@ module.exports = function (argv: string[]): void { gitlabBranch, baseContentUrl, baseImagesUrl, + packageManager, yarn, verify, noVerify, @@ -314,6 +322,7 @@ module.exports = function (argv: string[]): void { gitlabBranch, baseContentUrl, baseImagesUrl, + packageManager, useYarn: yarn, noVerify: noVerify || !verify, allowProposedApis, diff --git a/src/managers/exec.ts b/src/managers/exec.ts new file mode 100644 index 00000000..fd426a62 --- /dev/null +++ b/src/managers/exec.ts @@ -0,0 +1,41 @@ +import * as cp from "node:child_process"; +import { CancellationToken } from "../util"; + +interface IOptions { + cwd?: string; + stdio?: any; + customFds?: any; + env?: any; + timeout?: number; + maxBuffer?: number; + killSignal?: string; +} + +export function exec( + command: string, + options: IOptions = {}, + cancellationToken?: CancellationToken +): Promise<{ stdout: string; stderr: string }> { + return new Promise((c, e) => { + let disposeCancellationListener: Function | null = null; + + const child = cp.exec(command, { ...options, encoding: 'utf8' } as any, (err, stdout: string, stderr: string) => { + if (disposeCancellationListener) { + disposeCancellationListener(); + disposeCancellationListener = null; + } + + if (err) { + return e(err); + } + c({ stdout, stderr }); + }); + + if (cancellationToken) { + disposeCancellationListener = cancellationToken.subscribe((err: any) => { + child.kill(); + e(err); + }); + } + }); +} diff --git a/src/managers/manager.ts b/src/managers/manager.ts new file mode 100644 index 00000000..5063a649 --- /dev/null +++ b/src/managers/manager.ts @@ -0,0 +1,30 @@ +import { CancellationToken } from "../util"; +import { pmNone } from "./none"; +import { pmNPM } from "./npm"; +import { pmYarn } from "./yarn"; + +// Reminder: scr/api.ts (PackageManager enum). +const managers = ['none', 'npm', 'yarn'] as const +export const Managers = new Set(managers); +export type PackageManagerLiteral = typeof managers[number]; + +export interface IPackageManager { + binaryName: string; + selfVersion(cancellationToken?: CancellationToken): Promise; + selfCheck(cancellationToken?: CancellationToken): Promise; + pmRunCommand(scriptName: string): string; + pmFetchLatestVersion(name: string, cancellationToken?: CancellationToken): Promise; + pmProdDependencies(cwd: string, packagedDependencies?: string[]): Promise; +} + +export function getPackageManager( + preference: PackageManagerLiteral = "npm", +): IPackageManager { + const choice = { + "none": pmNone, + "npm": pmNPM, + "yarn": pmYarn, + } as Record + + return choice[preference] +} diff --git a/src/managers/none.ts b/src/managers/none.ts new file mode 100644 index 00000000..d6a6354c --- /dev/null +++ b/src/managers/none.ts @@ -0,0 +1,13 @@ +import type { IPackageManager } from "./manager"; +import { pmNPM } from './npm'; + +export const pmNone: IPackageManager = { + binaryName: "", + selfVersion: pmNPM.selfVersion.bind(pmNPM), + selfCheck: pmNPM.selfCheck.bind(pmNPM), + pmRunCommand: pmNPM.pmRunCommand, + async pmProdDependencies(cwd: string, _?: string[]): Promise { + return [cwd] + }, + pmFetchLatestVersion: pmNPM.pmFetchLatestVersion.bind(pmNPM), +} \ No newline at end of file diff --git a/src/managers/npm.ts b/src/managers/npm.ts new file mode 100644 index 00000000..eda6af1e --- /dev/null +++ b/src/managers/npm.ts @@ -0,0 +1,31 @@ +import * as path from 'path'; +import { exec } from "./exec"; +import type { IPackageManager } from "./manager"; +import type { CancellationToken } from "../util"; + +export const pmNPM: IPackageManager = { + binaryName: 'npm', + async selfVersion(cancellationToken?: CancellationToken): Promise { + const { stdout } = await exec('npm -v', {}, cancellationToken); + return stdout.trim(); + }, + async selfCheck(cancellationToken?: CancellationToken): Promise { + const version = await this.selfVersion(cancellationToken); + if (/^3\.7\.[0123]$/.test(version)) { + throw new Error(`npm@${version} doesn't work with vsce. Please update npm: npm install -g npm`); + } + }, + pmRunCommand(scriptName: string): string { + return `${this.binaryName} run ${scriptName}` + }, + async pmProdDependencies(cwd: string, _?: string[]): Promise { + await this.selfCheck() + const { stdout } = await exec('npm list --production --parseable --depth=99999 --loglevel=error', { cwd, maxBuffer: 5000 * 1024 }) + return stdout.split(/[\r\n]/).filter(dir => path.isAbsolute(dir)) + }, + async pmFetchLatestVersion(name: string, cancellationToken?: CancellationToken): Promise { + await this.selfCheck(cancellationToken) + const { stdout } = await exec(`npm show ${name} version`, {}, cancellationToken) + return stdout.split(/[\r\n]/).filter(line => !!line)[0]; + } +} \ No newline at end of file diff --git a/src/managers/yarn.ts b/src/managers/yarn.ts new file mode 100644 index 00000000..9ef2a48b --- /dev/null +++ b/src/managers/yarn.ts @@ -0,0 +1,152 @@ +import * as path from 'path'; +import * as cp from "child_process"; +import { type CancellationToken, nonnull } from '../util'; +import type { IPackageManager } from "./manager"; +import parseSemver from 'parse-semver'; +import { exec } from './exec'; + +export const pmYarn: IPackageManager = { + binaryName: 'yarn', + async selfVersion(cancellationToken?: CancellationToken): Promise { + const { stdout } = await exec('yarn -v', {}, cancellationToken); + return stdout.trim(); + }, + async selfCheck(cancellationToken?: CancellationToken): Promise { + const version = await this.selfVersion(cancellationToken); + if (!version.startsWith("1")) { + throw new Error(`yarn@${version} doesn't work with vsce. Please update yarn: npm install -g yarn`); + } + }, + pmRunCommand(scriptName: string): string { + return `${this.binaryName} run ${scriptName}`; + }, + async pmFetchLatestVersion(name: string, cancellationToken?: CancellationToken): Promise { + await this.selfCheck(cancellationToken) + const { stdout } = await exec(`yarn info ${name} version`, {}, cancellationToken) + return stdout.split(/[\r\n]/).filter(line => !!line)[1]; + }, + async pmProdDependencies(cwd: string, packagedDependencies?: string[]): Promise { + const result = new Set([cwd]); + + const deps = await getYarnProductionDependencies(cwd, packagedDependencies); + const flatten = (dep: YarnDependency) => { + result.add(dep.path); + dep.children.forEach(flatten); + }; + deps.forEach(flatten); + + return [...result]; + }, +} + +interface YarnTreeNode { + name: string; + children: YarnTreeNode[]; +} + +export interface YarnDependency { + name: string; + path: string; + children: YarnDependency[]; +} + +async function getYarnProductionDependencies(cwd: string, packagedDependencies?: string[]): Promise { + const raw = await new Promise((c, e) => + cp.exec( + 'yarn list --prod --json', + { cwd, encoding: 'utf8', env: { DISABLE_V8_COMPILE_CACHE: "1", ...process.env }, maxBuffer: 5000 * 1024 }, + (err, stdout) => (err ? e(err) : c(stdout)) + ) + ); + const match = /^{"type":"tree".*$/m.exec(raw); + + if (!match || match.length !== 1) { + throw new Error('Could not parse result of `yarn list --json`'); + } + + const usingPackagedDependencies = Array.isArray(packagedDependencies); + const trees = JSON.parse(match[0]).data.trees as YarnTreeNode[]; + + let result = trees + .map(tree => asYarnDependency(path.join(cwd, 'node_modules'), tree, !usingPackagedDependencies)) + .filter(nonnull); + + if (usingPackagedDependencies) { + result = selectYarnDependencies(result, packagedDependencies!); + } + + return result; +} + +function asYarnDependency(prefix: string, tree: YarnTreeNode, prune: boolean): YarnDependency | null { + if (prune && /@[\^~]/.test(tree.name)) { + return null; + } + + let name: string; + + try { + const parseResult = parseSemver(tree.name); + name = parseResult.name; + } catch (err) { + name = tree.name.replace(/^([^@+])@.*$/, '$1'); + } + + const dependencyPath = path.join(prefix, name); + const children: YarnDependency[] = []; + + for (const child of tree.children || []) { + const dep = asYarnDependency(path.join(prefix, name, 'node_modules'), child, prune); + + if (dep) { + children.push(dep); + } + } + + return { name, path: dependencyPath, children }; +} + +function selectYarnDependencies(deps: YarnDependency[], packagedDependencies: string[]): YarnDependency[] { + const index = new (class { + private data: { [name: string]: YarnDependency } = Object.create(null); + constructor() { + for (const dep of deps) { + if (this.data[dep.name]) { + throw Error(`Dependency seen more than once: ${dep.name}`); + } + this.data[dep.name] = dep; + } + } + find(name: string): YarnDependency { + let result = this.data[name]; + if (!result) { + throw new Error(`Could not find dependency: ${name}`); + } + return result; + } + })(); + + const reached = new (class { + values: YarnDependency[] = []; + add(dep: YarnDependency): boolean { + if (this.values.indexOf(dep) < 0) { + this.values.push(dep); + return true; + } + return false; + } + })(); + + const visit = (name: string) => { + let dep = index.find(name); + if (!reached.add(dep)) { + // already seen -> done + return; + } + for (const child of dep.children) { + visit(child.name); + } + }; + packagedDependencies.forEach(visit); + return reached.values; +} diff --git a/src/npm.ts b/src/npm.ts deleted file mode 100644 index 6ea6de5d..00000000 --- a/src/npm.ts +++ /dev/null @@ -1,230 +0,0 @@ -import * as path from 'path'; -import * as fs from 'fs'; -import * as cp from 'child_process'; -import parseSemver from 'parse-semver'; -import { CancellationToken, log, nonnull } from './util'; - -const exists = (file: string) => - fs.promises.stat(file).then( - _ => true, - _ => false - ); - -interface IOptions { - cwd?: string; - stdio?: any; - customFds?: any; - env?: any; - timeout?: number; - maxBuffer?: number; - killSignal?: string; -} - -function parseStdout({ stdout }: { stdout: string }): string { - return stdout.split(/[\r\n]/).filter(line => !!line)[0]; -} - -function exec( - command: string, - options: IOptions = {}, - cancellationToken?: CancellationToken -): Promise<{ stdout: string; stderr: string }> { - return new Promise((c, e) => { - let disposeCancellationListener: Function | null = null; - - const child = cp.exec(command, { ...options, encoding: 'utf8' } as any, (err, stdout: string, stderr: string) => { - if (disposeCancellationListener) { - disposeCancellationListener(); - disposeCancellationListener = null; - } - - if (err) { - return e(err); - } - c({ stdout, stderr }); - }); - - if (cancellationToken) { - disposeCancellationListener = cancellationToken.subscribe((err: any) => { - child.kill(); - e(err); - }); - } - }); -} - -async function checkNPM(cancellationToken?: CancellationToken): Promise { - const { stdout } = await exec('npm -v', {}, cancellationToken); - const version = stdout.trim(); - - if (/^3\.7\.[0123]$/.test(version)) { - throw new Error(`npm@${version} doesn't work with vsce. Please update npm: npm install -g npm`); - } -} - -function getNpmDependencies(cwd: string): Promise { - return checkNPM() - .then(() => - exec('npm list --production --parseable --depth=99999 --loglevel=error', { cwd, maxBuffer: 5000 * 1024 }) - ) - .then(({ stdout }) => stdout.split(/[\r\n]/).filter(dir => path.isAbsolute(dir))); -} - -interface YarnTreeNode { - name: string; - children: YarnTreeNode[]; -} - -export interface YarnDependency { - name: string; - path: string; - children: YarnDependency[]; -} - -function asYarnDependency(prefix: string, tree: YarnTreeNode, prune: boolean): YarnDependency | null { - if (prune && /@[\^~]/.test(tree.name)) { - return null; - } - - let name: string; - - try { - const parseResult = parseSemver(tree.name); - name = parseResult.name; - } catch (err) { - name = tree.name.replace(/^([^@+])@.*$/, '$1'); - } - - const dependencyPath = path.join(prefix, name); - const children: YarnDependency[] = []; - - for (const child of tree.children || []) { - const dep = asYarnDependency(path.join(prefix, name, 'node_modules'), child, prune); - - if (dep) { - children.push(dep); - } - } - - return { name, path: dependencyPath, children }; -} - -function selectYarnDependencies(deps: YarnDependency[], packagedDependencies: string[]): YarnDependency[] { - const index = new (class { - private data: { [name: string]: YarnDependency } = Object.create(null); - constructor() { - for (const dep of deps) { - if (this.data[dep.name]) { - throw Error(`Dependency seen more than once: ${dep.name}`); - } - this.data[dep.name] = dep; - } - } - find(name: string): YarnDependency { - let result = this.data[name]; - if (!result) { - throw new Error(`Could not find dependency: ${name}`); - } - return result; - } - })(); - - const reached = new (class { - values: YarnDependency[] = []; - add(dep: YarnDependency): boolean { - if (this.values.indexOf(dep) < 0) { - this.values.push(dep); - return true; - } - return false; - } - })(); - - const visit = (name: string) => { - let dep = index.find(name); - if (!reached.add(dep)) { - // already seen -> done - return; - } - for (const child of dep.children) { - visit(child.name); - } - }; - packagedDependencies.forEach(visit); - return reached.values; -} - -async function getYarnProductionDependencies(cwd: string, packagedDependencies?: string[]): Promise { - const raw = await new Promise((c, e) => - cp.exec( - 'yarn list --prod --json', - { cwd, encoding: 'utf8', env: { DISABLE_V8_COMPILE_CACHE: "1", ...process.env }, maxBuffer: 5000 * 1024 }, - (err, stdout) => (err ? e(err) : c(stdout)) - ) - ); - const match = /^{"type":"tree".*$/m.exec(raw); - - if (!match || match.length !== 1) { - throw new Error('Could not parse result of `yarn list --json`'); - } - - const usingPackagedDependencies = Array.isArray(packagedDependencies); - const trees = JSON.parse(match[0]).data.trees as YarnTreeNode[]; - - let result = trees - .map(tree => asYarnDependency(path.join(cwd, 'node_modules'), tree, !usingPackagedDependencies)) - .filter(nonnull); - - if (usingPackagedDependencies) { - result = selectYarnDependencies(result, packagedDependencies!); - } - - return result; -} - -async function getYarnDependencies(cwd: string, packagedDependencies?: string[]): Promise { - const result = new Set([cwd]); - - const deps = await getYarnProductionDependencies(cwd, packagedDependencies); - const flatten = (dep: YarnDependency) => { - result.add(dep.path); - dep.children.forEach(flatten); - }; - deps.forEach(flatten); - - return [...result]; -} - -export async function detectYarn(cwd: string): Promise { - for (const name of ['yarn.lock', '.yarnrc', '.yarnrc.yaml', '.pnp.cjs', '.yarn']) { - if (await exists(path.join(cwd, name))) { - if (!process.env['VSCE_TESTS']) { - log.info( - `Detected presence of ${name}. Using 'yarn' instead of 'npm' (to override this pass '--no-yarn' on the command line).` - ); - } - return true; - } - } - return false; -} - -export async function getDependencies( - cwd: string, - dependencies: 'npm' | 'yarn' | 'none' | undefined, - packagedDependencies?: string[] -): Promise { - if (dependencies === 'none') { - return [cwd]; - } else if (dependencies === 'yarn' || (dependencies === undefined && (await detectYarn(cwd)))) { - return await getYarnDependencies(cwd, packagedDependencies); - } else { - return await getNpmDependencies(cwd); - } -} - -export function getLatestVersion(name: string, cancellationToken?: CancellationToken): Promise { - return checkNPM(cancellationToken) - .then(() => exec(`npm show ${name} version`, {}, cancellationToken)) - .then(parseStdout); -} diff --git a/src/package.ts b/src/package.ts index 94e0ce0f..6c54cb8c 100644 --- a/src/package.ts +++ b/src/package.ts @@ -23,12 +23,12 @@ import { validatePublisher, validateExtensionDependencies, } from './validation'; -import { detectYarn, getDependencies } from './npm'; import * as GitHost from 'hosted-git-info'; import parseSemver from 'parse-semver'; import * as jsonc from 'jsonc-parser'; import * as vsceSign from '@vscode/vsce-sign'; import { getRuleNameFromRuleId, lintFiles, lintText, prettyPrintLintResult } from './secretLint'; +import { getPackageManager, Managers, PackageManagerLiteral } from './managers/manager'; const MinimatchOptions: minimatch.IOptions = { dot: true }; @@ -146,6 +146,11 @@ export interface IPackageOptions { */ readonly baseImagesUrl?: string; + /** + * The package manager to use. + */ + readonly packageManager?: PackageManagerLiteral; + /** * Should use Yarn instead of NPM. */ @@ -1667,11 +1672,11 @@ const defaultIgnore = [ async function collectAllFiles( cwd: string, - dependencies: 'npm' | 'yarn' | 'none' | undefined, + manager: PackageManagerLiteral | undefined, dependencyEntryPoints?: string[], followSymlinks: boolean = true ): Promise { - const deps = await getDependencies(cwd, dependencies, dependencyEntryPoints); + const deps = await getPackageManager(manager).pmProdDependencies(cwd, dependencyEntryPoints); const promises = deps.map(dep => glob('**', { cwd: dep, nodir: true, follow: followSymlinks, dot: true, ignore: 'node_modules/**' }).then(files => files.map(f => path.relative(cwd, path.join(dep, f))).map(f => f.replace(/\\/g, '/')) @@ -1681,24 +1686,21 @@ async function collectAllFiles( return Promise.all(promises).then(util.flatten); } -function getDependenciesOption(options: IPackageOptions): 'npm' | 'yarn' | 'none' | undefined { +function getDependenciesOption(options: IPackageOptions): PackageManagerLiteral | undefined { if (options.dependencies === false) { return 'none'; } - switch (options.useYarn) { - case true: - return 'yarn'; - case false: - return 'npm'; - default: - return undefined; + if (options.useYarn === undefined) { + return options.packageManager } + + return options.useYarn ? 'yarn' : 'npm' } function collectFiles( cwd: string, - dependencies: 'npm' | 'yarn' | 'none' | undefined, + dependencies: PackageManagerLiteral | undefined, dependencyEntryPoints?: string[], ignoreFile?: string, manifestFileIncludes?: string[], @@ -1871,24 +1873,39 @@ function getDefaultPackageName(manifest: ManifestPackage, options: IPackageOptio return `${manifest.name}-${version}.vsix`; } +async function detectYarn(cwd: string): Promise { + for (const name of ['yarn.lock', '.yarnrc', '.yarnrc.yaml', '.pnp.cjs', '.yarn']) { + const exists = await fs.promises.stat(path.join(cwd, name)).then(_ => true, _ => false) + if (!exists) { + continue; + } + if (!process.env['VSCE_TESTS']) { + util.log.info( + `Detected presence of ${name}. Using 'yarn' instead of 'npm' (to override this pass '--no-yarn' on the command line).` + ); + } + return true; + } + return false; +} + export async function prepublish(cwd: string, manifest: ManifestPackage, useYarn?: boolean): Promise { if (!manifest.scripts || !manifest.scripts['vscode:prepublish']) { return; } - if (useYarn === undefined) { - useYarn = await detectYarn(cwd); - } + useYarn ??= await detectYarn(cwd); - const tool = useYarn ? 'yarn' : 'npm'; - const prepublish = `${tool} run vscode:prepublish`; + const tool = useYarn === undefined ? undefined : (useYarn ? 'yarn' : 'npm'); + const manager = getPackageManager(tool) + const prepublish = manager.pmRunCommand("vscode:prepublish"); console.log(`Executing prepublish script '${prepublish}'...`); await new Promise((c, e) => { // Use string command to avoid Node.js DEP0190 warning (args + shell: true is deprecated). const child = cp.spawn(prepublish, { cwd, shell: true, stdio: 'inherit' }); - child.on('exit', code => (code === 0 ? c() : e(`${tool} failed with exit code ${code}`))); + child.on('exit', code => (code === 0 ? c() : e(`${manager.binaryName} failed with exit code ${code}`))); child.on('error', e); }); } @@ -1979,6 +1996,10 @@ export async function createSignatureArchive(manifestFile: string, signatureFile } export async function packageCommand(options: IPackageOptions = {}): Promise { + if (options.packageManager && !Managers.has(options.packageManager)) { + throw new Error(`'${options.packageManager}' is not a supported package manager. Valid managers: ${[...Managers].join(', ')}`); + } + const cwd = options.cwd || process.cwd(); const manifest = await readManifest(cwd); util.patchOptionsWithManifest(options, manifest); @@ -2025,6 +2046,7 @@ export async function listFiles(options: IListFilesOptions = {}): Promise { + if (options.packageManager && !Managers.has(options.packageManager)) { + throw new Error(`'${options.packageManager}' is not a supported package manager. Valid managers: ${[...Managers].join(', ')}`); + } + const cwd = process.cwd(); const manifest = await readManifest(cwd); diff --git a/src/publish.ts b/src/publish.ts index 7475afcb..df53cf16 100644 --- a/src/publish.ts +++ b/src/publish.ts @@ -14,6 +14,7 @@ import FormData from 'form-data'; import { basename } from 'path'; import { IterableBackoff, handleWhen, retry } from 'cockatiel'; import { getAzureCredentialAccessToken } from './auth'; +import { Managers, PackageManagerLiteral } from './managers/manager'; const tmpName = promisify(tmp.tmpName); @@ -51,6 +52,11 @@ export interface IPublishOptions { */ readonly baseImagesUrl?: string; + /** + * The package manager to use. + */ + readonly packageManager?: PackageManagerLiteral; + /** * Should use Yarn instead of NPM. */ @@ -92,6 +98,10 @@ export interface IPublishOptions { } export async function publish(options: IPublishOptions = {}): Promise { + if (options.packageManager && !Managers.has(options.packageManager)) { + throw new Error(`'${options.packageManager}' is not a supported package manager. Valid managers: ${[...Managers].join(', ')}`); + } + if (options.packagePath) { if (options.version) { throw new Error(`Both options not supported simultaneously: 'packagePath' and 'version'.`); From 92d3618b2581b67d55065ffdb999d34d45469e73 Mon Sep 17 00:00:00 2001 From: Mopsgamer <79159094+Mopsgamer@users.noreply.github.com> Date: Tue, 25 Nov 2025 11:44:07 +0100 Subject: [PATCH 02/12] support packageManager in main --- src/main.ts | 43 ++++++++++++++++++++++++----------------- src/managers/manager.ts | 1 + src/managers/none.ts | 3 ++- src/managers/npm.ts | 5 +++++ src/managers/yarn.ts | 5 +++++ 5 files changed, 38 insertions(+), 19 deletions(-) diff --git a/src/main.ts b/src/main.ts index e816ecc4..d87ba39f 100644 --- a/src/main.ts +++ b/src/main.ts @@ -8,8 +8,7 @@ import { listPublishers, deletePublisher, loginPublisher, logoutPublisher, verif import { CancellationToken, log } from './util'; import * as semver from 'semver'; import { isatty } from 'tty'; -import { pmNPM } from './managers/npm'; -import { Managers } from './managers/manager'; +import { getPackageManager, Managers, PackageManagerLiteral } from './managers/manager'; const pkg = require('../package.json'); @@ -36,13 +35,19 @@ See https://code.visualstudio.com/api/working-with-extensions/publishing-extensi process.exit(1); } -function main(task: Promise): void { +export interface IMainOptions { + readonly packageManager?: PackageManagerLiteral; + readonly useYarn?: boolean; +} + +function main(task: Promise, options: IMainOptions): void { let latestVersion: string | null = null; const token = new CancellationToken(); + const manager = getPackageManager(options.packageManager); if (isatty(1)) { - pmNPM.pmFetchLatestVersion(pkg.name, token) + manager.pmFetchLatestVersion(pkg.name, token) .then(version => (latestVersion = version)) .catch(_ => { /* noop */ @@ -51,7 +56,7 @@ function main(task: Promise): void { task.catch(fatal).then(() => { if (latestVersion && semver.gt(latestVersion, pkg.version)) { - log.warn(`The latest version of ${pkg.name} is ${latestVersion} and you have ${pkg.version}.\nUpdate it now: npm install -g ${pkg.name}`); + log.warn(`The latest version of ${pkg.name} is ${latestVersion} and you have ${pkg.version}.\nUpdate it now: ${manager.pmInstallCommand(pkg.name, true)}`); } else { token.cancel(); } @@ -86,7 +91,7 @@ module.exports = function (argv: string[]): void { .option('--readme-path ', 'Path to README file (defaults to README.md)') .option('--follow-symlinks', 'Recurse into symlinked directories instead of treating them as files') .action(({ tree, packageManager, yarn, packagedDependencies, ignoreFile, dependencies, readmePath, followSymlinks }) => - main(ls({ tree, packageManager, useYarn: yarn, packagedDependencies, ignoreFile, dependencies, readmePath, followSymlinks })) + main(ls({ tree, packageManager, useYarn: yarn, packagedDependencies, ignoreFile, dependencies, readmePath, followSymlinks }), { packageManager, useYarn: yarn }) ); program @@ -201,7 +206,8 @@ module.exports = function (argv: string[]): void { skipLicense, signTool, followSymlinks, - }) + }), + { packageManager, useYarn: yarn } ) ); @@ -340,7 +346,8 @@ module.exports = function (argv: string[]): void { skipLicense, signTool, followSymlinks - }) + }), + { packageManager, useYarn: yarn } ) ); @@ -350,14 +357,14 @@ module.exports = function (argv: string[]): void { .option('-p, --pat ', 'Personal Access Token') .option('--azure-credential', 'Use Microsoft Entra ID for authentication') .option('-f, --force', 'Skip confirmation prompt when unpublishing an extension') - .action((id, { pat, azureCredential, force }) => main(unpublish({ id, pat, azureCredential, force }))); + .action((id, { pat, azureCredential, force }) => main(unpublish({ id, pat, azureCredential, force }), { packageManager: 'npm', useYarn: false })); program .command('generate-manifest') .description('Generates the extension manifest from the provided VSIX package.') .requiredOption('-i, --packagePath ', 'Path to the VSIX package') .option('-o, --out ', 'Output the extension manifest to location (defaults to .manifest)') - .action(({ packagePath, out }) => main(generateManifest(packagePath, out))); + .action(({ packagePath, out }) => main(generateManifest(packagePath, out), { packageManager: 'npm', useYarn: false })); program .command('verify-signature') @@ -365,27 +372,27 @@ module.exports = function (argv: string[]): void { .requiredOption('-i, --packagePath ', 'Path to the VSIX package') .requiredOption('-m, --manifestPath ', 'Path to the Manifest file') .requiredOption('-s, --signaturePath ', 'Path to the Signature file') - .action(({ packagePath, manifestPath, signaturePath }) => main(verifySignature(packagePath, manifestPath, signaturePath))); + .action(({ packagePath, manifestPath, signaturePath }) => main(verifySignature(packagePath, manifestPath, signaturePath), { packageManager: 'npm', useYarn: false })); program .command('ls-publishers') .description('Lists all known publishers') - .action(() => main(listPublishers())); + .action(() => main(listPublishers(), { packageManager: 'npm', useYarn: false })); program .command('delete-publisher ') .description('Deletes a publisher from marketplace') - .action(publisher => main(deletePublisher(publisher))); + .action(publisher => main(deletePublisher(publisher), { packageManager: 'npm', useYarn: false })); program .command('login ') .description('Adds a publisher to the list of known publishers') - .action(name => main(loginPublisher(name))); + .action(name => main(loginPublisher(name), { packageManager: 'npm', useYarn: false })); program .command('logout ') .description('Removes a publisher from the list of known publishers') - .action(name => main(logoutPublisher(name))); + .action(name => main(logoutPublisher(name), { packageManager: 'npm', useYarn: false })); program .command('verify-pat [publisher]') @@ -396,13 +403,13 @@ module.exports = function (argv: string[]): void { process.env['VSCE_PAT'] ) .option('--azure-credential', 'Use Microsoft Entra ID for authentication') - .action((publisherName, { pat, azureCredential }) => main(verifyPat({ publisherName, pat, azureCredential }))); + .action((publisherName, { pat, azureCredential }) => main(verifyPat({ publisherName, pat, azureCredential }), { packageManager: 'npm', useYarn: false })); program .command('show ') .description(`Shows an extension's metadata`) .option('--json', 'Outputs data in json format', false) - .action((extensionid, { json }) => main(show(extensionid, json))); + .action((extensionid, { json }) => main(show(extensionid, json), { packageManager: 'npm', useYarn: false })); program .command('search ') @@ -410,7 +417,7 @@ module.exports = function (argv: string[]): void { .option('--json', 'Output results in json format', false) .option('--stats', 'Shows extensions rating and download count', false) .option('-p, --pagesize [value]', 'Number of results to return', '100') - .action((text, { json, pagesize, stats }) => main(search(text, json, parseInt(pagesize), stats))); + .action((text, { json, pagesize, stats }) => main(search(text, json, parseInt(pagesize), stats), { packageManager: 'npm', useYarn: false })); program.on('command:*', ([cmd]: string) => { if (cmd === 'create-publisher') { diff --git a/src/managers/manager.ts b/src/managers/manager.ts index 5063a649..787a4647 100644 --- a/src/managers/manager.ts +++ b/src/managers/manager.ts @@ -13,6 +13,7 @@ export interface IPackageManager { selfVersion(cancellationToken?: CancellationToken): Promise; selfCheck(cancellationToken?: CancellationToken): Promise; pmRunCommand(scriptName: string): string; + pmInstallCommand(packageName: string, global: boolean): string; pmFetchLatestVersion(name: string, cancellationToken?: CancellationToken): Promise; pmProdDependencies(cwd: string, packagedDependencies?: string[]): Promise; } diff --git a/src/managers/none.ts b/src/managers/none.ts index d6a6354c..0919f6fa 100644 --- a/src/managers/none.ts +++ b/src/managers/none.ts @@ -5,7 +5,8 @@ export const pmNone: IPackageManager = { binaryName: "", selfVersion: pmNPM.selfVersion.bind(pmNPM), selfCheck: pmNPM.selfCheck.bind(pmNPM), - pmRunCommand: pmNPM.pmRunCommand, + pmRunCommand: pmNPM.pmRunCommand.bind(pmNPM), + pmInstallCommand: pmNPM.pmInstallCommand.bind(pmNPM), async pmProdDependencies(cwd: string, _?: string[]): Promise { return [cwd] }, diff --git a/src/managers/npm.ts b/src/managers/npm.ts index eda6af1e..cb3afb4a 100644 --- a/src/managers/npm.ts +++ b/src/managers/npm.ts @@ -18,6 +18,11 @@ export const pmNPM: IPackageManager = { pmRunCommand(scriptName: string): string { return `${this.binaryName} run ${scriptName}` }, + pmInstallCommand(pkg: string, global: boolean): string { + let flag = (global ? '-g' : '') + flag &&= flag + " " + return `${this.binaryName} install ${flag}${pkg}` + }, async pmProdDependencies(cwd: string, _?: string[]): Promise { await this.selfCheck() const { stdout } = await exec('npm list --production --parseable --depth=99999 --loglevel=error', { cwd, maxBuffer: 5000 * 1024 }) diff --git a/src/managers/yarn.ts b/src/managers/yarn.ts index 9ef2a48b..617c5a41 100644 --- a/src/managers/yarn.ts +++ b/src/managers/yarn.ts @@ -20,6 +20,11 @@ export const pmYarn: IPackageManager = { pmRunCommand(scriptName: string): string { return `${this.binaryName} run ${scriptName}`; }, + pmInstallCommand(pkg: string, global: boolean): string { + let flag = (global ? 'global' : '') + flag &&= flag + " " + return `${this.binaryName} ${flag}add ${pkg}` + }, async pmFetchLatestVersion(name: string, cancellationToken?: CancellationToken): Promise { await this.selfCheck(cancellationToken) const { stdout } = await exec(`yarn info ${name} version`, {}, cancellationToken) From 9c9f56b5428c28a1f3593be92e6682d723b8648b Mon Sep 17 00:00:00 2001 From: Mopsgamer <79159094+Mopsgamer@users.noreply.github.com> Date: Tue, 25 Nov 2025 11:56:11 +0100 Subject: [PATCH 03/12] flatten(arr) -> arr.flat() --- src/package.ts | 2 +- src/test/package.test.ts | 6 +++--- src/util.ts | 4 ---- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/package.ts b/src/package.ts index 6c54cb8c..4a3ec3fb 100644 --- a/src/package.ts +++ b/src/package.ts @@ -1683,7 +1683,7 @@ async function collectAllFiles( ) ); - return Promise.all(promises).then(util.flatten); + return Promise.all(promises).then(arr => arr.flat()); } function getDependenciesOption(options: IPackageOptions): PackageManagerLiteral | undefined { diff --git a/src/test/package.test.ts b/src/test/package.test.ts index e250da37..72ba091c 100644 --- a/src/test/package.test.ts +++ b/src/test/package.test.ts @@ -24,7 +24,7 @@ import * as assert from 'assert'; import * as tmp from 'tmp'; import { spawnSync } from 'child_process'; import { XMLManifest, parseXmlManifest, parseContentTypes } from '../xml'; -import { flatten, log } from '../util'; +import { log } from '../util'; import { validatePublisher } from '../validation'; import * as jsonc from 'jsonc-parser'; @@ -53,8 +53,8 @@ const fixture = (name: string) => path.join(path.dirname(path.dirname(__dirname) function _toVsixManifest(manifest: ManifestPackage, files: IFile[], options: IPackageOptions = {}): Promise { const processors = createDefaultProcessors(manifest, options); return processFiles(processors, files).then(() => { - const assets = flatten(processors.map(p => p.assets)); - const tags = flatten(processors.map(p => p.tags)).join(','); + const assets = (processors.map(p => p.assets)).flat(); + const tags = (processors.map(p => p.tags)).flat().join(','); const vsix = processors.reduce((r, p) => ({ ...r, ...p.vsix }), { assets, tags } as VSIX); return toVsixManifest(vsix); diff --git a/src/util.ts b/src/util.ts index b18a4777..54641d4b 100644 --- a/src/util.ts +++ b/src/util.ts @@ -67,10 +67,6 @@ export function chain(initial: T, processors: P[], process: (a: T, b: P) = return chain2(initial, processors, process); } -export function flatten(arr: T[][]): T[] { - return ([] as T[]).concat.apply([], arr) as T[]; -} - export function nonnull(arg: T | null | undefined): arg is T { return !!arg; } From 04b818178a0a6c3f3f2dacbe7fbb3964c6345bc4 Mon Sep 17 00:00:00 2001 From: Mopsgamer <79159094+Mopsgamer@users.noreply.github.com> Date: Tue, 25 Nov 2025 12:47:05 +0100 Subject: [PATCH 04/12] style: rename --- src/main.ts | 4 ++-- src/managers/manager.ts | 11 +++++++---- src/managers/none.ts | 13 ++++++++----- src/managers/npm.ts | 19 +++++++++++-------- src/managers/yarn.ts | 12 ++++++++---- src/package.ts | 4 ++-- 6 files changed, 38 insertions(+), 25 deletions(-) diff --git a/src/main.ts b/src/main.ts index d87ba39f..a5051642 100644 --- a/src/main.ts +++ b/src/main.ts @@ -47,7 +47,7 @@ function main(task: Promise, options: IMainOptions): void { const manager = getPackageManager(options.packageManager); if (isatty(1)) { - manager.pmFetchLatestVersion(pkg.name, token) + manager.pkgRequestLatest(pkg.name, token) .then(version => (latestVersion = version)) .catch(_ => { /* noop */ @@ -56,7 +56,7 @@ function main(task: Promise, options: IMainOptions): void { task.catch(fatal).then(() => { if (latestVersion && semver.gt(latestVersion, pkg.version)) { - log.warn(`The latest version of ${pkg.name} is ${latestVersion} and you have ${pkg.version}.\nUpdate it now: ${manager.pmInstallCommand(pkg.name, true)}`); + log.warn(`The latest version of ${pkg.name} is ${latestVersion} and you have ${pkg.version}.\nUpdate it now: ${manager.commandInstall(pkg.name, true)}`); } else { token.cancel(); } diff --git a/src/managers/manager.ts b/src/managers/manager.ts index 787a4647..579b393d 100644 --- a/src/managers/manager.ts +++ b/src/managers/manager.ts @@ -10,12 +10,15 @@ export type PackageManagerLiteral = typeof managers[number]; export interface IPackageManager { binaryName: string; + selfVersion(cancellationToken?: CancellationToken): Promise; selfCheck(cancellationToken?: CancellationToken): Promise; - pmRunCommand(scriptName: string): string; - pmInstallCommand(packageName: string, global: boolean): string; - pmFetchLatestVersion(name: string, cancellationToken?: CancellationToken): Promise; - pmProdDependencies(cwd: string, packagedDependencies?: string[]): Promise; + + commandRun(scriptName: string): string; + commandInstall(packageName: string, global: boolean): string; + + pkgRequestLatest(name: string, cancellationToken?: CancellationToken): Promise; + pkgProdDependencies(cwd: string, packagedDependencies?: string[]): Promise; } export function getPackageManager( diff --git a/src/managers/none.ts b/src/managers/none.ts index 0919f6fa..1d389b28 100644 --- a/src/managers/none.ts +++ b/src/managers/none.ts @@ -3,12 +3,15 @@ import { pmNPM } from './npm'; export const pmNone: IPackageManager = { binaryName: "", + selfVersion: pmNPM.selfVersion.bind(pmNPM), selfCheck: pmNPM.selfCheck.bind(pmNPM), - pmRunCommand: pmNPM.pmRunCommand.bind(pmNPM), - pmInstallCommand: pmNPM.pmInstallCommand.bind(pmNPM), - async pmProdDependencies(cwd: string, _?: string[]): Promise { + + commandRun: pmNPM.commandRun.bind(pmNPM), + commandInstall: pmNPM.commandInstall.bind(pmNPM), + + pkgRequestLatest: pmNPM.pkgRequestLatest.bind(pmNPM), + async pkgProdDependencies(cwd: string, _?: string[]): Promise { return [cwd] - }, - pmFetchLatestVersion: pmNPM.pmFetchLatestVersion.bind(pmNPM), + } } \ No newline at end of file diff --git a/src/managers/npm.ts b/src/managers/npm.ts index cb3afb4a..5dab7459 100644 --- a/src/managers/npm.ts +++ b/src/managers/npm.ts @@ -5,6 +5,7 @@ import type { CancellationToken } from "../util"; export const pmNPM: IPackageManager = { binaryName: 'npm', + async selfVersion(cancellationToken?: CancellationToken): Promise { const { stdout } = await exec('npm -v', {}, cancellationToken); return stdout.trim(); @@ -15,22 +16,24 @@ export const pmNPM: IPackageManager = { throw new Error(`npm@${version} doesn't work with vsce. Please update npm: npm install -g npm`); } }, - pmRunCommand(scriptName: string): string { + + commandRun(scriptName: string): string { return `${this.binaryName} run ${scriptName}` }, - pmInstallCommand(pkg: string, global: boolean): string { + commandInstall(pkg: string, global: boolean): string { let flag = (global ? '-g' : '') flag &&= flag + " " return `${this.binaryName} install ${flag}${pkg}` }, - async pmProdDependencies(cwd: string, _?: string[]): Promise { - await this.selfCheck() - const { stdout } = await exec('npm list --production --parseable --depth=99999 --loglevel=error', { cwd, maxBuffer: 5000 * 1024 }) - return stdout.split(/[\r\n]/).filter(dir => path.isAbsolute(dir)) - }, - async pmFetchLatestVersion(name: string, cancellationToken?: CancellationToken): Promise { + + async pkgRequestLatest(name: string, cancellationToken?: CancellationToken): Promise { await this.selfCheck(cancellationToken) const { stdout } = await exec(`npm show ${name} version`, {}, cancellationToken) return stdout.split(/[\r\n]/).filter(line => !!line)[0]; + }, + async pkgProdDependencies(cwd: string, _?: string[]): Promise { + await this.selfCheck() + const { stdout } = await exec('npm list --production --parseable --depth=99999 --loglevel=error', { cwd, maxBuffer: 5000 * 1024 }) + return stdout.split(/[\r\n]/).filter(dir => path.isAbsolute(dir)) } } \ No newline at end of file diff --git a/src/managers/yarn.ts b/src/managers/yarn.ts index 617c5a41..3092e270 100644 --- a/src/managers/yarn.ts +++ b/src/managers/yarn.ts @@ -7,6 +7,7 @@ import { exec } from './exec'; export const pmYarn: IPackageManager = { binaryName: 'yarn', + async selfVersion(cancellationToken?: CancellationToken): Promise { const { stdout } = await exec('yarn -v', {}, cancellationToken); return stdout.trim(); @@ -17,19 +18,22 @@ export const pmYarn: IPackageManager = { throw new Error(`yarn@${version} doesn't work with vsce. Please update yarn: npm install -g yarn`); } }, - pmRunCommand(scriptName: string): string { + + commandRun(scriptName: string): string { return `${this.binaryName} run ${scriptName}`; }, - pmInstallCommand(pkg: string, global: boolean): string { + commandInstall(pkg: string, global: boolean): string { let flag = (global ? 'global' : '') flag &&= flag + " " return `${this.binaryName} ${flag}add ${pkg}` }, - async pmFetchLatestVersion(name: string, cancellationToken?: CancellationToken): Promise { + + async pkgRequestLatest(name: string, cancellationToken?: CancellationToken): Promise { await this.selfCheck(cancellationToken) const { stdout } = await exec(`yarn info ${name} version`, {}, cancellationToken) return stdout.split(/[\r\n]/).filter(line => !!line)[1]; }, + async pkgProdDependencies(cwd: string, packagedDependencies?: string[]): Promise { async pmProdDependencies(cwd: string, packagedDependencies?: string[]): Promise { const result = new Set([cwd]); @@ -41,7 +45,7 @@ export const pmYarn: IPackageManager = { deps.forEach(flatten); return [...result]; - }, + } } interface YarnTreeNode { diff --git a/src/package.ts b/src/package.ts index 4a3ec3fb..3f818a61 100644 --- a/src/package.ts +++ b/src/package.ts @@ -1676,7 +1676,7 @@ async function collectAllFiles( dependencyEntryPoints?: string[], followSymlinks: boolean = true ): Promise { - const deps = await getPackageManager(manager).pmProdDependencies(cwd, dependencyEntryPoints); + const deps = await getPackageManager(manager).pkgProdDependencies(cwd, dependencyEntryPoints); const promises = deps.map(dep => glob('**', { cwd: dep, nodir: true, follow: followSymlinks, dot: true, ignore: 'node_modules/**' }).then(files => files.map(f => path.relative(cwd, path.join(dep, f))).map(f => f.replace(/\\/g, '/')) @@ -1898,7 +1898,7 @@ export async function prepublish(cwd: string, manifest: ManifestPackage, useYarn const tool = useYarn === undefined ? undefined : (useYarn ? 'yarn' : 'npm'); const manager = getPackageManager(tool) - const prepublish = manager.pmRunCommand("vscode:prepublish"); + const prepublish = manager.commandRun("vscode:prepublish"); console.log(`Executing prepublish script '${prepublish}'...`); From abd5f7d9a7571e5f5badff1eef8de3855b5a4246 Mon Sep 17 00:00:00 2001 From: Mopsgamer <79159094+Mopsgamer@users.noreply.github.com> Date: Tue, 25 Nov 2025 12:47:25 +0100 Subject: [PATCH 05/12] fix: check versions --- src/managers/npm.ts | 5 +++-- src/managers/yarn.ts | 8 +++++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/managers/npm.ts b/src/managers/npm.ts index 5dab7459..44c72251 100644 --- a/src/managers/npm.ts +++ b/src/managers/npm.ts @@ -1,4 +1,5 @@ import * as path from 'path'; +import * as semver from 'semver'; import { exec } from "./exec"; import type { IPackageManager } from "./manager"; import type { CancellationToken } from "../util"; @@ -12,8 +13,8 @@ export const pmNPM: IPackageManager = { }, async selfCheck(cancellationToken?: CancellationToken): Promise { const version = await this.selfVersion(cancellationToken); - if (/^3\.7\.[0123]$/.test(version)) { - throw new Error(`npm@${version} doesn't work with vsce. Please update npm: npm install -g npm`); + if (semver.intersects(version, '< 6')) { + throw new Error(`npm@${version} doesn't work with vsce. Please update npm: ${this.commandInstall('npm', true)}`); } }, diff --git a/src/managers/yarn.ts b/src/managers/yarn.ts index 3092e270..1fc48f92 100644 --- a/src/managers/yarn.ts +++ b/src/managers/yarn.ts @@ -3,7 +3,9 @@ import * as cp from "child_process"; import { type CancellationToken, nonnull } from '../util'; import type { IPackageManager } from "./manager"; import parseSemver from 'parse-semver'; +import * as semver from 'semver'; import { exec } from './exec'; +import { pmNPM } from './npm'; export const pmYarn: IPackageManager = { binaryName: 'yarn', @@ -14,8 +16,8 @@ export const pmYarn: IPackageManager = { }, async selfCheck(cancellationToken?: CancellationToken): Promise { const version = await this.selfVersion(cancellationToken); - if (!version.startsWith("1")) { - throw new Error(`yarn@${version} doesn't work with vsce. Please update yarn: npm install -g yarn`); + if (semver.intersects(version, '>= 2')) { + throw new Error(`yarn@${version} doesn't work with vsce. Please update yarn: ${pmNPM.commandInstall('yarn', true)}`); } }, @@ -34,7 +36,7 @@ export const pmYarn: IPackageManager = { return stdout.split(/[\r\n]/).filter(line => !!line)[1]; }, async pkgProdDependencies(cwd: string, packagedDependencies?: string[]): Promise { - async pmProdDependencies(cwd: string, packagedDependencies?: string[]): Promise { + await this.selfCheck() const result = new Set([cwd]); const deps = await getYarnProductionDependencies(cwd, packagedDependencies); From 3a81d149e2db0e982359001cc410f9cd149f9f31 Mon Sep 17 00:00:00 2001 From: Mopsgamer <79159094+Mopsgamer@users.noreply.github.com> Date: Tue, 25 Nov 2025 13:08:06 +0100 Subject: [PATCH 06/12] jsdoc --- src/managers/manager.ts | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/src/managers/manager.ts b/src/managers/manager.ts index 579b393d..27cf068d 100644 --- a/src/managers/manager.ts +++ b/src/managers/manager.ts @@ -8,19 +8,50 @@ const managers = ['none', 'npm', 'yarn'] as const export const Managers = new Set(managers); export type PackageManagerLiteral = typeof managers[number]; +/** + * Interface for package manager implementations. + * Using interface to force explicit implementations. + */ export interface IPackageManager { + /** + * The binary name of the package manager. + * @example 'yarn' | 'npm' | 'bun' + */ binaryName: string; + /** + * Get the version of the package manager itself. + */ selfVersion(cancellationToken?: CancellationToken): Promise; + + /** + * Check if the package manager version and configs are compatible. + */ selfCheck(cancellationToken?: CancellationToken): Promise; + /** + * Get the command to run a script. + */ commandRun(scriptName: string): string; + /** + * Get the command to install a package. + */ commandInstall(packageName: string, global: boolean): string; + /** + * Request the latest version of a package from the registry. + */ pkgRequestLatest(name: string, cancellationToken?: CancellationToken): Promise; + /** + * Get the production dependencies of a package. + */ pkgProdDependencies(cwd: string, packagedDependencies?: string[]): Promise; } +/** + * Get package manager by preference. + * @returns Package manager implementation. + */ export function getPackageManager( preference: PackageManagerLiteral = "npm", ): IPackageManager { From b391392c8f5e824eb71c0222b7eb0de044eeab85 Mon Sep 17 00:00:00 2001 From: Mopsgamer <79159094+Mopsgamer@users.noreply.github.com> Date: Tue, 25 Nov 2025 13:27:08 +0100 Subject: [PATCH 07/12] add pkgProdDependenciesFiles --- src/managers/manager.ts | 4 + src/managers/npm.ts | 12 ++- src/package.ts | 177 +++++++++++++++++----------------------- 3 files changed, 92 insertions(+), 101 deletions(-) diff --git a/src/managers/manager.ts b/src/managers/manager.ts index 27cf068d..dd94bc02 100644 --- a/src/managers/manager.ts +++ b/src/managers/manager.ts @@ -46,6 +46,10 @@ export interface IPackageManager { * Get the production dependencies of a package. */ pkgProdDependencies(cwd: string, packagedDependencies?: string[]): Promise; + /** + * Get the files of production dependencies of a package. + */ + pkgProdDependenciesFiles(cwd: string, deps: string[], followSymlinks?: boolean): Promise; } /** diff --git a/src/managers/npm.ts b/src/managers/npm.ts index 44c72251..a4d72dbe 100644 --- a/src/managers/npm.ts +++ b/src/managers/npm.ts @@ -1,5 +1,6 @@ import * as path from 'path'; import * as semver from 'semver'; +import { glob } from 'glob'; import { exec } from "./exec"; import type { IPackageManager } from "./manager"; import type { CancellationToken } from "../util"; @@ -36,5 +37,14 @@ export const pmNPM: IPackageManager = { await this.selfCheck() const { stdout } = await exec('npm list --production --parseable --depth=99999 --loglevel=error', { cwd, maxBuffer: 5000 * 1024 }) return stdout.split(/[\r\n]/).filter(dir => path.isAbsolute(dir)) - } + }, + async pkgProdDependenciesFiles(cwd: string, deps: string[], followSymlinks?: boolean): Promise { + const promises = deps.map(dep => + glob('**', { cwd: dep, nodir: true, follow: followSymlinks, dot: true, ignore: 'node_modules/**' }).then(files => + files.map(f => path.relative(cwd, path.join(dep, f))).map(f => f.replace(/\\/g, '/')) + ) + ); + + return Promise.all(promises).then(arr => arr.flat()); + }, } \ No newline at end of file diff --git a/src/package.ts b/src/package.ts index 3f818a61..2b57b98b 100644 --- a/src/package.ts +++ b/src/package.ts @@ -6,7 +6,6 @@ import * as yazl from 'yazl'; import { ExtensionKind, ManifestPackage, UnverifiedManifest } from './manifest'; import { ITranslations, patchNLS } from './nls'; import * as util from './util'; -import { glob } from 'glob'; import minimatch from 'minimatch'; import markdownit from 'markdown-it'; import * as cheerio from 'cheerio'; @@ -1670,22 +1669,6 @@ const defaultIgnore = [ '**/.vscode-test-web/**', ]; -async function collectAllFiles( - cwd: string, - manager: PackageManagerLiteral | undefined, - dependencyEntryPoints?: string[], - followSymlinks: boolean = true -): Promise { - const deps = await getPackageManager(manager).pkgProdDependencies(cwd, dependencyEntryPoints); - const promises = deps.map(dep => - glob('**', { cwd: dep, nodir: true, follow: followSymlinks, dot: true, ignore: 'node_modules/**' }).then(files => - files.map(f => path.relative(cwd, path.join(dep, f))).map(f => f.replace(/\\/g, '/')) - ) - ); - - return Promise.all(promises).then(arr => arr.flat()); -} - function getDependenciesOption(options: IPackageOptions): PackageManagerLiteral | undefined { if (options.dependencies === false) { return 'none'; @@ -1698,7 +1681,7 @@ function getDependenciesOption(options: IPackageOptions): PackageManagerLiteral return options.useYarn ? 'yarn' : 'npm' } -function collectFiles( +async function collectFiles( cwd: string, dependencies: PackageManagerLiteral | undefined, dependencyEntryPoints?: string[], @@ -1710,91 +1693,85 @@ function collectFiles( readmePath = readmePath ?? 'README.md'; const notIgnored = ['!package.json', `!${readmePath}`]; - return collectAllFiles(cwd, dependencies, dependencyEntryPoints, followSymlinks).then(files => { - files = files.filter(f => !/\r$/m.test(f)); - - return ( - fs.promises - .readFile(ignoreFile ? ignoreFile : path.join(cwd, '.vscodeignore'), 'utf8') - .catch(err => - err.code !== 'ENOENT' ? - Promise.reject(err) : - ignoreFile ? - Promise.reject(err) : - // No .vscodeignore file exists - manifestFileIncludes ? - // include all files in manifestFileIncludes and ignore the rest - Promise.resolve(manifestFileIncludes.map(file => `!${file}`).concat(['**']).join('\n\r')) : - // "files" property not used in package.json - Promise.resolve('') - ) - - // Parse raw ignore by splitting output into lines and filtering out empty lines and comments - .then(rawIgnore => - rawIgnore - .split(/[\n\r]/) - .map(s => s.trim()) - .filter(s => !!s) - .filter(i => !/^\s*#/.test(i)) - ) - - // Add '/**' to possible folder names - .then(ignore => [ - ...ignore, - ...ignore.filter(i => !/(^|\/)[^/]*\*[^/]*$/.test(i)).map(i => (/\/$/.test(i) ? `${i}**` : `${i}/**`)), - ]) - - // Combine with default ignore list - .then(ignore => [...defaultIgnore, ...ignore, ...notIgnored]) - - // Split into ignore and negate list - .then(ignore => - ignore.reduce<[string[], string[]]>( - (r, e) => (!/^\s*!/.test(e) ? [[...r[0], e], r[1]] : [r[0], [...r[1], e]]), - [[], []] - ) - ) - .then(r => ({ ignore: r[0], negate: r[1] })) - - // Filter out files - .then(({ ignore, negate }) => - files.filter( - f => - !ignore.some(i => minimatch(f, i, MinimatchOptions)) || - negate.some(i => minimatch(f, i.substr(1), MinimatchOptions)) - ) - ) - ); - }); + const manager = getPackageManager(dependencies) + + const files = (await manager.pkgProdDependenciesFiles( + cwd, + await manager.pkgProdDependencies(cwd, dependencyEntryPoints), + followSymlinks + )) + .filter(f => !/\r$/m.test(f)); + return await ( + fs.promises + .readFile(ignoreFile ? ignoreFile : path.join(cwd, '.vscodeignore'), 'utf8') + .catch(err => err.code !== 'ENOENT' ? + Promise.reject(err) : + ignoreFile ? + Promise.reject(err) : + // No .vscodeignore file exists + manifestFileIncludes ? + // include all files in manifestFileIncludes and ignore the rest + Promise.resolve(manifestFileIncludes.map(file => `!${file}`).concat(['**']).join('\n\r')) : + // "files" property not used in package.json + Promise.resolve('') + ) + + // Parse raw ignore by splitting output into lines and filtering out empty lines and comments + .then(rawIgnore => rawIgnore + .split(/[\n\r]/) + .map(s => s.trim()) + .filter(s_1 => !!s_1) + .filter(i => !/^\s*#/.test(i)) + ) + + // Add '/**' to possible folder names + .then(ignore => [ + ...ignore, + ...ignore.filter(i_1 => !/(^|\/)[^/]*\*[^/]*$/.test(i_1)).map(i_2 => (/\/$/.test(i_2) ? `${i_2}**` : `${i_2}/**`)), + ]) + + // Combine with default ignore list + .then(ignore_1 => [...defaultIgnore, ...ignore_1, ...notIgnored]) + + // Split into ignore and negate list + .then(ignore_2 => ignore_2.reduce<[string[], string[]]>( + (r, e) => (!/^\s*!/.test(e) ? [[...r[0], e], r[1]] : [r[0], [...r[1], e]]), + [[], []] + ) + ) + .then(r_1 => ({ ignore: r_1[0], negate: r_1[1] })) + + // Filter out files + .then(({ ignore: ignore_3, negate }) => files.filter( + f_1 => !ignore_3.some(i_3 => minimatch(f_1, i_3, MinimatchOptions)) || + negate.some(i_4 => minimatch(f_1, i_4.substr(1), MinimatchOptions)) + )) + ); } -export function processFiles(processors: IProcessor[], files: IFile[]): Promise { - const processedFiles = files.map(file => util.chain(file, processors, (file, processor) => processor.onFile(file))); +export async function processFiles(processors: IProcessor[], files: IFile[]): Promise { + const processFiles = files.map(file => util.chain(file, processors, (file, processor) => processor.onFile(file))); - return Promise.all(processedFiles).then(files => { - return util.sequence(processors.map(p => () => p.onEnd())).then(() => { - const assets = processors.reduce((r, p) => [...r, ...p.assets], []); - const tags = [ - ...processors.reduce>((r, p) => { - for (const tag of p.tags) { - if (tag) { - r.add(tag); - } - } - return r; - }, new Set()), - ].join(','); - const vsix = processors.reduce((r, p) => ({ ...r, ...p.vsix }), { assets, tags } as VSIX); - - return Promise.all([toVsixManifest(vsix), toContentTypes(files)]).then(result => { - return [ - { path: 'extension.vsixmanifest', contents: Buffer.from(result[0], 'utf8') }, - { path: '[Content_Types].xml', contents: Buffer.from(result[1], 'utf8') }, - ...files, - ]; - }); - }); - }); + const processedFiles = await Promise.all(processFiles); + await util.sequence(processors.map(p => () => p.onEnd())); + const assets = processors.reduce((r, p_1) => [...r, ...p_1.assets], []); + const tags = [ + ...processors.reduce>((r_1, p_2) => { + for (const tag of p_2.tags) { + if (tag) { + r_1.add(tag); + } + } + return r_1; + }, new Set()), + ].join(','); + const vsix = processors.reduce((r_2, p_3) => ({ ...r_2, ...p_3.vsix }), { assets, tags } as VSIX); + const result_1 = await Promise.all([toVsixManifest(vsix), toContentTypes(processedFiles)]); + return [ + { path: 'extension.vsixmanifest', contents: Buffer.from(result_1[0], 'utf8') }, + { path: '[Content_Types].xml', contents: Buffer.from(result_1[1], 'utf8') }, + ...processedFiles, + ]; } export function createDefaultProcessors(manifest: ManifestPackage, options: IPackageOptions = {}): IProcessor[] { From cbba0a54771f668945be824c3de7eb0c13948fe9 Mon Sep 17 00:00:00 2001 From: Mopsgamer <79159094+Mopsgamer@users.noreply.github.com> Date: Tue, 25 Nov 2025 13:30:39 +0100 Subject: [PATCH 08/12] distribute pkgProdDependenciesFiles --- src/managers/manager.ts | 1 + src/managers/none.ts | 3 ++- src/managers/yarn.ts | 3 ++- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/managers/manager.ts b/src/managers/manager.ts index dd94bc02..5cefd79a 100644 --- a/src/managers/manager.ts +++ b/src/managers/manager.ts @@ -48,6 +48,7 @@ export interface IPackageManager { pkgProdDependencies(cwd: string, packagedDependencies?: string[]): Promise; /** * Get the files of production dependencies of a package. + * Should use pkgProdDependencies first to get the dependencies. */ pkgProdDependenciesFiles(cwd: string, deps: string[], followSymlinks?: boolean): Promise; } diff --git a/src/managers/none.ts b/src/managers/none.ts index 1d389b28..dcc1dded 100644 --- a/src/managers/none.ts +++ b/src/managers/none.ts @@ -13,5 +13,6 @@ export const pmNone: IPackageManager = { pkgRequestLatest: pmNPM.pkgRequestLatest.bind(pmNPM), async pkgProdDependencies(cwd: string, _?: string[]): Promise { return [cwd] - } + }, + pkgProdDependenciesFiles: pmNPM.pkgProdDependenciesFiles.bind(pmNPM), } \ No newline at end of file diff --git a/src/managers/yarn.ts b/src/managers/yarn.ts index 1fc48f92..5d0f887c 100644 --- a/src/managers/yarn.ts +++ b/src/managers/yarn.ts @@ -47,7 +47,8 @@ export const pmYarn: IPackageManager = { deps.forEach(flatten); return [...result]; - } + }, + pkgProdDependenciesFiles: pmNPM.pkgProdDependenciesFiles.bind(pmNPM), } interface YarnTreeNode { From 9b99a9f6315cc57e05c817d8b8b15050ecaa3cf6 Mon Sep 17 00:00:00 2001 From: Mopsgamer <79159094+Mopsgamer@users.noreply.github.com> Date: Sun, 30 Nov 2025 23:27:37 +0100 Subject: [PATCH 09/12] style: pkgProdDependenciesFiles await --- src/managers/manager.ts | 3 +++ src/managers/npm.ts | 3 ++- src/managers/yarn.ts | 3 ++- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/managers/manager.ts b/src/managers/manager.ts index 5cefd79a..80df95fb 100644 --- a/src/managers/manager.ts +++ b/src/managers/manager.ts @@ -33,6 +33,7 @@ export interface IPackageManager { * Get the command to run a script. */ commandRun(scriptName: string): string; + /** * Get the command to install a package. */ @@ -42,10 +43,12 @@ export interface IPackageManager { * Request the latest version of a package from the registry. */ pkgRequestLatest(name: string, cancellationToken?: CancellationToken): Promise; + /** * Get the production dependencies of a package. */ pkgProdDependencies(cwd: string, packagedDependencies?: string[]): Promise; + /** * Get the files of production dependencies of a package. * Should use pkgProdDependencies first to get the dependencies. diff --git a/src/managers/npm.ts b/src/managers/npm.ts index a4d72dbe..122c8625 100644 --- a/src/managers/npm.ts +++ b/src/managers/npm.ts @@ -45,6 +45,7 @@ export const pmNPM: IPackageManager = { ) ); - return Promise.all(promises).then(arr => arr.flat()); + const files = (await Promise.all(promises)).flat() + return files; }, } \ No newline at end of file diff --git a/src/managers/yarn.ts b/src/managers/yarn.ts index 5d0f887c..c74e89f8 100644 --- a/src/managers/yarn.ts +++ b/src/managers/yarn.ts @@ -46,7 +46,8 @@ export const pmYarn: IPackageManager = { }; deps.forEach(flatten); - return [...result]; + const files = [...result] + return files; }, pkgProdDependenciesFiles: pmNPM.pkgProdDependenciesFiles.bind(pmNPM), } From 893003bca36b0c4f2a027295795506a22280852d Mon Sep 17 00:00:00 2001 From: Mopsgamer <79159094+Mopsgamer@users.noreply.github.com> Date: Tue, 2 Dec 2025 17:51:23 +0100 Subject: [PATCH 10/12] style: classes --- src/managers/manager.ts | 22 +++++++++++----------- src/managers/none.ts | 21 +++++++-------------- src/managers/npm.ts | 24 +++++++++++++----------- src/managers/yarn.ts | 22 +++++++++++----------- 4 files changed, 42 insertions(+), 47 deletions(-) diff --git a/src/managers/manager.ts b/src/managers/manager.ts index 80df95fb..9b2e8e99 100644 --- a/src/managers/manager.ts +++ b/src/managers/manager.ts @@ -12,48 +12,48 @@ export type PackageManagerLiteral = typeof managers[number]; * Interface for package manager implementations. * Using interface to force explicit implementations. */ -export interface IPackageManager { +export abstract class PackageManager { /** * The binary name of the package manager. * @example 'yarn' | 'npm' | 'bun' */ - binaryName: string; + abstract binaryName: string; /** * Get the version of the package manager itself. */ - selfVersion(cancellationToken?: CancellationToken): Promise; + abstract selfVersion(cancellationToken?: CancellationToken): Promise; /** * Check if the package manager version and configs are compatible. */ - selfCheck(cancellationToken?: CancellationToken): Promise; + abstract selfCheck(cancellationToken?: CancellationToken): Promise; /** * Get the command to run a script. */ - commandRun(scriptName: string): string; + abstract commandRun(scriptName: string): string; /** * Get the command to install a package. */ - commandInstall(packageName: string, global: boolean): string; + abstract commandInstall(packageName: string, global: boolean): string; /** * Request the latest version of a package from the registry. */ - pkgRequestLatest(name: string, cancellationToken?: CancellationToken): Promise; + abstract pkgRequestLatest(name: string, cancellationToken?: CancellationToken): Promise; /** * Get the production dependencies of a package. */ - pkgProdDependencies(cwd: string, packagedDependencies?: string[]): Promise; + abstract pkgProdDependencies(cwd: string, packagedDependencies?: string[]): Promise; /** * Get the files of production dependencies of a package. * Should use pkgProdDependencies first to get the dependencies. */ - pkgProdDependenciesFiles(cwd: string, deps: string[], followSymlinks?: boolean): Promise; + abstract pkgProdDependenciesFiles(cwd: string, deps: string[], followSymlinks?: boolean): Promise; } /** @@ -62,12 +62,12 @@ export interface IPackageManager { */ export function getPackageManager( preference: PackageManagerLiteral = "npm", -): IPackageManager { +): PackageManager { const choice = { "none": pmNone, "npm": pmNPM, "yarn": pmYarn, - } as Record + } as Record return choice[preference] } diff --git a/src/managers/none.ts b/src/managers/none.ts index dcc1dded..1257d7ea 100644 --- a/src/managers/none.ts +++ b/src/managers/none.ts @@ -1,18 +1,11 @@ -import type { IPackageManager } from "./manager"; -import { pmNPM } from './npm'; +import { PackageManagerNpm } from './npm'; -export const pmNone: IPackageManager = { - binaryName: "", +class PackageManagerNone extends PackageManagerNpm { + binaryName = "" - selfVersion: pmNPM.selfVersion.bind(pmNPM), - selfCheck: pmNPM.selfCheck.bind(pmNPM), - - commandRun: pmNPM.commandRun.bind(pmNPM), - commandInstall: pmNPM.commandInstall.bind(pmNPM), - - pkgRequestLatest: pmNPM.pkgRequestLatest.bind(pmNPM), async pkgProdDependencies(cwd: string, _?: string[]): Promise { return [cwd] - }, - pkgProdDependenciesFiles: pmNPM.pkgProdDependenciesFiles.bind(pmNPM), -} \ No newline at end of file + } +} + +export const pmNone = new PackageManagerNone() diff --git a/src/managers/npm.ts b/src/managers/npm.ts index 122c8625..56c44168 100644 --- a/src/managers/npm.ts +++ b/src/managers/npm.ts @@ -2,42 +2,42 @@ import * as path from 'path'; import * as semver from 'semver'; import { glob } from 'glob'; import { exec } from "./exec"; -import type { IPackageManager } from "./manager"; +import { PackageManager } from "./manager"; import type { CancellationToken } from "../util"; -export const pmNPM: IPackageManager = { - binaryName: 'npm', +export class PackageManagerNpm extends PackageManager { + binaryName = 'npm' async selfVersion(cancellationToken?: CancellationToken): Promise { const { stdout } = await exec('npm -v', {}, cancellationToken); return stdout.trim(); - }, + } async selfCheck(cancellationToken?: CancellationToken): Promise { const version = await this.selfVersion(cancellationToken); if (semver.intersects(version, '< 6')) { throw new Error(`npm@${version} doesn't work with vsce. Please update npm: ${this.commandInstall('npm', true)}`); } - }, + } commandRun(scriptName: string): string { return `${this.binaryName} run ${scriptName}` - }, + } commandInstall(pkg: string, global: boolean): string { let flag = (global ? '-g' : '') flag &&= flag + " " return `${this.binaryName} install ${flag}${pkg}` - }, + } async pkgRequestLatest(name: string, cancellationToken?: CancellationToken): Promise { await this.selfCheck(cancellationToken) const { stdout } = await exec(`npm show ${name} version`, {}, cancellationToken) return stdout.split(/[\r\n]/).filter(line => !!line)[0]; - }, + } async pkgProdDependencies(cwd: string, _?: string[]): Promise { await this.selfCheck() const { stdout } = await exec('npm list --production --parseable --depth=99999 --loglevel=error', { cwd, maxBuffer: 5000 * 1024 }) return stdout.split(/[\r\n]/).filter(dir => path.isAbsolute(dir)) - }, + } async pkgProdDependenciesFiles(cwd: string, deps: string[], followSymlinks?: boolean): Promise { const promises = deps.map(dep => glob('**', { cwd: dep, nodir: true, follow: followSymlinks, dot: true, ignore: 'node_modules/**' }).then(files => @@ -47,5 +47,7 @@ export const pmNPM: IPackageManager = { const files = (await Promise.all(promises)).flat() return files; - }, -} \ No newline at end of file + } +} + +export const pmNPM = new PackageManagerNpm() diff --git a/src/managers/yarn.ts b/src/managers/yarn.ts index c74e89f8..94cee0ee 100644 --- a/src/managers/yarn.ts +++ b/src/managers/yarn.ts @@ -1,40 +1,39 @@ import * as path from 'path'; import * as cp from "child_process"; import { type CancellationToken, nonnull } from '../util'; -import type { IPackageManager } from "./manager"; import parseSemver from 'parse-semver'; import * as semver from 'semver'; import { exec } from './exec'; -import { pmNPM } from './npm'; +import { pmNPM, PackageManagerNpm } from './npm'; -export const pmYarn: IPackageManager = { - binaryName: 'yarn', +export class PackageManagerYarn extends PackageManagerNpm { + binaryName = 'yarn' async selfVersion(cancellationToken?: CancellationToken): Promise { const { stdout } = await exec('yarn -v', {}, cancellationToken); return stdout.trim(); - }, + } async selfCheck(cancellationToken?: CancellationToken): Promise { const version = await this.selfVersion(cancellationToken); if (semver.intersects(version, '>= 2')) { throw new Error(`yarn@${version} doesn't work with vsce. Please update yarn: ${pmNPM.commandInstall('yarn', true)}`); } - }, + } commandRun(scriptName: string): string { return `${this.binaryName} run ${scriptName}`; - }, + } commandInstall(pkg: string, global: boolean): string { let flag = (global ? 'global' : '') flag &&= flag + " " return `${this.binaryName} ${flag}add ${pkg}` - }, + } async pkgRequestLatest(name: string, cancellationToken?: CancellationToken): Promise { await this.selfCheck(cancellationToken) const { stdout } = await exec(`yarn info ${name} version`, {}, cancellationToken) return stdout.split(/[\r\n]/).filter(line => !!line)[1]; - }, + } async pkgProdDependencies(cwd: string, packagedDependencies?: string[]): Promise { await this.selfCheck() const result = new Set([cwd]); @@ -48,10 +47,11 @@ export const pmYarn: IPackageManager = { const files = [...result] return files; - }, - pkgProdDependenciesFiles: pmNPM.pkgProdDependenciesFiles.bind(pmNPM), + } } +export const pmYarn = new PackageManagerYarn() + interface YarnTreeNode { name: string; children: YarnTreeNode[]; From 876b63f80d82901c5f3bbb0bccb2adc821cfd80c Mon Sep 17 00:00:00 2001 From: Mopsgamer <79159094+Mopsgamer@users.noreply.github.com> Date: Tue, 2 Dec 2025 18:30:52 +0100 Subject: [PATCH 11/12] style: single assert func --- src/managers/manager.ts | 13 +++++++++++++ src/package.ts | 10 +++------- src/publish.ts | 6 ++---- 3 files changed, 18 insertions(+), 11 deletions(-) diff --git a/src/managers/manager.ts b/src/managers/manager.ts index 9b2e8e99..3d0d0c17 100644 --- a/src/managers/manager.ts +++ b/src/managers/manager.ts @@ -71,3 +71,16 @@ export function getPackageManager( return choice[preference] } + +/** + * Throws only for strings that are not valid package managers. + * `undefiend` is allowed. + */ +export function assertPackageManager(packageManager: string | undefined): asserts packageManager is PackageManagerLiteral | undefined { + if (packageManager === undefined) { + return + } + if (!Managers.has(packageManager as PackageManagerLiteral)) { + throw new Error(`'${packageManager}' is not a supported package manager. Valid managers: ${[...Managers].join(', ')}`); + } +} diff --git a/src/package.ts b/src/package.ts index 2b57b98b..109fd37e 100644 --- a/src/package.ts +++ b/src/package.ts @@ -27,7 +27,7 @@ import parseSemver from 'parse-semver'; import * as jsonc from 'jsonc-parser'; import * as vsceSign from '@vscode/vsce-sign'; import { getRuleNameFromRuleId, lintFiles, lintText, prettyPrintLintResult } from './secretLint'; -import { getPackageManager, Managers, PackageManagerLiteral } from './managers/manager'; +import { getPackageManager, PackageManagerLiteral, assertPackageManager } from './managers/manager'; const MinimatchOptions: minimatch.IOptions = { dot: true }; @@ -1973,9 +1973,7 @@ export async function createSignatureArchive(manifestFile: string, signatureFile } export async function packageCommand(options: IPackageOptions = {}): Promise { - if (options.packageManager && !Managers.has(options.packageManager)) { - throw new Error(`'${options.packageManager}' is not a supported package manager. Valid managers: ${[...Managers].join(', ')}`); - } + assertPackageManager(options.packageManager) const cwd = options.cwd || process.cwd(); const manifest = await readManifest(cwd); @@ -2036,9 +2034,7 @@ interface ILSOptions { * Lists the files included in the extension's package. */ export async function ls(options: ILSOptions = {}): Promise { - if (options.packageManager && !Managers.has(options.packageManager)) { - throw new Error(`'${options.packageManager}' is not a supported package manager. Valid managers: ${[...Managers].join(', ')}`); - } + assertPackageManager(options.packageManager) const cwd = process.cwd(); const manifest = await readManifest(cwd); diff --git a/src/publish.ts b/src/publish.ts index df53cf16..a3e6aa17 100644 --- a/src/publish.ts +++ b/src/publish.ts @@ -14,7 +14,7 @@ import FormData from 'form-data'; import { basename } from 'path'; import { IterableBackoff, handleWhen, retry } from 'cockatiel'; import { getAzureCredentialAccessToken } from './auth'; -import { Managers, PackageManagerLiteral } from './managers/manager'; +import { PackageManagerLiteral, assertPackageManager } from './managers/manager'; const tmpName = promisify(tmp.tmpName); @@ -98,9 +98,7 @@ export interface IPublishOptions { } export async function publish(options: IPublishOptions = {}): Promise { - if (options.packageManager && !Managers.has(options.packageManager)) { - throw new Error(`'${options.packageManager}' is not a supported package manager. Valid managers: ${[...Managers].join(', ')}`); - } + assertPackageManager(options.packageManager) if (options.packagePath) { if (options.version) { From 52761faa99954532295d498e989851d1c20db471 Mon Sep 17 00:00:00 2001 From: Mopsgamer <79159094+Mopsgamer@users.noreply.github.com> Date: Wed, 10 Dec 2025 13:15:27 +0100 Subject: [PATCH 12/12] fix: import cycle --- src/main.ts | 2 +- src/managers/index.ts | 38 ++++++++++++++++++++++++++++++++++++++ src/managers/manager.ts | 37 ------------------------------------- src/package.ts | 2 +- src/publish.ts | 2 +- 5 files changed, 41 insertions(+), 40 deletions(-) create mode 100644 src/managers/index.ts diff --git a/src/main.ts b/src/main.ts index a5051642..705e09d4 100644 --- a/src/main.ts +++ b/src/main.ts @@ -8,7 +8,7 @@ import { listPublishers, deletePublisher, loginPublisher, logoutPublisher, verif import { CancellationToken, log } from './util'; import * as semver from 'semver'; import { isatty } from 'tty'; -import { getPackageManager, Managers, PackageManagerLiteral } from './managers/manager'; +import { getPackageManager, Managers, PackageManagerLiteral } from './managers'; const pkg = require('../package.json'); diff --git a/src/managers/index.ts b/src/managers/index.ts new file mode 100644 index 00000000..4df1a690 --- /dev/null +++ b/src/managers/index.ts @@ -0,0 +1,38 @@ +import { PackageManager } from "./manager"; +import { pmNone } from "./none"; +import { pmNPM } from "./npm"; +import { pmYarn } from "./yarn"; + +// Reminder: scr/api.ts (PackageManager enum). +const managers = ['none', 'npm', 'yarn'] as const +export const Managers = new Set(managers); +export type PackageManagerLiteral = typeof managers[number]; + +/** + * Get package manager by preference. + * @returns Package manager implementation. + */ +export function getPackageManager( + preference: PackageManagerLiteral = "npm", +): PackageManager { + const choice = { + "none": pmNone, + "npm": pmNPM, + "yarn": pmYarn, + } as Record + + return choice[preference] +} + +/** + * Throws only for strings that are not valid package managers. + * `undefiend` is allowed. + */ +export function assertPackageManager(packageManager: string | undefined): asserts packageManager is PackageManagerLiteral | undefined { + if (packageManager === undefined) { + return + } + if (!Managers.has(packageManager as PackageManagerLiteral)) { + throw new Error(`'${packageManager}' is not a supported package manager. Valid managers: ${[...Managers].join(', ')}`); + } +} \ No newline at end of file diff --git a/src/managers/manager.ts b/src/managers/manager.ts index 3d0d0c17..33f7f6bd 100644 --- a/src/managers/manager.ts +++ b/src/managers/manager.ts @@ -1,12 +1,4 @@ import { CancellationToken } from "../util"; -import { pmNone } from "./none"; -import { pmNPM } from "./npm"; -import { pmYarn } from "./yarn"; - -// Reminder: scr/api.ts (PackageManager enum). -const managers = ['none', 'npm', 'yarn'] as const -export const Managers = new Set(managers); -export type PackageManagerLiteral = typeof managers[number]; /** * Interface for package manager implementations. @@ -55,32 +47,3 @@ export abstract class PackageManager { */ abstract pkgProdDependenciesFiles(cwd: string, deps: string[], followSymlinks?: boolean): Promise; } - -/** - * Get package manager by preference. - * @returns Package manager implementation. - */ -export function getPackageManager( - preference: PackageManagerLiteral = "npm", -): PackageManager { - const choice = { - "none": pmNone, - "npm": pmNPM, - "yarn": pmYarn, - } as Record - - return choice[preference] -} - -/** - * Throws only for strings that are not valid package managers. - * `undefiend` is allowed. - */ -export function assertPackageManager(packageManager: string | undefined): asserts packageManager is PackageManagerLiteral | undefined { - if (packageManager === undefined) { - return - } - if (!Managers.has(packageManager as PackageManagerLiteral)) { - throw new Error(`'${packageManager}' is not a supported package manager. Valid managers: ${[...Managers].join(', ')}`); - } -} diff --git a/src/package.ts b/src/package.ts index 109fd37e..a767f58d 100644 --- a/src/package.ts +++ b/src/package.ts @@ -27,7 +27,7 @@ import parseSemver from 'parse-semver'; import * as jsonc from 'jsonc-parser'; import * as vsceSign from '@vscode/vsce-sign'; import { getRuleNameFromRuleId, lintFiles, lintText, prettyPrintLintResult } from './secretLint'; -import { getPackageManager, PackageManagerLiteral, assertPackageManager } from './managers/manager'; +import { getPackageManager, assertPackageManager, PackageManagerLiteral } from './managers'; const MinimatchOptions: minimatch.IOptions = { dot: true }; diff --git a/src/publish.ts b/src/publish.ts index a3e6aa17..c415026a 100644 --- a/src/publish.ts +++ b/src/publish.ts @@ -14,7 +14,7 @@ import FormData from 'form-data'; import { basename } from 'path'; import { IterableBackoff, handleWhen, retry } from 'cockatiel'; import { getAzureCredentialAccessToken } from './auth'; -import { PackageManagerLiteral, assertPackageManager } from './managers/manager'; +import { assertPackageManager, PackageManagerLiteral } from './managers'; const tmpName = promisify(tmp.tmpName);