chore: import upstream snapshot with attribution
Deploy (to testing) and Test Playground Preview Worker / Deploy Playground Preview Worker (testing) (push) Has been skipped
Deploy Workers Shared Staging / Deploy Workers Shared Staging (push) Failing after 0s
Prerelease / build (push) Has been skipped
Handle Changesets / Handle Changesets (push) Has been cancelled
Semgrep OSS scan / semgrep-oss (push) Has been cancelled
Deploy (to testing) and Test Playground Preview Worker / Deploy Playground Preview Worker (testing) (push) Has been skipped
Deploy Workers Shared Staging / Deploy Workers Shared Staging (push) Failing after 0s
Prerelease / build (push) Has been skipped
Handle Changesets / Handle Changesets (push) Has been cancelled
Semgrep OSS scan / semgrep-oss (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
import { mkdirSync, writeFileSync } from "node:fs";
|
||||
import { dirname, join, relative, resolve } from "node:path";
|
||||
import * as recast from "recast";
|
||||
import { parseFile, parseTs } from "./index";
|
||||
|
||||
/**
|
||||
* Writing code-mods often requires some trial and error.
|
||||
* Often manual testing can become a hassle. This script was meant
|
||||
* to help test and develop transforms in isolation without having to write a throw-away script.
|
||||
*
|
||||
* Replace your codemod below and run the script with `pnpm run dev`
|
||||
*/
|
||||
|
||||
/**
|
||||
* This function mocks the `transformFile` API but outputs it to the console and writes it
|
||||
* to a dedicated output file for easier testing.
|
||||
*/
|
||||
export const testTransform = (
|
||||
filePath: string,
|
||||
methods: recast.types.Visitor
|
||||
) => {
|
||||
const devSnippetsDir = resolve(__dirname, "../dev-snippets");
|
||||
const resolvedInput = resolve(__dirname, filePath);
|
||||
const relativeInput = relative(devSnippetsDir, resolvedInput);
|
||||
|
||||
if (
|
||||
relativeInput.startsWith("..") ||
|
||||
resolve(relativeInput) === relativeInput
|
||||
) {
|
||||
throw new Error(`Input file must be under dev-snippets/. Got: ${filePath}`);
|
||||
}
|
||||
|
||||
const ast = parseFile(resolvedInput);
|
||||
|
||||
if (ast) {
|
||||
recast.visit(ast, methods);
|
||||
const code = recast.print(ast).code;
|
||||
console.log(code);
|
||||
const outputPath = join(
|
||||
__dirname,
|
||||
"../dev-snippets-outputs",
|
||||
relativeInput
|
||||
);
|
||||
mkdirSync(dirname(outputPath), {
|
||||
recursive: true,
|
||||
});
|
||||
writeFileSync(outputPath, code);
|
||||
}
|
||||
};
|
||||
|
||||
// Use this function to experiment with a codemod in isolation
|
||||
const testCodemod = () => {
|
||||
testTransform("../dev-snippets/test.ts", {
|
||||
visitIdentifier(n) {
|
||||
n.node.name = "Potato";
|
||||
|
||||
return false;
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
testCodemod();
|
||||
|
||||
// This function can be used to inspect the AST of a particular snippet
|
||||
const _printSnippet = () => {
|
||||
const snippet = `
|
||||
if(true) {
|
||||
console.log("potato");
|
||||
}
|
||||
`;
|
||||
|
||||
const program = parseTs(snippet).program;
|
||||
console.log(program.body[0]);
|
||||
};
|
||||
// _printSnippet();
|
||||
@@ -0,0 +1,124 @@
|
||||
import { readFileSync, writeFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import * as recast from "recast";
|
||||
import * as esprimaParser from "recast/parsers/esprima";
|
||||
import * as typescriptParser from "recast/parsers/typescript";
|
||||
import type { Program } from "esprima";
|
||||
|
||||
/*
|
||||
CODEMOD TIPS & TRICKS
|
||||
=====================
|
||||
|
||||
More info about parsing and transforming can be found in the `recast` docs:
|
||||
https://github.com/benjamn/recast
|
||||
|
||||
`recast` uses the `ast-types` library under the hood for basic AST operations
|
||||
and defining node types. If you need to manipulate or manually construct AST nodes as
|
||||
part of a code mod operation, be sure to check the `ast-types` documentation:
|
||||
https://github.com/benjamn/ast-types
|
||||
|
||||
Last but not least, AST viewers can be extremely helpful when trying to write
|
||||
a transformer:
|
||||
- https://astexplorer.net/
|
||||
- https://ts-ast-viewer.com/#
|
||||
|
||||
*/
|
||||
|
||||
// Parse an input string as javascript and return an ast
|
||||
export function parseJs(src: string) {
|
||||
src = src.trim();
|
||||
try {
|
||||
return recast.parse(src, { parser: esprimaParser });
|
||||
} catch {
|
||||
throw new Error("Error parsing js template.");
|
||||
}
|
||||
}
|
||||
|
||||
// Parse an input string as typescript and return an ast
|
||||
export function parseTs(src: string) {
|
||||
src = src.trim();
|
||||
try {
|
||||
return recast.parse(src, { parser: typescriptParser });
|
||||
} catch {
|
||||
throw new Error("Error parsing ts template.");
|
||||
}
|
||||
}
|
||||
|
||||
// Parse a provided file with recast and return an ast
|
||||
// Selects the correct parser based on the file extension
|
||||
export function parseFile(filePath: string) {
|
||||
const lang = path.extname(filePath).slice(1);
|
||||
const parser = lang === "js" ? esprimaParser : typescriptParser;
|
||||
|
||||
try {
|
||||
const fileContents = readFileSync(path.resolve(filePath), "utf-8");
|
||||
|
||||
if (fileContents) {
|
||||
return recast.parse(fileContents, { parser }).program as Program;
|
||||
}
|
||||
} catch {
|
||||
throw new Error(`Error parsing file: ${filePath}`);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Transform a file with the provided transformer methods and write it back to disk
|
||||
export function transformFile(filePath: string, methods: recast.types.Visitor) {
|
||||
const ast = parseFile(filePath);
|
||||
|
||||
if (ast) {
|
||||
recast.visit(ast, methods);
|
||||
writeFileSync(filePath, recast.print(ast).code);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* merges provided properties into a given object (updating the object itself), deeply merging them in case
|
||||
* some properties are object themselves
|
||||
*
|
||||
* @param sourceObject the object into which merge the new properties
|
||||
* @param newProperties the new properties to add/merge
|
||||
*/
|
||||
export function mergeObjectProperties(
|
||||
sourceObject: recast.types.namedTypes.ObjectExpression,
|
||||
newProperties: recast.types.namedTypes.ObjectProperty[]
|
||||
): void {
|
||||
newProperties.forEach((newProp) => {
|
||||
const newPropName = getPropertyName(newProp);
|
||||
if (!newPropName) {
|
||||
return false;
|
||||
}
|
||||
const indexOfExisting = sourceObject.properties.findIndex(
|
||||
(p) => p.type === "ObjectProperty" && getPropertyName(p) === newPropName
|
||||
);
|
||||
|
||||
const existing = sourceObject.properties[indexOfExisting];
|
||||
if (!existing) {
|
||||
sourceObject.properties.push(newProp);
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
existing.type === "ObjectProperty" &&
|
||||
existing.value.type === "ObjectExpression" &&
|
||||
newProp.value.type === "ObjectExpression"
|
||||
) {
|
||||
mergeObjectProperties(
|
||||
existing.value,
|
||||
newProp.value.properties as recast.types.namedTypes.ObjectProperty[]
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
sourceObject.properties[indexOfExisting] = newProp;
|
||||
});
|
||||
}
|
||||
|
||||
function getPropertyName(newProp: recast.types.namedTypes.ObjectProperty) {
|
||||
return newProp.key.type === "Identifier"
|
||||
? newProp.key.name
|
||||
: newProp.key.type === "StringLiteral"
|
||||
? newProp.key.value
|
||||
: null;
|
||||
}
|
||||
Reference in New Issue
Block a user