|
| 1 | +import { existsSync, lstatSync, readdirSync, writeFileSync } from "node:fs"; |
| 2 | +import path, { extname, join } from "node:path"; |
| 3 | +import { readFileSync } from "@cloudflare/workers-utils"; |
| 4 | +import * as recast from "recast"; |
| 5 | +import * as esprimaParser from "recast/parsers/esprima"; |
| 6 | +import * as typescriptParser from "recast/parsers/typescript"; |
| 7 | +import type { Program } from "esprima"; |
| 8 | + |
| 9 | +/* |
| 10 | + CODEMOD TIPS & TRICKS |
| 11 | + ===================== |
| 12 | +
|
| 13 | + More info about parsing and transforming can be found in the `recast` docs: |
| 14 | + https://github.com/benjamn/recast |
| 15 | +
|
| 16 | + `recast` uses the `ast-types` library under the hood for basic AST operations |
| 17 | + and defining node types. If you need to manipulate or manually construct AST nodes as |
| 18 | + part of a code mod operation, be sure to check the `ast-types` documentation: |
| 19 | + https://github.com/benjamn/ast-types |
| 20 | +
|
| 21 | + Last but not least, AST viewers can be extremely helpful when trying to write |
| 22 | + a transformer: |
| 23 | + - https://astexplorer.net/ |
| 24 | + - https://ts-ast-viewer.com/# |
| 25 | +
|
| 26 | +*/ |
| 27 | + |
| 28 | +// Parse an input string as javascript and return an ast |
| 29 | +export const parseJs = (src: string) => { |
| 30 | + src = src.trim(); |
| 31 | + try { |
| 32 | + return recast.parse(src, { parser: esprimaParser }); |
| 33 | + } catch { |
| 34 | + throw new Error("Error parsing js template."); |
| 35 | + } |
| 36 | +}; |
| 37 | + |
| 38 | +// Parse an input string as typescript and return an ast |
| 39 | +export const parseTs = (src: string) => { |
| 40 | + src = src.trim(); |
| 41 | + try { |
| 42 | + return recast.parse(src, { parser: typescriptParser }); |
| 43 | + } catch { |
| 44 | + throw new Error("Error parsing ts template."); |
| 45 | + } |
| 46 | +}; |
| 47 | + |
| 48 | +// Parse a provided file with recast and return an ast |
| 49 | +// Selects the correct parser based on the file extension |
| 50 | +export const parseFile = (filePath: string) => { |
| 51 | + const lang = path.extname(filePath).slice(1); |
| 52 | + const parser = lang === "js" ? esprimaParser : typescriptParser; |
| 53 | + |
| 54 | + try { |
| 55 | + const fileContents = readFileSync(path.resolve(filePath)); |
| 56 | + |
| 57 | + if (fileContents) { |
| 58 | + return recast.parse(fileContents, { parser }).program as Program; |
| 59 | + } |
| 60 | + } catch { |
| 61 | + throw new Error(`Error parsing file: ${filePath}`); |
| 62 | + } |
| 63 | + |
| 64 | + return null; |
| 65 | +}; |
| 66 | + |
| 67 | +// Transform a file with the provided transformer methods and write it back to disk |
| 68 | +export const transformFile = ( |
| 69 | + filePath: string, |
| 70 | + methods: recast.types.Visitor |
| 71 | +) => { |
| 72 | + const ast = parseFile(filePath); |
| 73 | + |
| 74 | + if (ast) { |
| 75 | + recast.visit(ast, methods); |
| 76 | + writeFileSync(filePath, recast.print(ast).code); |
| 77 | + } |
| 78 | +}; |
| 79 | + |
| 80 | +export const loadSnippets = (parentFolder: string) => { |
| 81 | + const snippetsPath = join(parentFolder, "snippets"); |
| 82 | + |
| 83 | + if (!existsSync(snippetsPath)) { |
| 84 | + return {}; |
| 85 | + } |
| 86 | + |
| 87 | + if (!lstatSync(snippetsPath).isDirectory) { |
| 88 | + return {}; |
| 89 | + } |
| 90 | + |
| 91 | + const files = readdirSync(snippetsPath); |
| 92 | + |
| 93 | + return ( |
| 94 | + files |
| 95 | + // don't try loading directories |
| 96 | + .filter((fileName) => lstatSync(join(snippetsPath, fileName)).isFile) |
| 97 | + // only load js or ts files |
| 98 | + .filter((fileName) => [".js", ".ts"].includes(extname(fileName))) |
| 99 | + .reduce((acc, snippetPath) => { |
| 100 | + const [file, ext] = snippetPath.split("."); |
| 101 | + const key = `${file}${ext === "js" ? "Js" : "Ts"}`; |
| 102 | + return { |
| 103 | + ...acc, |
| 104 | + [key]: parseFile(join(snippetsPath, snippetPath))?.body, |
| 105 | + }; |
| 106 | + }, {}) as Record<string, recast.types.ASTNode[]> |
| 107 | + ); |
| 108 | +}; |
| 109 | + |
| 110 | +/** |
| 111 | + * merges provided properties into a given object (updating the object itself), deeply merging them in case |
| 112 | + * some properties are object themselves |
| 113 | + * |
| 114 | + * @param sourceObject the object into which merge the new properties |
| 115 | + * @param newProperties the new properties to add/merge |
| 116 | + */ |
| 117 | +export const mergeObjectProperties = ( |
| 118 | + sourceObject: recast.types.namedTypes.ObjectExpression, |
| 119 | + newProperties: recast.types.namedTypes.ObjectProperty[] |
| 120 | +): void => { |
| 121 | + newProperties.forEach((newProp) => { |
| 122 | + const newPropName = getPropertyName(newProp); |
| 123 | + if (!newPropName) { |
| 124 | + return false; |
| 125 | + } |
| 126 | + const indexOfExisting = sourceObject.properties.findIndex( |
| 127 | + (p) => p.type === "ObjectProperty" && getPropertyName(p) === newPropName |
| 128 | + ); |
| 129 | + |
| 130 | + const existing = sourceObject.properties[indexOfExisting]; |
| 131 | + if (!existing) { |
| 132 | + sourceObject.properties.push(newProp); |
| 133 | + return; |
| 134 | + } |
| 135 | + |
| 136 | + if ( |
| 137 | + existing.type === "ObjectProperty" && |
| 138 | + existing.value.type === "ObjectExpression" && |
| 139 | + newProp.value.type === "ObjectExpression" |
| 140 | + ) { |
| 141 | + mergeObjectProperties( |
| 142 | + existing.value, |
| 143 | + newProp.value.properties as recast.types.namedTypes.ObjectProperty[] |
| 144 | + ); |
| 145 | + return; |
| 146 | + } |
| 147 | + |
| 148 | + sourceObject.properties[indexOfExisting] = newProp; |
| 149 | + }); |
| 150 | +}; |
| 151 | + |
| 152 | +const getPropertyName = (newProp: recast.types.namedTypes.ObjectProperty) => { |
| 153 | + return newProp.key.type === "Identifier" |
| 154 | + ? newProp.key.name |
| 155 | + : newProp.key.type === "StringLiteral" |
| 156 | + ? newProp.key.value |
| 157 | + : null; |
| 158 | +}; |
0 commit comments