Skip to content

Commit 0fd1920

Browse files
committed
Implement RFC-2 infra compiler
1 parent 7588182 commit 0fd1920

File tree

9 files changed

+207
-60
lines changed

9 files changed

+207
-60
lines changed

packages/esbuild-plugins/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
"dev": "npm run build -- --watch"
1010
},
1111
"dependencies": {
12-
"esbuild": "^0.19.3"
12+
"esbuild": "^0.19.3",
13+
"typescript": "^5.2.2"
1314
},
1415
"devDependencies": {
1516
"@types/common-tags": "^1.8.2",

packages/esbuild-plugins/src/function-plugin.ts renamed to packages/esbuild-plugins/src/function-infra-plugin.ts

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,18 @@
11
import esbuild, { Plugin } from "esbuild";
2+
import { readConfigExport } from "./read-config-export";
23

34
/**
45
* compiles cloud function modules
56
* runs separate esbuild process to get exports metadata
67
*/
7-
export function functionPlugin(opts: {
8-
fileIsFunction: (filePath: string) => boolean;
8+
export function functionInfraPlugin(opts: {
99
getFile: (filePath: string) => string | Promise<string>;
1010
esbuildInstance?: typeof esbuild;
1111
}): Plugin {
1212
return {
1313
name: "function",
1414
setup(build) {
15-
build.onLoad({ filter: /.*$/ }, async (args) => {
16-
const isFunction = await opts.fileIsFunction(args.path);
17-
if (!isFunction) return;
18-
15+
build.onLoad({ filter: /.\.fn*/ }, async (args) => {
1916
const esbuild = opts.esbuildInstance || (await import("esbuild"));
2017
const fileContent = await opts.getFile(args.path);
2118

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

38-
let infraCode = `import { createWorkflowNode } from "@notation/sdk"`;
35+
let infraCode = `import { fn } from "@notation/sdk"`;
36+
37+
if (exports.includes("config")) {
38+
const config = readConfigExport(fileContent);
39+
infraCode = infraCode.concat(`\nconst config = ${config};`);
40+
} else {
41+
infraCode = infraCode.concat(`\nconst config = {};`);
42+
}
3943

4044
for (const handlerName of userExports) {
4145
infraCode = infraCode.concat(
42-
`\nexport const ${handlerName} = createWorkflowNode("${handlerName}");`,
46+
`\nexport const ${handlerName} = fn({ handler: "${handlerName}", ...config });`,
4347
);
4448
}
4549

packages/esbuild-plugins/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
export * from "./function-plugin";
1+
export * from "./function-infra-plugin";
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import * as ts from "typescript";
2+
3+
export function readConfigExport(input: string) {
4+
const sourceFile = ts.createSourceFile(
5+
"file.ts",
6+
input,
7+
ts.ScriptTarget.ESNext,
8+
true,
9+
);
10+
11+
const configExport = sourceFile.statements.find(
12+
(stmt) =>
13+
ts.isVariableStatement(stmt) &&
14+
stmt.declarationList.declarations.some(
15+
(decl) => decl.name.getText() === "config",
16+
),
17+
) as ts.VariableStatement | undefined;
18+
19+
if (!configExport) {
20+
throw new Error("No named export 'config' found.");
21+
}
22+
23+
const configDeclaration = configExport.declarationList.declarations.find(
24+
(decl) => decl.name.getText() === "config",
25+
) as ts.VariableDeclaration;
26+
27+
if (
28+
!configDeclaration ||
29+
!ts.isObjectLiteralExpression(configDeclaration.initializer!)
30+
) {
31+
throw new Error("'config' is not an object literal.");
32+
}
33+
34+
let configObjectStr = "{ ";
35+
36+
configDeclaration.initializer.properties.forEach((prop, index, array) => {
37+
if (ts.isPropertyAssignment(prop)) {
38+
const key = prop.name.getText();
39+
const valueNode = prop.initializer;
40+
if (
41+
ts.isStringLiteral(valueNode) ||
42+
ts.isNumericLiteral(valueNode) ||
43+
valueNode.kind === ts.SyntaxKind.TrueKeyword ||
44+
valueNode.kind === ts.SyntaxKind.FalseKeyword
45+
) {
46+
const value = valueNode.getText();
47+
configObjectStr += `${key}: ${value}${
48+
index < array.length - 1 ? ", " : " "
49+
}`;
50+
} else {
51+
throw new Error(
52+
`Invalid value type for key '${key}': only numbers, strings, and booleans are allowed.`,
53+
);
54+
}
55+
} else {
56+
throw new Error(
57+
`Invalid property assignment in 'config': ${prop.getText()}`,
58+
);
59+
}
60+
});
61+
62+
configObjectStr += "}";
63+
64+
return configObjectStr;
65+
}

packages/esbuild-plugins/test/esbuild-test-utils.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,15 @@ import esbuild, { BuildOptions, Plugin } from "esbuild";
33
const virtualFilePlugin = (input: any): Plugin => ({
44
name: "virtual-file",
55
setup(build) {
6-
build.onResolve({ filter: /.*/ }, (args) => {
6+
build.onResolve({ filter: /\..*/ }, (args) => {
77
return { path: args.path, namespace: "stdin" };
88
});
9-
build.onLoad({ filter: /.*/, namespace: "stdin" }, (args) => {
10-
return { contents: input, loader: "ts", resolveDir: args.path };
9+
build.onLoad({ filter: /\..*/, namespace: "stdin" }, (args) => {
10+
return {
11+
contents: input,
12+
loader: "ts",
13+
resolveDir: args.path,
14+
};
1115
});
1216
},
1317
});
@@ -19,7 +23,7 @@ export function createBuilder(
1923
const buildOptions = getBuildOptions(input);
2024

2125
const result = await esbuild.build({
22-
entryPoints: ["entry.ts"],
26+
entryPoints: [".entry.fn.ts"],
2327
write: false,
2428
...buildOptions,
2529
plugins: [...(buildOptions.plugins || []), virtualFilePlugin(input)],
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { expect, it } from "bun:test";
2+
import { stripIndent } from "common-tags";
3+
import { functionInfraPlugin } from "../src/function-infra-plugin";
4+
import { createBuilder } from "./esbuild-test-utils";
5+
6+
const buildInfra = createBuilder((input) => ({
7+
plugins: [functionInfraPlugin({ getFile: () => input })],
8+
}));
9+
10+
it("remaps exports", async () => {
11+
const input = `
12+
import { handler, FnConfig } from "@notation/sdk";
13+
export const getNum = handler(() => 1);
14+
`;
15+
16+
const expected = stripIndent`
17+
import { fn } from "@notation/sdk";
18+
const config = {};
19+
export const getNum = fn({ handler: "getNum", ...config });
20+
`;
21+
22+
const output = await buildInfra(input);
23+
24+
expect(output).toContain(expected);
25+
});
26+
27+
it("merges config", async () => {
28+
const input = `
29+
import { handler, FnConfig } from "@notation/sdk";
30+
export const getNum = handler(() => 1);
31+
export const config: FnConfig = { memory: 64 };
32+
`;
33+
34+
const expected = stripIndent`
35+
import { fn } from "@notation/sdk";
36+
const config = { memory: 64 };
37+
export const getNum = fn({ handler: "getNum", ...config });
38+
`;
39+
40+
const output = await buildInfra(input);
41+
42+
expect(output).toContain(expected);
43+
});
44+
45+
it("should strip runtime code", async () => {
46+
const input = `
47+
import { handler } from "@notation/sdk";
48+
import lib from "lib";
49+
50+
let num = lib.getNum();
51+
52+
export const getNum = () => num;
53+
export const getDoubleNum = () => num * 2;
54+
`;
55+
56+
const expected = stripIndent`
57+
import { fn } from "@notation/sdk";
58+
const config = {};
59+
export const getDoubleNum = fn({ handler: "getDoubleNum", ...config });
60+
export const getNum = fn({ handler: "getNum", ...config });
61+
`;
62+
63+
const output = await buildInfra(input);
64+
65+
expect(output).toContain(expected);
66+
});

packages/esbuild-plugins/test/function-plugin.test.ts

Lines changed: 0 additions & 46 deletions
This file was deleted.
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { describe, expect, test } from "bun:test";
2+
import { readConfigExport } from "../src/read-config-export";
3+
4+
test("Valid config object", () => {
5+
const input = `
6+
export const config = {
7+
key1: "value1",
8+
key2: 123,
9+
key3: true,
10+
key4: false
11+
};
12+
`;
13+
expect(readConfigExport(input)).toBe(
14+
'{ key1: "value1", key2: 123, key3: true, key4: false }',
15+
);
16+
});
17+
18+
test("Config object with invalid value type", () => {
19+
const input = `
20+
export const config = {
21+
key1: [123],
22+
};
23+
`;
24+
expect(() => readConfigExport(input)).toThrow(
25+
/Invalid value type for key 'key1'/,
26+
);
27+
});
28+
29+
test("No config object", () => {
30+
const input = `export const somethingElse = { key1: "value1" };`;
31+
expect(() => readConfigExport(input)).toThrow(
32+
/No named export 'config' found./,
33+
);
34+
});
35+
36+
describe("invalid inputs", () => {
37+
const invalidInputs = {
38+
identifier: "export const config = identifier;",
39+
callExpression: "export const config = identifier;",
40+
string: "export const config = 123;",
41+
number: "export const config = '123';",
42+
array: "export const config = [];",
43+
};
44+
for (const [invalidType, invalidStatement] of Object.entries(invalidInputs)) {
45+
test(invalidType, () => {
46+
expect(() => readConfigExport(invalidStatement)).toThrow(
47+
/'config' is not an object literal./,
48+
);
49+
});
50+
}
51+
});

pnpm-lock.yaml

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)