Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(create): support relative path from root as lerna create location #3478

Merged
merged 5 commits into from Jan 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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.