From dd9bfc9c6d10f1e88d8bc1dcca28741d84b97967 Mon Sep 17 00:00:00 2001 From: Tobias Date: Mon, 15 Jun 2026 10:20:00 +0200 Subject: [PATCH] GLSP-1636: Make CLI commands package-manager aware Make the CLI work against both pnpm- and yarn/lerna-based GLSP repositories so the same commands can be used during a phased migration. This is the minimal, package-manager-agnostic CLI change to drive forward a phased migration of repo per repo. - Add pnpm/yarn detection and workspace discovery to package-util (detectPackageManager, getWorkspacePackages, command helpers) - Route coverageReport, updateNext, releng version/prepare and the repo build/run/start/vscode commands through the new helpers so they work in both pnpm- and yarn-based repositories - Add 'glsp releng publish ' (replaces 'lerna publish'): detects the package manager and dispatches to 'pnpm publish -r' for pnpm repos or legacy 'lerna publish' (canary next / from-package latest) for not-yet-migrated yarn repos; '--dry-run' is pnpm-only - releng version: package-manager aware via getWorkspacePackages; lerna.json is updated only when present (root package.json is the version source of truth) and workspace: ranges are preserved Part-of: eclipse-glsp/glsp#1636 --- dev-packages/cli/README.md | 41 ++- .../cli/src/commands/coverage-report.ts | 16 +- .../cli/src/commands/releng/common.ts | 35 +++ .../cli/src/commands/releng/prepare.ts | 18 +- .../cli/src/commands/releng/publish.spec.ts | 270 ++++++++++++++++++ .../cli/src/commands/releng/publish.ts | 179 ++++++++++++ .../cli/src/commands/releng/releng.ts | 4 +- .../cli/src/commands/releng/version.spec.ts | 142 +++++++++ .../cli/src/commands/releng/version.ts | 25 +- .../cli/src/commands/repo/build.spec.ts | 48 +++- dev-packages/cli/src/commands/repo/build.ts | 38 ++- dev-packages/cli/src/commands/repo/run.ts | 9 +- .../cli/src/commands/repo/start.spec.ts | 29 +- dev-packages/cli/src/commands/repo/start.ts | 21 +- dev-packages/cli/src/commands/repo/vscode.ts | 14 +- dev-packages/cli/src/commands/update-next.ts | 39 ++- .../cli/src/util/package-util.spec.ts | 110 ++++++- dev-packages/cli/src/util/package-util.ts | 90 ++++++ .../cli/tests/e2e/update-next.e2e.spec.ts | 2 + 19 files changed, 1055 insertions(+), 75 deletions(-) create mode 100644 dev-packages/cli/src/commands/releng/publish.spec.ts create mode 100644 dev-packages/cli/src/commands/releng/publish.ts create mode 100644 dev-packages/cli/src/commands/releng/version.spec.ts diff --git a/dev-packages/cli/README.md b/dev-packages/cli/README.md index d340de6..e0f014e 100644 --- a/dev-packages/cli/README.md +++ b/dev-packages/cli/README.md @@ -63,7 +63,8 @@ Options: ## coverageReport -The `coverageReport` command can be used to create a full nyc test coverage report for a lerna/yarn mono repository. +The `coverageReport` command can be used to create a full nyc test coverage report for a pnpm/yarn mono repository. +The package manager is auto-detected from the repository (pnpm-workspace.yaml/pnpm-lock.yaml vs. yarn.lock). Individual coverage reports for each package are created and then combined to a full report. ```console @@ -142,14 +143,15 @@ Commands: version [options] [customVersion] Set the version of all packages in a GLSP repository prepare [options] [customVersion] Prepare a new release for a GLSP component (version bump, changelog, PR creation ...) + publish [options] Publish all workspace packages of a GLSP repository help [command] display help for command ``` ### version Command to bump the version of all packages in a GLSP repository. -Similar to "lerna version" this bumps the version of all workspace packages. -In addition, external GLSP dependencies are considered and bumped as well. +This bumps the version of all workspace packages (the root `package.json` version is the source of truth). +In addition, external GLSP dependencies are considered and bumped as well; `workspace:` ranges are preserved. The glsp repository type ("glsp-client", "glsp-server-node" etc.) is auto detected from the given repository path. If the command is invoked in a non-GLSP repository it will fail. @@ -199,6 +201,39 @@ Options: -h, --help display help for command ``` +### publish + +Publishes all (public) workspace packages of a GLSP repository (replaces `lerna publish`). +The package manager is auto-detected: pnpm-based repositories publish via `pnpm publish -r`, while +not-yet-migrated yarn/lerna-based repositories fall back to the legacy `lerna publish`. + +- `next`: applies a canary version (`.`, e.g. `2.8.0-next.42`) to all + workspace packages and publishes them under the `next` dist-tag. Requires the full git history + (`fetch-depth: 0` in CI) to derive the commit count. +- `latest`: publishes the current package versions under the `latest` dist-tag. Packages whose version + already exists on the registry are skipped. + +For pnpm repositories publishing is delegated to `pnpm publish -r`, so `workspace:` dependency ranges are +rewritten to exact versions; in both cases npm provenance/trusted publishing (`NPM_CONFIG_PROVENANCE`) is +preserved. `--dry-run` is only supported for pnpm-based repositories. + +```console +$ glsp releng publish -h +Usage: glsp releng publish [options] + +Publish all workspace packages of a GLSP repository (pnpm: `pnpm publish`, yarn/lerna: `lerna publish`) + +Arguments: + distTag The npm dist-tag to publish under (choices: "next", "latest") + +Options: + -v, --verbose Enable verbose (debug) log output (default: false) + -r, --repoDir Path to the component repository (default: "") + --dry-run Derive versions and run `pnpm publish` in dry-run mode without applying changes (default: false) + --registry Publish to a custom npm registry (e.g. a local verdaccio for testing) + -h, --help display help for command +``` + ## repo Multi-repository workspace management for GLSP development. diff --git a/dev-packages/cli/src/commands/coverage-report.ts b/dev-packages/cli/src/commands/coverage-report.ts index 2cd279a..a558cb1 100644 --- a/dev-packages/cli/src/commands/coverage-report.ts +++ b/dev-packages/cli/src/commands/coverage-report.ts @@ -21,11 +21,14 @@ import { PackageHelper, baseCommand, cd, + detectPackageManager, exec, execAsync, + execBinCommand, findFiles, - getYarnWorkspacePackages, + getWorkspacePackages, moveFile, + runScriptCommand, validateDirectory } from '../util'; @@ -42,7 +45,7 @@ export const CoverageReportCommand = baseCommand() // .action(generateCoverageReport); /** - * Generates and aggregates an 'nyc' coverage report for lerna/yarn mono repositories. + * Generates and aggregates an 'nyc' coverage report for pnpm/yarn mono repositories. * First, individual reports for each package are generated. Then, they are aggregated into one combined HTML report. * @param options configuration options */ @@ -56,9 +59,10 @@ export async function generateCoverageReport(options: CoverageCmdOptions): Promi } export function validateAndRetrievePackages(options: CoverageCmdOptions): PackageHelper[] { - exec('yarn nyc -h', { silent: true, errorMsg: 'Nyc is not installed!' }); + const pm = detectPackageManager(options.projectRoot); + exec(execBinCommand(pm, 'nyc -h'), { silent: true, errorMsg: 'Nyc is not installed!' }); - const workspacePackages = getYarnWorkspacePackages(options.projectRoot, true); + const workspacePackages = getWorkspacePackages(options.projectRoot, true); const rootPackage = workspacePackages.pop()!; if (!rootPackage.hasScript(options.coverageScript)) { @@ -71,7 +75,7 @@ export function validateAndRetrievePackages(options: CoverageCmdOptions): Packag export async function collectPackageReportFiles(packages: PackageHelper[], options: CoverageCmdOptions): Promise { LOGGER.info('Create individual package coverage reports'); - await execAsync(`yarn ${options.coverageScript}`, { silent: false }); + await execAsync(runScriptCommand(detectPackageManager(options.projectRoot), options.coverageScript), { silent: false }); const reports: string[] = packages.flatMap(pkg => findFiles(pkg.location, '**/coverage-final.json')); LOGGER.info(`Collected ${reports.length} coverage reports from ${packages.length} packages`); return reports; @@ -102,7 +106,7 @@ async function combineReports(reportFiles: string[], options: CoverageCmdOptions }); // Generate report - await execAsync('yarn nyc report --reporter html', { silent: false }); + await execAsync(execBinCommand(detectPackageManager(options.projectRoot), 'nyc report --reporter html'), { silent: false }); // Restore nyc configs (if any) tempFiles.forEach(config => moveFile(config, config.substring(1))); diff --git a/dev-packages/cli/src/commands/releng/common.ts b/dev-packages/cli/src/commands/releng/common.ts index ab41209..214c906 100644 --- a/dev-packages/cli/src/commands/releng/common.ts +++ b/dev-packages/cli/src/commands/releng/common.ts @@ -146,6 +146,41 @@ export function isNextVersion(version: string): boolean { return version.endsWith('-next') || version.endsWith('.SNAPSHOT'); } +export interface CanaryVersion { + /** The base version from the root package.json, e.g. `2.8.0-next` */ + base: string; + /** The most recent git tag */ + lastTag: string; + /** The number of commits since {@link lastTag} */ + commitCount: number; + /** The derived canary version, e.g. `2.8.0-next.42` */ + version: string; +} + +/** + * Derives a canary version for `next` publishing (replacement for `lerna publish --canary`). + * The version is the root package version suffixed with the number of commits since the last + * git tag, e.g. `2.8.0-next.42`. Requires the full git history (fetch-depth: 0 in CI). + * @param repoDir The root path of the repository + */ +export function deriveCanaryVersion(repoDir: string): CanaryVersion { + const base = getVersionFromPackage(repoDir); + let lastTag: string; + try { + lastTag = exec('git describe --tags --abbrev=0', { cwd: repoDir, silent: true }).trim(); + } catch (error) { + throw new Error( + `Could not determine the last git tag in '${repoDir}'.` + + ' Deriving a canary version requires at least one tag and the full git history (fetch-depth: 0 in CI).' + ); + } + const commitCount = Number.parseInt(exec(`git rev-list --count ${lastTag}..HEAD`, { cwd: repoDir, silent: true }).trim(), 10); + if (Number.isNaN(commitCount)) { + throw new Error(`Could not determine the number of commits since tag '${lastTag}' in '${repoDir}'.`); + } + return { base, lastTag, commitCount, version: `${base}.${commitCount}` }; +} + export async function checkIfNpmVersionIsNew(pckgName: string, newVersion: string): Promise { LOGGER.debug(`Check that the release version is new i.e. does not exist on npm: ${newVersion}`); diff --git a/dev-packages/cli/src/commands/releng/prepare.ts b/dev-packages/cli/src/commands/releng/prepare.ts index 75320d7..e111fba 100644 --- a/dev-packages/cli/src/commands/releng/prepare.ts +++ b/dev-packages/cli/src/commands/releng/prepare.ts @@ -24,11 +24,14 @@ import { commitChanges, createBranch, deleteFile, + detectPackageManager, exec, execAsync, getDefaultBranch, - getYarnWorkspacePackages, + getWorkspacePackages, + installCommand, replaceInFile, + runScriptCommand, validateGitDirectory, writeFile } from '../../util'; @@ -92,7 +95,7 @@ export async function prepareRelease(options: PrepareReleaseOptions): Promise { } async function buildNpm(options: PrepareReleaseOptions): Promise { - LOGGER.info('Install & Build with yarn'); - await execAsync('yarn', { silent: false, cwd: options.repoDir, errorMsg: 'Yarn build failed' }); - LOGGER.debug('Yarn build succeeded'); + const pm = detectPackageManager(options.repoDir); + LOGGER.info(`Install & Build with ${pm}`); + await execAsync(installCommand(pm), { silent: false, cwd: options.repoDir, errorMsg: `${pm} install failed` }); + if (pm === 'pnpm') { + // bare `yarn` triggers the root prepare/build script implicitly; with pnpm we build explicitly + await execAsync(runScriptCommand(pm, '--if-present build'), { silent: false, cwd: options.repoDir, errorMsg: 'Build failed' }); + } + LOGGER.debug('Build succeeded'); } async function buildJavaServer(options: PrepareReleaseOptions): Promise { diff --git a/dev-packages/cli/src/commands/releng/publish.spec.ts b/dev-packages/cli/src/commands/releng/publish.spec.ts new file mode 100644 index 0000000..d52649e --- /dev/null +++ b/dev-packages/cli/src/commands/releng/publish.spec.ts @@ -0,0 +1,270 @@ +/******************************************************************************** + * Copyright (c) 2026 EclipseSource 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. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import { expect } from 'chai'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as sinon from 'sinon'; +import { cleanupTempDir, createTempDir } from '../../../tests/helpers/test-helper'; +import { PackageData, PackageHelper } from '../../util'; +import * as packageUtil from '../../util/package-util'; +import * as processUtil from '../../util/process-util'; +import { deriveCanaryVersion } from './common'; +import { PublishCmdOptions, publish } from './publish'; + +describe('releng publish', () => { + const sandbox = sinon.createSandbox(); + let tempDir: string; + let execStub: sinon.SinonStub; + let execAsyncStub: sinon.SinonStub; + + beforeEach(() => { + tempDir = createTempDir(); + execStub = sandbox.stub(processUtil, 'exec'); + execAsyncStub = sandbox.stub(processUtil, 'execAsync').resolves(''); + }); + + afterEach(() => { + sandbox.restore(); + cleanupTempDir(tempDir); + }); + + function createRootPackage(version: string): void { + fs.writeFileSync(path.join(tempDir, 'package.json'), JSON.stringify({ name: 'parent', version, private: true })); + } + + function markAsPnpmRepo(): void { + fs.writeFileSync(path.join(tempDir, 'pnpm-workspace.yaml'), "packages:\n - 'packages/*'\n"); + } + + function markAsYarnRepo(): void { + fs.writeFileSync(path.join(tempDir, 'yarn.lock'), ''); + } + + function createPackage(relativePath: string, content: Partial & { name: string; version: string }): PackageHelper { + const pkgDir = path.join(tempDir, relativePath); + fs.mkdirSync(pkgDir, { recursive: true }); + const filePath = path.join(pkgDir, 'package.json'); + fs.writeFileSync(filePath, JSON.stringify(content, undefined, 4)); + return new PackageHelper(filePath, content.name); + } + + function stubGit(lastTag: string, commitCount: string): void { + execStub.withArgs(sinon.match(/git describe/)).returns(lastTag); + execStub.withArgs(sinon.match(/git rev-list/)).returns(commitCount); + } + + function makeOptions(overrides: Partial = {}): PublishCmdOptions { + return { verbose: false, repoDir: tempDir, dryRun: false, ...overrides }; + } + + describe('deriveCanaryVersion', () => { + it('should derive the canary version from the base version and commit count', () => { + createRootPackage('2.8.0-next'); + stubGit('v2.7.0', '42'); + const canary = deriveCanaryVersion(tempDir); + expect(canary).to.deep.equal({ base: '2.8.0-next', lastTag: 'v2.7.0', commitCount: 42, version: '2.8.0-next.42' }); + }); + + it('should throw a helpful error when no git tag can be found', () => { + createRootPackage('2.8.0-next'); + execStub.withArgs(sinon.match(/git describe/)).throws(new Error('fatal: No names found')); + expect(() => deriveCanaryVersion(tempDir)).to.throw(/fetch-depth: 0/); + }); + }); + + describe('publish (yarn/lerna fallback)', () => { + it('should fall back to `lerna publish ... --canary` for next on yarn repositories', async () => { + createRootPackage('2.8.0-next'); + markAsYarnRepo(); + + await publish('next', makeOptions()); + + expect(execAsyncStub.calledOnce).to.be.true; + const cmd = execAsyncStub.firstCall.args[0] as string; + expect(cmd).to.equal( + 'yarn lerna publish preminor --exact --canary --preid next --dist-tag next ' + + '--no-git-tag-version --no-push --ignore-scripts --yes' + ); + expect(execAsyncStub.firstCall.args[1].cwd).to.equal(tempDir); + }); + + it('should fall back to `lerna publish from-package` for latest on yarn repositories', async () => { + createRootPackage('2.9.0'); + markAsYarnRepo(); + + await publish('latest', makeOptions()); + + expect(execAsyncStub.calledOnce).to.be.true; + expect(execAsyncStub.firstCall.args[0]).to.equal('yarn lerna publish from-package --no-git-reset -y'); + }); + + it('should append a custom registry to the lerna command', async () => { + createRootPackage('2.9.0'); + markAsYarnRepo(); + + await publish('latest', makeOptions({ registry: 'http://localhost:4873' })); + + expect(execAsyncStub.firstCall.args[0]).to.contain('--registry http://localhost:4873'); + }); + + it('should reject --dry-run for yarn/lerna repositories', async () => { + createRootPackage('2.8.0-next'); + markAsYarnRepo(); + try { + await publish('next', makeOptions({ dryRun: true })); + expect.fail('should have thrown'); + } catch (error) { + expect((error as Error).message).to.contain('only supported for pnpm-based repositories'); + } + expect(execAsyncStub.notCalled).to.be.true; + }); + }); + + describe('publish next', () => { + it('should apply the canary version to all workspace packages and publish with --tag next', async () => { + createRootPackage('2.8.0-next'); + markAsPnpmRepo(); + stubGit('v2.7.0', '42'); + const pkgA = createPackage('packages/a', { name: '@eclipse-glsp/a', version: '2.8.0-next' }); + const pkgB = createPackage('packages/b', { name: '@eclipse-glsp/b', version: '2.8.0-next' }); + sandbox.stub(packageUtil, 'getWorkspacePackages').returns([pkgA, pkgB]); + + await publish('next', makeOptions()); + + const writtenA = JSON.parse(fs.readFileSync(pkgA.filePath, 'utf8')); + const writtenB = JSON.parse(fs.readFileSync(pkgB.filePath, 'utf8')); + expect(writtenA.version).to.equal('2.8.0-next.42'); + expect(writtenB.version).to.equal('2.8.0-next.42'); + // the root package keeps the plain base version + const root = JSON.parse(fs.readFileSync(path.join(tempDir, 'package.json'), 'utf8')); + expect(root.version).to.equal('2.8.0-next'); + + expect(execAsyncStub.calledOnce).to.be.true; + expect(execAsyncStub.firstCall.args[0]).to.equal('pnpm publish -r --tag next --no-git-checks --report-summary'); + expect(execAsyncStub.firstCall.args[1].cwd).to.equal(tempDir); + }); + + it('should not write versions and pass --dry-run in dry-run mode', async () => { + createRootPackage('2.8.0-next'); + markAsPnpmRepo(); + stubGit('v2.7.0', '7'); + const pkgA = createPackage('packages/a', { name: '@eclipse-glsp/a', version: '2.8.0-next' }); + sandbox.stub(packageUtil, 'getWorkspacePackages').returns([pkgA]); + + await publish('next', makeOptions({ dryRun: true })); + + const writtenA = JSON.parse(fs.readFileSync(pkgA.filePath, 'utf8')); + expect(writtenA.version).to.equal('2.8.0-next'); + expect(execAsyncStub.firstCall.args[0]).to.contain('--dry-run'); + }); + + it('should pass a custom registry to pnpm publish', async () => { + createRootPackage('2.8.0-next'); + markAsPnpmRepo(); + stubGit('v2.7.0', '7'); + sandbox.stub(packageUtil, 'getWorkspacePackages').returns([]); + + await publish('next', makeOptions({ registry: 'http://localhost:4873' })); + + expect(execAsyncStub.firstCall.args[0]).to.contain('--registry http://localhost:4873'); + }); + + it('should refuse to publish a canary if the root version is not a next version', async () => { + createRootPackage('2.8.0'); + markAsPnpmRepo(); + stubGit('v2.7.0', '7'); + try { + await publish('next', makeOptions()); + expect.fail('should have thrown'); + } catch (error) { + expect((error as Error).message).to.contain('not a next version'); + } + }); + }); + + describe('publish latest', () => { + it('should refuse to publish next versions under the latest dist-tag', async () => { + createRootPackage('2.8.0-next'); + markAsPnpmRepo(); + try { + await publish('latest', makeOptions()); + expect.fail('should have thrown'); + } catch (error) { + expect((error as Error).message).to.contain("Refusing to publish under the 'latest' dist-tag"); + } + }); + + it('should publish with --tag latest when unpublished packages exist', async () => { + createRootPackage('2.9.0'); + markAsPnpmRepo(); + const pkgA = createPackage('packages/a', { name: '@eclipse-glsp/a', version: '2.9.0' }); + sandbox.stub(packageUtil, 'getWorkspacePackages').returns([pkgA]); + // npm view fails -> version does not exist yet + execStub.withArgs(sinon.match(/npm view/)).throws(new Error('404')); + + await publish('latest', makeOptions()); + + expect(execAsyncStub.calledOnce).to.be.true; + expect(execAsyncStub.firstCall.args[0]).to.equal('pnpm publish -r --tag latest --no-git-checks --report-summary'); + }); + + it('should skip publishing when all package versions already exist', async () => { + createRootPackage('2.9.0'); + markAsPnpmRepo(); + const pkgA = createPackage('packages/a', { name: '@eclipse-glsp/a', version: '2.9.0' }); + sandbox.stub(packageUtil, 'getWorkspacePackages').returns([pkgA]); + // npm view returns the version -> already published + execStub.withArgs(sinon.match(/npm view/)).returns('2.9.0'); + + await publish('latest', makeOptions()); + + expect(execAsyncStub.notCalled).to.be.true; + }); + + it('should ignore private packages when checking for unpublished versions', async () => { + createRootPackage('2.9.0'); + markAsPnpmRepo(); + const pkgA = createPackage('packages/a', { name: '@eclipse-glsp/a', version: '2.9.0' }); + const examplePkg = createPackage('examples/e', { name: '@eclipse-glsp-examples/e', version: '2.9.0', private: true }); + sandbox.stub(packageUtil, 'getWorkspacePackages').returns([pkgA, examplePkg]); + execStub.withArgs(sinon.match(/npm view @eclipse-glsp\/a/)).returns('2.9.0'); + + await publish('latest', makeOptions()); + + // the only public package is already published -> nothing to publish + expect(execAsyncStub.notCalled).to.be.true; + }); + }); + + describe('publish summary', () => { + it('should report and remove the pnpm publish summary', async () => { + createRootPackage('2.8.0-next'); + markAsPnpmRepo(); + stubGit('v2.7.0', '7'); + sandbox.stub(packageUtil, 'getWorkspacePackages').returns([]); + const summaryPath = path.join(tempDir, 'pnpm-publish-summary.json'); + execAsyncStub.callsFake(() => { + fs.writeFileSync(summaryPath, JSON.stringify({ publishedPackages: [{ name: '@eclipse-glsp/a', version: '2.8.0-next.7' }] })); + return Promise.resolve(''); + }); + + await publish('next', makeOptions()); + + expect(fs.existsSync(summaryPath)).to.be.false; + }); + }); +}); diff --git a/dev-packages/cli/src/commands/releng/publish.ts b/dev-packages/cli/src/commands/releng/publish.ts new file mode 100644 index 0000000..a236583 --- /dev/null +++ b/dev-packages/cli/src/commands/releng/publish.ts @@ -0,0 +1,179 @@ +/******************************************************************************** + * Copyright (c) 2026 EclipseSource 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. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import { Argument } from 'commander'; +import * as fs from 'fs'; +import * as path from 'path'; +import { + LOGGER, + PackageManager, + baseCommand, + detectPackageManager, + execAsync, + execBinCommand, + getWorkspacePackages, + validateGitDirectory +} from '../../util'; +import { configureEnv, deriveCanaryVersion, getVersionFromPackage, isNextVersion, npmVersionExists } from './common'; + +export type PublishDistTag = 'next' | 'latest'; + +export interface PublishCmdOptions { + verbose: boolean; + repoDir: string; + dryRun: boolean; + registry?: string; +} + +export const PublishCommand = baseCommand() + .name('publish') + .description('Publish all workspace packages of a GLSP repository (pnpm: `pnpm publish`, yarn/lerna: `lerna publish`)') + .addArgument(new Argument('', 'The npm dist-tag to publish under').choices(['next', 'latest'])) + .option('-v, --verbose', 'Enable verbose (debug) log output', false) + .option('-r, --repoDir ', 'Path to the component repository', validateGitDirectory, process.cwd()) + .option('--dry-run', 'Derive versions and run `pnpm publish` in dry-run mode without applying changes', false) + .option('--registry ', 'Publish to a custom npm registry (e.g. a local verdaccio for testing)') + .action((distTag: PublishDistTag, cmdOptions: PublishCmdOptions) => { + configureEnv(cmdOptions); + return publish(distTag, cmdOptions); + }); + +/** + * Publishes all (public) workspace packages of a GLSP repository, dispatching on the detected + * package manager so that pnpm-based and not-yet-migrated yarn/lerna-based repositories are both + * supported during the migration period. + * - `next`: applies a canary version (`.`) and publishes it + * under the `next` dist-tag. + * - `latest`: publishes the current package versions under the `latest` dist-tag. Already published + * versions are skipped. + * + * For pnpm repositories publishing is delegated to `pnpm publish -r` (so that `workspace:` ranges are + * rewritten to exact versions); for yarn/lerna repositories it falls back to the legacy `lerna publish`. + * In both cases npm provenance/trusted publishing (configured via environment) is preserved. + */ +export async function publish(distTag: PublishDistTag, options: PublishCmdOptions): Promise { + const packageManager = detectPackageManager(options.repoDir); + LOGGER.info(`Publish workspace packages of '${options.repoDir}' with dist-tag '${distTag}' (package manager: ${packageManager})`); + if (packageManager !== 'pnpm') { + return publishWithLerna(distTag, packageManager, options); + } + if (distTag === 'next') { + return publishNext(options); + } + return publishLatest(options); +} + +/** + * Legacy publishing path for yarn/lerna-based repositories that have not been migrated to pnpm yet. + * Mirrors the former root `package.json` `publish:next`/`publish:latest` scripts; `lerna` itself derives + * the canary version (for `next`) and skips already-published versions (for `latest`). The `lerna` binary + * is resolved from the repository's `node_modules` via the detected package manager. + */ +async function publishWithLerna(distTag: PublishDistTag, packageManager: PackageManager, options: PublishCmdOptions): Promise { + if (options.dryRun) { + throw new Error("'--dry-run' is only supported for pnpm-based repositories ('lerna publish' has no dry-run mode)."); + } + const lernaArgs = + distTag === 'next' + ? 'publish preminor --exact --canary --preid next --dist-tag next --no-git-tag-version --no-push --ignore-scripts --yes' + : 'publish from-package --no-git-reset -y'; + let cmd = `${execBinCommand(packageManager, 'lerna')} ${lernaArgs}`; + if (options.registry) { + cmd += ` --registry ${options.registry}`; + } + await execAsync(cmd, { cwd: options.repoDir, silent: false, errorMsg: 'lerna publish failed' }); +} + +async function publishNext(options: PublishCmdOptions): Promise { + const canary = deriveCanaryVersion(options.repoDir); + if (!isNextVersion(canary.base)) { + throw new Error(`The root package version '${canary.base}' is not a next version. Cannot publish a 'next' canary release.`); + } + LOGGER.info(`Applying canary version ${canary.version} (base: ${canary.base}, ${canary.commitCount} commits since ${canary.lastTag})`); + + // Only the workspace packages get the canary version; the root keeps the plain base version. + // workspace:* dependency ranges are resolved to the exact canary version by pnpm on publish. + const packages = getWorkspacePackages(options.repoDir); + packages.forEach(pkg => { + if (options.dryRun) { + LOGGER.info(`[dry-run] Would set version of ${pkg.name} to ${canary.version}`); + return; + } + LOGGER.debug(`Set version of ${pkg.name} to ${canary.version}`); + pkg.content.version = canary.version; + pkg.write(); + }); + + await pnpmPublish('next', options); +} + +async function publishLatest(options: PublishCmdOptions): Promise { + const version = getVersionFromPackage(options.repoDir); + if (isNextVersion(version)) { + throw new Error(`The root package version '${version}' is a next version. Refusing to publish under the 'latest' dist-tag.`); + } + + const publicPackages = getWorkspacePackages(options.repoDir).filter(pkg => !pkg.content.private); + const unpublished = publicPackages.filter(pkg => { + if (npmVersionExists(pkg.name, pkg.content.version)) { + LOGGER.info(`Skipping ${pkg.name}@${pkg.content.version} - already published`); + return false; + } + return true; + }); + if (unpublished.length === 0) { + LOGGER.warn('All package versions are already published. Nothing to publish.'); + return; + } + LOGGER.info(`Publishing ${unpublished.length} of ${publicPackages.length} public packages`); + + await pnpmPublish('latest', options); +} + +async function pnpmPublish(distTag: PublishDistTag, options: PublishCmdOptions): Promise { + let cmd = `pnpm publish -r --tag ${distTag} --no-git-checks --report-summary`; + if (options.dryRun) { + cmd += ' --dry-run'; + } + if (options.registry) { + cmd += ` --registry ${options.registry}`; + } + // plain env passthrough preserves NPM_CONFIG_PROVENANCE and the OIDC token for trusted publishing + await execAsync(cmd, { cwd: options.repoDir, silent: false, errorMsg: 'pnpm publish failed' }); + reportPublishSummary(options); +} + +function reportPublishSummary(options: PublishCmdOptions): void { + const summaryPath = path.resolve(options.repoDir, 'pnpm-publish-summary.json'); + if (!fs.existsSync(summaryPath)) { + LOGGER.warn('No pnpm publish summary found.'); + return; + } + try { + const summary = JSON.parse(fs.readFileSync(summaryPath, 'utf8')) as { + publishedPackages?: { name: string; version: string }[]; + }; + const published = summary.publishedPackages ?? []; + if (published.length === 0) { + LOGGER.warn('No packages were published.'); + } else { + LOGGER.info(`Published ${published.length} packages:`); + published.forEach(pkg => LOGGER.info(` - ${pkg.name}@${pkg.version}`)); + } + } finally { + fs.rmSync(summaryPath, { force: true }); + } +} diff --git a/dev-packages/cli/src/commands/releng/releng.ts b/dev-packages/cli/src/commands/releng/releng.ts index cba5465..3b850b6 100644 --- a/dev-packages/cli/src/commands/releng/releng.ts +++ b/dev-packages/cli/src/commands/releng/releng.ts @@ -16,10 +16,12 @@ import { baseCommand } from '../../util'; import { PrepareReleaseCommand } from './prepare'; +import { PublishCommand } from './publish'; import { VersionCommand } from './version'; export const RelengCommand = baseCommand() .name('releng') .description('Commands for GLSP release engineering (Linux only, intended for CI/Maintainer use).') .addCommand(VersionCommand) - .addCommand(PrepareReleaseCommand); + .addCommand(PrepareReleaseCommand) + .addCommand(PublishCommand); diff --git a/dev-packages/cli/src/commands/releng/version.spec.ts b/dev-packages/cli/src/commands/releng/version.spec.ts new file mode 100644 index 0000000..19daae1 --- /dev/null +++ b/dev-packages/cli/src/commands/releng/version.spec.ts @@ -0,0 +1,142 @@ +/******************************************************************************** + * Copyright (c) 2026 EclipseSource 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. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import { expect } from 'chai'; +import * as fs from 'fs'; +import * as path from 'path'; +import { cleanupTempDir, createTempDir } from '../../../tests/helpers/test-helper'; +import { PackageData, PackageHelper } from '../../util'; +import { VersionType } from './common'; +import { SetVersionOptions, setVersion } from './version'; + +describe('releng version', () => { + let tempDir: string; + let originalCwd: string; + + beforeEach(() => { + originalCwd = process.cwd(); + tempDir = createTempDir(); + }); + + afterEach(() => { + process.chdir(originalCwd); + cleanupTempDir(tempDir); + }); + + function createPackage(relativePath: string, content: Partial & { name: string }): PackageHelper { + const pkgDir = path.join(tempDir, relativePath); + fs.mkdirSync(pkgDir, { recursive: true }); + const filePath = path.join(pkgDir, 'package.json'); + fs.writeFileSync(filePath, JSON.stringify({ version: '2.8.0-next', ...content }, undefined, 4)); + return new PackageHelper(filePath, content.name); + } + + function readPackageJson(relativePath: string): PackageData { + return JSON.parse(fs.readFileSync(path.join(tempDir, relativePath, 'package.json'), 'utf8')); + } + + function makeOptions(version: string, workspacePackages: PackageHelper[]): SetVersionOptions { + return { + verbose: false, + repoDir: tempDir, + repo: 'glsp-client', + version, + versionType: (version.endsWith('-next') ? 'next' : 'custom') as VersionType, + workspacePackages + }; + } + + it('should bump the version of all workspace packages including the root', async () => { + const root = createPackage('.', { name: 'parent', private: true }); + const pkgA = createPackage('packages/a', { name: '@eclipse-glsp/a' }); + + await setVersion(makeOptions('2.9.0', [pkgA, root])); + + expect(readPackageJson('.').version).to.equal('2.9.0'); + expect(readPackageJson('packages/a').version).to.equal('2.9.0'); + }); + + it('should preserve workspace: dependency ranges', async () => { + const root = createPackage('.', { name: 'parent', private: true }); + const pkgA = createPackage('packages/a', { name: '@eclipse-glsp/a' }); + const pkgB = createPackage('packages/b', { + name: '@eclipse-glsp/b', + dependencies: { '@eclipse-glsp/a': 'workspace:*' } + }); + + await setVersion(makeOptions('2.9.0', [pkgA, pkgB, root])); + + expect(readPackageJson('packages/b').dependencies!['@eclipse-glsp/a']).to.equal('workspace:*'); + expect(readPackageJson('packages/b').version).to.equal('2.9.0'); + }); + + it('should bump exact-pinned workspace dependencies to the new version', async () => { + const root = createPackage('.', { name: 'parent', private: true }); + const pkgA = createPackage('packages/a', { name: '@eclipse-glsp/a' }); + const pkgB = createPackage('packages/b', { + name: '@eclipse-glsp/b', + dependencies: { '@eclipse-glsp/a': '2.8.0-next' } + }); + + await setVersion(makeOptions('2.9.0-next', [pkgA, pkgB, root])); + + expect(readPackageJson('packages/b').dependencies!['@eclipse-glsp/a']).to.equal('2.9.0-next'); + }); + + it("should set external @eclipse-glsp dependencies to 'next' for next versions", async () => { + const root = createPackage('.', { name: 'parent', private: true }); + const pkgA = createPackage('packages/a', { + name: '@eclipse-glsp/a', + dependencies: { '@eclipse-glsp/protocol': '2.8.0' } + }); + + await setVersion(makeOptions('2.9.0-next', [pkgA, root])); + + expect(readPackageJson('packages/a').dependencies!['@eclipse-glsp/protocol']).to.equal('next'); + }); + + it('should bump external @eclipse-glsp dependencies to the release version for release versions', async () => { + const root = createPackage('.', { name: 'parent', private: true }); + const pkgA = createPackage('packages/a', { + name: '@eclipse-glsp/a', + dependencies: { '@eclipse-glsp/protocol': 'next' } + }); + + await setVersion(makeOptions('2.9.0', [pkgA, root])); + + expect(readPackageJson('packages/a').dependencies!['@eclipse-glsp/protocol']).to.equal('2.9.0'); + }); + + it('should update lerna.json if present', async () => { + const root = createPackage('.', { name: 'parent', private: true }); + fs.writeFileSync(path.join(tempDir, 'lerna.json'), JSON.stringify({ version: '2.8.0-next', npmClient: 'yarn' })); + + await setVersion(makeOptions('2.9.0', [root])); + + const lernaJson = JSON.parse(fs.readFileSync(path.join(tempDir, 'lerna.json'), 'utf8')); + expect(lernaJson.version).to.equal('2.9.0'); + expect(lernaJson.npmClient).to.equal('yarn'); + }); + + it('should succeed without lerna.json (pnpm repos)', async () => { + const root = createPackage('.', { name: 'parent', private: true }); + + await setVersion(makeOptions('2.9.0', [root])); + + expect(fs.existsSync(path.join(tempDir, 'lerna.json'))).to.be.false; + expect(readPackageJson('.').version).to.equal('2.9.0'); + }); +}); diff --git a/dev-packages/cli/src/commands/releng/version.ts b/dev-packages/cli/src/commands/releng/version.ts index 8baaec7..3e08f30 100644 --- a/dev-packages/cli/src/commands/releng/version.ts +++ b/dev-packages/cli/src/commands/releng/version.ts @@ -15,6 +15,7 @@ ********************************************************************************/ import { Argument } from 'commander'; +import * as fs from 'fs'; import * as path from 'path'; import * as semver from 'semver'; import { @@ -25,7 +26,7 @@ import { exec, execAsync, findFiles, - getYarnWorkspacePackages, + getWorkspacePackages, readFile, readJson, replaceInFile, @@ -52,7 +53,7 @@ export const VersionCommand = baseCommand() const options = { ...cmdOptions, repo, version, versionType }; let workspacePackages: PackageHelper[] | undefined; if (GLSPRepo.isNpmRepo(repo)) { - workspacePackages = GLSPRepo.isNpmRepo(repo) ? getYarnWorkspacePackages(path.join(cmdOptions.repoDir, ''), true) : undefined; + workspacePackages = getWorkspacePackages(cmdOptions.repoDir, true); } return setVersion({ ...options, workspacePackages }); }); @@ -102,10 +103,15 @@ function setVersionNpm(options: NpmSetVersionOptions): void { pkg.write(); }); - // Update lerna file - const lernaJson = readJson<{ version: string }>('lerna.json'); - lernaJson.version = options.version; - writeJson('lerna.json', lernaJson); + // Update lerna file if present (repos migrated to pnpm no longer have one — the root package.json is the source of truth) + const lernaJsonPath = path.resolve(options.repoDir, 'lerna.json'); + if (fs.existsSync(lernaJsonPath)) { + const lernaJson = readJson<{ version: string }>(lernaJsonPath); + lernaJson.version = options.version; + writeJson(lernaJsonPath, lernaJson); + } else { + LOGGER.debug('No lerna.json found, skipping update'); + } // Repo specific changes if (options.repo === 'glsp-theia-integration') { @@ -124,7 +130,10 @@ function updateGLSPDependencies(pkg: PackageHelper, version: string, workspacePa Object.keys(pkg.content[depType] || {}) .filter(dep => workspacePackageNames.has(dep) || dep.startsWith('@eclipse-glsp')) .forEach(dep => { - if (workspacePackageNames.has(dep) || !isNextVersion(version)) { + if (pkg.content[depType]![dep].startsWith('workspace:')) { + // workspace: ranges are resolved by pnpm on publish and must not be rewritten + LOGGER.debug(` - Keep ${depType} ${dep} at '${pkg.content[depType]![dep]}'`); + } else if (workspacePackageNames.has(dep) || !isNextVersion(version)) { LOGGER.debug(` - Bump ${depType} ${dep} to version ${version}`); pkg.content[depType]![dep] = `${version}`; } else { @@ -244,7 +253,7 @@ function setVersionEclipseClient(options: JavaSetVersionOptions): void { ...options, repoDir: clientPath, version: options.version, - workspacePackages: getYarnWorkspacePackages(path.join(options.repoDir, 'client'), true) + workspacePackages: getWorkspacePackages(path.join(options.repoDir, 'client'), true) }); cd(options.repoDir); } diff --git a/dev-packages/cli/src/commands/repo/build.spec.ts b/dev-packages/cli/src/commands/repo/build.spec.ts index afd841f..5b52e1e 100644 --- a/dev-packages/cli/src/commands/repo/build.spec.ts +++ b/dev-packages/cli/src/commands/repo/build.spec.ts @@ -43,6 +43,14 @@ describe('build-action', () => { } } + function markAsYarnRepo(name: string): void { + fs.writeFileSync(path.join(tempDir, name, 'yarn.lock'), ''); + } + + function markAsPnpmRepo(name: string): void { + fs.writeFileSync(path.join(tempDir, name, 'pnpm-workspace.yaml'), ''); + } + beforeEach(() => { tempDir = createTempDir(); execAsyncStub = sandbox.stub(processUtil, 'execAsync').resolves(''); @@ -54,23 +62,45 @@ describe('build-action', () => { }); describe('buildSingleRepo', () => { - it('should run yarn for standard npm repos', async () => { + it('should run yarn install for standard yarn repos', async () => { createRepoDirs('glsp-client'); + markAsYarnRepo('glsp-client'); await buildSingleRepo('glsp-client', makeOptions()); expect(execAsyncStub.calledOnce).to.be.true; - expect(execAsyncStub.firstCall.args[0]).to.equal('yarn'); + expect(execAsyncStub.firstCall.args[0]).to.equal('yarn install'); + }); + + it('should run pnpm install and explicit build for standard pnpm repos', async () => { + createRepoDirs('glsp-client'); + markAsPnpmRepo('glsp-client'); + await buildSingleRepo('glsp-client', makeOptions()); + expect(execAsyncStub.callCount).to.equal(2); + expect(execAsyncStub.firstCall.args[0]).to.equal('pnpm install'); + expect(execAsyncStub.secondCall.args[0]).to.equal('pnpm run --if-present build'); }); - it('should run yarn then yarn browser build for theia-integration', async () => { + it('should run yarn install then yarn browser build for theia-integration (yarn)', async () => { createRepoDirs('glsp-theia-integration'); + markAsYarnRepo('glsp-theia-integration'); await buildSingleRepo('glsp-theia-integration', makeOptions()); expect(execAsyncStub.callCount).to.equal(2); - expect(execAsyncStub.firstCall.args[0]).to.equal('yarn'); + expect(execAsyncStub.firstCall.args[0]).to.equal('yarn install'); expect(execAsyncStub.secondCall.args[0]).to.equal('yarn browser build'); }); - it('should run yarn then yarn electron build with --electron', async () => { + it('should run pnpm install, build and browser build for theia-integration (pnpm)', async () => { + createRepoDirs('glsp-theia-integration'); + markAsPnpmRepo('glsp-theia-integration'); + await buildSingleRepo('glsp-theia-integration', makeOptions()); + expect(execAsyncStub.callCount).to.equal(3); + expect(execAsyncStub.firstCall.args[0]).to.equal('pnpm install'); + expect(execAsyncStub.secondCall.args[0]).to.equal('pnpm run --if-present build'); + expect(execAsyncStub.thirdCall.args[0]).to.equal('pnpm run browser build'); + }); + + it('should run yarn electron build with --electron', async () => { createRepoDirs('glsp-theia-integration'); + markAsYarnRepo('glsp-theia-integration'); await buildSingleRepo('glsp-theia-integration', makeOptions({ electron: true })); expect(execAsyncStub.callCount).to.equal(2); expect(execAsyncStub.secondCall.args[0]).to.equal('yarn electron build'); @@ -84,10 +114,11 @@ describe('build-action', () => { }); it('should build client and server for eclipse-integration', async () => { - createRepoDirs('glsp-eclipse-integration'); + createRepoDirs(path.join('glsp-eclipse-integration', 'client')); + markAsYarnRepo(path.join('glsp-eclipse-integration', 'client')); await buildSingleRepo('glsp-eclipse-integration', makeOptions()); expect(execAsyncStub.callCount).to.equal(2); - expect(execAsyncStub.firstCall.args[0]).to.equal('yarn'); + expect(execAsyncStub.firstCall.args[0]).to.equal('yarn install'); expect(execAsyncStub.firstCall.args[1].cwd).to.contain(path.join('glsp-eclipse-integration', 'client')); expect(execAsyncStub.secondCall.args[0]).to.equal('mvn clean verify -Dstyle.color=always -B'); expect(execAsyncStub.secondCall.args[1].cwd).to.contain(path.join('glsp-eclipse-integration', 'server')); @@ -97,6 +128,7 @@ describe('build-action', () => { describe('runBuildOrdered', () => { it('should build repos sequentially in dependency order', async () => { createRepoDirs('glsp', 'glsp-client', 'glsp-server-node'); + ['glsp', 'glsp-client', 'glsp-server-node'].forEach(markAsYarnRepo); const failures = await runBuildOrdered(['glsp', 'glsp-client', 'glsp-server-node'], makeOptions()); expect(failures).to.equal(0); expect(execAsyncStub.callCount).to.equal(3); @@ -114,6 +146,7 @@ describe('build-action', () => { it('should stop on first failure when failFast is true', async () => { createRepoDirs('glsp', 'glsp-client', 'glsp-server-node'); + ['glsp', 'glsp-client', 'glsp-server-node'].forEach(markAsYarnRepo); execAsyncStub.onFirstCall().rejects(new Error('build failed')); const failures = await runBuildOrdered(['glsp', 'glsp-client', 'glsp-server-node'], makeOptions({ failFast: true })); expect(failures).to.equal(1); @@ -122,6 +155,7 @@ describe('build-action', () => { it('should continue on failure when failFast is false', async () => { createRepoDirs('glsp', 'glsp-client', 'glsp-server-node'); + ['glsp', 'glsp-client', 'glsp-server-node'].forEach(markAsYarnRepo); execAsyncStub.onFirstCall().rejects(new Error('build failed')); const failures = await runBuildOrdered(['glsp', 'glsp-client', 'glsp-server-node'], makeOptions({ failFast: false })); expect(failures).to.equal(1); diff --git a/dev-packages/cli/src/commands/repo/build.ts b/dev-packages/cli/src/commands/repo/build.ts index e0929f3..17a85a3 100644 --- a/dev-packages/cli/src/commands/repo/build.ts +++ b/dev-packages/cli/src/commands/repo/build.ts @@ -16,7 +16,16 @@ import { Command, Option } from 'commander'; import * as path from 'path'; -import { GLSPRepo, LOGGER, PRESET_NAMES, baseCommand, execAsync } from '../../util'; +import { + GLSPRepo, + LOGGER, + PRESET_NAMES, + baseCommand, + detectPackageManager, + execAsync, + installCommand, + runScriptCommand +} from '../../util'; import { configureRepoEnv, formatError, getBuildOrder, resolveTargetRepos, resolveWorkspaceDir, validateReposExist } from './common/utils'; // ── Action ────────────────────────────────────────────────────────────────── @@ -28,20 +37,30 @@ export interface BuildActionOptions { failFast: boolean; } +async function installAndBuildNpm(repoDir: string, execOpts: { cwd?: string; verbose: boolean; silent: boolean }): Promise { + const pm = detectPackageManager(repoDir); + LOGGER.debug(`${pm} install`); + await execAsync(installCommand(pm), { ...execOpts, cwd: repoDir }); + if (pm === 'pnpm') { + // bare `yarn` triggers the root prepare/build script implicitly; with pnpm we build explicitly + LOGGER.debug('pnpm build'); + await execAsync(runScriptCommand(pm, '--if-present build'), { ...execOpts, cwd: repoDir }); + } +} + export async function buildSingleRepo(repo: GLSPRepo, options: BuildActionOptions): Promise { const repoDir = path.resolve(options.dir, repo); - const yarnOpts = { cwd: repoDir, verbose: options.verbose, silent: false, env: { FORCE_COLOR: '1' } }; + const npmOpts = { cwd: repoDir, verbose: options.verbose, silent: false, env: { FORCE_COLOR: '1' } }; const mvnOpts = { cwd: repoDir, verbose: options.verbose, silent: false }; LOGGER.label(`Building ${repo}...`); switch (repo) { case 'glsp-theia-integration': { - LOGGER.debug('Yarn install'); - await execAsync('yarn', yarnOpts); + await installAndBuildNpm(repoDir, npmOpts); const target = options.electron ? 'electron' : 'browser'; - LOGGER.debug('Yarn build for target: ' + target); - await execAsync(`yarn ${target} build`, yarnOpts); + LOGGER.debug('Build for target: ' + target); + await execAsync(runScriptCommand(detectPackageManager(repoDir), `${target} build`), npmOpts); break; } case 'glsp-server': { @@ -49,15 +68,14 @@ export async function buildSingleRepo(repo: GLSPRepo, options: BuildActionOption break; } case 'glsp-eclipse-integration': { - LOGGER.debug('Yarn build for client'); - await execAsync('yarn', { ...yarnOpts, cwd: path.join(repoDir, 'client') }); + LOGGER.debug('Build client'); + await installAndBuildNpm(path.join(repoDir, 'client'), npmOpts); LOGGER.debug('Maven build for server'); await execAsync('mvn clean verify -Dstyle.color=always -B', { ...mvnOpts, cwd: path.join(repoDir, 'server') }); break; } default: { - LOGGER.debug('Yarn build'); - await execAsync('yarn', yarnOpts); + await installAndBuildNpm(repoDir, npmOpts); break; } } diff --git a/dev-packages/cli/src/commands/repo/run.ts b/dev-packages/cli/src/commands/repo/run.ts index 66d8423..4987819 100644 --- a/dev-packages/cli/src/commands/repo/run.ts +++ b/dev-packages/cli/src/commands/repo/run.ts @@ -16,7 +16,7 @@ import * as path from 'path'; import { Command } from 'commander'; -import { GLSPRepo, baseCommand, execForeground } from '../../util'; +import { GLSPRepo, baseCommand, detectPackageManager, execForeground, runScriptCommand } from '../../util'; import { configureRepoEnv, resolveWorkspaceDir } from './common/utils'; export function createScopedRunCommand(repo: GLSPRepo): Command { @@ -24,8 +24,8 @@ export function createScopedRunCommand(repo: GLSPRepo): Command { .name('run') .allowUnknownOption(true) .allowExcessArguments(true) - .description(`Run an arbitrary yarn script in ${repo}`) - .argument('