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

Add codemod for glob syntax issues. #5184

Merged
merged 5 commits into from
Jun 5, 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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"
]
}
}
}
155 changes: 155 additions & 0 deletions packages/turbo-codemod/__tests__/clean-globs.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
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,
},
}
`);
});

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

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/**"],
];

// 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", () => {
// Now let's test the function
expect(fixGlobPattern("a/b/c/*", log)).toBe("a/b/c/*");
});

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"],
];

// 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"],
];

// Now let's test the function
badGlobPatterns.forEach(([input, output]) => {
expect(fixGlobPattern(input, log)).toBe(output);
});
});
arlyon marked this conversation as resolved.
Show resolved Hide resolved

it("should collapse back-to-back doublestars to a single doublestar", () => {
expect(fixGlobPattern("../../app-store/**/**", log)).toBe(
"../../app-store/**"
);
expect(fixGlobPattern("**/**/result.json", log)).toBe("**/result.json");
});

it("should change **.ext to **/*.ext", () => {
expect(fixGlobPattern("**.js", log)).toBe("**/*.js");
expect(fixGlobPattern("**.json", log)).toBe("**/*.json");
expect(fixGlobPattern("**.ext", log)).toBe("**/*.ext");
});

it("should change prefix** to prefix*/**", () => {
expect(fixGlobPattern("app**", log)).toBe("app*/**");
expect(fixGlobPattern("src**", log)).toBe("src*/**");
expect(fixGlobPattern("prefix**", log)).toBe("prefix*/**");
});

it("should collapse back-to-back doublestars and change **.ext to **/*.ext", () => {
expect(fixGlobPattern("../../app-store/**/**/*.js", log)).toBe(
"../../app-store/**/*.js"
);
expect(fixGlobPattern("**/**/result.json", log)).toBe("**/result.json");
});

it("should collapse back-to-back doublestars and change prefix** to prefix*/**", () => {
expect(fixGlobPattern("../../app-store/**/**prefix**", log)).toBe(
"../../app-store/**/*prefix*/**"
);
expect(fixGlobPattern("**/**/prefix**", log)).toBe("**/prefix*/**");
});

it("should not modify valid glob patterns", () => {
expect(fixGlobPattern("src/**/*.js", log)).toBe("src/**/*.js");
expect(fixGlobPattern("src/**/test/*.js", log)).toBe("src/**/test/*.js");
expect(fixGlobPattern("src/**/test/**/*.js", log)).toBe(
"src/**/test/**/*.js"
);
expect(fixGlobPattern("src/**/test/**/result.json", log)).toBe(
"src/**/test/**/result.json"
);
});

it("should handle glob patterns with non-ASCII characters", () => {
expect(fixGlobPattern("src/日本語/**/*.js", log)).toBe(
"src/日本語/**/*.js"
);
expect(fixGlobPattern("src/中文/**/*.json", log)).toBe(
"src/中文/**/*.json"
);
expect(fixGlobPattern("src/русский/**/*.ts", log)).toBe(
"src/русский/**/*.ts"
);
});
it("should handle glob patterns with emojis", () => {
expect(fixGlobPattern("src/👋**/*.js", log)).toBe("src/👋*/**/*.js");
expect(fixGlobPattern("src/🌎**/*.json", log)).toBe("src/🌎*/**/*.json");
expect(fixGlobPattern("src/🚀**/*.ts", log)).toBe("src/🚀*/**/*.ts");
});
});
91 changes: 91 additions & 0 deletions packages/turbo-codemod/src/transforms/clean-globs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
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) {
pattern = newPattern;
newPattern = pattern.replace(/\*\*\/\*\*/g, "**");
}

// For '**.ext' change to '**/*.ext'
// 'ext' is a filename or extension and can contain almost any character except '*' and '/'
newPattern = pattern.replace(/(\*\*)([^*/]+)/g, "$1/*$2");
if (newPattern !== pattern) {
pattern = newPattern;
}

// For 'prefix**' change to 'prefix*/**'
// 'prefix' is a folder name and can contain almost any character except '*' and '/'
newPattern = pattern.replace(/([^*/]+)(\*\*)/g, "$1*/$2");
if (newPattern !== pattern) {
pattern = newPattern;
}

return pattern;
}

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

export default transformerMeta;