Skip to content

Commit

Permalink
feat: add basic codemod for common glob syntax issues
Browse files Browse the repository at this point in the history
  • Loading branch information
arlyon committed Jun 2, 2023
1 parent d1e1afe commit 46c0f53
Show file tree
Hide file tree
Showing 4 changed files with 267 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"$schema": "../../../../../../docs/public/schema.json",
"pipeline": {
"case_1": {
"inputs": ["../../app-store/**/**", "**/**/result.json"],
"outputs": ["../../app-store/**/**", "**/**/result.json"]
},
"case_2": {
"inputs": ["!**/dist", "!**/node_modules"],
"outputs": ["!**/dist", "!**/node_modules"]
},
"case_3": {
"inputs": [
"cypress/integration/**.test.ts",
"src/types/generated/**.ts",
"scripts/**.mjs",
"scripts/**.js"
],
"outputs": [
"cypress/integration/**.test.ts",
"src/types/generated/**.ts",
"scripts/**.mjs",
"scripts/**.js"
]
}
}
}
133 changes: 133 additions & 0 deletions packages/turbo-codemod/__tests__/clean-globs.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import { transformer, fixGlobPattern } from "../src/transforms/clean-globs";
import { setupTestFixtures } from "@turbo/test-utils";
import getTransformerHelpers from "../src/utils/getTransformerHelpers";

describe("clean-globs", () => {
const { useFixture } = setupTestFixtures({
directory: __dirname,
test: "clean-globs",
});

test("basic", () => {
// load the fixture for the test
const { root, read, readJson } = useFixture({
fixture: "clean-globs",
});

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

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

test("collapses back-to-back doublestars", () => {
let badGlobPatterns = [
["../../app-store/**/**", "../../app-store/**"],
["**/**/result.json", "**/result.json"],
["**/**/**/**", "**"],
["**/foo/**/**/bar/**", "**/foo/**/bar/**"],
["**/foo/**/**/**/bar/**/**", "**/foo/**/bar/**"],
["**/foo/**/**/**/**/bar/**/**/**", "**/foo/**/bar/**"],
];

const { log } = getTransformerHelpers({
transformer: "test",
rootPath: ".",
options: { force: false, dry: false, print: false },
});

// Now let's test the function
badGlobPatterns.forEach(([input, output]) => {
expect(fixGlobPattern(input, log)).toBe(output);
});
});

test("doesn't update valid globs and prints a message", () => {
const { log } = getTransformerHelpers({
transformer: "test",
rootPath: ".",
options: { force: false, dry: false, print: false },
});

// Now let's test the function
expect(fixGlobPattern("a/b/c/*", log)).toBe("a/b/c/*");
});

test("transforms '!**/folder' to '**/[!folder]'", () => {
let badGlobPatterns = [
["!**/dist", "**/[!dist]"],
["!**/node_modules", "**/[!node_modules]"],
["!**/foo/bar", "**/[!foo/bar]"],
["!**/foo/bar/baz", "**/[!foo/bar/baz]"],
["!**/foo/bar/baz/qux", "**/[!foo/bar/baz/qux]"],
];

const { log } = getTransformerHelpers({
transformer: "test",
rootPath: ".",
options: { force: false, dry: false, print: false },
});

// Now let's test the function
badGlobPatterns.forEach(([input, output]) => {
expect(fixGlobPattern(input, log)).toBe(output);
});
});

test("transforms '**ext' to '**/*ext'", () => {
let badGlobPatterns = [
["cypress/integration/**.test.ts", "cypress/integration/**/*.test.ts"],
["scripts/**.mjs", "scripts/**/*.mjs"],
["scripts/**.js", "scripts/**/*.js"],
["src/types/generated/**.ts", "src/types/generated/**/*.ts"],
["**md", "**/*md"],
["**txt", "**/*txt"],
["**html", "**/*html"],
];

const { log } = getTransformerHelpers({
transformer: "test",
rootPath: ".",
options: { force: false, dry: false, print: false },
});

// Now let's test the function
badGlobPatterns.forEach(([input, output]) => {
expect(fixGlobPattern(input, log)).toBe(output);
});
});

test("transforms 'pre**' to pre*/**", () => {
let badGlobPatterns = [
["pre**", "pre*/**"],
["pre**/foo", "pre*/**/foo"],
["pre**/foo/bar", "pre*/**/foo/bar"],
["pre**/foo/bar/baz", "pre*/**/foo/bar/baz"],
["pre**/foo/bar/baz/qux", "pre*/**/foo/bar/baz/qux"],
];

const { log } = getTransformerHelpers({
transformer: "test",
rootPath: ".",
options: { force: false, dry: false, print: false },
});

// Now let's test the function
badGlobPatterns.forEach(([input, output]) => {
expect(fixGlobPattern(input, log)).toBe(output);
});
});
});
106 changes: 106 additions & 0 deletions packages/turbo-codemod/src/transforms/clean-globs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { TransformerArgs } from "../types";
import type { Schema as TurboJsonSchema } from "@turbo/types";
import { TransformerResults } from "../runner";
import path from "path";
import fs from "fs-extra";
import getTransformerHelpers from "../utils/getTransformerHelpers";
import { getTurboConfigs } from "@turbo/utils";
import Logger from "../utils/logger";

// transformer details
const TRANSFORMER = "clean-globs";
const DESCRIPTION =
"Automatically clean up invalid globs from your `turbo.json` file";
const INTRODUCED_IN = "1.11.0";

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

const turboConfigPath = path.join(root, "turbo.json");

const turboJson: TurboJsonSchema = fs.readJsonSync(turboConfigPath);
runner.modifyFile({
filePath: turboConfigPath,
after: migrateConfig(turboJson, log),
});

// find and migrate any workspace configs
const workspaceConfigs = getTurboConfigs(root);
workspaceConfigs.forEach((workspaceConfig) => {
const { config, turboConfigPath } = workspaceConfig;
runner.modifyFile({
filePath: turboConfigPath,
after: migrateConfig(config, log),
});
});

return runner.finish();
}

function migrateConfig(config: TurboJsonSchema, log: Logger) {
const mapGlob = (glob: string) => fixGlobPattern(glob, log);
for (const [_, taskDef] of Object.entries(config.pipeline)) {
taskDef.inputs = taskDef.inputs?.map(mapGlob);
taskDef.outputs = taskDef.outputs?.map(mapGlob);
}

return config;
}

export function fixGlobPattern(pattern: string, log: Logger): string {
let oldPattern = pattern;
// For '../../app-store/**/**' and '**/**/result.json'
// Collapse back-to-back doublestars '**/**' to a single doublestar '**'
let newPattern = pattern.replace(/\*\*\/\*\*/g, "**");
while (newPattern !== pattern) {
log.modified(`${pattern} to ${newPattern}`);
pattern = newPattern;
newPattern = pattern.replace(/\*\*\/\*\*/g, "**");
}

// For '!**/dist' and '!**/node_modules'
// Change '!**/dist' and '!**/node_modules' to '**/[!dist]' and '**/[!node_modules]'
newPattern = pattern.replace(/^!\*\*\/([a-z_\/]+)$/g, "**/[!$1]");
if (newPattern !== pattern) {
log.modified(`${pattern} to ${newPattern}`);
log.info("please make the previous transform is correct");
pattern = newPattern;
}

// For 'cypress/integration/**.test.ts', 'scripts/**.mjs', 'scripts/**.js', 'src/types/generated/**.ts'
// Change '**.ext' to '**/*.ext' where 'ext' is 'test.ts', 'mjs', 'js', 'ts'
newPattern = pattern.replace(/(\*\*)([a-z\.]+)/g, "$1/*$2");
if (newPattern !== pattern) {
log.modified(`${pattern} to ${newPattern}`);
pattern = newPattern;
}

// For 'test/prefix**' change to 'test/prefix*/**'
newPattern = pattern.replace(/([a-z_]+)(\*\*)/g, "$1*/$2");
if (newPattern !== pattern) {
log.modified(`${pattern} to ${newPattern}`);
pattern = newPattern;
}

if (oldPattern === pattern) {
log.unchanged(pattern);
}

return pattern;
}

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

export default transformerMeta;

0 comments on commit 46c0f53

Please sign in to comment.