diff --git a/cli/CHANGELOG.md b/cli/CHANGELOG.md index a2b092880..303864667 100644 --- a/cli/CHANGELOG.md +++ b/cli/CHANGELOG.md @@ -4,15 +4,21 @@ This change log covers only the command line interface (CLI) of Open VSX. ### [next] (unreleased) +#### Breaking Changes + +- The minimum version of Node.js required is now `22`, as `20` reached its end of life + #### Added - Add an encrypted filestore as fallback to the system keychain if it cant be accessed ([#1950](https://github.com/eclipse/openvsx/pull/1950)) - Add `--allow-missing-repository` option to the `publish` command, passed on to `vsce` to package an extension whose `package.json` has no `repository` field without asking for confirmation ([#1735](https://github.com/eclipse-openvsx/openvsx/issues/1735)) +- Add `publishVSIX` and `createVSIX` to the programmatic API, a `log` option to redirect or silence the output and an `interactive` option to control prompting ([#1591](https://github.com/eclipse-openvsx/openvsx/issues/1591)) #### Changed - Replace `keytar` with `cross-keychain` to store credentials in the system keychain ([#1950](https://github.com/eclipse/openvsx/pull/1950)) - Mask token input when using `login` command ([#1966](https://github.com/eclipse-openvsx/openvsx/pull/1966) +- `publish` resolves with the published extensions instead of `void`, and no longer copies the environment into the options object of the caller ([#1591](https://github.com/eclipse-openvsx/openvsx/issues/1591)) #### Dependencies diff --git a/cli/README.md b/cli/README.md index bfbbc6427..a36790a16 100644 --- a/cli/README.md +++ b/cli/README.md @@ -60,3 +60,39 @@ The `logout` command lets you remove a stored access token. the name must correspond to the `publisher` of your extension. By default `ovsx` stores access tokens in the operating system's credential manager (via [`cross-keychain`](https://www.npmjs.com/package/cross-keychain)), falling back to storing them as plaintext in the `~/.ovsx` file if the credential manager can't be used. You can also set the environment variable `OVSX_STORE=file` to force plaintext storage; this is strongly discouraged, as it leaves your tokens readable by anyone with access to your home directory. + +### Programmatic API + +Every command is available as a function, so `ovsx` can be scripted instead of shelled out to. This is +useful for publishing to Open VSX and the Visual Studio Marketplace from a single script: + +```ts +import { createVSIX } from '@vscode/vsce'; +import { publishVSIX } from 'ovsx'; + +const vsix = 'my-extension-1.0.0.vsix'; +await createVSIX({ packagePath: vsix }); +await publishVSIX(vsix, { pat: process.env.OVSX_PAT }); +``` + +`publishVSIX` publishes packages that exist already and resolves with the metadata of the published +extensions, rejecting as soon as one of them fails. `createVSIX` packages an extension — delegating to +`vsce` and validating the license if the target registry requires one — and resolves with the path of +the package it wrote. + +Unlike the command line interface, these two functions never ask for input: a missing access token is +an error rather than a prompt. Pass `interactive: true` to opt back into prompting. + +Progress messages go to the console by default. Pass a `log` implementation to capture or silence +them (note that this covers the output of `ovsx` itself; `vsce` reports the progress of packaging on +its own): + +```ts +import { publishVSIX, silentLogger } from 'ovsx'; + +await publishVSIX(vsix, { pat: process.env.OVSX_PAT, log: silentLogger }); +``` + +The lower level building blocks are exported as well: `publish` (packages and publishes, reporting the +outcome of every package and target separately), `getExtension`, `createNamespace`, `verifyPat` and the +`Registry` class that wraps the registry API. diff --git a/cli/bin/ovsx b/cli/bin/ovsx index e0b049365..afac11d34 100755 --- a/cli/bin/ovsx +++ b/cli/bin/ovsx @@ -2,8 +2,8 @@ const semver = require('semver'); -if (semver.lt(process.versions.node, '20.0.0')) { - console.error('ovsx requires at least NodeJS version 20. Check your installed version with `node --version`.'); +if (semver.lt(process.versions.node, '22.0.0')) { + console.error('ovsx requires at least NodeJS version 22. Check your installed version with `node --version`.'); process.exit(1); } diff --git a/cli/package.json b/cli/package.json index 275ec9984..9877d1f13 100644 --- a/cli/package.json +++ b/cli/package.json @@ -31,7 +31,7 @@ "types": "lib/index", "bin": "bin/ovsx", "engines": { - "node": ">= 20" + "node": ">= 22" }, "dependencies": { "@inquirer/prompts": "^7.10.1", @@ -51,7 +51,7 @@ "@stylistic/eslint-plugin": "^2.11.0", "@types/follow-redirects": "^1.13.1", "@types/is-ci": "^2.0.0", - "@types/node": "^20.14.8", + "@types/node": "^22.10.2", "@types/semver": "^7.5.8", "@types/tmp": "^0.2.2", "@types/yauzl-promise": "^4", @@ -60,14 +60,16 @@ "eslint": "^9.15.0", "limiter": "^2.1.0", "rimraf": "^6.0.1", - "typescript": "^5.6.3" + "typescript": "^5.6.3", + "vitest": "^4.1.10" }, "scripts": { "clean": "rimraf lib", "prebuild": "node -p \"'export const LIB_VERSION = ' + JSON.stringify(require('./package.json').version) + ';'\" > src/version.ts", "build": "tsc -p ./tsconfig.json && yarn run lint", "watch": "tsc -w -p ./tsconfig.json", - "lint": "eslint -c ./configs/eslintrc.mjs src", + "test": "vitest run", + "lint": "eslint -c ./configs/eslintrc.mjs src test", "prepare": "yarn run clean && yarn run prebuild && yarn run build", "publish:next": "yarn npm publish --tag next", "publish:latest": "yarn npm publish --tag latest", diff --git a/cli/src/api.ts b/cli/src/api.ts new file mode 100644 index 000000000..f93a93c1d --- /dev/null +++ b/cli/src/api.ts @@ -0,0 +1,118 @@ +/******************************************************************************** + * Copyright (c) 2026 Eclipse Foundation and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + ********************************************************************************/ + +import { createVSIX as createVsceVSIX, IPackageOptions } from '@vscode/vsce'; +import { checkLicense } from './check-license'; +import { publishPackage } from './publish-package'; +import { PublishCommonOptions } from './publish-options'; +import { Extension, Registry } from './registry'; +import { addEnvOptions, readManifest, validateManifest } from './util'; + +/** + * Options of {@link publishVSIX}. + */ +export type PublishVSIXOptions = Omit; + +/** + * Options of {@link createVSIX}. + */ +export interface CreateVSIXOptions extends Omit { + /** + * The location of the extension to package. Defaults to the current working directory. + */ + packagePath?: string; + /** + * Where to write the package. Defaults to `NAME-VERSION.vsix` in {@link packagePath}. + */ + outputPath?: string; + /** + * Target architecture the package is built for. + */ + target?: string; + /** + * Whether to detect dependencies via npm or yarn. + */ + dependencies?: boolean; +} + +/** + * Publishes extensions that are packaged already. + * + * Unlike {@link publish}, this rejects as soon as one of the packages cannot be published, and it + * never asks the user for input unless {@link PublishVSIXOptions.interactive} says so: a missing + * access token is an error rather than a prompt. + * + * @param packagePath path of the `.vsix` file to publish, or several of them + * @returns the published extensions, excluding the ones skipped as duplicates + */ +export async function publishVSIX( + packagePath: string | string[], + options: PublishVSIXOptions = {} +): Promise { + const packagePaths = typeof packagePath === 'string' ? [packagePath] : packagePath; + // Work on a copy: the environment must not leak into the options object of the caller. + const resolvedOptions = { interactive: false, ...options }; + addEnvOptions(resolvedOptions); + + const published: Extension[] = []; + // Sequentially and failing fast: a caller of this API wants the first error, not a summary of + // everything that went wrong. + for (const extensionFile of packagePaths) { + const extension = await publishPackage(extensionFile, resolvedOptions); + if (extension) { + published.push(extension); + } + } + + return published; +} + +/** + * Packages an extension without publishing it, using `vsce` under the hood. + * + * Note that this validates the license of the extension when the target registry requires one, so + * the resulting package can be published to that registry. It never asks the user for input unless + * {@link CreateVSIXOptions.interactive} says so. + * + * @returns the path of the packaged extension + */ +export async function createVSIX(options: CreateVSIXOptions = {}): Promise { + options = { interactive: false, ...options }; + const manifest = await readManifest(options.packagePath); + validateManifest(manifest); + + if (new Registry(options).requiresLicense) { + await checkLicense(options.packagePath ?? '.', options); + } + + // vsce resolves a missing package path relative to its own cwd, so compute it here to be able to + // report back where the package ended up. + const outputPath = options.outputPath ?? defaultPackagePath(manifest.name, manifest.version, options); + const packageOptions: IPackageOptions = { + packagePath: outputPath, + target: options.target, + cwd: options.packagePath, + baseContentUrl: options.baseContentUrl, + baseImagesUrl: options.baseImagesUrl, + useYarn: options.yarn, + dependencies: options.dependencies, + preRelease: options.preRelease, + allowMissingRepository: options.allowMissingRepository, + version: options.packageVersion + }; + await createVsceVSIX(packageOptions); + + return outputPath; +} + +function defaultPackagePath(name: string, version: string, options: CreateVSIXOptions): string { + const fileName = `${name}-${options.packageVersion ?? version}.vsix`; + return options.packagePath ? `${options.packagePath.replace(/[\\/]+$/, '')}/${fileName}` : fileName; +} diff --git a/cli/src/check-license.ts b/cli/src/check-license.ts index 7a7e23f3a..b5ff91242 100644 --- a/cli/src/check-license.ts +++ b/cli/src/check-license.ts @@ -14,9 +14,11 @@ import { input, select } from '@inquirer/prompts'; import { readManifest, writeManifest, Manifest, writeFile, validateManifest, promisify } from './util'; +import { Logger, consoleLogger } from './logger'; +import { RegistryOptions } from './registry-options'; -async function addLicense(packagePath: string, manifest: Manifest): Promise { - console.log('Extension ' + manifest.publisher + '.' + manifest.name + ' has no license. All Open VSX ' +async function addLicense(packagePath: string, manifest: Manifest, log: Logger): Promise { + log.log('Extension ' + manifest.publisher + '.' + manifest.name + ' has no license. All Open VSX ' + 'Registry Content Offerings must be licensed. You may choose to publish this extension under ' + 'the MIT License (https://opensource.org/licenses/MIT). Please note you are responsible to ' + 'ensure that you have the necessary rights to permit this extension to be made available under ' @@ -36,14 +38,14 @@ async function addLicense(packagePath: string, manifest: Manifest): Promise { +export async function checkLicense(packagePath: string, options: RegistryOptions = {}): Promise { const manifest = await readManifest(packagePath); - if (!await isLicenseOk(packagePath, manifest) && !isCI) { - await addLicense(packagePath, manifest); + // Adding a license needs the user to accept it, so this is skipped whenever there is nobody to + // ask: the registry rejects the extension in that case, just like it does on CI today. + const canAskUser = !isCI && options.interactive !== false; + if (!await isLicenseOk(packagePath, manifest) && canAskUser) { + await addLicense(packagePath, manifest, options.log ?? consoleLogger); } } -async function useMITLicense(manifest: Manifest, packagePath?: string) { - console.log('Please enter a value for Copyright Year and Copyright Holder.\n' +async function useMITLicense(manifest: Manifest, log: Logger, packagePath?: string) { + log.log('Please enter a value for Copyright Year and Copyright Holder.\n' + 'Example: "Copyright 2020 John Doe"\n'); const copyright = await input({ message: 'Copyright', @@ -88,7 +93,7 @@ async function useMITLicense(manifest: Manifest, packagePath?: string) { await writeManifest(manifest, packagePath); const license = MIT_LICENSE_TEXT.replace(' ', copyright); await writeFile('LICENSE', license, packagePath); - console.log('LICENSE file has been written. Please commit it to the source repository.'); + log.log('LICENSE file has been written. Please commit it to the source repository.'); } const LICENSE_FILE_NAMES = ['license.md', 'license', 'license.txt', 'licence.md', 'licence', 'licence.txt']; diff --git a/cli/src/create-namespace.ts b/cli/src/create-namespace.ts index d787e581e..f86e2f5ee 100644 --- a/cli/src/create-namespace.ts +++ b/cli/src/create-namespace.ts @@ -12,11 +12,14 @@ import { CreateNamespaceOptions } from './create-namespace-options'; import { getPAT } from './pat'; import { Registry } from './registry'; import { addEnvOptions } from './util'; +import { consoleLogger } from './logger'; /** * Creates a namespace (corresponds to `publisher` in package.json). */ export async function createNamespace(options: CreateNamespaceOptions = {}): Promise { + // Work on a copy: the environment must not leak into the options object of the caller. + options = { ...options }; addEnvOptions(options); if (!options.name) { throw new Error('The namespace name is mandatory.'); @@ -29,5 +32,5 @@ export async function createNamespace(options: CreateNamespaceOptions = {}): Pro if (result.error) { throw new Error(result.error); } - console.log(`\ud83d\ude80 Created namespace ${options.name}`); + (options.log ?? consoleLogger).log(`\ud83d\ude80 Created namespace ${options.name}`); } diff --git a/cli/src/get.ts b/cli/src/get.ts index bd5a917d2..9e10b0449 100644 --- a/cli/src/get.ts +++ b/cli/src/get.ts @@ -13,13 +13,17 @@ import * as path from 'path'; import * as semver from 'semver'; import { Registry, Extension } from "./registry"; import { promisify, matchExtensionId, optionalStat, makeDirs, addEnvOptions, rejectError } from './util'; +import { Logger, consoleLogger } from './logger'; import { GetOptions } from './get-options'; /** * Downloads an extension or its metadata. */ export async function getExtension(options: GetOptions): Promise { + // Work on a copy: the environment must not leak into the options object of the caller. + options = { ...options }; addEnvOptions(options); + const log = options.log ?? consoleLogger; const registry = new Registry(options); const match = matchExtensionId(options.extensionId); if (!match) { @@ -37,9 +41,9 @@ export async function getExtension(options: GetOptions): Promise { } if (options.metadata) { - await printMetadata(registry, matchingVersion, options.output); + await printMetadata(registry, matchingVersion, log, options.output); } else { - await download(registry, matchingVersion, options.output); + await download(registry, matchingVersion, log, options.output); } } @@ -63,10 +67,10 @@ function isAlias(extension: Extension, version: string): boolean { return extension.versionAlias.includes(version); } -async function printMetadata(registry: Registry, extension: Extension, output?: string): Promise { +async function printMetadata(registry: Registry, extension: Extension, log: Logger, output?: string): Promise { const metadata = JSON.stringify(extension, null, 4); if (!output) { - console.log(metadata); + log.log(metadata); return; } let filePath: string | undefined; @@ -81,7 +85,7 @@ async function printMetadata(registry: Registry, extension: Extension, output?: await promisify(fs.writeFile)(filePath, metadata); } -async function download(registry: Registry, extension: Extension, output?: string): Promise { +async function download(registry: Registry, extension: Extension, log: Logger, output?: string): Promise { const downloadUrl = extension.files.download; if (!downloadUrl) { throw new Error(`Extension ${extension.namespace}.${extension.name} does not provide a download URL.`); @@ -101,6 +105,6 @@ async function download(registry: Registry, extension: Extension, output?: strin } await makeDirs(path.dirname(filePath)); const target = extension.targetPlatform !== 'universal' ? '@' + extension.targetPlatform : ''; - console.log(`Downloading ${extension.namespace}.${extension.name}-${extension.version}${target} to ${filePath}`); + log.log(`Downloading ${extension.namespace}.${extension.name}-${extension.version}${target} to ${filePath}`); await registry.download(filePath, new URL(downloadUrl)); } diff --git a/cli/src/index.ts b/cli/src/index.ts index 060b9947e..3c99cecc9 100644 --- a/cli/src/index.ts +++ b/cli/src/index.ts @@ -8,18 +8,37 @@ * SPDX-License-Identifier: EPL-2.0 ********************************************************************************/ -export * from './create-namespace'; -export * from './create-namespace-options'; -export * from './get'; -export * from './get-options'; -export * from './login'; -export * from './login-options'; -export * from './logout'; -export * from './publish'; -export * from './publish-options'; -export * from './registry'; -export * from './registry-options'; -export * from './verify-pat'; -export * from './verify-pat-options'; +// Everything listed here is part of the public API of this package and therefore a compatibility +// promise. The exports are named on purpose: with `export *` every symbol a module below adds would +// silently become public, even when it is only meant to be shared between two modules of this +// package. + +// Packaging and publishing. +export { createVSIX, publishVSIX, CreateVSIXOptions, PublishVSIXOptions } from './api'; +export { publish } from './publish'; +export { PublishCommonOptions, PublishOptions } from './publish-options'; + +// Namespaces and access tokens. +export { createNamespace } from './create-namespace'; +export { CreateNamespaceOptions } from './create-namespace-options'; +export { verifyPat } from './verify-pat'; +export { VerifyPatOptions } from './verify-pat-options'; +export { LoginOptions } from './login-options'; + +// Downloading extensions. +export { getExtension } from './get'; +export { GetOptions } from './get-options'; + +// The registry API client the commands above are built on. +export { + Registry, DEFAULT_URL, DEFAULT_NAMESPACE_SIZE, DEFAULT_PUBLISH_SIZE, + Response, Extension, UserData, Badge, ExtensionReference, ErrorResponse +} from './registry'; +export { RegistryOptions } from './registry-options'; + +// Where the commands report their progress to. +export { Logger, consoleLogger, silentLogger } from './logger'; + +// Helpers for inspecting an extension before publishing it. export { isLicenseOk } from './check-license'; export { validateManifest, readManifest } from './util'; diff --git a/cli/src/logger.ts b/cli/src/logger.ts new file mode 100644 index 000000000..1f33d1719 --- /dev/null +++ b/cli/src/logger.ts @@ -0,0 +1,42 @@ +/******************************************************************************** + * Copyright (c) 2026 Eclipse Foundation and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + ********************************************************************************/ + +/** + * Sink for the progress messages this library writes. + * + * Pass an implementation via {@link RegistryOptions.log} to capture or silence the output when using + * `ovsx` programmatically; the command line interface uses {@link consoleLogger}. + */ +export interface Logger { + /** + * Reports progress. Called without a message to separate blocks of output. + */ + log(message?: string): void; + /** + * Reports a condition that does not stop the operation. + */ + warn(message: string): void; +} + +/** + * The default logger, writing to the console. + */ +export const consoleLogger: Logger = { + log: (message = '') => console.log(message), + warn: message => console.warn(message) +}; + +/** + * A logger that discards everything, for callers that only care about the result. + */ +export const silentLogger: Logger = { + log: () => { }, + warn: () => { } +}; diff --git a/cli/src/login.ts b/cli/src/login.ts index 62966ff14..3c9b1ea44 100644 --- a/cli/src/login.ts +++ b/cli/src/login.ts @@ -9,20 +9,24 @@ ********************************************************************************/ import { confirm } from '@inquirer/prompts'; import { addEnvOptions } from './util'; +import { consoleLogger } from './logger'; import { openDefaultStore } from './store'; import { LoginOptions } from './login-options'; import { requestPAT } from './pat'; export default async function login(options: LoginOptions) { + // Work on a copy: the environment must not leak into the options object of the caller. + options = { ...options }; addEnvOptions(options); + const log = options.log ?? consoleLogger; if (!options.namespace) { throw new Error('Missing namespace name.'); } - const store = await openDefaultStore(); + const store = await openDefaultStore(log); let pat = await store.get(options.namespace); if (pat) { - console.log(`Namespace '${options.namespace}' is already known.`); + log.log(`Namespace '${options.namespace}' is already known.`); const overwrite = await confirm({ message: 'Do you want to overwrite its PAT?', default: false diff --git a/cli/src/logout.ts b/cli/src/logout.ts index 783411955..072dac308 100644 --- a/cli/src/logout.ts +++ b/cli/src/logout.ts @@ -8,17 +8,18 @@ * SPDX-License-Identifier: EPL-2.0 ********************************************************************************/ import { openDefaultStore } from "./store"; +import { Logger, consoleLogger } from './logger'; -export default async function logout(namespaceName: string) { +export default async function logout(namespaceName: string, log: Logger = consoleLogger) { if (!namespaceName) { throw new Error('Missing namespace name.'); } - const store = await openDefaultStore(); + const store = await openDefaultStore(log); if (!await store.get(namespaceName)) { throw new Error(`Unknown namespace '${namespaceName}'.`); } await store.delete(namespaceName); - console.log(`\ud83d\ude80 ${namespaceName} removed from the list of known namespaces`); + log.log(`\ud83d\ude80 ${namespaceName} removed from the list of known namespaces`); } diff --git a/cli/src/main.ts b/cli/src/main.ts index ba5fc956b..52910567c 100644 --- a/cli/src/main.ts +++ b/cli/src/main.ts @@ -12,6 +12,7 @@ import * as commander from 'commander'; import * as leven from 'leven'; import { createNamespace } from './create-namespace'; import { verifyPat } from './verify-pat'; +import { publishVSIX } from './api'; import { publish } from './publish'; import { handleError } from './util'; import { getExtension } from './get'; @@ -74,8 +75,17 @@ module.exports = function (argv: string[]): void { console.warn("Ignoring option '--packageVersion' for prepackaged extension."); if (extensionFile !== undefined && allowMissingRepository !== undefined) console.warn("Ignoring option '--allow-missing-repository' for prepackaged extension."); + if (extensionFile !== undefined && preRelease !== undefined) + console.warn("Ignoring option '--pre-release' for prepackaged extension."); const { registryUrl, pat } = program.opts(); - publish({ extensionFile, registryUrl, pat, targets: typeof target === 'string' ? [target] : target, packagePath: typeof packagePath === 'string' ? [packagePath] : packagePath, baseContentUrl, baseImagesUrl, yarn, preRelease, allowMissingRepository, dependencies, skipDuplicate, packageVersion }) + const options = { registryUrl, pat, baseContentUrl, baseImagesUrl, yarn, preRelease, allowMissingRepository, dependencies, skipDuplicate, packageVersion, interactive: true }; + // An extension that is packaged already is exactly what the programmatic API publishes, so + // use it here rather than going through the packaging path a second time. Its rejection is + // settled to report it like the outcome of every other package. + const command = extensionFile !== undefined + ? Promise.allSettled([publishVSIX(extensionFile, options)]) + : publish({ ...options, targets: typeof target === 'string' ? [target] : target, packagePath: typeof packagePath === 'string' ? [packagePath] : packagePath }); + command .then(results => { const reasons = results.filter(result => result.status === 'rejected') .map(rejectedResult => rejectedResult.reason); diff --git a/cli/src/pat.ts b/cli/src/pat.ts index fc10022e4..e30c038da 100644 --- a/cli/src/pat.ts +++ b/cli/src/pat.ts @@ -7,12 +7,14 @@ * * SPDX-License-Identifier: EPL-2.0 * ****************************************************************************** */ + import { password } from '@inquirer/prompts'; import { CreateNamespaceOptions } from './create-namespace-options'; import { PublishOptions } from './publish-options'; import { VerifyPatOptions } from './verify-pat-options'; import { Registry } from './registry'; import { openDefaultStore } from './store'; +import { consoleLogger } from './logger'; export async function doVerifyPat(options: VerifyPatOptions) { const registry = new Registry(options); @@ -22,10 +24,14 @@ export async function doVerifyPat(options: VerifyPatOptions) { if (result.error) { throw new Error(result.error); } - console.log(`\ud83d\ude80 PAT valid to publish at ${namespace}`); + (options.log ?? consoleLogger).log(`\ud83d\ude80 PAT valid to publish at ${namespace}`); } export async function requestPAT(namespace: string, options: CreateNamespaceOptions | PublishOptions | VerifyPatOptions, verify: boolean = true): Promise { + if (options.interactive === false) { + throw new Error(`Cannot ask for the personal access token of namespace '${namespace}' without user interaction.`); + } + const pat = await password({ message: `Personal Access Token for namespace '${namespace}':`, mask: true, @@ -43,12 +49,19 @@ export async function getPAT(namespace: string, options: CreateNamespaceOptions return options.pat; } - const store = await openDefaultStore(); + const store = await openDefaultStore(options.log ?? consoleLogger); let pat = await store.get(namespace); if (pat) { return pat; } + if (options.interactive === false) { + throw new Error( + `No personal access token found for namespace '${namespace}'.` + + ` Pass the 'pat' option, set the OVSX_PAT environment variable` + + ` or run 'ovsx login ${namespace}' to store one.`); + } + pat = await requestPAT(namespace, options, verify); await store.add(namespace, pat); diff --git a/cli/src/publish-package.ts b/cli/src/publish-package.ts new file mode 100644 index 000000000..357620801 --- /dev/null +++ b/cli/src/publish-package.ts @@ -0,0 +1,61 @@ +/******************************************************************************** + * Copyright (c) 2026 Eclipse Foundation and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + ********************************************************************************/ + +import { consoleLogger } from './logger'; +import { getPAT } from './pat'; +import { PublishCommonOptions } from './publish-options'; +import { Extension, Registry } from './registry'; +import { readVSIXPackage } from './zip'; + +/** + * Publishes a single packaged extension, the step `publish` and `publishVSIX` share. + * + * Resolves with the published extension, or with `undefined` if the version existed already and + * {@link PublishCommonOptions.skipDuplicate} is set. + */ +export async function publishPackage( + extensionFile: string, + options: PublishCommonOptions = {} +): Promise { + const log = options.log ?? consoleLogger; + const registry = new Registry(options); + let pat = options.pat; + if (!pat) { + const namespace = (await readVSIXPackage(extensionFile)).publisher; + pat = await getPAT(namespace, options); + } + + let extension: Extension; + try { + extension = await registry.publish(extensionFile, pat); + } catch (err) { + if (options.skipDuplicate && err.message.endsWith('is already published.')) { + log.log(err.message + ' Skipping publish.'); + return undefined; + } else { + throw err; + } + } + if (extension.error) { + throw new Error(extension.error); + } + + let description = `${extension.namespace}.${extension.name} v${extension.version}`; + if (extension.targetPlatform !== 'universal') { + description += `@${extension.targetPlatform}`; + } + + log.log(`\ud83d\ude80 Published ${description}`); + if (extension.warning) { + log.log(`\n!! ${extension.warning}`); + } + + return extension; +} diff --git a/cli/src/publish.ts b/cli/src/publish.ts index 113af9354..912cdacc1 100644 --- a/cli/src/publish.ts +++ b/cli/src/publish.ts @@ -7,97 +7,62 @@ * * SPDX-License-Identifier: EPL-2.0 ********************************************************************************/ -import { createVSIX, IPackageOptions } from '@vscode/vsce'; -import { getPAT } from './pat'; +import { createVSIX } from './api'; +import { publishPackage } from './publish-package'; import { createTempFile, addEnvOptions } from './util'; -import { Extension, Registry } from './registry'; -import { checkLicense } from './check-license'; -import { readVSIXPackage } from './zip'; +import { Extension } from './registry'; import { PublishOptions, PublishCommonOptions } from './publish-options'; +import { consoleLogger } from './logger'; /** - * Publishes an extension. + * Publishes an extension, packaging it first if necessary. + * + * Every combination of package path and target is published independently, so the returned array + * reports the outcome of each one. Use {@link publishVSIX} to have the first failure reject instead. */ -export async function publish(options: PublishOptions = {}): Promise[]> { - addEnvOptions(options); +export async function publish(options: PublishOptions = {}): Promise[]> { + // Work on a copy: the environment must not leak into the options object of the caller. Prompting + // stays allowed here, unlike in the programmatic API. + const resolvedOptions = { interactive: true, ...options }; + addEnvOptions(resolvedOptions); const internalPublishOptions: InternalPublishOptions[] = []; - const packagePaths = options.packagePath || [undefined]; - const targets = options.targets || [undefined]; + const packagePaths = resolvedOptions.packagePath || [undefined]; + const targets = resolvedOptions.targets || [undefined]; for (const packagePath of packagePaths) { for (const target of targets) { - internalPublishOptions.push({ ...options, packagePath: packagePath, target: target }); + internalPublishOptions.push({ ...resolvedOptions, packagePath: packagePath, target: target }); } } return Promise.allSettled(internalPublishOptions.map(publishOptions => doPublish(publishOptions))); } -async function doPublish(options: InternalPublishOptions = {}): Promise { +/** + * Publishes a single extension, packaging it first if necessary. Resolves with the published + * extension, or with `undefined` if the version existed already and `skipDuplicate` is set. + */ +async function doPublish(options: InternalPublishOptions = {}): Promise { + const log = options.log ?? consoleLogger; // if the packagePath is a link to a vsix, don't need to package it if (options.packagePath?.endsWith('.vsix')) { options.extensionFile = options.packagePath; delete options.packagePath; delete options.target; } - const registry = new Registry(options); + if (!options.extensionFile) { - await packageExtension(options, registry); - console.log(); // new line + // Package into a temporary file instead of the location vsce would pick, so that publishing + // does not leave a package behind in the extension directory. + options.extensionFile = await createVSIX({ + ...options, + outputPath: await createTempFile({ postfix: '.vsix' }) + }); + log.log(); // new line } else if (options.preRelease) { - console.warn("Ignoring option '--pre-release' for prepackaged extension."); - } - - if (!options.pat) { - const namespace = (await readVSIXPackage(options.extensionFile!)).publisher; - options.pat = await getPAT(namespace, options); - } - - let extension: Extension | undefined; - try { - extension = await registry.publish(options.extensionFile!, options.pat); - } catch (err) { - if (options.skipDuplicate && err.message.endsWith('is already published.')) { - console.log(err.message + ' Skipping publish.'); - return; - } else { - throw err; - } - } - if (extension.error) { - throw new Error(extension.error); - } - - const name = `${extension.namespace}.${extension.name}`; - let description = `${name} v${extension.version}`; - if (extension.targetPlatform !== 'universal') { - description += `@${extension.targetPlatform}`; - } - - console.log(`\ud83d\ude80 Published ${description}`); - if (extension.warning) { - console.log(`\n!! ${extension.warning}`); - } -} - -async function packageExtension(options: InternalPublishOptions, registry: Registry): Promise { - if (registry.requiresLicense) { - await checkLicense(options.packagePath!); + log.warn("Ignoring option '--pre-release' for prepackaged extension."); } - options.extensionFile = await createTempFile({ postfix: '.vsix' }); - const packageOptions: IPackageOptions = { - packagePath: options.extensionFile, - target: options.target, - cwd: options.packagePath, - baseContentUrl: options.baseContentUrl, - baseImagesUrl: options.baseImagesUrl, - useYarn: options.yarn, - dependencies: options.dependencies, - preRelease: options.preRelease, - allowMissingRepository: options.allowMissingRepository, - version: options.packageVersion - }; - await createVSIX(packageOptions); + return publishPackage(options.extensionFile, options); } // Interface used internally by the doPublish method diff --git a/cli/src/registry-options.ts b/cli/src/registry-options.ts index 03c83ec2f..ae84437eb 100644 --- a/cli/src/registry-options.ts +++ b/cli/src/registry-options.ts @@ -8,11 +8,23 @@ * SPDX-License-Identifier: EPL-2.0 ********************************************************************************/ +import { Logger } from './logger'; + export interface RegistryOptions { /** * The base URL of the registry API. */ registryUrl?: string; + /** + * Where to write progress messages. Defaults to the console. + */ + log?: Logger; + /** + * Whether asking the user for input is allowed, e.g. for a missing access token. Defaults to + * `true`, except for the programmatic API in {@link publishVSIX} and {@link createVSIX}, which + * never prompts unless this is set explicitly. + */ + interactive?: boolean; /** * Personal access token. */ diff --git a/cli/src/store.ts b/cli/src/store.ts index b870f09c4..485a6dd5f 100644 --- a/cli/src/store.ts +++ b/cli/src/store.ts @@ -11,6 +11,7 @@ import * as fs from 'fs'; import * as path from 'path'; import { homedir } from 'os'; +import { Logger, consoleLogger } from './logger'; interface StoreEntry { name: string @@ -134,7 +135,7 @@ export class EncryptedFileStore extends CrossKeychainStore { } } -export async function openDefaultStore(): Promise { +export async function openDefaultStore(log: Logger = consoleLogger): Promise { // OVSX_STORE=file forces the encrypted file store and skips the OS credential store. const forceFile = /^file$/i.test(process.env['OVSX_STORE'] ?? ''); @@ -145,7 +146,7 @@ export async function openDefaultStore(): Promise { try { store = await KeychainStore.open(); } catch (err) { - console.warn(`WARN: ${err.message}`); + log.warn(`WARN: ${err.message}`); } } @@ -154,31 +155,31 @@ export async function openDefaultStore(): Promise { try { store = await EncryptedFileStore.open(); if (!forceFile) { - console.warn(`WARN: Storing secrets in cross-keychain's encrypted file store as system keychain is not available..`); + log.warn(`WARN: Storing secrets in cross-keychain's encrypted file store as system keychain is not available..`); } } catch (err) { - console.warn(`WARN: ${err.message}`); + log.warn(`WARN: ${err.message}`); } } // Last resort: if no cross-keychain store works, keep publishing usable by storing secrets clear-text. if (!store) { - console.warn(`WARN: Falling back to storing secrets clear-text at '${FileStore.DefaultPath}' (not recommended).`); + log.warn(`WARN: Falling back to storing secrets clear-text at '${FileStore.DefaultPath}' (not recommended).`); return await FileStore.open(); } try { - await migrateLegacyStore(store); + await migrateLegacyStore(store, log); } catch (err) { - console.warn(`WARN: Failed to read legacy file store at '${FileStore.DefaultPath}': ${err.message}`); - console.warn(`WARN: Skipping migration of Legacy file store.`); + log.warn(`WARN: Failed to read legacy file store at '${FileStore.DefaultPath}': ${err.message}`); + log.warn(`WARN: Skipping migration of Legacy file store.`); } return store; } // Migrate secrets from the legacy clear-text file store into the given store, then delete it. -async function migrateLegacyStore(target: Store): Promise { +async function migrateLegacyStore(target: Store, log: Logger): Promise { const fileStore = await FileStore.open(); if (!fileStore.size) { return; @@ -190,5 +191,5 @@ async function migrateLegacyStore(target: Store): Promise { } await fileStore.deleteStore(); - console.info(`INFO: Migrated ${migrated} publishers to the credential store. Deleted local store '${fileStore.path}'.`); + log.log(`INFO: Migrated ${migrated} publishers to the credential store. Deleted local store '${fileStore.path}'.`); } diff --git a/cli/src/verify-pat.ts b/cli/src/verify-pat.ts index cbd6eacdf..350c2aa85 100644 --- a/cli/src/verify-pat.ts +++ b/cli/src/verify-pat.ts @@ -16,6 +16,8 @@ import { VerifyPatOptions } from './verify-pat-options'; * Validates that a Personal Access Token can publish to a namespace. */ export async function verifyPat(options: VerifyPatOptions): Promise { + // Work on a copy: the environment must not leak into the options object of the caller. + options = { ...options }; addEnvOptions(options); if (!options.namespace) { let error; diff --git a/cli/test/unit/api.spec.ts b/cli/test/unit/api.spec.ts new file mode 100644 index 000000000..d35b885be --- /dev/null +++ b/cli/test/unit/api.spec.ts @@ -0,0 +1,330 @@ +/****************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation. + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * https://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + *****************************************************************************/ + +import * as fs from 'fs'; +import * as http from 'http'; +import * as os from 'os'; +import * as path from 'path'; +import { AddressInfo } from 'net'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { createVSIX, CreateVSIXOptions, publishVSIX } from '../../src/api'; +import { Logger } from '../../src/logger'; + +// The credential store is never supposed to be consulted by the API, and touching the keychain of the +// machine running the tests would be a side effect either way. +vi.mock('../../src/store', () => ({ + openDefaultStore: async () => ({ + get: async () => undefined, + add: async () => { }, + delete: async () => { } + }) +})); + +interface PublishRequest { + query: URLSearchParams; + body: Buffer; +} + +interface RegistryService { + url: string; + requests: PublishRequest[]; + close: () => Promise; +} + +/** + * Stands in for the registry API endpoint that accepts a packaged extension. + */ +async function startRegistry(responses: { status: number, body: unknown }[]): Promise { + const requests: PublishRequest[] = []; + const server = http.createServer((req, res) => { + const chunks: Buffer[] = []; + req.on('data', chunk => chunks.push(chunk)); + req.on('end', () => { + const url = new URL(req.url ?? '/', 'http://127.0.0.1'); + requests.push({ query: url.searchParams, body: Buffer.concat(chunks) }); + const response = responses[Math.min(requests.length - 1, responses.length - 1)]; + res.writeHead(response.status, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify(response.body)); + }); + }); + await new Promise(resolve => server.listen(0, '127.0.0.1', resolve)); + const port = (server.address() as AddressInfo).port; + return { + url: `http://127.0.0.1:${port}`, + requests, + close: () => new Promise(resolve => server.close(() => resolve())) + }; +} + +function extensionJson(version: string = '1.0.0', overrides: Record = {}) { + return { + namespace: 'testpub', + name: 'test-extension', + version, + targetPlatform: 'universal', + ...overrides + }; +} + +/** + * Writes an extension that `vsce` can package without emitting warnings: it declares a repository, so + * it is not asked for, and ships the LICENSE its manifest promises as well as a `.vscodeignore`. + */ +function writeExtension(directory: string, manifest: Record = {}): void { + fs.writeFileSync(path.join(directory, 'package.json'), JSON.stringify({ + name: 'test-extension', + publisher: 'testpub', + version: '1.0.0', + license: 'MIT', + repository: 'https://github.com/testpub/test-extension', + engines: { vscode: '^1.57.0' }, + ...manifest + })); + fs.writeFileSync(path.join(directory, 'README.md'), '# Test Extension'); + fs.writeFileSync(path.join(directory, 'LICENSE.txt'), 'MIT'); + fs.writeFileSync(path.join(directory, '.vscodeignore'), '.vscodeignore\n'); +} + +function recordingLogger(): Logger & { messages: string[] } { + const messages: string[] = []; + return { + messages, + log: (message = '') => messages.push(message), + warn: message => messages.push(message) + }; +} + +describe('publishVSIX', () => { + + const services: RegistryService[] = []; + const temporaryFiles: string[] = []; + const temporaryDirectories: string[] = []; + const environment = { ...process.env }; + + beforeEach(() => { + delete process.env['OVSX_PAT']; + delete process.env['OVSX_REGISTRY_URL']; + }); + + afterEach(async () => { + process.env = { ...environment }; + temporaryFiles.splice(0).forEach(file => fs.rmSync(file, { force: true })); + temporaryDirectories.splice(0).forEach(dir => fs.rmSync(dir, { recursive: true, force: true })); + await Promise.all(services.splice(0).map(service => service.close())); + }); + + async function givenRegistry(...responses: { status: number, body: unknown }[]): Promise { + const service = await startRegistry(responses.length > 0 ? responses : [{ status: 201, body: extensionJson() }]); + services.push(service); + return service; + } + + /** + * A package that only has to exist, for the cases where the token does not have to be looked up. + */ + function givenPackage(content: string = 'not really a zip'): string { + const file = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'ovsx-test-')), 'test.vsix'); + fs.writeFileSync(file, content); + temporaryFiles.push(file); + return file; + } + + async function givenRealPackage(): Promise { + const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'ovsx-test-')); + temporaryDirectories.push(directory); + writeExtension(directory); + return createVSIX({ packagePath: directory, dependencies: false }); + } + + it('returns the published extension', async () => { + const registry = await givenRegistry(); + const log = recordingLogger(); + + const published = await publishVSIX(givenPackage(), { registryUrl: registry.url, pat: 'my-pat', log }); + + expect(published).toHaveLength(1); + expect(published[0].namespace).toBe('testpub'); + expect(published[0].name).toBe('test-extension'); + expect(published[0].version).toBe('1.0.0'); + expect(registry.requests).toHaveLength(1); + expect(registry.requests[0].query.get('token')).toBe('my-pat'); + }); + + it('publishes every given package', async () => { + const registry = await givenRegistry( + { status: 201, body: extensionJson('1.0.0') }, + { status: 201, body: extensionJson('2.0.0') }); + + const published = await publishVSIX( + [givenPackage(), givenPackage()], + { registryUrl: registry.url, pat: 'my-pat', log: recordingLogger() }); + + expect(published.map(extension => extension.version)).toEqual(['1.0.0', '2.0.0']); + }); + + it('rejects on the first failure instead of settling', async () => { + const registry = await givenRegistry({ status: 400, body: { error: 'Something went wrong' } }); + + await expect(publishVSIX( + [givenPackage(), givenPackage()], + { registryUrl: registry.url, pat: 'my-pat', log: recordingLogger() })) + .rejects.toThrow('Something went wrong'); + + expect(registry.requests).toHaveLength(1); + }); + + it('reports an error response as a rejection', async () => { + const registry = await givenRegistry({ status: 201, body: { error: 'Unknown publisher' } }); + + await expect(publishVSIX(givenPackage(), { registryUrl: registry.url, pat: 'my-pat', log: recordingLogger() })) + .rejects.toThrow('Unknown publisher'); + }); + + it('skips an already published version when asked to', async () => { + const registry = await givenRegistry( + { status: 400, body: { error: 'Extension testpub.test-extension 1.0.0 is already published.' } }); + const log = recordingLogger(); + + const published = await publishVSIX( + givenPackage(), + { registryUrl: registry.url, pat: 'my-pat', skipDuplicate: true, log }); + + expect(published).toEqual([]); + expect(log.messages.join('\n')).toContain('Skipping publish'); + }); + + it('does not ask for a token but fails with a hint', async () => { + const registry = await givenRegistry(); + // A real package: without a token the namespace is read from it to look one up. + const vsix = await givenRealPackage(); + + await expect(publishVSIX(vsix, { registryUrl: registry.url, log: recordingLogger() })) + .rejects.toThrow(/No personal access token found for namespace 'testpub'/); + + expect(registry.requests).toEqual([]); + }); + + it('picks the token up from the environment', async () => { + const registry = await givenRegistry(); + process.env['OVSX_PAT'] = 'pat-from-env'; + + await publishVSIX(givenPackage(), { registryUrl: registry.url, log: recordingLogger() }); + + expect(registry.requests[0].query.get('token')).toBe('pat-from-env'); + }); + + it('leaves the options of the caller untouched', async () => { + const registry = await givenRegistry(); + process.env['OVSX_PAT'] = 'pat-from-env'; + const options = { registryUrl: registry.url, log: recordingLogger() }; + + await publishVSIX(givenPackage(), options); + + expect(options).not.toHaveProperty('pat'); + expect(options).not.toHaveProperty('extensionFile'); + }); + + it('writes progress to the given logger and not to the console', async () => { + const registry = await givenRegistry(); + const log = recordingLogger(); + const consoleLog = vi.spyOn(console, 'log').mockImplementation(() => { }); + try { + await publishVSIX(givenPackage(), { registryUrl: registry.url, pat: 'my-pat', log }); + } finally { + consoleLog.mockRestore(); + } + + expect(log.messages.join('\n')).toContain('Published testpub.test-extension v1.0.0'); + expect(consoleLog).not.toHaveBeenCalled(); + }); +}); + +describe('createVSIX', () => { + + const directories: string[] = []; + const environment = { ...process.env }; + + beforeEach(() => { + // Keeps vsce from blocking on its confirmation prompt when it has something to complain about, + // and from reporting warnings as workflow commands instead of to the console. + process.env['VSCE_TESTS'] = '1'; + delete process.env['GITHUB_ACTIONS']; + }); + + afterEach(() => { + process.env = { ...environment }; + directories.splice(0).forEach(directory => fs.rmSync(directory, { recursive: true, force: true })); + }); + + /** + * Collects what vsce reported while packaging. + */ + async function warningsWhilePackaging(options: CreateVSIXOptions): Promise { + const warn = vi.spyOn(console, 'warn').mockImplementation(() => { }); + try { + await createVSIX(options); + return warn.mock.calls.map(call => call.join(' ')).join('\n'); + } finally { + warn.mockRestore(); + } + } + + function givenExtension(manifest: Record = {}): string { + const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'ovsx-test-')); + directories.push(directory); + writeExtension(directory, manifest); + return directory; + } + + it('packages the extension and returns its path', async () => { + const packagePath = givenExtension(); + + const vsix = await createVSIX({ packagePath, dependencies: false }); + + expect(vsix).toBe(path.join(packagePath, 'test-extension-1.0.0.vsix')); + expect(fs.existsSync(vsix)).toBe(true); + }); + + it('honours an explicit output path and package version', async () => { + const packagePath = givenExtension(); + const outputPath = path.join(packagePath, 'out.vsix'); + + const vsix = await createVSIX({ packagePath, outputPath, packageVersion: '2.3.4', dependencies: false }); + + expect(vsix).toBe(outputPath); + expect(fs.existsSync(outputPath)).toBe(true); + }); + + it('lets vsce accept a missing repository when asked to', async () => { + const packagePath = givenExtension({ repository: undefined }); + + const warnings = await warningsWhilePackaging( + { packagePath, dependencies: false, allowMissingRepository: true }); + + expect(warnings).not.toContain("'repository' field is missing"); + }); + + it('has vsce complain about a missing repository otherwise', async () => { + const packagePath = givenExtension({ repository: undefined }); + + const warnings = await warningsWhilePackaging({ packagePath, dependencies: false }); + + expect(warnings).toContain("'repository' field is missing"); + }); + + it('rejects an incomplete manifest', async () => { + const packagePath = givenExtension({ publisher: undefined }); + + await expect(createVSIX({ packagePath, dependencies: false })).rejects.toThrow("Missing required field 'publisher'"); + }); +}); diff --git a/cli/test/unit/pat.spec.ts b/cli/test/unit/pat.spec.ts new file mode 100644 index 000000000..83869ba01 --- /dev/null +++ b/cli/test/unit/pat.spec.ts @@ -0,0 +1,69 @@ +/****************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation. + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * https://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + *****************************************************************************/ + +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { getPAT, requestPAT } from '../../src/pat'; + +const storedTokens = new Map(); + +vi.mock('../../src/store', () => ({ + openDefaultStore: async () => ({ + get: async (name: string) => storedTokens.get(name), + add: async (name: string, value: string) => { + storedTokens.set(name, value); + }, + delete: async (name: string) => { + storedTokens.delete(name); + } + }) +})); + +// Prompting must never be reached in these tests, so the prompt itself fails the test. +function failOnPrompt(): never { + throw new Error('the user was asked for input'); +} + +vi.mock('@inquirer/prompts', () => ({ + password: failOnPrompt, + input: failOnPrompt, + select: failOnPrompt, + confirm: failOnPrompt +})); + +describe('getPAT', () => { + + beforeEach(() => { + storedTokens.clear(); + }); + + it('prefers the token from the options', async () => { + expect(await getPAT('testpub', { pat: 'from-options', interactive: false })).toBe('from-options'); + }); + + it('falls back to the stored token', async () => { + storedTokens.set('testpub', 'from-store'); + + expect(await getPAT('testpub', { interactive: false })).toBe('from-store'); + }); + + it('explains how to provide a token instead of prompting', async () => { + await expect(getPAT('testpub', { interactive: false })).rejects.toThrow( + /No personal access token found for namespace 'testpub'/); + await expect(getPAT('testpub', { interactive: false })).rejects.toThrow(/OVSX_PAT/); + }); + + it('refuses to request a token without user interaction', async () => { + await expect(requestPAT('testpub', { interactive: false })).rejects.toThrow( + /without user interaction/); + }); +}); diff --git a/cli/vitest.config.mts b/cli/vitest.config.mts new file mode 100644 index 000000000..485300aa2 --- /dev/null +++ b/cli/vitest.config.mts @@ -0,0 +1,9 @@ +/// +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + include: ['test/unit/**/*.spec.ts'], + environment: 'node' + } +}); diff --git a/cli/yarn.lock b/cli/yarn.lock index c57f61809..d74affb1d 100644 --- a/cli/yarn.lock +++ b/cli/yarn.lock @@ -192,6 +192,16 @@ __metadata: languageName: node linkType: hard +"@emnapi/core@npm:1.11.1": + version: 1.11.1 + resolution: "@emnapi/core@npm:1.11.1" + dependencies: + "@emnapi/wasi-threads": "npm:1.2.2" + tslib: "npm:^2.4.0" + checksum: 10/9aba37e0c11a75ef8372fd0a9c6e5396f4e8c1ebdd6fee737414787610a9dc1cd9bf188f525153561ca9363896e1135dd240f1ce28f3470dba3ad7e683e6db1a + languageName: node + linkType: hard + "@emnapi/core@npm:^1.4.0": version: 1.4.3 resolution: "@emnapi/core@npm:1.4.3" @@ -202,6 +212,15 @@ __metadata: languageName: node linkType: hard +"@emnapi/runtime@npm:1.11.1": + version: 1.11.1 + resolution: "@emnapi/runtime@npm:1.11.1" + dependencies: + tslib: "npm:^2.4.0" + checksum: 10/8f7c622a49314df4d07952110e108e83b0fe129a8ddb9ef1e0ae372d754616169d5b0dd47a0d354a0fea2612abe42cedb582d15916936d1320c6c468acc804cc + languageName: node + linkType: hard + "@emnapi/runtime@npm:^1.4.0": version: 1.4.3 resolution: "@emnapi/runtime@npm:1.4.3" @@ -220,6 +239,15 @@ __metadata: languageName: node linkType: hard +"@emnapi/wasi-threads@npm:1.2.2": + version: 1.2.2 + resolution: "@emnapi/wasi-threads@npm:1.2.2" + dependencies: + tslib: "npm:^2.4.0" + checksum: 10/297fb6b1d89744bd0b41d5fec32bade05dc8dcf1f70eba86527226609fb3f6ad3fa96b3b2377b7449709715b3bd1569654c9def1dbbc85fb6b9cb0cff5bc5ebf + languageName: node + linkType: hard + "@eslint-community/eslint-utils@npm:^4.2.0": version: 4.4.0 resolution: "@eslint-community/eslint-utils@npm:4.4.0" @@ -615,6 +643,13 @@ __metadata: languageName: node linkType: hard +"@jridgewell/sourcemap-codec@npm:^1.5.5": + version: 1.5.5 + resolution: "@jridgewell/sourcemap-codec@npm:1.5.5" + checksum: 10/5d9d207b462c11e322d71911e55e21a4e2772f71ffe8d6f1221b8eb5ae6774458c1d242f897fb0814e8714ca9a6b498abfa74dfe4f434493342902b1a48b33a5 + languageName: node + linkType: hard + "@napi-rs/keyring-darwin-arm64@npm:1.3.0": version: 1.3.0 resolution: "@napi-rs/keyring-darwin-arm64@npm:1.3.0" @@ -755,6 +790,18 @@ __metadata: languageName: node linkType: hard +"@napi-rs/wasm-runtime@npm:^1.1.6": + version: 1.2.0 + resolution: "@napi-rs/wasm-runtime@npm:1.2.0" + dependencies: + "@tybys/wasm-util": "npm:^0.10.3" + peerDependencies: + "@emnapi/core": ^2.0.0-alpha.3 + "@emnapi/runtime": ^2.0.0-alpha.3 + checksum: 10/e227c1d9405e0e0830f810bd5aebbcda79168842ce3ec124704cad94d326611b23e013e4602366c86f7c25449ab99c145aa71e3e1e3112a59a0ca906332b989a + languageName: node + linkType: hard + "@node-rs/crc32-android-arm-eabi@npm:1.10.6": version: 1.10.6 resolution: "@node-rs/crc32-android-arm-eabi@npm:1.10.6" @@ -955,6 +1002,129 @@ __metadata: languageName: node linkType: hard +"@oxc-project/types@npm:=0.139.0": + version: 0.139.0 + resolution: "@oxc-project/types@npm:0.139.0" + checksum: 10/7f5e958bf700c1777737f0b27fe2a207fd9ff6494e2ec7529d20d7b1d2c668f7687182d490e72d59a58dd9c176f7cacc3938e824b3e664e4d6e5ce4e0ff44872 + languageName: node + linkType: hard + +"@rolldown/binding-android-arm64@npm:1.1.5": + version: 1.1.5 + resolution: "@rolldown/binding-android-arm64@npm:1.1.5" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + +"@rolldown/binding-darwin-arm64@npm:1.1.5": + version: 1.1.5 + resolution: "@rolldown/binding-darwin-arm64@npm:1.1.5" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@rolldown/binding-darwin-x64@npm:1.1.5": + version: 1.1.5 + resolution: "@rolldown/binding-darwin-x64@npm:1.1.5" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@rolldown/binding-freebsd-x64@npm:1.1.5": + version: 1.1.5 + resolution: "@rolldown/binding-freebsd-x64@npm:1.1.5" + conditions: os=freebsd & cpu=x64 + languageName: node + linkType: hard + +"@rolldown/binding-linux-arm-gnueabihf@npm:1.1.5": + version: 1.1.5 + resolution: "@rolldown/binding-linux-arm-gnueabihf@npm:1.1.5" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + +"@rolldown/binding-linux-arm64-gnu@npm:1.1.5": + version: 1.1.5 + resolution: "@rolldown/binding-linux-arm64-gnu@npm:1.1.5" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + +"@rolldown/binding-linux-arm64-musl@npm:1.1.5": + version: 1.1.5 + resolution: "@rolldown/binding-linux-arm64-musl@npm:1.1.5" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + +"@rolldown/binding-linux-ppc64-gnu@npm:1.1.5": + version: 1.1.5 + resolution: "@rolldown/binding-linux-ppc64-gnu@npm:1.1.5" + conditions: os=linux & cpu=ppc64 & libc=glibc + languageName: node + linkType: hard + +"@rolldown/binding-linux-s390x-gnu@npm:1.1.5": + version: 1.1.5 + resolution: "@rolldown/binding-linux-s390x-gnu@npm:1.1.5" + conditions: os=linux & cpu=s390x & libc=glibc + languageName: node + linkType: hard + +"@rolldown/binding-linux-x64-gnu@npm:1.1.5": + version: 1.1.5 + resolution: "@rolldown/binding-linux-x64-gnu@npm:1.1.5" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + +"@rolldown/binding-linux-x64-musl@npm:1.1.5": + version: 1.1.5 + resolution: "@rolldown/binding-linux-x64-musl@npm:1.1.5" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + +"@rolldown/binding-openharmony-arm64@npm:1.1.5": + version: 1.1.5 + resolution: "@rolldown/binding-openharmony-arm64@npm:1.1.5" + conditions: os=openharmony & cpu=arm64 + languageName: node + linkType: hard + +"@rolldown/binding-wasm32-wasi@npm:1.1.5": + version: 1.1.5 + resolution: "@rolldown/binding-wasm32-wasi@npm:1.1.5" + dependencies: + "@emnapi/core": "npm:1.11.1" + "@emnapi/runtime": "npm:1.11.1" + "@napi-rs/wasm-runtime": "npm:^1.1.6" + conditions: cpu=wasm32 + languageName: node + linkType: hard + +"@rolldown/binding-win32-arm64-msvc@npm:1.1.5": + version: 1.1.5 + resolution: "@rolldown/binding-win32-arm64-msvc@npm:1.1.5" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + +"@rolldown/binding-win32-x64-msvc@npm:1.1.5": + version: 1.1.5 + resolution: "@rolldown/binding-win32-x64-msvc@npm:1.1.5" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + +"@rolldown/pluginutils@npm:^1.0.0": + version: 1.0.1 + resolution: "@rolldown/pluginutils@npm:1.0.1" + checksum: 10/4e95cf9ce23d75e5aa03ea0249cd86f7d1e21f83fbf6f8520e4edd8a251ba1b82c4ba9bc13cd24b6c4661daec6225b06e6d35c64c604e731b230b2a49af47d05 + languageName: node + linkType: hard + "@secretlint/config-creator@npm:^10.2.2": version: 10.2.2 resolution: "@secretlint/config-creator@npm:10.2.2" @@ -1088,6 +1258,13 @@ __metadata: languageName: node linkType: hard +"@standard-schema/spec@npm:^1.1.0": + version: 1.1.0 + resolution: "@standard-schema/spec@npm:1.1.0" + checksum: 10/a209615c9e8b2ea535d7db0a5f6aa0f962fd4ab73ee86a46c100fb78116964af1f55a27c1794d4801e534a196794223daa25ff5135021e03c7828aa3d95e1763 + languageName: node + linkType: hard + "@stylistic/eslint-plugin@npm:^2.11.0": version: 2.11.0 resolution: "@stylistic/eslint-plugin@npm:2.11.0" @@ -1155,6 +1332,15 @@ __metadata: languageName: node linkType: hard +"@tybys/wasm-util@npm:^0.10.3": + version: 0.10.3 + resolution: "@tybys/wasm-util@npm:0.10.3" + dependencies: + tslib: "npm:^2.4.0" + checksum: 10/6cf39f7a2926b1c8bc6fe3f9f03318a33dd6dae81bdbd059983f9c6ee22d10a827f12564d648c05a2d4926e03c86cbe2799fb20609ee65e9efc39603039b4765 + languageName: node + linkType: hard + "@tybys/wasm-util@npm:^0.9.0": version: 0.9.0 resolution: "@tybys/wasm-util@npm:0.9.0" @@ -1164,6 +1350,16 @@ __metadata: languageName: node linkType: hard +"@types/chai@npm:^5.2.2": + version: 5.2.3 + resolution: "@types/chai@npm:5.2.3" + dependencies: + "@types/deep-eql": "npm:*" + assertion-error: "npm:^2.0.1" + checksum: 10/e79947307dc235953622e65f83d2683835212357ca261389116ab90bed369ac862ba28b146b4fed08b503ae1e1a12cb93ce783f24bb8d562950469f4320e1c7c + languageName: node + linkType: hard + "@types/ci-info@npm:*": version: 2.0.0 resolution: "@types/ci-info@npm:2.0.0" @@ -1171,6 +1367,20 @@ __metadata: languageName: node linkType: hard +"@types/deep-eql@npm:*": + version: 4.0.2 + resolution: "@types/deep-eql@npm:4.0.2" + checksum: 10/249a27b0bb22f6aa28461db56afa21ec044fa0e303221a62dff81831b20c8530502175f1a49060f7099e7be06181078548ac47c668de79ff9880241968d43d0c + languageName: node + linkType: hard + +"@types/estree@npm:^1.0.0": + version: 1.0.9 + resolution: "@types/estree@npm:1.0.9" + checksum: 10/16aabfa703b5bdac83f719b07ce92a11b2d3c9b8628eacc92889d8af46cab2d78fc45c7b5378de383d0500585cea5c2f79125eeddfe5fbc6bd6a27eb0c8ccee5 + languageName: node + linkType: hard + "@types/estree@npm:^1.0.6": version: 1.0.6 resolution: "@types/estree@npm:1.0.6" @@ -1212,12 +1422,12 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:^20.14.8": - version: 20.16.7 - resolution: "@types/node@npm:20.16.7" +"@types/node@npm:^22.10.2": + version: 22.20.1 + resolution: "@types/node@npm:22.20.1" dependencies: - undici-types: "npm:~6.19.2" - checksum: 10/931d6689af8d02b578a1490475df025063ca1f1c7c064ce891f7ac4da38f28bfda949e79569ac49cb5cf492b2caf91d31aecda6a4efc099e4af4be42ae7315e5 + undici-types: "npm:~6.21.0" + checksum: 10/0949e49d0383569ebd1222a2f65a9adb9328a6a6dbed459a02cdb37c61147755386c004c4ffbe436de59f5859df1774b1dd6bf168f7aeee46884bb190e0c384f languageName: node linkType: hard @@ -1379,6 +1589,88 @@ __metadata: languageName: node linkType: hard +"@vitest/expect@npm:4.1.10": + version: 4.1.10 + resolution: "@vitest/expect@npm:4.1.10" + dependencies: + "@standard-schema/spec": "npm:^1.1.0" + "@types/chai": "npm:^5.2.2" + "@vitest/spy": "npm:4.1.10" + "@vitest/utils": "npm:4.1.10" + chai: "npm:^6.2.2" + tinyrainbow: "npm:^3.1.0" + checksum: 10/487fcad404a68968a54ae5fb9d099f12170cd793420a04b34a5606516317090c50a8303ab687c70166ee181864e3e138941d4a96d0405434dcd37696b3105350 + languageName: node + linkType: hard + +"@vitest/mocker@npm:4.1.10": + version: 4.1.10 + resolution: "@vitest/mocker@npm:4.1.10" + dependencies: + "@vitest/spy": "npm:4.1.10" + estree-walker: "npm:^3.0.3" + magic-string: "npm:^0.30.21" + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + checksum: 10/ae9645d1bcdad3ab7de7182feb4f1c9148a5ff97cef19581eec9257112aace94889eee9a1ad12e40ce59453ac05f52453b5fdb49ff76a31af8ccdbaaa4471ef3 + languageName: node + linkType: hard + +"@vitest/pretty-format@npm:4.1.10": + version: 4.1.10 + resolution: "@vitest/pretty-format@npm:4.1.10" + dependencies: + tinyrainbow: "npm:^3.1.0" + checksum: 10/e4f6907143ab0e40dda29d70b17027586c92921d622091321f10512e660b3995dcee7aa56e17b750b72560f295e25f96035372348415f18ebfd39b66a55b4704 + languageName: node + linkType: hard + +"@vitest/runner@npm:4.1.10": + version: 4.1.10 + resolution: "@vitest/runner@npm:4.1.10" + dependencies: + "@vitest/utils": "npm:4.1.10" + pathe: "npm:^2.0.3" + checksum: 10/2c962cb13af0880990036808a35679b7ac6657c8f542490234c2faa6ffd2ab080ac6bf21b487c64d84aa635cfb37b49eb679098c2003a100dfc6c4d5e87bf055 + languageName: node + linkType: hard + +"@vitest/snapshot@npm:4.1.10": + version: 4.1.10 + resolution: "@vitest/snapshot@npm:4.1.10" + dependencies: + "@vitest/pretty-format": "npm:4.1.10" + "@vitest/utils": "npm:4.1.10" + magic-string: "npm:^0.30.21" + pathe: "npm:^2.0.3" + checksum: 10/7940d83ffd2fbebf9a04ea31e196b7e8bf981093ec739950959fe8dd29caa33c80823780fb4b1063d9459c44a0a8d8b2748c00dfb6941becd7404e6d687eea01 + languageName: node + linkType: hard + +"@vitest/spy@npm:4.1.10": + version: 4.1.10 + resolution: "@vitest/spy@npm:4.1.10" + checksum: 10/7c1b79a95474338e0659f0f2e43be4df1ef7939ff5b37b044954e0287582947803bd417508f44a7f244809672309d9b3dd67660b704ec3fe7f323cc958ae47a3 + languageName: node + linkType: hard + +"@vitest/utils@npm:4.1.10": + version: 4.1.10 + resolution: "@vitest/utils@npm:4.1.10" + dependencies: + "@vitest/pretty-format": "npm:4.1.10" + convert-source-map: "npm:^2.0.0" + tinyrainbow: "npm:^3.1.0" + checksum: 10/95484aad55c7b00bbcd4963e27cbb86fe207620a6093973d68da9d0a06bad37c388d84c9ab43d5f35d88e46c8f376a5592d9c54c025c958361160f4802bb25ee + languageName: node + linkType: hard + "@vscode/vsce-sign-alpine-arm64@npm:2.0.2": version: 2.0.2 resolution: "@vscode/vsce-sign-alpine-arm64@npm:2.0.2" @@ -1632,6 +1924,13 @@ __metadata: languageName: node linkType: hard +"assertion-error@npm:^2.0.1": + version: 2.0.1 + resolution: "assertion-error@npm:2.0.1" + checksum: 10/a0789dd882211b87116e81e2648ccb7f60340b34f19877dd020b39ebb4714e475eb943e14ba3e22201c221ef6645b7bfe10297e76b6ac95b48a9898c1211ce66 + languageName: node + linkType: hard + "astral-regex@npm:^2.0.0": version: 2.0.0 resolution: "astral-regex@npm:2.0.0" @@ -1832,6 +2131,13 @@ __metadata: languageName: node linkType: hard +"chai@npm:^6.2.2": + version: 6.2.2 + resolution: "chai@npm:6.2.2" + checksum: 10/13cda42cc40aa46da04a41cf7e5c61df6b6ae0b4e8a8c8b40e04d6947e4d7951377ea8c14f9fa7fe5aaa9e8bd9ba414f11288dc958d4cee6f5221b9436f2778f + languageName: node + linkType: hard + "chalk@npm:^4.0.0, chalk@npm:^4.1.2": version: 4.1.2 resolution: "chalk@npm:4.1.2" @@ -1966,6 +2272,13 @@ __metadata: languageName: node linkType: hard +"convert-source-map@npm:^2.0.0": + version: 2.0.0 + resolution: "convert-source-map@npm:2.0.0" + checksum: 10/c987be3ec061348cdb3c2bfb924bec86dea1eacad10550a85ca23edb0fe3556c3a61c7399114f3331ccb3499d7fd0285ab24566e5745929412983494c3926e15 + languageName: node + linkType: hard + "cross-keychain@npm:^1.1.0": version: 1.1.0 resolution: "cross-keychain@npm:1.1.0" @@ -2103,6 +2416,13 @@ __metadata: languageName: node linkType: hard +"detect-libc@npm:^2.0.3": + version: 2.1.2 + resolution: "detect-libc@npm:2.1.2" + checksum: 10/b736c8d97d5d46164c0d1bed53eb4e6a3b1d8530d460211e2d52f1c552875e706c58a5376854e4e54f8b828c9cada58c855288c968522eb93ac7696d65970766 + languageName: node + linkType: hard + "dom-serializer@npm:^2.0.0": version: 2.0.0 resolution: "dom-serializer@npm:2.0.0" @@ -2260,6 +2580,13 @@ __metadata: languageName: node linkType: hard +"es-module-lexer@npm:^2.0.0": + version: 2.3.1 + resolution: "es-module-lexer@npm:2.3.1" + checksum: 10/15bd9f6d70ec7f046a308b7fbeb56a1fd4bde09e6a46df0015caee58bd5888fc114029754e922ca68577b761890ac95cc450683424209464530cfe54f69b8566 + languageName: node + linkType: hard + "es-object-atoms@npm:^1.0.0, es-object-atoms@npm:^1.1.1": version: 1.1.1 resolution: "es-object-atoms@npm:1.1.1" @@ -2397,6 +2724,15 @@ __metadata: languageName: node linkType: hard +"estree-walker@npm:^3.0.3": + version: 3.0.3 + resolution: "estree-walker@npm:3.0.3" + dependencies: + "@types/estree": "npm:^1.0.0" + checksum: 10/a65728d5727b71de172c5df323385755a16c0fdab8234dc756c3854cfee343261ddfbb72a809a5660fac8c75d960bb3e21aa898c2d7e9b19bb298482ca58a3af + languageName: node + linkType: hard + "esutils@npm:^2.0.2": version: 2.0.3 resolution: "esutils@npm:2.0.3" @@ -2418,6 +2754,13 @@ __metadata: languageName: node linkType: hard +"expect-type@npm:^1.3.0": + version: 1.4.0 + resolution: "expect-type@npm:1.4.0" + checksum: 10/bad91f4b7eb807248695ee840a935d12818fe531ad523bc7ac7dcc540a4d86dc566a3ef829f4da6e8b5a458ac84092771739865114c91e7e8a9317302f401f09 + languageName: node + linkType: hard + "exponential-backoff@npm:^3.1.1": version: 3.1.1 resolution: "exponential-backoff@npm:3.1.1" @@ -2614,6 +2957,25 @@ __metadata: languageName: node linkType: hard +"fsevents@npm:~2.3.3": + version: 2.3.3 + resolution: "fsevents@npm:2.3.3" + dependencies: + node-gyp: "npm:latest" + checksum: 10/4c1ade961ded57cdbfbb5cac5106ec17bc8bccd62e16343c569a0ceeca83b9dfef87550b4dc5cbb89642da412b20c5071f304c8c464b80415446e8e155a038c0 + conditions: os=darwin + languageName: node + linkType: hard + +"fsevents@patch:fsevents@npm%3A~2.3.3#optional!builtin": + version: 2.3.3 + resolution: "fsevents@patch:fsevents@npm%3A2.3.3#optional!builtin::version=2.3.3&hash=df0bf1" + dependencies: + node-gyp: "npm:latest" + conditions: os=darwin + languageName: node + linkType: hard + "function-bind@npm:^1.1.2": version: 1.1.2 resolution: "function-bind@npm:1.1.2" @@ -3307,6 +3669,126 @@ __metadata: languageName: node linkType: hard +"lightningcss-android-arm64@npm:1.33.0": + version: 1.33.0 + resolution: "lightningcss-android-arm64@npm:1.33.0" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + +"lightningcss-darwin-arm64@npm:1.33.0": + version: 1.33.0 + resolution: "lightningcss-darwin-arm64@npm:1.33.0" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"lightningcss-darwin-x64@npm:1.33.0": + version: 1.33.0 + resolution: "lightningcss-darwin-x64@npm:1.33.0" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"lightningcss-freebsd-x64@npm:1.33.0": + version: 1.33.0 + resolution: "lightningcss-freebsd-x64@npm:1.33.0" + conditions: os=freebsd & cpu=x64 + languageName: node + linkType: hard + +"lightningcss-linux-arm-gnueabihf@npm:1.33.0": + version: 1.33.0 + resolution: "lightningcss-linux-arm-gnueabihf@npm:1.33.0" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + +"lightningcss-linux-arm64-gnu@npm:1.33.0": + version: 1.33.0 + resolution: "lightningcss-linux-arm64-gnu@npm:1.33.0" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + +"lightningcss-linux-arm64-musl@npm:1.33.0": + version: 1.33.0 + resolution: "lightningcss-linux-arm64-musl@npm:1.33.0" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + +"lightningcss-linux-x64-gnu@npm:1.33.0": + version: 1.33.0 + resolution: "lightningcss-linux-x64-gnu@npm:1.33.0" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + +"lightningcss-linux-x64-musl@npm:1.33.0": + version: 1.33.0 + resolution: "lightningcss-linux-x64-musl@npm:1.33.0" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + +"lightningcss-win32-arm64-msvc@npm:1.33.0": + version: 1.33.0 + resolution: "lightningcss-win32-arm64-msvc@npm:1.33.0" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + +"lightningcss-win32-x64-msvc@npm:1.33.0": + version: 1.33.0 + resolution: "lightningcss-win32-x64-msvc@npm:1.33.0" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + +"lightningcss@npm:^1.32.0": + version: 1.33.0 + resolution: "lightningcss@npm:1.33.0" + dependencies: + detect-libc: "npm:^2.0.3" + lightningcss-android-arm64: "npm:1.33.0" + lightningcss-darwin-arm64: "npm:1.33.0" + lightningcss-darwin-x64: "npm:1.33.0" + lightningcss-freebsd-x64: "npm:1.33.0" + lightningcss-linux-arm-gnueabihf: "npm:1.33.0" + lightningcss-linux-arm64-gnu: "npm:1.33.0" + lightningcss-linux-arm64-musl: "npm:1.33.0" + lightningcss-linux-x64-gnu: "npm:1.33.0" + lightningcss-linux-x64-musl: "npm:1.33.0" + lightningcss-win32-arm64-msvc: "npm:1.33.0" + lightningcss-win32-x64-msvc: "npm:1.33.0" + dependenciesMeta: + lightningcss-android-arm64: + optional: true + lightningcss-darwin-arm64: + optional: true + lightningcss-darwin-x64: + optional: true + lightningcss-freebsd-x64: + optional: true + lightningcss-linux-arm-gnueabihf: + optional: true + lightningcss-linux-arm64-gnu: + optional: true + lightningcss-linux-arm64-musl: + optional: true + lightningcss-linux-x64-gnu: + optional: true + lightningcss-linux-x64-musl: + optional: true + lightningcss-win32-arm64-msvc: + optional: true + lightningcss-win32-x64-msvc: + optional: true + checksum: 10/9b3b5db404af352fe5861926b9909281d29bede175539035c1a01e1ad4dcee8bb6f871e8e3d01a67a85e9e1cd18a63e52871a48cf62feab4d1aa868d4f2c3c2e + languageName: node + linkType: hard + "limiter@npm:^2.1.0": version: 2.1.0 resolution: "limiter@npm:2.1.0" @@ -3434,6 +3916,15 @@ __metadata: languageName: node linkType: hard +"magic-string@npm:^0.30.21": + version: 0.30.21 + resolution: "magic-string@npm:0.30.21" + dependencies: + "@jridgewell/sourcemap-codec": "npm:^1.5.5" + checksum: 10/57d5691f41ed40d962d8bd300148114f53db67fadbff336207db10a99f2bdf4a1be9cac3a68ee85dba575912ee1d4402e4396408196ec2d3afd043b076156221 + languageName: node + linkType: hard + "make-fetch-happen@npm:^15.0.0": version: 15.0.3 resolution: "make-fetch-happen@npm:15.0.3" @@ -3691,6 +4182,15 @@ __metadata: languageName: node linkType: hard +"nanoid@npm:^3.3.16": + version: 3.3.16 + resolution: "nanoid@npm:3.3.16" + bin: + nanoid: bin/nanoid.cjs + checksum: 10/8004af92b5541af1dbd23b69845b5026f777d5b7ef07163cea1837aae86e052ced8b383cecbf8a4f1b5e77ae207df96dc45e16b9e0fa3c4b761d085f1e42851b + languageName: node + linkType: hard + "napi-build-utils@npm:^1.0.1": version: 1.0.2 resolution: "napi-build-utils@npm:1.0.2" @@ -3805,6 +4305,13 @@ __metadata: languageName: node linkType: hard +"obug@npm:^2.1.1": + version: 2.1.4 + resolution: "obug@npm:2.1.4" + checksum: 10/05e3ac83f60ef18edb935d67703bada2cbc0feb1a1cef575240b1e48e6e877df95b74048e69bbe549759df18c57ad1808e1e60a9fc4f785525c8549bf7fe81db + languageName: node + linkType: hard + "once@npm:^1.3.1, once@npm:^1.4.0": version: 1.4.0 resolution: "once@npm:1.4.0" @@ -3849,7 +4356,7 @@ __metadata: "@stylistic/eslint-plugin": "npm:^2.11.0" "@types/follow-redirects": "npm:^1.13.1" "@types/is-ci": "npm:^2.0.0" - "@types/node": "npm:^20.14.8" + "@types/node": "npm:^22.10.2" "@types/semver": "npm:^7.5.8" "@types/tmp": "npm:^0.2.2" "@types/yauzl-promise": "npm:^4" @@ -3867,6 +4374,7 @@ __metadata: semver: "npm:^7.6.0" tmp: "npm:^0.2.3" typescript: "npm:^5.6.3" + vitest: "npm:^4.1.10" yauzl-promise: "npm:^4.0.0" bin: ovsx: bin/ovsx @@ -3991,6 +4499,13 @@ __metadata: languageName: node linkType: hard +"pathe@npm:^2.0.3": + version: 2.0.3 + resolution: "pathe@npm:2.0.3" + checksum: 10/01e9a69928f39087d96e1751ce7d6d50da8c39abf9a12e0ac2389c42c83bc76f78c45a475bd9026a02e6a6f79be63acc75667df855862fe567d99a00a540d23d + languageName: node + linkType: hard + "pend@npm:~1.2.0": version: 1.2.0 resolution: "pend@npm:1.2.0" @@ -4019,6 +4534,13 @@ __metadata: languageName: node linkType: hard +"picomatch@npm:^4.0.4, picomatch@npm:^4.0.5": + version: 4.0.5 + resolution: "picomatch@npm:4.0.5" + checksum: 10/8bad770af9dcdb7f94ad1a893adcbe08a97d75a18872f8fed37161d3124217471c402b3929f051532f795f4ff2e53dcebb1644df4c1a5f3a9c476fef3b9f8c51 + languageName: node + linkType: hard + "pluralize@npm:^2.0.0": version: 2.0.0 resolution: "pluralize@npm:2.0.0" @@ -4033,6 +4555,17 @@ __metadata: languageName: node linkType: hard +"postcss@npm:^8.5.17": + version: 8.5.24 + resolution: "postcss@npm:8.5.24" + dependencies: + nanoid: "npm:^3.3.16" + picocolors: "npm:^1.1.1" + source-map-js: "npm:^1.2.1" + checksum: 10/4e26169ed6fe8c2a32702d233b16368124d162a9399606381ed7a2f99ba6237360717aab339a0bd0f072280663413ff5115189b282085d1be74ffb2d25bdb562 + languageName: node + linkType: hard + "prebuild-install@npm:^7.0.1": version: 7.1.2 resolution: "prebuild-install@npm:7.1.2" @@ -4225,6 +4758,64 @@ __metadata: languageName: node linkType: hard +"rolldown@npm:~1.1.5": + version: 1.1.5 + resolution: "rolldown@npm:1.1.5" + dependencies: + "@oxc-project/types": "npm:=0.139.0" + "@rolldown/binding-android-arm64": "npm:1.1.5" + "@rolldown/binding-darwin-arm64": "npm:1.1.5" + "@rolldown/binding-darwin-x64": "npm:1.1.5" + "@rolldown/binding-freebsd-x64": "npm:1.1.5" + "@rolldown/binding-linux-arm-gnueabihf": "npm:1.1.5" + "@rolldown/binding-linux-arm64-gnu": "npm:1.1.5" + "@rolldown/binding-linux-arm64-musl": "npm:1.1.5" + "@rolldown/binding-linux-ppc64-gnu": "npm:1.1.5" + "@rolldown/binding-linux-s390x-gnu": "npm:1.1.5" + "@rolldown/binding-linux-x64-gnu": "npm:1.1.5" + "@rolldown/binding-linux-x64-musl": "npm:1.1.5" + "@rolldown/binding-openharmony-arm64": "npm:1.1.5" + "@rolldown/binding-wasm32-wasi": "npm:1.1.5" + "@rolldown/binding-win32-arm64-msvc": "npm:1.1.5" + "@rolldown/binding-win32-x64-msvc": "npm:1.1.5" + "@rolldown/pluginutils": "npm:^1.0.0" + dependenciesMeta: + "@rolldown/binding-android-arm64": + optional: true + "@rolldown/binding-darwin-arm64": + optional: true + "@rolldown/binding-darwin-x64": + optional: true + "@rolldown/binding-freebsd-x64": + optional: true + "@rolldown/binding-linux-arm-gnueabihf": + optional: true + "@rolldown/binding-linux-arm64-gnu": + optional: true + "@rolldown/binding-linux-arm64-musl": + optional: true + "@rolldown/binding-linux-ppc64-gnu": + optional: true + "@rolldown/binding-linux-s390x-gnu": + optional: true + "@rolldown/binding-linux-x64-gnu": + optional: true + "@rolldown/binding-linux-x64-musl": + optional: true + "@rolldown/binding-openharmony-arm64": + optional: true + "@rolldown/binding-wasm32-wasi": + optional: true + "@rolldown/binding-win32-arm64-msvc": + optional: true + "@rolldown/binding-win32-x64-msvc": + optional: true + bin: + rolldown: ./bin/cli.mjs + checksum: 10/a2c90a68751591fe6e05952d3dc5c92e8cef18511568af43912fc95ebd2c1a1ee9650df9c998a6a66a2f5aa5e02853b66bbc255ab38ef62f86fa8af925a669f9 + languageName: node + linkType: hard + "run-parallel@npm:^1.1.9": version: 1.2.0 resolution: "run-parallel@npm:1.2.0" @@ -4365,6 +4956,13 @@ __metadata: languageName: node linkType: hard +"siginfo@npm:^2.0.0": + version: 2.0.0 + resolution: "siginfo@npm:2.0.0" + checksum: 10/e93ff66c6531a079af8fb217240df01f980155b5dc408d2d7bebc398dd284e383eb318153bf8acd4db3c4fe799aa5b9a641e38b0ba3b1975700b1c89547ea4e7 + languageName: node + linkType: hard + "signal-exit@npm:^4.0.1, signal-exit@npm:^4.1.0": version: 4.1.0 resolution: "signal-exit@npm:4.1.0" @@ -4443,6 +5041,13 @@ __metadata: languageName: node linkType: hard +"source-map-js@npm:^1.2.1": + version: 1.2.1 + resolution: "source-map-js@npm:1.2.1" + checksum: 10/ff9d8c8bf096d534a5b7707e0382ef827b4dd360a577d3f34d2b9f48e12c9d230b5747974ee7c607f0df65113732711bb701fe9ece3c7edbd43cb2294d707df3 + languageName: node + linkType: hard + "spdx-correct@npm:^3.0.0": version: 3.2.0 resolution: "spdx-correct@npm:3.2.0" @@ -4486,6 +5091,20 @@ __metadata: languageName: node linkType: hard +"stackback@npm:0.0.2": + version: 0.0.2 + resolution: "stackback@npm:0.0.2" + checksum: 10/2d4dc4e64e2db796de4a3c856d5943daccdfa3dd092e452a1ce059c81e9a9c29e0b9badba91b43ef0d5ff5c04ee62feb3bcc559a804e16faf447bac2d883aa99 + languageName: node + linkType: hard + +"std-env@npm:^4.0.0-rc.1": + version: 4.2.0 + resolution: "std-env@npm:4.2.0" + checksum: 10/d30c3ae49c5568b4e61dca628eaafe12944dbcfe76980e5b1b1499b48e23f85f1ee01fe2306eeef5964a80b763948bbf2ba40490bc53709f45cf989071c9e4e7 + languageName: node + linkType: hard + "stoppable@npm:^1.1.0": version: 1.1.0 resolution: "stoppable@npm:1.1.0" @@ -4670,6 +5289,20 @@ __metadata: languageName: node linkType: hard +"tinybench@npm:^2.9.0": + version: 2.9.0 + resolution: "tinybench@npm:2.9.0" + checksum: 10/cfa1e1418e91289219501703c4693c70708c91ffb7f040fd318d24aef419fb5a43e0c0160df9471499191968b2451d8da7f8087b08c3133c251c40d24aced06c + languageName: node + linkType: hard + +"tinyexec@npm:^1.0.2": + version: 1.2.4 + resolution: "tinyexec@npm:1.2.4" + checksum: 10/f20b3e6f56f24c3ebe0129d0b6e657e561d225df2cf93c1a10362996232dd6ad4b8af8c9e81d258a64d09020e723772baf6fe0be26512dba7c61bb366d67b1f9 + languageName: node + linkType: hard + "tinyglobby@npm:^0.2.12": version: 0.2.15 resolution: "tinyglobby@npm:0.2.15" @@ -4680,6 +5313,23 @@ __metadata: languageName: node linkType: hard +"tinyglobby@npm:^0.2.15, tinyglobby@npm:^0.2.17": + version: 0.2.17 + resolution: "tinyglobby@npm:0.2.17" + dependencies: + fdir: "npm:^6.5.0" + picomatch: "npm:^4.0.4" + checksum: 10/f85e8a217d675c3f78d5f0ad25ea4557e7e023ed13ddc2b014da10bd0312eea53a34cd52356af07ccdff777f1243012547656282a4ca70936f68bf5065fbaa71 + languageName: node + linkType: hard + +"tinyrainbow@npm:^3.1.0": + version: 3.1.1 + resolution: "tinyrainbow@npm:3.1.1" + checksum: 10/6aa4aadf89cc8ecf8c227ef189616911292c4f0d2d5708b669b00375c5a8b43058115685f043034ffb11a8744c24650a30493525ff62134b436d94c84fa33ed5 + languageName: node + linkType: hard + "tmp@npm:^0.2.3": version: 0.2.7 resolution: "tmp@npm:0.2.7" @@ -4803,10 +5453,10 @@ __metadata: languageName: node linkType: hard -"undici-types@npm:~6.19.2": - version: 6.19.8 - resolution: "undici-types@npm:6.19.8" - checksum: 10/cf0b48ed4fc99baf56584afa91aaffa5010c268b8842f62e02f752df209e3dea138b372a60a963b3b2576ed932f32329ce7ddb9cb5f27a6c83040d8cd74b7a70 +"undici-types@npm:~6.21.0": + version: 6.21.0 + resolution: "undici-types@npm:6.21.0" + checksum: 10/ec8f41aa4359d50f9b59fa61fe3efce3477cc681908c8f84354d8567bb3701fafdddf36ef6bff307024d3feb42c837cf6f670314ba37fc8145e219560e473d14 languageName: node linkType: hard @@ -4898,6 +5548,131 @@ __metadata: languageName: node linkType: hard +"vite@npm:^6.0.0 || ^7.0.0 || ^8.0.0": + version: 8.1.5 + resolution: "vite@npm:8.1.5" + dependencies: + fsevents: "npm:~2.3.3" + lightningcss: "npm:^1.32.0" + picomatch: "npm:^4.0.5" + postcss: "npm:^8.5.17" + rolldown: "npm:~1.1.5" + tinyglobby: "npm:^0.2.17" + peerDependencies: + "@types/node": ^20.19.0 || >=22.12.0 + "@vitejs/devtools": ^0.3.0 + esbuild: ^0.27.0 || ^0.28.0 + jiti: ">=1.21.0" + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: ">=0.54.8" + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + dependenciesMeta: + fsevents: + optional: true + peerDependenciesMeta: + "@types/node": + optional: true + "@vitejs/devtools": + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + bin: + vite: bin/vite.js + checksum: 10/7278baa0723097a181566a46050f8218bb2c57c559cb68494709b6eb9a9eb332bfbc8fb3638857300eeaedbae37343e131e323da9ebda157e5f9ccfc48eefe02 + languageName: node + linkType: hard + +"vitest@npm:^4.1.10": + version: 4.1.10 + resolution: "vitest@npm:4.1.10" + dependencies: + "@vitest/expect": "npm:4.1.10" + "@vitest/mocker": "npm:4.1.10" + "@vitest/pretty-format": "npm:4.1.10" + "@vitest/runner": "npm:4.1.10" + "@vitest/snapshot": "npm:4.1.10" + "@vitest/spy": "npm:4.1.10" + "@vitest/utils": "npm:4.1.10" + es-module-lexer: "npm:^2.0.0" + expect-type: "npm:^1.3.0" + magic-string: "npm:^0.30.21" + obug: "npm:^2.1.1" + pathe: "npm:^2.0.3" + picomatch: "npm:^4.0.3" + std-env: "npm:^4.0.0-rc.1" + tinybench: "npm:^2.9.0" + tinyexec: "npm:^1.0.2" + tinyglobby: "npm:^0.2.15" + tinyrainbow: "npm:^3.1.0" + vite: "npm:^6.0.0 || ^7.0.0 || ^8.0.0" + why-is-node-running: "npm:^2.3.0" + peerDependencies: + "@edge-runtime/vm": "*" + "@opentelemetry/api": ^1.9.0 + "@types/node": ^20.0.0 || ^22.0.0 || >=24.0.0 + "@vitest/browser-playwright": 4.1.10 + "@vitest/browser-preview": 4.1.10 + "@vitest/browser-webdriverio": 4.1.10 + "@vitest/coverage-istanbul": 4.1.10 + "@vitest/coverage-v8": 4.1.10 + "@vitest/ui": 4.1.10 + happy-dom: "*" + jsdom: "*" + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + "@edge-runtime/vm": + optional: true + "@opentelemetry/api": + optional: true + "@types/node": + optional: true + "@vitest/browser-playwright": + optional: true + "@vitest/browser-preview": + optional: true + "@vitest/browser-webdriverio": + optional: true + "@vitest/coverage-istanbul": + optional: true + "@vitest/coverage-v8": + optional: true + "@vitest/ui": + optional: true + happy-dom: + optional: true + jsdom: + optional: true + vite: + optional: false + bin: + vitest: ./vitest.mjs + checksum: 10/020843460fe696c23be2a363634dde4daf54625f1c443c24066ba3f87c478b0ccfdd5124343ba30eb092f54902ffea09f4bed0af4a16a9ee805e494ee2dce34e + languageName: node + linkType: hard + "which@npm:^2.0.1": version: 2.0.2 resolution: "which@npm:2.0.2" @@ -4920,6 +5695,18 @@ __metadata: languageName: node linkType: hard +"why-is-node-running@npm:^2.3.0": + version: 2.3.0 + resolution: "why-is-node-running@npm:2.3.0" + dependencies: + siginfo: "npm:^2.0.0" + stackback: "npm:0.0.2" + bin: + why-is-node-running: cli.js + checksum: 10/0de6e6cd8f2f94a8b5ca44e84cf1751eadcac3ebedcdc6e5fbbe6c8011904afcbc1a2777c53496ec02ced7b81f2e7eda61e76bf8262a8bc3ceaa1f6040508051 + languageName: node + linkType: hard + "wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": version: 7.0.0 resolution: "wrap-ansi@npm:7.0.0"