Skip to content
Open
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
39 changes: 39 additions & 0 deletions src/compile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ const FunctionlessSalt = "8269d1a8";
*
* All Function Declarations, Expressions and Arrow Expressions are decorated with
* the `register` function which attaches its AST as a property.
*
* @deprecated - ast-reflection now uses the sequence commands below.
*/
export const RegisterFunctionName = `register_${FunctionlessSalt}`;

Expand Down Expand Up @@ -88,9 +90,46 @@ export const RegisterFunctionName = `register_${FunctionlessSalt}`;
* are added to the bound Function.
*
* If `<expr>` is not a Function, then the call is proxied without modification.
*
* @deprecated - ast-reflection now uses the sequence commands below.
*/
export const BindFunctionName = `bind_${FunctionlessSalt}`;

/**
* AST-Reflection injected s-expression commands.
*
* (stash=[COMMAND], arguments...)
*/
// (stash=REGISTER,stash=func,stash[]=ast,stash)
export const RegisterCommand = "REGISTER";
// (stash=REGISTER_REF,stash[]=ast)
export const RegisterRefCommand = "REGISTER_REF";
/**
* (stash="BIND",
* stash={ args: args, self: 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
* )
*/
export const BindCommand = "BIND";
/**
* (stash="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
* )
*/
export const ProxyCommand = "PROXY";

/**
* 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
148 changes: 146 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,121 @@ 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.
// (stash="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
// (stash="REGISTER_REF", ref[Symbol.AST]=ast)
// => undefined
// TODO support returning no node.
return ts.factory.createIdentifier("undefined");
} else if (commandFlag === BindCommand) {
/**
* Bind -
* (stash="BIND",
* stash={ args: args, self: 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.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) {
/**
* (stash="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)
* ),
* proxy
* )
* TODO - rebuild proxy
*/
return ts.factory.createIdentifier("undefined");
}
}

return ts.visitEachChild(node, eraseBindAndRegister, ctx);
},
],
Expand Down Expand Up @@ -1277,5 +1398,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