Skip to content

Commit

Permalink
feat(codemod): add missing name codemod
Browse files Browse the repository at this point in the history
fixes
  • Loading branch information
tknickman committed May 22, 2024
1 parent 28643f5 commit e2d35e2
Show file tree
Hide file tree
Showing 14 changed files with 349 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"name": "root",
"workspaces": [
"packages/*"
],
"version": "1.0.0",
"dependencies": {},
"devDependencies": {},
"packageManager": "[email protected]"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"name": "ui",
"version": "1.0.0",
"dependencies": {},
"devDependencies": {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"name": "utils",
"version": "1.0.0",
"dependencies": {},
"devDependencies": {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"name": "root",
"workspaces": [
"packages/*"
],
"version": "1.0.0",
"dependencies": {},
"devDependencies": {},
"packageManager": "[email protected]"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"name": "@acme/docs",
"version": "1.0.0",
"dependencies": {},
"devDependencies": {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"name": "@acme/docs",
"version": "1.0.0",
"dependencies": {},
"devDependencies": {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"name": "some-pkg",
"version": "1.0.0",
"dependencies": {},
"devDependencies": {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"name": "some-pkg",
"version": "1.0.0",
"dependencies": {},
"devDependencies": {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"name": "root",
"workspaces": [
"packages/*"
],
"version": "1.0.0",
"dependencies": {},
"devDependencies": {},
"packageManager": "[email protected]"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"version": "1.0.0",
"dependencies": {},
"devDependencies": {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"version": "1.0.0",
"dependencies": {},
"devDependencies": {}
}
117 changes: 117 additions & 0 deletions packages/turbo-codemod/__tests__/add-package-names.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { setupTestFixtures } from "@turbo/test-utils";
import { transformer } from "../src/transforms/add-package-names";

describe("add-package-names", () => {
const { useFixture } = setupTestFixtures({
directory: __dirname,
test: "add-package-names",
});

test("missing names", async () => {
// load the fixture for the test
const { root, readJson } = useFixture({
fixture: "missing-names",
});

// run the transformer
const result = await transformer({
root,
options: { force: false, dry: false, print: false },
});

// result should be correct
expect(result.fatalError).toBeUndefined();
expect(result.changes).toMatchInlineSnapshot(`
Object {
"packages/ui/package.json": Object {
"action": "modified",
"additions": 1,
"deletions": 0,
},
"packages/utils/package.json": Object {
"action": "modified",
"additions": 1,
"deletions": 0,
},
}
`);

// validate unique names
const names = new Set();

for (const pkg of ["ui", "utils"]) {
const pkgJson = readJson<{ name: string }>(
`packages/${pkg}/package.json`
);
expect(pkgJson?.name).toBeDefined();
expect(names.has(pkgJson?.name)).toBe(false);
names.add(pkgJson?.name);
}
});

test("duplicate names", async () => {
// load the fixture for the test
const { root, readJson } = useFixture({
fixture: "duplicate-names",
});

// run the transformer
const result = await transformer({
root,
options: { force: false, dry: false, print: false },
});

// result should be correct
expect(result.fatalError).toBeUndefined();
expect(result.changes).toMatchInlineSnapshot(`
Object {
"packages/utils/package.json": Object {
"action": "modified",
"additions": 1,
"deletions": 1,
},
}
`);

// validate unique names
const names = new Set();

for (const pkg of ["ui", "utils"]) {
const pkgJson = readJson<{ name: string }>(
`packages/${pkg}/package.json`
);
expect(pkgJson?.name).toBeDefined();
expect(names.has(pkgJson?.name)).toBe(false);
names.add(pkgJson?.name);
}
});

test("correct names", async () => {
// load the fixture for the test
const { root, readJson } = useFixture({
fixture: "correct-names",
});

// run the transformer
const result = await transformer({
root,
options: { force: false, dry: false, print: false },
});

// result should be correct
expect(result.fatalError).toBeUndefined();
expect(result.changes).toMatchInlineSnapshot(`Object {}`);

// validate unique names
const names = new Set();

for (const pkg of ["ui", "utils"]) {
const pkgJson = readJson<{ name: string }>(
`packages/${pkg}/package.json`
);
expect(pkgJson?.name).toBeDefined();
expect(names.has(pkgJson?.name)).toBe(false);
names.add(pkgJson?.name);
}
});
});
29 changes: 29 additions & 0 deletions packages/turbo-codemod/__tests__/generate-package-name.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { getNewPkgName } from "../src/transforms/add-package-names";

describe("getNewPkgName", () => {
it.each([
{
pkgPath: "/packages/ui/package.json",
pkgName: "old-name",
expected: "ui-old-name",
},
// scoped
{
pkgPath: "/packages/ui/package.json",
pkgName: "@acme/name",
expected: "@acme/ui-name",
},
// no name
{
pkgPath: "/packages/ui/package.json",
pkgName: undefined,
expected: "ui",
},
])(
"should return a new package name for pkgPath: $pkgPath and pkgName: $pkgName",
({ pkgPath, pkgName, expected }) => {
const newName = getNewPkgName({ pkgPath, pkgName });
expect(newName).toBe(expected);
}
);
});
127 changes: 127 additions & 0 deletions packages/turbo-codemod/src/transforms/add-package-names.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import path from "node:path";
import { getWorkspaceDetails, type Project } from "@turbo/workspaces";
import { readJson } from "fs-extra";
import type { TransformerArgs } from "../types";
import type { TransformerResults } from "../runner";
import { getTransformerHelpers } from "../utils/getTransformerHelpers";

// transformer details
const TRANSFORMER = "add-package-names";
const DESCRIPTION = "Ensure all packages have a name in their package.json";
const INTRODUCED_IN = "2.0.0";

interface PartialPackageJson {
name?: string;
}

async function readPkgJson(
pkgJsonPath: string
): Promise<PartialPackageJson | null> {
try {
return (await readJson(pkgJsonPath)) as { name?: string };
} catch (e) {
return null;
}
}

export function getNewPkgName({
pkgPath,
pkgName,
}: {
pkgPath: string;
pkgName?: string;
}): string {
// find the scope if it exists
let scope = "";
let name = pkgName;
if (pkgName && pkgName.startsWith("@") && pkgName.includes("/")) {
const parts = pkgName.split("/");
scope = `${parts[0]}/`;
name = parts[1];
}

const dirName = path.basename(path.dirname(pkgPath));
if (pkgName) {
return `${scope}${dirName}-${name}`;
}

return `${scope}${dirName}`;
}

export async function transformer({
root,
options,
}: TransformerArgs): Promise<TransformerResults> {
const { log, runner } = getTransformerHelpers({
transformer: TRANSFORMER,
rootPath: root,
options,
});

log.info('Validating that each package has a unique "name"...');

let project: Project;
try {
project = await getWorkspaceDetails({ root });
} catch (e) {
return runner.abortTransform({
reason: `Unable to determine package manager for ${root}`,
});
}

const packagePaths: Array<string> = [project.paths.packageJson];
const packagePromises: Array<Promise<PartialPackageJson | null>> = [
readPkgJson(project.paths.packageJson),
];

// add all workspace package.json files
project.workspaceData.workspaces.forEach((workspace) => {
const pkgJsonPath = workspace.paths.packageJson;
packagePaths.push(pkgJsonPath);
packagePromises.push(readPkgJson(pkgJsonPath));
});

// await, and then zip the paths and promise results together
const packageContent = await Promise.all(packagePromises);
const packageToContent = Object.fromEntries(
packagePaths.map((pkgJsonPath, idx) => [pkgJsonPath, packageContent[idx]])
);

// wait for all package.json files to be read
const names = new Set();
for (const [pkgJsonPath, pkgJsonContent] of Object.entries(
packageToContent
)) {
if (pkgJsonContent) {
// name is missing or isn't unique
if (!pkgJsonContent.name || names.has(pkgJsonContent.name)) {
const newName = getNewPkgName({
pkgPath: pkgJsonPath,
pkgName: pkgJsonContent.name,
});
runner.modifyFile({
filePath: pkgJsonPath,
after: {
...pkgJsonContent,
name: newName,
},
});
names.add(newName);
} else {
names.add(pkgJsonContent.name);
}
}
}

return runner.finish();
}

const transformerMeta = {
name: `${TRANSFORMER}: ${DESCRIPTION}`,
value: TRANSFORMER,
introducedIn: INTRODUCED_IN,
transformer,
};

// eslint-disable-next-line import/no-default-export -- transforms require default export
export default transformerMeta;

0 comments on commit e2d35e2

Please sign in to comment.