Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
5 changes: 5 additions & 0 deletions src/compile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,11 @@ export const RegisterFunctionName = `register_${FunctionlessSalt}`;
*/
export const BindFunctionName = `bind_${FunctionlessSalt}`;

export const RegisterCommand = `REGISTER_${FunctionlessSalt}`;
export const RegisterRefCommand = `REGISTER_REF_${FunctionlessSalt}`;
export const BindCommand = `BIND_${FunctionlessSalt}`;
export const ProxyCommand = `PROXY_${FunctionlessSalt}`;

/**
* TypeScript Transformer which transforms functionless functions, such as `AppsyncResolver`,
* into an AST that can be interpreted at CDK synth time to produce VTL templates and AppSync
Expand Down
147 changes: 145 additions & 2 deletions src/function.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,18 @@ import type { Context } from "aws-lambda";
// eslint-disable-next-line import/no-extraneous-dependencies
import { Construct } from "constructs";
import esbuild from "esbuild";
import ts from "typescript";
import ts, { BinaryExpression, SyntaxKind } from "typescript";
import { ApiGatewayVtlIntegration } from "./api";
import type { AppSyncVtlIntegration } from "./appsync";
import { ASL, ASLGraph } from "./asl";
import { BindFunctionName, RegisterFunctionName } from "./compile";
import {
BindCommand,
BindFunctionName,
ProxyCommand,
RegisterCommand,
RegisterFunctionName,
RegisterRefCommand,
} from "./compile";
import { IntegrationInvocation } from "./declaration";
import { ErrorCodes, formatErrorMessage, SynthError } from "./error-code";
import {
Expand Down Expand Up @@ -1044,7 +1051,120 @@ export async function serialize(
)
);
}
} else if (
// (stash=x,stash[]=ast,stash)
// => x
isSequenceExpr(node)
) {
// Functionless AST-Reflection commands are in the form (command, ...args)
const [commandFlagPosition, entry1] =
flattenSequenceExpression(node);

const commandFlag =
commandFlagPosition &&
ts.isBinaryExpression(commandFlagPosition) &&
ts.isStringLiteral(commandFlagPosition.right)
? commandFlagPosition.right.text
: undefined;

// Register - retrieve the value from the second position's assignment.
// ("REGISTER", stash=value, stash[Symbol.AST]=ast, stash)
// => value
if (commandFlag === RegisterCommand) {
if (
!entry1 ||
!ts.isBinaryExpression(entry1) ||
entry1.operatorToken.kind !==
ts.SyntaxKind.EqualsToken
) {
throw new SynthError(
ErrorCodes.Unexpected_Error,
"Compilation Error: found an invalid register command. Check the versions of AST-Reflection and Functionless."
);
}
return eraseBindAndRegister(entry1.right);
} else if (commandFlag === RegisterRefCommand) {
// Register Ref - remove
// ("REGISTER_REF", ref[Symbol.AST]=ast)
// => undefined
// TODO support returning no node.
return ts.factory.createIdentifier("undefined");
} else if (commandFlag === BindCommand) {
/**
* Bind -
* ("BIND",
* stash={ args: args, this: this, func: func },
* stash={ f: stash.func.bind(stash.self, ...stash.args), ...stash },
* typeof stash.f === "function" && (
* stash.f[Symbol.for("functionless:BoundThis")] = stash.self,
* stash.f[Symbol.for("functionless:BoundArgs")] = stash.args,
* stash.f[Symbol.for("functionless:TargetFunction")] = stash.func
* ),
* stash.func
* )
* => func.bind(this, ...args)
*/
if (
!entry1 ||
!ts.isBinaryExpression(entry1) ||
entry1.operatorToken.kind !==
SyntaxKind.EqualsToken ||
!ts.isObjectLiteralExpression(entry1.right)
) {
throw new SynthError(
ErrorCodes.Unexpected_Error,
"Compilation Error: found an invalid register command. Check the versions of AST-Reflection and Functionless."
);
}
const {
args,
this: _this,
func,
} = Object.fromEntries(
entry1.right.properties
.filter(ts.isPropertyAssignment)
.map((p) => [
p.name && ts.isIdentifier(p.name)
? p.name.text
: "UNKNOWN",
p.initializer,
])
);
if (!args || !_this || !func) {
throw new SynthError(
ErrorCodes.Unexpected_Error,
"Compilation Error: found an invalid register command. Check the versions of AST-Reflection and Functionless."
);
Comment on lines +1134 to +1137
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not just make it a no-op?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My thought was: The user wouldn't know it is an issue until runtime. If we cannot re-construct the command then we should fail.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But how do you know it's not just someone else using similar looking syntax? The compiler should not produce an invalid contract right?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We check for our flag on the left first. The chance someone has stash_salt=bind is very low.

That leaves all parsing errors as a bug or mis-matched package version ( with breaking changes)

}
return ts.factory.createCallExpression(
ts.factory.createPropertyAccessExpression(
eraseBindAndRegister(func) as ts.Expression,
"bind"
),
undefined,
[
eraseBindAndRegister(_this) as ts.Expression,
ts.factory.createSpreadElement(
eraseBindAndRegister(args) as ts.Expression
),
]
);
} else if (commandFlag === ProxyCommand) {
/**
* ("PROXY",
* stash={ args }, // ensure the args are only evaluated once
* stash={ proxy: new clss(...stash.args), ...stash }, // create the proxy
* (globalThis.util.types.isProxy(stash.proxy) &&
* (globalThis.proxies = globalThis.proxies ?? new globalThis.WeakMap()).set(stash.proxy, stash.args))
* ),
* stash.proxy
* )
* TODO - rebuild proxy
*/
return ts.factory.createIdentifier("undefined");
}
}

return ts.visitEachChild(node, eraseBindAndRegister, ctx);
},
],
Expand Down Expand Up @@ -1277,5 +1397,28 @@ export async function bundle(
return bundle.outputFiles[0]!;
}

function isSequenceExpr(node: ts.Node): node is BinaryExpression & {
operatorToken: { kind: typeof ts.SyntaxKind.CommaToken };
} {
return (
ts.isBinaryExpression(node) &&
node.operatorToken.kind === ts.SyntaxKind.CommaToken
);
}

function flattenSequenceExpression(
expr: BinaryExpression & {
operatorToken: { kind: typeof ts.SyntaxKind.CommaToken };
}
): ts.Expression[] {
const left = isSequenceExpr(expr.left)
? flattenSequenceExpression(expr.left)
: [expr.left];
const right = isSequenceExpr(expr.right)
? flattenSequenceExpression(expr.right)
: [expr.right];
return [...left, ...right];
}

// to prevent the closure serializer from trying to import all of functionless.
export const deploymentOnlyModule = true;
2 changes: 1 addition & 1 deletion src/serialize-closure/serialize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -403,7 +403,7 @@ export function serializeClosure(
);
} else if (typeof value === "object") {
if (Globals.has(value)) {
return emitVarDecl("const", uniqueName(), Globals.get(value)!());
return Globals.get(value)!();
}

const mod = requireCache.get(value);
Expand Down
3 changes: 2 additions & 1 deletion swc-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@ exports.config = {
plugins: [["@functionless/ast-reflection", {}]],
},
},
minify: true,
minify: false,
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you mean to check this in? It can cause problems?

sourceMaps: "inline",
inlineSourcesContent: false,
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this has an impact where identifiers that have been renamed cannot be re-mapped.

module: {
type: "commonjs",
},
Expand Down
Loading