From f78860d158dc4e3772ca8e33113f79508c4fda83 Mon Sep 17 00:00:00 2001 From: ghiscoding Date: Sun, 15 May 2022 17:49:22 -0400 Subject: [PATCH] fix(init): add missing packages/workspaces - add new option `--use-workspaces` - create `packages` property in "lerna.json" unless `--use-workspaces` is enabled which will create "workspaces" property in "package.json" file --- README.md | 11 +- package.json | 2 +- .../cli/src/cli-commands/cli-init-commands.ts | 5 + packages/core/src/__tests__/command.spec.ts | 36 +-- packages/core/src/models/command-options.ts | 3 + packages/core/src/models/interfaces.ts | 16 +- packages/core/src/package.ts | 8 + packages/core/src/project/project.ts | 2 +- packages/init/README.md | 39 ++- .../__fixtures__/updates/package.json | 2 +- .../init/src/__tests__/init-command.spec.ts | 241 ++++++++++++------ packages/init/src/init-command.ts | 95 +++---- .../run/src/__tests__/run-command.spec.ts | 6 +- 13 files changed, 278 insertions(+), 188 deletions(-) diff --git a/README.md b/README.md index e84d6e86..d3d6b240 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ - [Troubleshooting](https://github.com/ghiscoding/lerna-lite/wiki/Troubleshooting) - Commands - included with CLI - - 🏁 [`init`](https://github.com/ghiscoding/lerna-lite/tree/main/packages/init#readme) - create/initialize a new Lerna-Lite repo + - 🧰 [`init`](https://github.com/ghiscoding/lerna-lite/tree/main/packages/init#readme) - create/initialize a new Lerna-Lite repo - 💻 [`info`](https://github.com/ghiscoding/lerna-lite/tree/main/packages/info#readme) - print local environment information (useful when opening new issue) - ☁️ [`publish`](https://github.com/ghiscoding/lerna-lite/tree/main/packages/publish#readme) - publish workspace packages - 📑 [`version`](https://github.com/ghiscoding/lerna-lite/tree/main/packages/version#readme) - create new version for each workspace packages @@ -34,8 +34,8 @@ --- -### 📢 Lerna-Lite now supports yarn/pnpm `workspace:` protocol -#### _this new feature was introduced with version [1.2.0](https://github.com/ghiscoding/lerna-lite/releases/tag/v1.2.0) of Lerna-Lite, however we recommend using [1.3.0](https://github.com/ghiscoding/lerna-lite/releases/tag/v1.3.0)._ +### 📢 Lerna-Lite now supports yarn/pnpm `workspace:` protocol +#### _this new feature was introduced with release [1.2.0](https://github.com/ghiscoding/lerna-lite/releases/tag/v1.2.0) of Lerna-Lite, however we recommend using [1.3.0](https://github.com/ghiscoding/lerna-lite/releases/tag/v1.3.0)._ If you use this new feature, please take 30sec. to fill in this small [poll #156](https://github.com/ghiscoding/lerna-lite/discussions/156) survey just to see which package manager is the most popular to use with this new `workspace:` protocol. --- @@ -106,18 +106,19 @@ This will create a `lerna.json` configuration file as well as a `packages` folde ``` lerna-repo/ packages/ + package-a package.json lerna.json ``` ## Installation -Run the following commands to install Lerna-Lite in your project and/or install it globally by adding the `-g` option. +Run the following commands to install Lerna-Lite in your project and/or install it globally by adding the `-g` option. If you are new to Lerna-Lite, you could also run the [lerna init](https://github.com/ghiscoding/lerna-lite/tree/main/packages/init#readme) command which will create the `lerna.json` for you. | Command | Install | Description | Included | |---------|-------------|-------------| ---------| -| 🏁 [init](https://github.com/ghiscoding/lerna-lite/tree/main/packages/init#readme) | `npm i @lerna-lite/cli -D -W` | create/initialize a new Lerna-Lite repo | Yes | +| 🧰 [init](https://github.com/ghiscoding/lerna-lite/tree/main/packages/init#readme) | `npm i @lerna-lite/cli -D -W` | create/initialize a new Lerna-Lite repo | Yes | | 💻 [info](https://github.com/ghiscoding/lerna-lite/tree/main/packages/info#readme) | `npm i @lerna-lite/cli -D -W` | print local environment information | Yes | | 📑 [version](https://github.com/ghiscoding/lerna-lite/tree/main/packages/version#readme) | `npm i @lerna-lite/cli -D -W` | create new version for each workspace package | Yes | | ☁️ [publish](https://github.com/ghiscoding/lerna-lite/tree/main/packages/publish#readme) | `npm i @lerna-lite/cli -D -W` | publish each workspace package | Yes | diff --git a/package.json b/package.json index 40babde7..0197ebde 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "build": "npm run build --ws", "lint": "eslint packages --ext .ts", "dist-info-cmd": "node ./packages/cli/dist/cli.js info", - "dist-init-cmd": "node ./packages/cli/dist/cli.js init --independent --exact", + "dist-init-cmd": "node ./packages/cli/dist/cli.js init --independent --exact --use-workspaces", "dist-roll-version": "node ./packages/cli/dist/cli.js version", "dist-roll-version-dry-run": "node ./packages/cli/dist/cli.js version --git-dry-run", "dist-roll-publish": "node ./packages/cli/dist/cli.js publish from-package", diff --git a/packages/cli/src/cli-commands/cli-init-commands.ts b/packages/cli/src/cli-commands/cli-init-commands.ts index 524e765e..f30a94cb 100644 --- a/packages/cli/src/cli-commands/cli-init-commands.ts +++ b/packages/cli/src/cli-commands/cli-init-commands.ts @@ -16,6 +16,11 @@ exports.builder = { alias: 'i', type: 'boolean', }, + 'use-workspaces': { + group: 'Command Options:', + describe: 'Enable integration with Yarn workspaces.', + type: 'boolean', + }, }; exports.handler = function handler(argv) { diff --git a/packages/core/src/__tests__/command.spec.ts b/packages/core/src/__tests__/command.spec.ts index a4c72a12..d5b84fb4 100644 --- a/packages/core/src/__tests__/command.spec.ts +++ b/packages/core/src/__tests__/command.spec.ts @@ -19,7 +19,7 @@ import { updateLernaConfig } from '@lerna-test/update-lerna-config'; import { Command } from '../command'; describe('core-command', () => { - let testDir; + let testDir = ''; beforeAll(async () => { testDir = await initFixture('basic'); @@ -38,7 +38,7 @@ describe('core-command', () => { // swallow errors when passed in argv const onRejected = () => { }; - class OkCommand extends Command { + class OkCommand extends Command { initialize() { return true; } @@ -49,7 +49,7 @@ describe('core-command', () => { } // convenience to avoid silly 'not implemented errors' - const testFactory = (argv = {}) => new OkCommand(Object.assign({ cwd: testDir }, argv)); + const testFactory = (argv = {}) => new OkCommand(Object.assign({ cwd: testDir } as any, argv)); describe('.logger', () => { it('should be added to the instance', async () => { @@ -168,7 +168,7 @@ describe('core-command', () => { }); it('logs stdout and stderr of error from package', async () => { - class PkgErrorCommand extends Command { + class PkgErrorCommand extends Command { initialize() { return true; } @@ -187,7 +187,7 @@ describe('core-command', () => { } } - const command = new PkgErrorCommand({ cwd: testDir }); + const command = new PkgErrorCommand({ cwd: testDir } as any); await expect(command).rejects.toThrow( expect.objectContaining({ @@ -206,7 +206,7 @@ describe('core-command', () => { }); it('does not log stdout/stderr after streaming ends', async () => { - class PkgErrorCommand extends Command { + class PkgErrorCommand extends Command { initialize() { return true; } @@ -225,7 +225,7 @@ describe('core-command', () => { } } - const command = new PkgErrorCommand({ cwd: testDir, stream: true }); + const command = new PkgErrorCommand({ cwd: testDir, stream: true } as any); await expect(command).rejects.toThrow('message'); expect(console.error).not.toHaveBeenCalled(); @@ -257,9 +257,9 @@ describe('core-command', () => { }); describe('.options', () => { - class TestACommand extends Command { } - class TestBCommand extends Command { } - class TestCCommand extends Command { + class TestACommand extends Command { } + class TestBCommand extends Command { } + class TestCCommand extends Command { get otherCommandConfigs() { return ['testb']; } @@ -267,7 +267,7 @@ describe('core-command', () => { it('does not mutate argv parameter', async () => { const argv = { cwd: testDir, onRejected }; - const instance = new TestACommand(argv); + const instance = new TestACommand(argv as any); await instance; expect(argv).toEqual({ cwd: testDir, onRejected }); @@ -275,28 +275,28 @@ describe('core-command', () => { }); it('should pick up global options', async () => { - const instance = new TestACommand({ cwd: testDir, onRejected }); + const instance = new TestACommand({ cwd: testDir, onRejected } as any); await instance; expect(instance.options.testOption).toBe('default'); }); it('should override global options with command-level options', async () => { - const instance = new TestBCommand({ cwd: testDir, onRejected }); + const instance = new TestBCommand({ cwd: testDir, onRejected } as any); await instance; expect(instance.options.testOption).toBe('b'); }); it('should override global options with inherited command-level options', async () => { - const instance = new TestCCommand({ cwd: testDir, onRejected }); + const instance = new TestCCommand({ cwd: testDir, onRejected } as any); await instance; expect(instance.options.testOption).toBe('b'); }); it('should override inherited command-level options with local command-level options', async () => { - const instance = new TestCCommand({ cwd: testDir, onRejected }); + const instance = new TestCCommand({ cwd: testDir, onRejected } as any); await instance; expect(instance.options.testOption2).toBe('c'); @@ -307,7 +307,7 @@ describe('core-command', () => { cwd: testDir, onRejected, testOption2: 'f', - }); + } as any); await instance; expect(instance.options.testOption2).toBe('f'); @@ -318,7 +318,7 @@ describe('core-command', () => { cwd: testDir, onRejected, testOption: undefined, // yargs does this when --test-option is not passed - }); + } as any); await instance; expect(instance.options.testOption).toBe('b'); @@ -328,7 +328,7 @@ describe('core-command', () => { describe('subclass implementation', () => { ['initialize', 'execute'].forEach((method) => { it(`throws if ${method}() is not overridden`, () => { - const command = new Command({ cwd: testDir, onRejected }); + const command = new Command({ cwd: testDir, onRejected } as any); expect(() => command[method]()).toThrow(); }); }); diff --git a/packages/core/src/models/command-options.ts b/packages/core/src/models/command-options.ts index 3ed95e18..3ccc21d7 100644 --- a/packages/core/src/models/command-options.ts +++ b/packages/core/src/models/command-options.ts @@ -41,6 +41,9 @@ export interface InitCommandOption { /** version packages independently */ independent?: boolean; + + /** enables integration with Yarn or other package manager that use `workspaces` property in `package.json` */ + useWorkspaces?: boolean; } export interface PublishCommandOption extends VersionCommandOption { diff --git a/packages/core/src/models/interfaces.ts b/packages/core/src/models/interfaces.ts index 4741f652..45e9c690 100644 --- a/packages/core/src/models/interfaces.ts +++ b/packages/core/src/models/interfaces.ts @@ -191,22 +191,8 @@ export interface ProjectConfig extends LernaConfig, QueryGraphConfig { } /** The subset of package.json properties that Lerna-Lite uses */ -export interface RawManifest { - name: string; - location: string; - version: string; - private?: boolean; - bin?: Record | string; - scripts?: Record; - dependencies?: Record; - devDependencies?: Record; - optionalDependencies?: Record; - peerDependencies?: Record; +export interface RawManifest extends Package { publishConfig?: Record<'directory' | 'registry' | 'tag', string>; - - /** workspace package globs when `useWorkspaces` is enabled */ - workspaces?: string[] | { packages: string[] }; - get: (str: string) => { packages?: string[] } | string[]; } export interface ReleaseClient { diff --git a/packages/core/src/package.ts b/packages/core/src/package.ts index 1819d992..058abb03 100644 --- a/packages/core/src/package.ts +++ b/packages/core/src/package.ts @@ -149,6 +149,14 @@ export class Package { this[PKG].version = version; } + get workspaces(): string[] | { packages: string[] } { + return this[PKG].workspaces; + } + + set workspaces(workspaces: string[] | { packages: string[] }) { + this[PKG].workspaces = workspaces; + } + get contents() { // if modified with setter, use that value if (this[_contents]) { diff --git a/packages/core/src/project/project.ts b/packages/core/src/project/project.ts index 7696dd94..9cd57d3e 100644 --- a/packages/core/src/project/project.ts +++ b/packages/core/src/project/project.ts @@ -121,7 +121,7 @@ export class Project { return (this.packageConfigs as any).map(globParent).map((parentDir: string) => path.resolve(this.rootPath, parentDir)); } - get manifest(): Package { + get manifest(): RawManifest { let manifest; try { diff --git a/packages/init/README.md b/packages/init/README.md index 977db0c7..3c2cafd7 100644 --- a/packages/init/README.md +++ b/packages/init/README.md @@ -4,7 +4,7 @@ [![npm](https://img.shields.io/npm/v/@lerna-lite/init.svg?logo=npm&logoColor=fff&label=npm&color=limegreen)](https://www.npmjs.com/package/@lerna-lite/init) # @lerna-lite/init -## (`lerna init`) - Init command 🏁 +## (`lerna init`) - Init command 🧰 Create/initialize a new Lerna-Lite repo or upgrade an existing repo to the current version of Lerna-Lite CLI @@ -27,14 +27,14 @@ npx lerna init $ lerna init ``` -Create/initialize a new Lerna=Lite repo or upgrade an existing repo to the current version of Lerna-Lite. +Create/initialize a new Lerna-Lite repo or upgrade an existing repo to the current version of Lerna-Lite. > Lerna assumes the repo has already been initialized with `git init`. When run, this command will: 1. Add `lerna` as a [`devDependency`](https://docs.npmjs.com/files/package.json#devdependencies) in `package.json` if it doesn't already exist. -2. Create a `lerna.json` config file to store the `version` number. +2. Create a `lerna.json` config file to store the `version` number and also add a `packages` property (unless you use the `--use-workspaces` flag) Example output on a new git repo: @@ -54,7 +54,7 @@ lerna success Initialized Lerna files $ lerna init --independent ``` -This flag tells Lerna to use independent versioning mode. +This flag tells Lerna-Lite to use independent versioning mode. ### `--exact` @@ -65,7 +65,7 @@ $ lerna init --exact By default, `lerna init` will use a caret range when adding or updating the local version of `lerna`, just like `npm install --save-dev lerna`. -To retain the `lerna` 1.x behavior of "exact" comparison, pass this flag. +To retain the `lerna` of "exact" comparison, pass this flag. It will configure `lerna.json` to enforce exact match for all subsequent executions. ```json @@ -78,3 +78,32 @@ It will configure `lerna.json` to enforce exact match for all subsequent executi "version": "0.0.0" } ``` + +### `--use-workspaces` + +```sh +$ lerna init --use-workspaces +``` + +This flag tells Lerna-Lite to add a `workspaces` property in the root `package.json` instead of the default `lerna.json` file. + +#### `lerna.json` +```json +{ + "version": "0.0.0" +} +``` + +#### `package.json` +```json +{ + "name": "monorepo", + "devDependencies": { + "@lerna-lite/cli": "^1.3.0" + }, + "workspaces": [ + "./packages/a", + "./packages/a" + ] +} +``` \ No newline at end of file diff --git a/packages/init/src/__tests__/__fixtures__/updates/package.json b/packages/init/src/__tests__/__fixtures__/updates/package.json index f4ade756..8fbe0c47 100644 --- a/packages/init/src/__tests__/__fixtures__/updates/package.json +++ b/packages/init/src/__tests__/__fixtures__/updates/package.json @@ -1,6 +1,6 @@ { "name": "updates", - "devDependencies": { + "dependencies": { "@lerna-lite/cli": "__OVERWRITTEN__" } } \ No newline at end of file diff --git a/packages/init/src/__tests__/init-command.spec.ts b/packages/init/src/__tests__/init-command.spec.ts index 449e2d60..c5ec7630 100644 --- a/packages/init/src/__tests__/init-command.spec.ts +++ b/packages/init/src/__tests__/init-command.spec.ts @@ -15,7 +15,7 @@ import { factory } from '../init-command'; // file under test const yargParser = require('yargs-parser'); -const createArgv = (cwd, ...args) => { +const createArgv = (cwd: string, ...args: string[]) => { args.unshift('init'); const parserArgs = args.map(String); const argv = yargParser(parserArgs); @@ -28,12 +28,12 @@ describe('Init Command', () => { const lernaVersion = "__TEST_VERSION__"; it('should execute methods when initializing the command via its class', async () => { - const testDir = await initFixture("empty"); + const testDir = await initFixture('empty'); const ensurePkgJsonSpy = jest.spyOn(InitCommand.prototype, 'ensurePackageJSON'); const ensureLernaConfSpy = jest.spyOn(InitCommand.prototype, 'ensureLernaConfig'); const ensurePkgDirSpy = jest.spyOn(InitCommand.prototype, 'ensurePackagesDir'); - const cmd = new InitCommand(createArgv(testDir, "")); + const cmd = new InitCommand(createArgv(testDir, '')); await cmd; expect(cmd.requiresGit).toBe(false); @@ -43,12 +43,11 @@ describe('Init Command', () => { }); it('should execute methods when initializing the command via a factory', async () => { - const testDir = await initFixture("empty"); + const testDir = await initFixture('empty'); const ensurePkgJsonSpy = jest.spyOn(InitCommand.prototype, 'ensurePackageJSON'); const ensureLernaConfSpy = jest.spyOn(InitCommand.prototype, 'ensureLernaConfig'); const ensurePkgDirSpy = jest.spyOn(InitCommand.prototype, 'ensurePackagesDir'); - - await factory(createArgv(testDir, "")); + await factory(createArgv(testDir, '')); expect(ensurePkgJsonSpy).toHaveBeenCalled(); expect(ensureLernaConfSpy).toHaveBeenCalled(); @@ -56,84 +55,107 @@ describe('Init Command', () => { }); it('should ensure lerna config changes to "independent" when provided as argument', async () => { - const testDir = await initFixture("empty"); + const testDir = await initFixture('empty'); - const cmd = new InitCommand(createArgv(testDir, "--independent")); + const cmd = new InitCommand(createArgv(testDir, '--independent')); await cmd; expect(cmd.project.config.version).toEqual('independent'); }); + it('should ensure manifest includes "workspaces" when "--use-workspaces" provided as argument', async () => { + const testDir = await initFixture('empty'); + + const cmd = new InitCommand(createArgv(testDir, '--use-workspaces')); + await cmd; + + expect(cmd.project.manifest.workspaces).toEqual(['packages/*']); + }); + it('should ensure lerna config changes version to "0.0.0" when no version found in project package', async () => { - const testDir = await initFixture("empty"); + const testDir = await initFixture('empty'); - const cmd = new InitCommand(createArgv(testDir, "")); + const cmd = new InitCommand(createArgv(testDir, '')); await cmd; expect(cmd.project.config.version).toEqual('0.0.0'); }); + it('should ensure when Git will become initialized when it is not at the start', async () => { + jest.spyOn(InitCommand.prototype, 'gitInitialized').mockReturnValue(false); + const testDir = await initFixture('empty'); + + const cmd = new InitCommand(createArgv(testDir, '')); + await cmd; + const loggerSpy = jest.spyOn(cmd.logger, 'info'); + cmd.initialize(); + + expect(loggerSpy).toHaveBeenCalledWith('', 'Initializing Git repository'); + expect(cmd.project.config.version).toEqual('0.0.0'); + }); + it('should ensure lerna config changes version to what is found in project package version', async () => { - const testDir = await initFixture("updates"); + const testDir = await initFixture('updates'); - const cmd = new InitCommand(createArgv(testDir, "--exact")); + const cmd = new InitCommand(createArgv(testDir, '--exact')); await cmd; expect(cmd.project.config.version).toEqual('1.0.0'); }); - describe("in an empty directory", () => { - it("initializes git repo with lerna files", async () => { + describe('in an empty directory', () => { + it('initializes git repo with lerna files', async () => { const testDir = tempy.directory(); await lernaInit(testDir)(); const [lernaJson, pkgJson, packagesDirExists, gitDirExists] = await Promise.all([ - fs.readJSON(path.join(testDir, "lerna.json")), - fs.readJSON(path.join(testDir, "package.json")), - fs.exists(path.join(testDir, "packages"), null), - fs.exists(path.join(testDir, ".git"), null), + fs.readJSON(path.join(testDir, 'lerna.json')), + fs.readJSON(path.join(testDir, 'package.json')), + fs.exists(path.join(testDir, 'packages'), null), + fs.exists(path.join(testDir, '.git'), null), ]); expect(lernaJson).toMatchObject({ - version: "0.0.0", + packages: ['packages/*'], + version: '0.0.0', }); expect(pkgJson).toMatchObject({ devDependencies: { - "@lerna-lite/cli": `^${lernaVersion}`, + '@lerna-lite/cli': `^${lernaVersion}`, }, }); expect(packagesDirExists).toBe(true); expect(gitDirExists).toBe(true); }); - it("initializes git repo with lerna files in independent mode", async () => { + it('initializes git repo with lerna files in independent mode', async () => { const testDir = tempy.directory(); - await lernaInit(testDir)("--independent"); + await lernaInit(testDir)('--independent'); - expect(await fs.readJSON(path.join(testDir, "lerna.json"))).toHaveProperty("version", "independent"); + expect(await fs.readJSON(path.join(testDir, 'lerna.json'))).toHaveProperty('version', 'independent'); }); - describe("with --exact", () => { - it("uses exact version when adding lerna dependency", async () => { + describe('with --exact', () => { + it('uses exact version when adding lerna dependency', async () => { const testDir = tempy.directory(); - await lernaInit(testDir)("--exact"); + await lernaInit(testDir)('--exact'); - expect(await fs.readJSON(path.join(testDir, "package.json"))).toMatchObject({ + expect(await fs.readJSON(path.join(testDir, 'package.json'))).toMatchObject({ devDependencies: { - "@lerna-lite/cli": lernaVersion, + '@lerna-lite/cli': lernaVersion, }, }); }); - it("sets lerna.json command.init.exact to true", async () => { + it('sets lerna.json command.init.exact to true', async () => { const testDir = tempy.directory(); - await lernaInit(testDir)("--exact"); + await lernaInit(testDir)('--exact'); - expect(await fs.readJSON(path.join(testDir, "lerna.json"))).toMatchObject({ + expect(await fs.readJSON(path.join(testDir, 'lerna.json'))).toMatchObject({ command: { init: { exact: true, @@ -142,43 +164,79 @@ describe('Init Command', () => { }); }); }); + + describe('when initializing with --use-workspaces', () => { + it('sets lerna.json command.init.useWorkspaces to true', async () => { + const testDir = await initFixture('empty'); + const lernaJsonPath = path.join(testDir, 'lerna.json'); + const pkgJsonPath = path.join(testDir, 'package.json'); + + await fs.outputJSON(lernaJsonPath, { + '@lerna-lite/cli': '0.1.100', + command: { + bootstrap: { + hoist: true, + }, + }, + version: '1.2.3', + }); + await fs.outputJSON(pkgJsonPath, { + devDependencies: { + '@lerna-lite/cli': lernaVersion, + }, + workspaces: ['packages/*'], + }); + + await lernaInit(testDir)('--use-workspaces'); + + expect(await fs.readJSON(lernaJsonPath)).toEqual({ + command: { + bootstrap: { + hoist: true, + }, + }, + version: '1.2.3', + }); + }); + }); }); - describe("in a subdirectory of a git repo", () => { - it("creates lerna files", async () => { - const dir = await initFixture("empty"); - const testDir = path.join(dir, "subdir"); + describe('in a subdirectory of a git repo', () => { + it('creates lerna files', async () => { + const dir = await initFixture('empty'); + const testDir = path.join(dir, 'subdir'); await fs.ensureDir(testDir); await lernaInit(testDir)(); const [lernaJson, pkgJson, packagesDirExists] = await Promise.all([ - fs.readJSON(path.join(testDir, "lerna.json")), - fs.readJSON(path.join(testDir, "package.json")), - fs.exists(path.join(testDir, "packages"), null), + fs.readJSON(path.join(testDir, 'lerna.json')), + fs.readJSON(path.join(testDir, 'package.json')), + fs.exists(path.join(testDir, 'packages'), null), ]); expect(lernaJson).toMatchObject({ - version: "0.0.0", + packages: ['packages/*'], + version: '0.0.0', }); expect(pkgJson).toMatchObject({ devDependencies: { - "@lerna-lite/cli": `^${lernaVersion}`, + '@lerna-lite/cli': `^${lernaVersion}`, }, }); expect(packagesDirExists).toBe(true); }); }); - describe("when package.json exists", () => { - it("adds lerna to sorted devDependencies", async () => { - const testDir = await initFixture("has-package"); - const pkgJsonPath = path.join(testDir, "package.json"); + describe('when package.json exists', () => { + it('adds lerna to sorted devDependencies', async () => { + const testDir = await initFixture('has-package'); + const pkgJsonPath = path.join(testDir, 'package.json'); await fs.outputJSON(pkgJsonPath, { devDependencies: { - alpha: "first", - omega: "last", + alpha: 'first', + omega: 'last', }, }); @@ -186,24 +244,24 @@ describe('Init Command', () => { expect(await fs.readJSON(pkgJsonPath)).toMatchObject({ devDependencies: { - alpha: "first", - "@lerna-lite/cli": `^${lernaVersion}`, - omega: "last", + alpha: 'first', + '@lerna-lite/cli': `^${lernaVersion}`, + omega: 'last', }, }); }); - it("updates existing lerna in devDependencies", async () => { - const testDir = await initFixture("has-package"); - const pkgJsonPath = path.join(testDir, "package.json"); + it('updates existing lerna in devDependencies', async () => { + const testDir = await initFixture('has-package'); + const pkgJsonPath = path.join(testDir, 'package.json'); await fs.outputJSON(pkgJsonPath, { dependencies: { - alpha: "first", - omega: "last", + alpha: 'first', + omega: 'last', }, devDependencies: { - "@lerna-lite/cli": "0.1.100", + '@lerna-lite/cli': '0.1.100', }, }); @@ -211,24 +269,24 @@ describe('Init Command', () => { expect(await fs.readJSON(pkgJsonPath)).toMatchObject({ dependencies: { - alpha: "first", - omega: "last", + alpha: 'first', + omega: 'last', }, devDependencies: { - "@lerna-lite/cli": `^${lernaVersion}`, + '@lerna-lite/cli': `^${lernaVersion}`, }, }); }); - it("updates existing lerna in sorted dependencies", async () => { - const testDir = await initFixture("has-package"); - const pkgJsonPath = path.join(testDir, "package.json"); + it('updates existing lerna in sorted dependencies', async () => { + const testDir = await initFixture('has-package'); + const pkgJsonPath = path.join(testDir, 'package.json'); await fs.outputJSON(pkgJsonPath, { dependencies: { - alpha: "first", - "@lerna-lite/cli": "0.1.100", - omega: "last", + alpha: 'first', + '@lerna-lite/cli': '0.1.100', + omega: 'last', }, }); @@ -236,54 +294,68 @@ describe('Init Command', () => { expect(await fs.readJSON(pkgJsonPath)).toMatchObject({ dependencies: { - alpha: "first", - "@lerna-lite/cli": `^${lernaVersion}`, - omega: "last", + alpha: 'first', + '@lerna-lite/cli': `^${lernaVersion}`, + omega: 'last', }, }); }); }); - describe("when lerna.json exists", () => { - it("deletes lerna property if found", async () => { - const testDir = await initFixture("has-lerna"); - const lernaJsonPath = path.join(testDir, "lerna.json"); + describe('when lerna.json exists', () => { + it('deletes lerna property if found', async () => { + const testDir = await initFixture('has-lerna'); + const lernaJsonPath = path.join(testDir, 'lerna.json'); await fs.outputJSON(lernaJsonPath, { - "@lerna-lite/cli": "0.1.100", - version: "1.2.3", + '@lerna-lite/cli': '0.1.100', + version: '1.2.3', }); await lernaInit(testDir)(); expect(await fs.readJSON(lernaJsonPath)).toEqual({ - version: "1.2.3", + packages: ['packages/*'], + version: '1.2.3', }); }); + + // it('creates package directories when glob is configured', async () => { + // const testDir = await initFixture('has-lerna'); + // const lernaJsonPath = path.join(testDir, 'lerna.json'); + + // await fs.outputJSON(lernaJsonPath, { + // packages: ['modules/*'], + // }); + + // await lernaInit(testDir)(); + + // expect(await fs.exists(path.join(testDir, 'modules'), null)).toBe(true); + // }); }); - describe("when re-initializing with --exact", () => { - it("sets lerna.json command.init.exact to true", async () => { - const testDir = await initFixture("updates"); - const lernaJsonPath = path.join(testDir, "lerna.json"); - const pkgJsonPath = path.join(testDir, "package.json"); + describe('when re-initializing with --exact', () => { + it('sets lerna.json command.init.exact to true', async () => { + const testDir = await initFixture('updates'); + const lernaJsonPath = path.join(testDir, 'lerna.json'); + const pkgJsonPath = path.join(testDir, 'package.json'); await fs.outputJSON(lernaJsonPath, { - "@lerna-lite/cli": "0.1.100", + '@lerna-lite/cli': '0.1.100', command: { bootstrap: { hoist: true, }, }, - version: "1.2.3", + version: '1.2.3', }); await fs.outputJSON(pkgJsonPath, { devDependencies: { - "@lerna-lite/cli": lernaVersion, + '@lerna-lite/cli': lernaVersion, }, }); - await lernaInit(testDir)("--exact"); + await lernaInit(testDir)('--exact'); expect(await fs.readJSON(lernaJsonPath)).toEqual({ command: { @@ -294,7 +366,8 @@ describe('Init Command', () => { exact: true, }, }, - version: "1.2.3", + packages: ['packages/*'], + version: '1.2.3', }); }); }); diff --git a/packages/init/src/init-command.ts b/packages/init/src/init-command.ts index 3f843bc2..5fb54d74 100644 --- a/packages/init/src/init-command.ts +++ b/packages/init/src/init-command.ts @@ -1,4 +1,4 @@ -import { Command, CommandType, exec, InitCommandOption, } from '@lerna-lite/core'; +import { Command, CommandType, exec, InitCommandOption, ProjectConfig, RawManifest } from '@lerna-lite/core'; import fs from 'fs-extra'; import path from 'path'; import pMap from 'p-map'; @@ -14,7 +14,7 @@ export class InitCommand extends Command { /** command name */ name = 'init' as CommandType; exact?: boolean = false; - lernaVersion?: string = ''; + lernaVersion = ''; constructor(argv: InitCommandOption) { super(argv); @@ -38,75 +38,61 @@ export class InitCommand extends Command { if (!this.gitInitialized()) { this.logger.info('', 'Initializing Git repository'); - return exec('git', ['init'], this.execOpts); } } - execute() { - let chain: Promise = Promise.resolve(); - - chain = chain.then(() => this.ensurePackageJSON()); - chain = chain.then(() => this.ensureLernaConfig()); - chain = chain.then(() => this.ensurePackagesDir()); - - return chain.then(() => { - this.logger.success('', 'Initialized Lerna files'); - }); + async execute() { + await this.ensurePackageJSON(); + await this.ensureLernaConfig(); + await this.ensurePackagesDir(); + this.logger.success('', 'Initialized Lerna files'); } - ensurePackageJSON() { - let chain: Promise = Promise.resolve(); - + async ensurePackageJSON() { if (!this.project.manifest) { this.logger.info('', 'Creating package.json'); // initialize with default indentation so write-pkg doesn't screw it up with tabs - chain = chain.then(() => - writeJsonFile( - path.join(this.project.rootPath, 'package.json'), - { - name: 'root', - private: true, - }, - { indent: 2 } - ) + await writeJsonFile( + path.join(this.project.rootPath, 'package.json'), + { name: 'root', private: true }, + { indent: 2 } ); } else { this.logger.info('', 'Updating package.json'); } - chain = chain.then(() => { - const rootPkg = this.project.manifest; - - let targetDependencies; - - if ((rootPkg.dependencies as any)?.[LERNA_CLI_PKG_NAME]) { - // lerna is a dependency in the current project - targetDependencies = rootPkg.dependencies; - } else { - // lerna is a devDependency or no dependency, yet - if (!rootPkg.devDependencies) { - // mutate raw JSON object - rootPkg.set('devDependencies', {}); - } + const rootPkg = this.project.manifest; + let targetDependencies: string[]; - targetDependencies = rootPkg.devDependencies; + if (rootPkg.dependencies?.[LERNA_CLI_PKG_NAME]) { + // lerna is a dependency in the current project + targetDependencies = rootPkg.dependencies; + } else { + // lerna is a devDependency or no dependency, yet + if (!rootPkg.devDependencies) { + // mutate raw JSON object + rootPkg.set('devDependencies', {}); } + targetDependencies = rootPkg.devDependencies; + } - targetDependencies[LERNA_CLI_PKG_NAME] = this.exact ? this.lernaVersion : `^${this.lernaVersion}`; + targetDependencies[LERNA_CLI_PKG_NAME] = this.exact ? this.lernaVersion : `^${this.lernaVersion}`; - return rootPkg.serialize(); - }); + // add workspace packages in package.json when `useWorkspaces` enabled + if (this.options.useWorkspaces && !rootPkg.workspaces) { + rootPkg.workspaces = ['packages/*']; + } - return chain; + return rootPkg.serialize(); } ensureLernaConfig() { // config already defaulted to empty object in Project constructor const { config, version: projectVersion } = this.project; - let version = ''; + let version; if (this.options.independent) { version = 'independent'; @@ -116,25 +102,24 @@ export class InitCommand extends Command { version = '0.0.0'; } - if (!projectVersion) { - this.logger.info('', 'Creating lerna.json'); - } else { - this.logger.info('', 'Updating lerna.json'); - } + const logMessage = (!projectVersion) ? 'Creating lerna.json' : 'Updating lerna.json'; + this.logger.info('', logMessage); - delete (config as any)[LERNA_CLI_PKG_NAME]; // no longer relevant + delete config[LERNA_CLI_PKG_NAME]; // no longer relevant if (this.exact) { // ensure --exact is preserved for future init commands - const commandConfig = config.command || (config.command = {} as any); + const commandConfig = config.command || (config.command = {}); const initConfig = commandConfig.init || (commandConfig.init = {}); initConfig.exact = true; } - Object.assign(config, { - version, - }); + const lernaConfig: Partial = { version }; + if (!this.options.useWorkspaces) { + lernaConfig.packages = ['packages/*']; + } + Object.assign(config, lernaConfig); return this.project.serializeConfig(); } diff --git a/packages/run/src/__tests__/run-command.spec.ts b/packages/run/src/__tests__/run-command.spec.ts index 3db9ba54..d944013b 100644 --- a/packages/run/src/__tests__/run-command.spec.ts +++ b/packages/run/src/__tests__/run-command.spec.ts @@ -13,7 +13,7 @@ import globby from 'globby'; import yargParser from 'yargs-parser'; // make sure to import the output mock -import { logOutput } from '@lerna-lite/core'; +import { logOutput, RunCommandOption } from '@lerna-lite/core'; // mocked modules const { npmRunScript, npmRunScriptStreaming } = require('../lib/npm-run-script'); @@ -34,7 +34,7 @@ const ranInPackagesStreaming = (testDir: string) => return arr; }, []); -const createArgv = (cwd: string, script?: string, ...args: string[]) => { +const createArgv = (cwd: string, script?: string, ...args: any[]) => { args.unshift('run'); const parserArgs = args.join(' '); const argv = yargParser(parserArgs); @@ -43,7 +43,7 @@ const createArgv = (cwd: string, script?: string, ...args: string[]) => { argv.script = script; } args['logLevel'] = 'silent'; - return argv; + return argv as unknown as RunCommandOption; }; describe('RunCommand', () => {