Skip to content

RFC-2: Draft implementation of runtime module compiler #3

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

Merged
merged 2 commits into from
Oct 12, 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
3 changes: 2 additions & 1 deletion packages/esbuild-plugins/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
"dev": "npm run build -- --watch"
},
"dependencies": {
"esbuild": "^0.19.3"
"esbuild": "^0.19.3",
"typescript": "^5.2.2"
},
"devDependencies": {
"@types/common-tags": "^1.8.2",
Expand Down
3 changes: 2 additions & 1 deletion packages/esbuild-plugins/src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from "./function-plugin";
export * from "./plugins/function-infra-plugin";
export * from "./plugins/function-runtime-plugin";
65 changes: 65 additions & 0 deletions packages/esbuild-plugins/src/parsers/read-config-export.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import * as ts from "typescript";

export function readConfigExport(input: string) {
const sourceFile = ts.createSourceFile(
"file.ts",
input,
ts.ScriptTarget.ESNext,
true,
);

const configExport = sourceFile.statements.find(
(stmt) =>
ts.isVariableStatement(stmt) &&
stmt.declarationList.declarations.some(
(decl) => decl.name.getText() === "config",
),
) as ts.VariableStatement | undefined;

if (!configExport) {
throw new Error("No named export 'config' found.");
}

const configDeclaration = configExport.declarationList.declarations.find(
(decl) => decl.name.getText() === "config",
) as ts.VariableDeclaration;

if (
!configDeclaration ||
!ts.isObjectLiteralExpression(configDeclaration.initializer!)
) {
throw new Error("'config' is not an object literal.");
}

let configObjectStr = "{ ";

configDeclaration.initializer.properties.forEach((prop, index, array) => {
if (ts.isPropertyAssignment(prop)) {
const key = prop.name.getText();
const valueNode = prop.initializer;
if (
ts.isStringLiteral(valueNode) ||
ts.isNumericLiteral(valueNode) ||
valueNode.kind === ts.SyntaxKind.TrueKeyword ||
valueNode.kind === ts.SyntaxKind.FalseKeyword
) {
const value = valueNode.getText();
configObjectStr += `${key}: ${value}${
index < array.length - 1 ? ", " : " "
}`;
} else {
throw new Error(
`Invalid value type for key '${key}': only numbers, strings, and booleans are allowed.`,
);
}
} else {
throw new Error(
`Invalid property assignment in 'config': ${prop.getText()}`,
);
}
});

configObjectStr += "}";

return configObjectStr;
}
40 changes: 40 additions & 0 deletions packages/esbuild-plugins/src/parsers/remove-config-export.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import * as ts from "typescript";

export function removeConfigExport(sourceText: string): string {
const sourceFile = ts.createSourceFile(
"file.ts",
sourceText,
ts.ScriptTarget.ES2015,
true,
);

const printer = ts.createPrinter();
const resultFile = ts.transform(sourceFile, [removeConfigExportTransformer])
.transformed[0];

const result = printer.printNode(
ts.EmitHint.Unspecified,
resultFile,
sourceFile,
);

return result;
}

function removeConfigExportTransformer(context: ts.TransformationContext) {
return (node: ts.Node): ts.Node => {
if (
ts.isVariableStatement(node) &&
node.declarationList.declarations.some(
(decl) => decl.name.getText() === "config",
)
) {
return ts.factory.createNotEmittedStatement(node);
}
return ts.visitEachChild(
node,
(child) => removeConfigExportTransformer(context)(child),
context,
);
};
}
Original file line number Diff line number Diff line change
@@ -1,21 +1,14 @@
import esbuild, { Plugin } from "esbuild";
import { readConfigExport } from "src/parsers/read-config-export";

/**
* compiles cloud function modules
* runs separate esbuild process to get exports metadata
*/
export function functionPlugin(opts: {
fileIsFunction: (filePath: string) => boolean;
export function functionInfraPlugin(opts: {
getFile: (filePath: string) => string | Promise<string>;
esbuildInstance?: typeof esbuild;
}): Plugin {
return {
name: "function",
setup(build) {
build.onLoad({ filter: /.*$/ }, async (args) => {
const isFunction = await opts.fileIsFunction(args.path);
if (!isFunction) return;

build.onLoad({ filter: /.\.fn*/ }, async (args) => {
const esbuild = opts.esbuildInstance || (await import("esbuild"));
const fileContent = await opts.getFile(args.path);

Expand All @@ -35,11 +28,18 @@ export function functionPlugin(opts: {
const reservedExports = ["preload", "config"];
const userExports = exports.filter((e) => !reservedExports.includes(e));

let infraCode = `import { createWorkflowNode } from "@notation/sdk"`;
let infraCode = `import { fn } from "@notation/sdk"`;

if (exports.includes("config")) {
const config = readConfigExport(fileContent);
infraCode = infraCode.concat(`\nconst config = ${config};`);
} else {
infraCode = infraCode.concat(`\nconst config = {};`);
}

for (const handlerName of userExports) {
infraCode = infraCode.concat(
`\nexport const ${handlerName} = createWorkflowNode("${handlerName}");`,
`\nexport const ${handlerName} = fn({ handler: "${handlerName}", ...config });`,
);
}

Expand Down
21 changes: 21 additions & 0 deletions packages/esbuild-plugins/src/plugins/function-runtime-plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import esbuild, { Plugin } from "esbuild";
import { removeConfigExport } from "src/parsers/remove-config-export";

export function functionRuntimePlugin(opts: {
getFile: (filePath: string) => string | Promise<string>;
esbuildInstance?: typeof esbuild;
}): Plugin {
return {
name: "function",
setup(build) {
build.onLoad({ filter: /.\.fn*/ }, async (args) => {
const fileContent = await opts.getFile(args.path);
const runtimeCode = removeConfigExport(fileContent);
return {
loader: "ts",
contents: runtimeCode,
};
});
},
};
}
12 changes: 8 additions & 4 deletions packages/esbuild-plugins/test/esbuild-test-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,15 @@ import esbuild, { BuildOptions, Plugin } from "esbuild";
const virtualFilePlugin = (input: any): Plugin => ({
name: "virtual-file",
setup(build) {
build.onResolve({ filter: /.*/ }, (args) => {
build.onResolve({ filter: /\..*/ }, (args) => {
return { path: args.path, namespace: "stdin" };
});
build.onLoad({ filter: /.*/, namespace: "stdin" }, (args) => {
return { contents: input, loader: "ts", resolveDir: args.path };
build.onLoad({ filter: /\..*/, namespace: "stdin" }, (args) => {
return {
contents: input,
loader: "ts",
resolveDir: args.path,
};
});
},
});
Expand All @@ -19,7 +23,7 @@ export function createBuilder(
const buildOptions = getBuildOptions(input);

const result = await esbuild.build({
entryPoints: ["entry.ts"],
entryPoints: [".entry.fn.ts"],
write: false,
...buildOptions,
plugins: [...(buildOptions.plugins || []), virtualFilePlugin(input)],
Expand Down
46 changes: 0 additions & 46 deletions packages/esbuild-plugins/test/function-plugin.test.ts

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { expect, it } from "bun:test";
import { stripIndent } from "common-tags";
import { functionInfraPlugin } from "src/plugins/function-infra-plugin";
import { createBuilder } from "test/esbuild-test-utils";

const buildInfra = createBuilder((input) => ({
plugins: [functionInfraPlugin({ getFile: () => input })],
}));

it("remaps exports", async () => {
const input = `
import { handler, FnConfig } from "@notation/sdk";
export const getNum = handler(() => 1);
`;

const expected = stripIndent`
import { fn } from "@notation/sdk";
const config = {};
export const getNum = fn({ handler: "getNum", ...config });
`;

const output = await buildInfra(input);

expect(output).toContain(expected);
});

it("merges config", async () => {
const input = `
import { handler, FnConfig } from "@notation/sdk";
export const getNum = handler(() => 1);
export const config: FnConfig = { memory: 64 };
`;

const expected = stripIndent`
import { fn } from "@notation/sdk";
const config = { memory: 64 };
export const getNum = fn({ handler: "getNum", ...config });
`;

const output = await buildInfra(input);

expect(output).toContain(expected);
});

it("should strip runtime code", async () => {
const input = `
import { handler } from "@notation/sdk";
import lib from "lib";

let num = lib.getNum();

export const getNum = () => num;
export const getDoubleNum = () => num * 2;
`;

const expected = stripIndent`
import { fn } from "@notation/sdk";
const config = {};
export const getDoubleNum = fn({ handler: "getDoubleNum", ...config });
export const getNum = fn({ handler: "getNum", ...config });
`;

const output = await buildInfra(input);

expect(output).toContain(expected);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { expect, it } from "bun:test";
import { stripIndent } from "common-tags";
import { functionRuntimePlugin } from "src/plugins/function-runtime-plugin";
import { createBuilder } from "test/esbuild-test-utils";

const buildRuntime = createBuilder((input) => ({
plugins: [functionRuntimePlugin({ getFile: () => input })],
}));

it("should strip infra code", async () => {
const input = stripIndent`
import { FnConfig, handler } from "@notation/aws";

const num = await fetch("http://api.com/num").then((res) => res.json());

export const getNum = handler(() => num);
export const getDoubleNum = handler(() => num * 2);

export const config: FnConfig = {
memory: 64,
timeout: 5,
environment: "node:16",
};
`;

const expected = stripIndent`
import { handler } from "@notation/aws";
const num = await fetch("http://api.com/num").then((res) => res.json());
export const getNum = handler(() => num);
export const getDoubleNum = handler(() => num * 2);
`;

const output = await buildRuntime(input);

expect(output).toContain(expected);
});
Loading