diff --git a/packages/esbuild-plugins/src/parsers/remove-unsafe-references.ts b/packages/esbuild-plugins/src/parsers/remove-unsafe-references.ts new file mode 100644 index 0000000..ea343e5 --- /dev/null +++ b/packages/esbuild-plugins/src/parsers/remove-unsafe-references.ts @@ -0,0 +1,125 @@ +import ts from "typescript"; + +export function removeUnsafeReferences(sourceText: string): string { + const sourceFile = ts.createSourceFile( + "file.ts", + sourceText, + ts.ScriptTarget.ES2015, + true, + ); + const printer = ts.createPrinter(); + + let infraImports: Set = new Set(); + + function transformer(context: ts.TransformationContext) { + return (node: ts.Node, isTopLevel: boolean = true): ts.Node => { + if ( + ts.isImportDeclaration(node) && + ts.isStringLiteral(node.moduleSpecifier) + ) { + const path = node.moduleSpecifier.text; + + // todo: enable relative paths by resolving path to validate location + if (path.startsWith("infra/")) { + node.importClause?.namedBindings?.forEachChild((namedBinding) => { + if (ts.isImportSpecifier(namedBinding)) { + infraImports.add(namedBinding.name.text); + } + }); + return node; + } else { + return ts.factory.createNotEmittedStatement(node); + } + } + + if ( + ts.isStatement(node) && + containsUnsafeReferences(node, infraImports) + ) { + return ts.factory.createNotEmittedStatement(node); + } + + return ts.visitEachChild( + node, + (child) => transformer(context)(child), + context, + ); + }; + } + + const transformedSourceFile = ts.transform(sourceFile, [transformer]) + .transformed[0]; + + return printer.printFile(transformedSourceFile as ts.SourceFile); +} + +function containsUnsafeReferences( + node: ts.Node, + safeReferences: Set, +): boolean { + let hasUnsafeReference = false; + + function visit(n: ts.Node, parent: ts.Node | undefined = undefined): void { + if (ts.isIdentifier(n)) { + if (isReference(n, parent) && !safeReferences.has(n.text)) { + hasUnsafeReference = true; + } + } else { + n.forEachChild((child) => visit(child, n)); + } + } + + visit(node); + + return hasUnsafeReference; +} + +function isReference( + node: ts.Identifier, + parent: ts.Node | undefined, +): boolean { + if (!parent) { + return false; + } + + if (ts.isTypeReferenceNode(parent)) { + return false; + } + + if (ts.isTypeParameterDeclaration(parent) && parent.name === node) { + return false; + } + + if ( + (ts.isInterfaceDeclaration(parent) || ts.isTypeAliasDeclaration(parent)) && + parent.name === node + ) { + return false; + } + + if (ts.isPropertyAssignment(parent) && parent.name === node) { + return false; + } + + if (ts.isPropertyAccessExpression(parent) && parent.name === node) { + return false; + } + + if (ts.isMethodDeclaration(parent) && parent.name === node) { + return false; + } + + if (ts.isParameter(parent) && parent.name === node) { + return false; + } + + if (ts.isVariableDeclaration(parent) && parent.name === node) { + return false; + } + + if (ts.isFunctionDeclaration(parent) && parent.name === node) { + return false; + } + + return true; +} diff --git a/packages/esbuild-plugins/src/plugins/function-infra-plugin.ts b/packages/esbuild-plugins/src/plugins/function-infra-plugin.ts index 72beca8..2e852bc 100644 --- a/packages/esbuild-plugins/src/plugins/function-infra-plugin.ts +++ b/packages/esbuild-plugins/src/plugins/function-infra-plugin.ts @@ -1,6 +1,7 @@ import path from "node:path"; import { Plugin } from "esbuild"; import { parseFnModule } from "src/parsers/parse-fn-module"; +import { removeUnsafeReferences } from "src/parsers/remove-unsafe-references"; import { GetFile, fsGetFile, withFileCheck } from "src/utils/get-file"; import { filePaths } from "@notation/core"; @@ -17,18 +18,19 @@ export function functionInfraPlugin(opts: PluginOpts = {}): Plugin { const fileContent = await getFile(args.path); const fileName = path.relative(process.cwd(), args.path); const outFileName = filePaths.dist.runtime.index(fileName); - const { config, configRaw, exports } = parseFnModule(fileContent); + const safeFnModule = removeUnsafeReferences(fileContent); + const { config, exports } = parseFnModule(fileContent); const reservedNames = ["preload", "config"]; const [platform, service] = (config.service as string).split("/"); - let infraCode = `import { ${service} } from "@notation/${platform}/${service}";`; - infraCode = infraCode.concat(`\nconst config = ${configRaw};`); + let infraCode = `import { ${service} } from "@notation/${platform}/${service}";\n`; + infraCode = infraCode.concat(`\n${safeFnModule}\n`); for (const handlerName of exports) { if (reservedNames.includes(handlerName)) continue; infraCode = infraCode.concat( - `\nexport const ${handlerName} = ${service}({ fileName: "${outFileName}", handler: "${handlerName}", ...config });`, + `export const ${handlerName} = ${service}({ fileName: "${outFileName}", handler: "${handlerName}", ...config });\n`, ); }