Skip to content

Commit

Permalink
feat(create): support relative path from root as lerna create location (
Browse files Browse the repository at this point in the history
  • Loading branch information
fahslaj committed Jan 1, 2023
1 parent 329eb99 commit 82825ce
Show file tree
Hide file tree
Showing 6 changed files with 146 additions and 5 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Expand Up @@ -44,7 +44,7 @@ jobs:
cmd5: npx nx run-many --target=test --projects=\"libs-*,nx-*\" --parallel=3

# e2e tests for everything except the primary task runner
- run: PUBLISHED_VERSION=999.9.9-e2e.0 npx nx run-many --target=run-e2e-tests-process --parallel=2
- run: PUBLISHED_VERSION=999.9.9-e2e.0 npx nx run-many --target=run-e2e-tests-process --parallel=1

- name: Stop all running agents for this CI run
# It's important that we always run this step, otherwise in the case of any failures in preceding non-Nx steps, the agents will keep running and waste billable minutes
Expand Down
7 changes: 6 additions & 1 deletion commands/create/README.md
Expand Up @@ -15,8 +15,13 @@ Positionals:
name The package name (including scope), which must be locally unique _and_
publicly available [string] [required]
loc A custom package location, defaulting to the first configured package
location [string]
location. The location must match a configured packages directory.
[string]
```

**Note**: For more info on how Lerna package directories are configured, see https://lerna.js.org/docs/faq#how-does-lerna-detect-packages.

```
Command Options:
--access When using a scope, set publishConfig.access value
[choices: "public", "restricted"] [default: public]
Expand Down
37 changes: 34 additions & 3 deletions commands/create/index.js
Expand Up @@ -52,12 +52,18 @@ class CreateCommand extends Command {
// npm-package-arg handles all the edge-cases with scopes
const { name, scope } = npa(pkgName);

if (!name && pkgName.includes("/")) {
throw new ValidationError(
"ENOPKGNAME",
"Invalid package name. Use the <loc> positional to specify package directory.\nSee https://github.com/lerna/lerna/tree/main/commands/create#usage for details."
);
}

// optional scope is _not_ included in the directory name
this.dirName = scope ? name.split("/").pop() : name;
this.pkgName = name;
this.pkgsDir =
this.project.packageParentDirs.find((pd) => pd.indexOf(pkgLocation) > -1) ||
this.project.packageParentDirs[0];

this.pkgsDir = this._getPackagesDir(pkgLocation);

this.camelName = camelCase(this.dirName);

Expand Down Expand Up @@ -144,6 +150,31 @@ class CreateCommand extends Command {
return Promise.resolve(this.setDependencies());
}

_getPackagesDir(pkgLocation) {
const packageParentDirs = this.project.packageParentDirs;

if (!pkgLocation) {
return packageParentDirs[0];
}

const normalizedPkgLocation = path
.resolve(this.project.rootPath, path.normalize(pkgLocation))
.toLowerCase();
const packageParentDirsLower = packageParentDirs.map((p) => p.toLowerCase());

// using indexOf over includes due to platform differences (/private/tmp should match /tmp on macOS)
const matchingPathIndex = packageParentDirsLower.findIndex((p) => p.indexOf(normalizedPkgLocation) > -1);

if (matchingPathIndex > -1) {
return packageParentDirs[matchingPathIndex];
}

throw new ValidationError(
"ENOPKGDIR",
`Location "${pkgLocation}" is not configured as a workspace directory.`
);
}

execute() {
let chain = Promise.resolve();

Expand Down
90 changes: 90 additions & 0 deletions e2e/create/src/create.spec.ts
Expand Up @@ -27,6 +27,10 @@ describe("lerna-create", () => {
runLernaInit: true,
installDependencies: true,
});
await fixture.updateJson("lerna.json", (json) => ({
...json,
packages: ["packages/*", "apps/*", "libs/react/*"],
}));
});
afterAll(() => fixture.destroy());

Expand Down Expand Up @@ -1175,6 +1179,92 @@ describe("lerna-create", () => {
`);
});
});

describe("and a location", () => {
it("one segment long", async () => {
const packageName = "one-segment";
await fixture.lerna(`create @scope/${packageName} apps -y`);

const fileExists = await fixture.workspaceFileExists(`apps/${packageName}/README.md`);
expect(fileExists).toBe(true);
});

it("two segments long", async () => {
const packageName = "two-segments";
await fixture.lerna(`create @scope/${packageName} libs/react -y`);

const fileExists = await fixture.workspaceFileExists(`libs/react/${packageName}/README.md`);
expect(fileExists).toBe(true);
});

it("two segments long with mismatched casing", async () => {
const packageName = "two-segments-case-sensitivity";
await fixture.lerna(`create @scope/${packageName} Libs/React -y`);

const fileExists = await fixture.workspaceFileExists(`libs/react/${packageName}/README.md`);
expect(fileExists).toBe(true);
});

it("two segments long relative path from the root", async () => {
const packageName = "two-segments-relative";
await fixture.lerna(`create @scope/${packageName} ./libs/react -y`);

const fileExists = await fixture.workspaceFileExists(`libs/react/${packageName}/README.md`);
expect(fileExists).toBe(true);
});

it("two segments long absolute path", async () => {
const packageName = "two-segments-absolute";
const absolutePath = fixture.getWorkspacePath("libs/react");
await fixture.lerna(`create @scope/${packageName} ${absolutePath} -y`);

const fileExists = await fixture.workspaceFileExists(`libs/react/${packageName}/README.md`);
expect(fileExists).toBe(true);
});

describe("throws an error", () => {
it("when the location does not match a configured workspace directory", async () => {
const packageName = "invalid-location-with-scope";
const result = await fixture.lerna(`create @scope/${packageName} invalid-location -y`, {
silenceError: true,
});

expect(result.combinedOutput).toMatchInlineSnapshot(`
lerna notice cli v999.9.9-e2e.0
lerna ERR! ENOPKGDIR Location "invalid-location" is not configured as a workspace directory.
`);
});
});
});

describe("throws an error", () => {
it("when the name is invalid and appears to be a path", async () => {
const packageName = "apps/@scope/invalid-package-name";
const result = await fixture.lerna(`create ${packageName} -y`, { silenceError: true });

expect(result.combinedOutput).toMatchInlineSnapshot(`
lerna notice cli v999.9.9-e2e.0
lerna ERR! ENOPKGNAME Invalid package name. Use the <loc> positional to specify package directory.
lerna ERR! ENOPKGNAME See https://github.com/lerna/lerna/tree/main/commands/create#usage for details.
`);
});
});
});

describe("throws an error", () => {
it("when package name appears to be a path", async () => {
const packageName = "apps/invalid-package-name";
const result = await fixture.lerna(`create ${packageName} -y`, { silenceError: true });

expect(result.combinedOutput).toMatchInlineSnapshot(`
lerna notice cli v999.9.9-e2e.0
lerna ERR! ENOPKGNAME Invalid package name. Use the <loc> positional to specify package directory.
lerna ERR! ENOPKGNAME See https://github.com/lerna/lerna/tree/main/commands/create#usage for details.
`);
});
});

describe("created test script", () => {
Expand Down
7 changes: 7 additions & 0 deletions libs/e2e-utils/src/lib/fixture.ts
Expand Up @@ -162,6 +162,13 @@ export class Fixture {
return readFile(this.getWorkspacePath(file), "utf-8");
}

/**
* Check if a workspace file exists.
*/
async workspaceFileExists(file: string): Promise<boolean> {
return existsSync(this.getWorkspacePath(file));
}

/**
* Execute a given command in the root of the lerna workspace under test within the fixture.
* This has been given a terse name to help with readability in the spec files.
Expand Down
8 changes: 8 additions & 0 deletions website/docs/faq.md
Expand Up @@ -89,3 +89,11 @@ dependencies:
post:
- npm run bootstrap
```

## How does Lerna detect packages?

By default, Lerna uses the `workspaces` property in `package.json` to search for packages. For details on this property, see the [npm documentation](https://docs.npmjs.com/cli/v9/configuring-npm/package-json#workspaces) or the [Yarn documentation](https://classic.yarnpkg.com/lang/en/docs/workspaces/).

If you are using `pnpm`, you might have set `npmClient` to `pnpm` in `lerna.json`. In this case, Lerna will use the `packages` property in `pnpm-workspace.yaml` to search for packages. For details on this property, see the [pnpm documentation](https://pnpm.io/workspaces).

If you are using an older version of Lerna or have explicitly opted out of using workspaces, then Lerna will use the `packages` property in `lerna.json` to search for packages.

0 comments on commit 82825ce

Please sign in to comment.