Compare commits

...

1 commit

Author SHA1 Message Date
evanboyle 5310ecbdfc remove all typescript dependencies 2021-06-01 13:57:05 -07:00
10 changed files with 1 additions and 10346 deletions

View file

@ -1,408 +0,0 @@
// Copyright 2016-2018, Pulumi Corporation.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import * as minimist from "minimist";
import * as path from "path";
import * as grpc from "@grpc/grpc-js";
import * as dynamic from "../../dynamic";
import * as resource from "../../resource";
import * as runtime from "../../runtime";
import { version } from "../../version";
const requireFromString = require("require-from-string");
const anyproto = require("google-protobuf/google/protobuf/any_pb.js");
const emptyproto = require("google-protobuf/google/protobuf/empty_pb.js");
const structproto = require("google-protobuf/google/protobuf/struct_pb.js");
const provproto = require("../../proto/provider_pb.js");
const provrpc = require("../../proto/provider_grpc_pb.js");
const plugproto = require("../../proto/plugin_pb.js");
const statusproto = require("../../proto/status_pb.js");
const providerKey: string = "__provider";
// maxRPCMessageSize raises the gRPC Max Message size from `4194304` (4mb) to `419430400` (400mb)
const maxRPCMessageSize: number = 1024 * 1024 * 400;
// We track all uncaught errors here. If we have any, we will make sure we always have a non-0 exit
// code.
const uncaughtErrors = new Set<Error>();
const uncaughtHandler = (err: Error) => {
if (!uncaughtErrors.has(err)) {
uncaughtErrors.add(err);
console.error(err.stack || err.message || ("" + err));
}
};
process.on("uncaughtException", uncaughtHandler);
// @ts-ignore 'unhandledRejection' will almost always invoke uncaughtHandler with an Error. so just
// suppress the TS strictness here.
process.on("unhandledRejection", uncaughtHandler);
process.on("exit", (code: number) => {
// If there were any uncaught errors at all, we always want to exit with an error code.
if (code === 0 && uncaughtErrors.size > 0) {
process.exitCode = 1;
}
});
const providerCache: { [key: string]: dynamic.ResourceProvider } = {};
function getProvider(props: any): dynamic.ResourceProvider {
const providerString = props[providerKey];
let provider: any = providerCache[providerString];
if (!provider) {
provider = requireFromString(providerString).handler();
providerCache[providerString] = provider;
}
// TODO[pulumi/pulumi#414]: investigate replacing requireFromString with eval
return provider;
}
// Each of the *RPC functions below implements a single method of the resource provider gRPC interface. The CRUD
// functions--checkRPC, diffRPC, createRPC, updateRPC, and deleteRPC--all operate in a similar fashion:
// 1. Deserialize the dyanmic provider for the resource on which the function is operating
// 2. Call the dynamic provider's corresponding {check,diff,create,update,delete} method
// 3. Convert and return the results
// In all cases, the dynamic provider is available in its serialized form as a property of the resource;
// getProvider` is responsible for handling its deserialization. In the case of diffRPC, if the provider itself
// has changed, `diff` reports that the resource requires replacement and does not delegate to the dynamic provider.
// This allows the creation of the replacement resource to use the new provider while the deletion of the old
// resource uses the provider with which it was created.
function cancelRPC(call: any, callback: any): void {
callback(undefined, new emptyproto.Empty());
}
function configureRPC(call: any, callback: any): void {
const resp = new provproto.ConfigureResponse();
resp.setAcceptsecrets(false);
callback(undefined, resp);
}
async function invokeRPC(call: any, callback: any): Promise<void> {
const req: any = call.request;
// TODO[pulumi/pulumi#406]: implement this.
callback(new Error(`unknown function ${req.getTok()}`), undefined);
}
async function streamInvokeRPC(call: any, callback: any): Promise<void> {
const req: any = call.request;
// TODO[pulumi/pulumi#406]: implement this.
callback(new Error(`unknown function ${req.getTok()}`), undefined);
}
async function checkRPC(call: any, callback: any): Promise<void> {
try {
const req: any = call.request;
const resp = new provproto.CheckResponse();
const olds = req.getOlds().toJavaScript();
const news = req.getNews().toJavaScript();
const provider = getProvider(news[providerKey] === runtime.unknownValue ? olds : news);
let inputs: any = news;
let failures: any[] = [];
if (provider.check) {
const result = await provider.check(olds, news);
if (result.inputs) {
inputs = result.inputs;
}
if (result.failures) {
failures = result.failures;
}
} else {
// If no check method was provided, propagate the new inputs as-is.
inputs = news;
}
inputs[providerKey] = news[providerKey];
resp.setInputs(structproto.Struct.fromJavaScript(inputs));
if (failures.length !== 0) {
const failureList = [];
for (const f of failures) {
const failure = new provproto.CheckFailure();
failure.setProperty(f.property);
failure.setReason(f.reason);
failureList.push(failure);
}
resp.setFailuresList(failureList);
}
callback(undefined, resp);
} catch (e) {
console.error(`${e}: ${e.stack}`);
callback(e, undefined);
}
}
function checkConfigRPC(call: any, callback: any): void {
callback({
code: grpc.status.UNIMPLEMENTED,
details: "CheckConfig is not implemented by the dynamic provider",
}, undefined);
}
async function diffRPC(call: any, callback: any): Promise<void> {
try {
const req: any = call.request;
const resp = new provproto.DiffResponse();
// Note that we do not take any special action if the provider has changed. This allows a user to iterate on a
// dynamic provider's implementation. This does require some care on the part of the user: each iteration of a
// dynamic provider's implementation must be able to handle all state produced by prior iterations.
//
// Prior versions of the dynamic provider required that a dynamic resource be replaced any time its provider
// implementation changed. This made iteration painful, especially if the dynamic resource was managing a
// physical resource--in this case, the physical resource would be unnecessarily deleted and recreated each
// time the provider was updated.
const olds = req.getOlds().toJavaScript();
const news = req.getNews().toJavaScript();
const provider = getProvider(news[providerKey] === runtime.unknownValue ? olds : news);
if (provider.diff) {
const result: any = await provider.diff(req.getId(), olds, news);
if (result.changes === true) {
resp.setChanges(provproto.DiffResponse.DiffChanges.DIFF_SOME);
} else if (result.changes === false) {
resp.setChanges(provproto.DiffResponse.DiffChanges.DIFF_NONE);
} else {
resp.setChanges(provproto.DiffResponse.DiffChanges.DIFF_UNKNOWN);
}
if (result.replaces && result.replaces.length !== 0) {
resp.setReplacesList(result.replaces);
}
if (result.deleteBeforeReplace) {
resp.setDeletebeforereplace(result.deleteBeforeReplace);
}
}
callback(undefined, resp);
} catch (e) {
console.error(`${e}: ${e.stack}`);
callback(e, undefined);
}
}
function diffConfigRPC(call: any, callback: any): void {
callback({
code: grpc.status.UNIMPLEMENTED,
details: "DiffConfig is not implemented by the dynamic provider",
}, undefined);
}
async function createRPC(call: any, callback: any): Promise<void> {
try {
const req: any = call.request;
const resp = new provproto.CreateResponse();
const props = req.getProperties().toJavaScript();
const provider = getProvider(props);
const result = await provider.create(props);
const resultProps = resultIncludingProvider(result.outs, props);
resp.setId(result.id);
resp.setProperties(structproto.Struct.fromJavaScript(resultProps));
callback(undefined, resp);
} catch (e) {
const response = grpcResponseFromError(e);
return callback(/*err:*/ response, /*value:*/ null, /*metadata:*/ response.metadata);
}
}
async function readRPC(call: any, callback: any): Promise<void> {
try {
const req: any = call.request;
const resp = new provproto.ReadResponse();
const id = req.getId();
const props = req.getProperties().toJavaScript();
const provider = getProvider(props);
if (provider.read) {
// If there's a read function, consult the provider. Ensure to propagate the special __provider
// value too, so that the provider's CRUD operations continue to function after a refresh.
const result: any = await provider.read(id, props);
resp.setId(result.id);
const resultProps = resultIncludingProvider(result.props, props);
resp.setProperties(structproto.Struct.fromJavaScript(resultProps));
} else {
// In the event of a missing read, simply return back the input state.
resp.setId(id);
resp.setProperties(req.getProperties());
}
callback(undefined, resp);
} catch (e) {
console.error(`${e}: ${e.stack}`);
callback(e, undefined);
}
}
async function updateRPC(call: any, callback: any): Promise<void> {
try {
const req: any = call.request;
const resp = new provproto.UpdateResponse();
const olds = req.getOlds().toJavaScript();
const news = req.getNews().toJavaScript();
let result: any = {};
const provider = getProvider(news);
if (provider.update) {
result = await provider.update(req.getId(), olds, news) || {};
}
const resultProps = resultIncludingProvider(result.outs, news);
resp.setProperties(structproto.Struct.fromJavaScript(resultProps));
callback(undefined, resp);
} catch (e) {
const response = grpcResponseFromError(e);
return callback(/*err:*/ response, /*value:*/ null, /*metadata:*/ response.metadata);
}
}
async function deleteRPC(call: any, callback: any): Promise<void> {
try {
const req: any = call.request;
const props: any = req.getProperties().toJavaScript();
const provider: any = await getProvider(props);
if (provider.delete) {
await provider.delete(req.getId(), props);
}
callback(undefined, new emptyproto.Empty());
} catch (e) {
console.error(`${e}: ${e.stack}`);
callback(e, undefined);
}
}
async function getPluginInfoRPC(call: any, callback: any): Promise<void> {
const resp: any = new plugproto.PluginInfo();
resp.setVersion(version);
callback(undefined, resp);
}
function getSchemaRPC(call: any, callback: any): void {
callback({
code: grpc.status.UNIMPLEMENTED,
details: "GetSchema is not implemented by the dynamic provider",
}, undefined);
}
function constructRPC(call: any, callback: any): void {
callback({
code: grpc.status.UNIMPLEMENTED,
details: "Construct is not implemented by the dynamic provider",
}, undefined);
}
function resultIncludingProvider(result: any, props: any): any {
return Object.assign(result || {}, {
[providerKey]: props[providerKey],
});
}
// grpcResponseFromError creates a gRPC response representing an error from a dynamic provider's
// resource. This is typically either a creation error, in which the API server has (virtually)
// rejected the resource, or an initialization error, where the API server has accepted the
// resource, but it failed to initialize (e.g., the app code is continually crashing and the
// resource has failed to become alive).
function grpcResponseFromError(e: {id: string, properties: any, message: string, reasons?: string[]}) {
// Create response object.
const resp = new statusproto.Status();
resp.setCode(grpc.status.UNKNOWN);
resp.setMessage(e.message);
const metadata = new grpc.Metadata();
if (e.id) {
// Object created successfully, but failed to initialize. Pack initialization failure into
// details.
const detail = new provproto.ErrorResourceInitFailed();
detail.setId(e.id);
detail.setProperties(structproto.Struct.fromJavaScript(e.properties || {}));
detail.setReasonsList(e.reasons || []);
const details = new anyproto.Any();
details.pack(detail.serializeBinary(), "pulumirpc.ErrorResourceInitFailed");
// Add details to metadata.
resp.addDetails(details);
// NOTE: `grpc-status-details-bin` is a magic field that allows us to send structured
// protobuf data as an error back through gRPC. This notion of details is a first-class in
// the Go gRPC implementation, and the nodejs implementation has not quite caught up to it,
// which is why it's cumbersome here.
metadata.add("grpc-status-details-bin", Buffer.from(resp.serializeBinary()));
}
return {
code: grpc.status.UNKNOWN,
message: e.message,
metadata: metadata,
};
}
/** @internal */
export async function main(args: string[]) {
// The program requires a single argument: the address of the RPC endpoint for the engine. It
// optionally also takes a second argument, a reference back to the engine, but this may be missing.
if (args.length === 0) {
console.error("fatal: Missing <engine> address");
process.exit(-1);
return;
}
const engineAddr: string = args[0];
// Finally connect up the gRPC client/server and listen for incoming requests.
const server = new grpc.Server({
"grpc.max_receive_message_length": maxRPCMessageSize,
});
server.addService(provrpc.ResourceProviderService, {
cancel: cancelRPC,
configure: configureRPC,
invoke: invokeRPC,
streamInvoke: streamInvokeRPC,
check: checkRPC,
checkConfig: checkConfigRPC,
diff: diffRPC,
diffConfig: diffConfigRPC,
create: createRPC,
read: readRPC,
update: updateRPC,
delete: deleteRPC,
getPluginInfo: getPluginInfoRPC,
getSchema: getSchemaRPC,
construct: constructRPC,
});
const port: number = await new Promise<number>((resolve, reject) => {
server.bindAsync(`0.0.0.0:0`, grpc.ServerCredentials.createInsecure(), (err, p) => {
if (err) {
reject(err);
} else {
resolve(p);
}
});
});
server.start();
// Emit the address so the monitor can read it to connect. The gRPC server will keep the message loop alive.
console.log(port);
}
main(process.argv.slice(2));

View file

@ -1,206 +0,0 @@
// Copyright 2016-2018, Pulumi Corporation.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Inputs } from "../output";
import * as resource from "../resource";
import * as runtime from "../runtime";
/**
* CheckResult represents the results of a call to `ResourceProvider.check`.
*/
export interface CheckResult {
/**
* The inputs to use, if any.
*/
readonly inputs?: any;
/**
* Any validation failures that occurred.
*/
readonly failures?: CheckFailure[];
}
/**
* CheckFailure represents a single failure in the results of a call to `ResourceProvider.check`
*/
export interface CheckFailure {
/**
* The property that failed validation.
*/
readonly property: string;
/**
* The reason that the property failed validation.
*/
readonly reason: string;
}
/**
* DiffResult represents the results of a call to `ResourceProvider.diff`.
*/
export interface DiffResult {
/**
* If true, this diff detected changes and suggests an update.
*/
readonly changes?: boolean;
/**
* If this update requires a replacement, the set of properties triggering it.
*/
readonly replaces?: string[];
/**
* An optional list of properties that will not ever change.
*/
readonly stables?: string[];
/**
* If true, and a replacement occurs, the resource will first be deleted before being recreated. This is to
* void potential side-by-side issues with the default create before delete behavior.
*/
readonly deleteBeforeReplace?: boolean;
}
/**
* CreateResult represents the results of a call to `ResourceProvider.create`.
*/
export interface CreateResult {
/**
* The ID of the created resource.
*/
readonly id: resource.ID;
/**
* Any properties that were computed during creation.
*/
readonly outs?: any;
}
export interface ReadResult {
/**
* The ID of the resource ready back (or blank if missing).
*/
readonly id?: resource.ID;
/**
* The current property state read from the live environment.
*/
readonly props?: any;
}
/**
* UpdateResult represents the results of a call to `ResourceProvider.update`.
*/
export interface UpdateResult {
/**
* Any properties that were computed during updating.
*/
readonly outs?: any;
}
/**
* ResourceProvider represents an object that provides CRUD operations for a particular type of resource.
*/
export interface ResourceProvider {
/**
* Check validates that the given property bag is valid for a resource of the given type.
*
* @param olds The old input properties to use for validation.
* @param news The new input properties to use for validation.
*/
check?: (olds: any, news: any) => Promise<CheckResult>;
/**
* Diff checks what impacts a hypothetical update will have on the resource's properties.
*
* @param id The ID of the resource to diff.
* @param olds The old values of properties to diff.
* @param news The new values of properties to diff.
*/
diff?: (id: resource.ID, olds: any, news: any) => Promise<DiffResult>;
/**
* Create allocates a new instance of the provided resource and returns its unique ID afterwards.
* If this call fails, the resource must not have been created (i.e., it is "transactional").
*
* @param inputs The properties to set during creation.
*/
create: (inputs: any) => Promise<CreateResult>;
/**
* Reads the current live state associated with a resource. Enough state must be included in the inputs to uniquely
* identify the resource; this is typically just the resource ID, but it may also include some properties.
*/
read?: (id: resource.ID, props?: any) => Promise<ReadResult>;
/**
* Update updates an existing resource with new values.
*
* @param id The ID of the resource to update.
* @param olds The old values of properties to update.
* @param news The new values of properties to update.
*/
update?: (id: resource.ID, olds: any, news: any) => Promise<UpdateResult>;
/**
* Delete tears down an existing resource with the given ID. If it fails, the resource is assumed to still exist.
*
* @param id The ID of the resource to delete.
* @param props The current properties on the resource.
*/
delete?: (id: resource.ID, props: any) => Promise<void>;
}
const providerCache = new WeakMap<ResourceProvider, Promise<string>>();
function serializeProvider(provider: ResourceProvider): Promise<string> {
let result: Promise<string>;
// caching is enabled by default as of 3.0
if (runtime.cacheDynamicProviders()) {
const cachedProvider = providerCache.get(provider);
if (cachedProvider) {
result = cachedProvider;
} else {
result = runtime.serializeFunction(() => provider).then(sf => sf.text);
providerCache.set(provider, result);
}
} else {
result = runtime.serializeFunction(() => provider).then(sf => sf.text);
}
return result;
}
/**
* Resource represents a Pulumi Resource that incorporates an inline implementation of the Resource's CRUD operations.
*/
export abstract class Resource extends resource.CustomResource {
/**
* Creates a new dynamic resource.
*
* @param provider The implementation of the resource's CRUD operations.
* @param name The name of the resource.
* @param props The arguments to use to populate the new resource. Must not define the reserved
* property "__provider".
* @param opts A bag of options that control this resource's behavior.
*/
constructor(provider: ResourceProvider, name: string, props: Inputs,
opts?: resource.CustomResourceOptions) {
const providerKey: string = "__provider";
if (props[providerKey]) {
throw new Error("A dynamic resource must not define the __provider key");
}
props[providerKey] = serializeProvider(provider);
super("pulumi-nodejs:dynamic:Resource", name, props, opts);
}
}

View file

@ -27,13 +27,12 @@ export * from "./stackReference";
// Export submodules individually.
import * as asset from "./asset";
import * as automation from "./automation";
import * as dynamic from "./dynamic";
import * as iterable from "./iterable";
import * as log from "./log";
import * as provider from "./provider";
import * as runtime from "./runtime";
import * as utils from "./utils";
export { asset, automation, dynamic, iterable, log, provider, runtime, utils };
export { asset, automation, iterable, log, provider, runtime, utils };
// @pulumi is a deployment-only module. If someone tries to capture it, and we fail for some reason
// we want to give a good message about what the problem likely is. Note that capturing a

File diff suppressed because it is too large Load diff

View file

@ -1,895 +0,0 @@
// Copyright 2016-2018, Pulumi Corporation.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import * as ts from "typescript";
import * as log from "../../log";
import * as utils from "./utils";
/** @internal */
export interface ParsedFunctionCode {
// The serialized code for the function, usable as an expression. Valid for all functions forms
// (functions, lambdas, methods, etc.).
funcExprWithoutName: string;
// The serialized code for the function, usable as an function-declaration. Valid only for
// non-lambda function forms.
funcExprWithName?: string;
// the name of the function if it was a function-declaration. This is needed so
// that we can include an entry in the environment mapping this function name to
// the actual function we generate for it. This is needed so that nested recursive calls
// to the function see the function we're generating.
functionDeclarationName?: string;
// Whether or not this was an arrow function.
isArrowFunction: boolean;
}
/** @internal */
export interface ParsedFunction extends ParsedFunctionCode {
// The set of variables the function attempts to capture.
capturedVariables: CapturedVariables;
// Whether or not the real 'this' (i.e. not a lexically captured this) is used in the function.
usesNonLexicalThis: boolean;
}
// Information about a captured property. Both the name and whether or not the property was
// invoked.
/** @internal */
export interface CapturedPropertyInfo {
name: string;
invoked: boolean;
}
// Information about a chain of captured properties. i.e. if you have "foo.bar.baz.quux()", we'll
// say that 'foo' was captured, but that "[bar, baz, quux]" was accessed off of it. We'll also note
// that 'quux' was invoked.
/** @internal */
export interface CapturedPropertyChain {
infos: CapturedPropertyInfo[];
}
// A mapping from the names of variables we captured, to information about how those variables were
// used. For example, if we see "a.b.c()" (and 'a' is not declared in the function), we'll record a
// mapping of { "a": ['b', 'c' (invoked)] }. i.e. we captured 'a', accessed the properties 'b.c'
// off of it, and we invoked that property access. With this information we can decide the totality
// of what we need to capture for 'a'.
//
// Note: if we want to capture everything, we just use an empty array for 'CapturedPropertyChain[]'.
// Otherwise, we'll use the chains to determine what portions of the object to serialize.
/** @internal */
export type CapturedVariableMap = Map<string, CapturedPropertyChain[]>;
// The set of variables the function attempts to capture. There is a required set an an optional
// set. The optional set will not block closure-serialization if we cannot find them, while the
// required set will. For each variable that is captured we also specify the list of properties of
// that variable we need to serialize. An empty-list means 'serialize all properties'.
/** @internal */
export interface CapturedVariables {
required: CapturedVariableMap;
optional: CapturedVariableMap;
}
// These are the special globals we've marked as ones we do not want to capture by value.
// These values have a dual meaning. They mean one thing at deployment time and one thing
// at cloud-execution time. By **not** capturing-by-value we take the view that the user
// wants the cloud-execution time view of things.
const nodeModuleGlobals: {[key: string]: boolean} = {
"__dirname": true,
"__filename": true,
// We definitely should not try to capture/serialize 'require'. Not only will it bottom
// out as a native function, but it is definitely something the user intends to run
// against the right module environment at cloud-execution time and not deployment time.
"require": true,
};
// Gets the text of the provided function (using .toString()) and massages it so that it is a legal
// function declaration. Note: this ties us heavily to V8 and its representation for functions. In
// particular, it has expectations around how functions/lambdas/methods/generators/constructors etc.
// are represented. If these change, this will likely break us.
/** @internal */
export function parseFunction(funcString: string): [string, ParsedFunction] {
const [error, functionCode] = parseFunctionCode(funcString);
if (error) {
return [error, <any>undefined];
}
// In practice it's not guaranteed that a function's toString is parsable by TypeScript.
// V8 intrinsics are prefixed with a '%' and TypeScript does not consider that to be a valid
// identifier.
const [parseError, file] = createSourceFile(functionCode);
if (parseError) {
return [parseError, <any>undefined];
}
const capturedVariables = computeCapturedVariableNames(file!);
// if we're looking at an arrow function, the it is always using lexical 'this's
// so we don't have to bother even examining it.
const usesNonLexicalThis = !functionCode.isArrowFunction && computeUsesNonLexicalThis(file!);
const result = <ParsedFunction>functionCode;
result.capturedVariables = capturedVariables;
result.usesNonLexicalThis = usesNonLexicalThis;
if (result.capturedVariables.required.has("this")) {
return [
"arrow function captured 'this'. Assign 'this' to another name outside function and capture that.",
result,
];
}
return ["", result];
}
function parseFunctionCode(funcString: string): [string, ParsedFunctionCode] {
if (funcString.startsWith("[Function:")) {
return [`the function form was not understood.`, <any>undefined];
}
// Split this constant out so that if this function *itself* is closure serialized,
// it will not be thought to be native code itself.
const nativeCodeString = "[native " + "code]";
if (funcString.indexOf(nativeCodeString) !== -1) {
return [`it was a native code function.`, <any>undefined];
}
// There are three general forms of node toString'ed Functions we're trying to find out here.
//
// 1. `[mods] (...) => ...
//
// i.e. an arrow function. We need to ensure that arrow-functions stay arrow-functions,
// and non-arrow-functions end up looking like normal `function` functions. This will make
// it so that we can correctly handle 'this' properly depending on if that should be
// treated as the lexical capture of 'this' or the non-lexical 'this'.
//
// 2. `class Foo { ... }`
//
// i.e. node uses the entire string of a class when toString'ing the constructor function
// for it.
//
// 3. `[mods] function ...
//
// i.e. a normal function (maybe async, maybe a get/set function, but def not an arrow
// function)
if (tryParseAsArrowFunction(funcString)) {
return ["", { funcExprWithoutName: funcString, isArrowFunction: true }];
}
// First check to see if this startsWith 'class'. If so, this is definitely a class. This
// works as Node does not place preceding comments on a class/function, allowing us to just
// directly see if we've started with the right text.
if (funcString.startsWith("class ")) {
// class constructor function. We want to get the actual constructor
// in the class definition (synthesizing an empty one if one does not)
// exist.
const [file, firstDiagnostic] = tryCreateSourceFile(funcString);
if (firstDiagnostic) {
return [`the class could not be parsed: ${firstDiagnostic}`, <any>undefined];
}
const classDecl = <ts.ClassDeclaration>file!.statements.find(x => ts.isClassDeclaration(x));
if (!classDecl) {
return [`the class form was not understood:\n${funcString}`, <any>undefined];
}
const constructor = <ts.ConstructorDeclaration>classDecl.members.find(m => ts.isConstructorDeclaration(m));
if (!constructor) {
// class without explicit constructor.
const isSubClass = classDecl.heritageClauses && classDecl.heritageClauses.some(
c => c.token === ts.SyntaxKind.ExtendsKeyword);
return isSubClass
? makeFunctionDeclaration("constructor() { super(); }", /*isAsync:*/ false, /*isFunctionDeclaration:*/ false)
: makeFunctionDeclaration("constructor() { }", /*isAsync:*/ false, /*isFunctionDeclaration:*/ false);
}
const constructorCode = funcString.substring(constructor.getStart(file, /*includeJsDocComment*/ false), constructor.end).trim();
return makeFunctionDeclaration(constructorCode, /*isAsync:*/ false, /*isFunctionDeclaration: */ false);
}
let isAsync = false;
if (funcString.startsWith("async ")) {
isAsync = true;
funcString = funcString.substr("async".length).trimLeft();
}
if (funcString.startsWith("function get ") || funcString.startsWith("function set ")) {
const trimmed = funcString.substr("function get".length);
return makeFunctionDeclaration(trimmed, isAsync, /*isFunctionDeclaration: */ false);
}
if (funcString.startsWith("get ") || funcString.startsWith("set ")) {
const trimmed = funcString.substr("get ".length);
return makeFunctionDeclaration(trimmed, isAsync, /*isFunctionDeclaration: */ false);
}
if (funcString.startsWith("function")) {
const trimmed = funcString.substr("function".length);
return makeFunctionDeclaration(trimmed, isAsync, /*isFunctionDeclaration: */ true);
}
// Add "function" (this will make methods parseable). i.e. "foo() { }" becomes
// "function foo() { }"
// this also does the right thing for functions with computed names.
return makeFunctionDeclaration(funcString, isAsync, /*isFunctionDeclaration: */ false);
}
function tryParseAsArrowFunction(toParse: string): boolean {
const [file] = tryCreateSourceFile(toParse);
if (!file || file.statements.length !== 1) {
return false;
}
const firstStatement = file.statements[0];
return ts.isExpressionStatement(firstStatement) &&
ts.isArrowFunction(firstStatement.expression);
}
function makeFunctionDeclaration(
v: string, isAsync: boolean, isFunctionDeclaration: boolean): [string, ParsedFunctionCode] {
let prefix = isAsync ? "async " : "";
prefix += "function ";
v = v.trimLeft();
if (v.startsWith("*")) {
v = v.substr(1).trimLeft();
prefix = "function* ";
}
const openParenIndex = v.indexOf("(");
if (openParenIndex < 0) {
return [`the function form was not understood.`, <any>undefined];
}
if (isComputed(v, openParenIndex)) {
v = v.substr(openParenIndex);
return ["", {
funcExprWithoutName: prefix + v,
funcExprWithName: prefix + "__computed" + v,
functionDeclarationName: undefined,
isArrowFunction: false,
}];
}
const nameChunk = v.substr(0, openParenIndex);
const funcName = utils.isLegalMemberName(nameChunk)
? utils.isLegalFunctionName(nameChunk) ? nameChunk : "/*" + nameChunk + "*/"
: "";
const commentedName = utils.isLegalMemberName(nameChunk) ? "/*" + nameChunk + "*/" : "";
v = v.substr(openParenIndex).trimLeft();
return ["", {
funcExprWithoutName: prefix + commentedName + v,
funcExprWithName: prefix + funcName + v,
functionDeclarationName: isFunctionDeclaration ? nameChunk : undefined,
isArrowFunction: false,
}];
}
function isComputed(v: string, openParenIndex: number) {
if (openParenIndex === 0) {
// node 8 and lower use no name at all for computed members.
return true;
}
if (v.length > 0 && v.charAt(0) === "[") {
// node 10 actually has the name as: [expr]
return true;
}
return false;
}
function createSourceFile(serializedFunction: ParsedFunctionCode): [string, ts.SourceFile | null] {
const funcstr = serializedFunction.funcExprWithName || serializedFunction.funcExprWithoutName;
// Wrap with parens to make into something parseable. This is necessary as many
// types of functions are valid function expressions, but not valid function
// declarations. i.e. "function () { }". This is not a valid function declaration
// (it's missing a name). But it's totally legal as "(function () { })".
const toParse = "(" + funcstr + ")";
const [file, firstDiagnostic] = tryCreateSourceFile(toParse);
if (firstDiagnostic) {
return [`the function could not be parsed: ${firstDiagnostic}`, null];
}
return ["", file!];
}
function tryCreateSourceFile(toParse: string): [ts.SourceFile | undefined, string | undefined] {
const file = ts.createSourceFile(
"", toParse, ts.ScriptTarget.Latest, /*setParentNodes:*/ true, ts.ScriptKind.TS);
const diagnostics: ts.Diagnostic[] = (<any>file).parseDiagnostics;
if (diagnostics.length) {
return [undefined, `${diagnostics[0].messageText}`];
}
return [file, undefined];
}
function computeUsesNonLexicalThis(file: ts.SourceFile): boolean {
let inTopmostFunction = false;
let usesNonLexicalThis = false;
ts.forEachChild(file, walk);
return usesNonLexicalThis;
function walk(node: ts.Node | undefined) {
if (!node) {
return;
}
switch (node.kind) {
case ts.SyntaxKind.SuperKeyword:
case ts.SyntaxKind.ThisKeyword:
usesNonLexicalThis = true;
break;
case ts.SyntaxKind.CallExpression:
return visitCallExpression(<ts.CallExpression>node);
case ts.SyntaxKind.MethodDeclaration:
case ts.SyntaxKind.FunctionDeclaration:
case ts.SyntaxKind.FunctionExpression:
return visitBaseFunction(<ts.FunctionLikeDeclarationBase>node);
// Note: it is intentional that we ignore ArrowFunction. If we use 'this' inside of it,
// then that should be considered a use of the non-lexical-this from an outer function.
// i.e.
// function f() { var v = () => console.log(this) }
//
// case ts.SyntaxKind.ArrowFunction:
default:
break;
}
ts.forEachChild(node, walk);
}
function visitBaseFunction(node: ts.FunctionLikeDeclarationBase): void {
if (inTopmostFunction) {
// we're already in the topmost function. No need to descend into any
// further functions.
return;
}
// Entering the topmost function.
inTopmostFunction = true;
// Now, visit its body to see if we use 'this/super'.
walk(node.body);
inTopmostFunction = false;
}
function visitCallExpression(node: ts.CallExpression) {
// Most call expressions are normal. But we must special case one kind of function:
// TypeScript's __awaiter functions. They are of the form `__awaiter(this, void 0, void 0,
// function* (){})`,
// The first 'this' argument is passed along in case the expression awaited uses 'this'.
// However, doing that can be very bad for us as in many cases the 'this' just refers to the
// surrounding module, and the awaited expression won't be using that 'this' at all.
walk(node.expression);
if (isAwaiterCall(node)) {
const lastFunction = <ts.FunctionExpression>node.arguments[3];
walk(lastFunction.body);
return;
}
// For normal calls, just walk all arguments normally.
for (const arg of node.arguments) {
walk(arg);
}
}
}
/**
* computeCapturedVariableNames computes the set of free variables in a given function string. Note that this string is
* expected to be the usual V8-serialized function expression text.
*/
function computeCapturedVariableNames(file: ts.SourceFile): CapturedVariables {
// Now that we've parsed the file, compute the free variables, and return them.
let required: CapturedVariableMap = new Map();
let optional: CapturedVariableMap = new Map();
const scopes: Set<string>[] = [];
let functionVars: Set<string> = new Set();
// Recurse through the tree. We use typescript's AST here and generally walk the entire
// tree. One subtlety to be aware of is that we generally assume that when we hit an
// identifier that it either introduces a new variable, or it lexically references a
// variable. This clearly doesn't make sense for *all* identifiers. For example, if you
// have "console.log" then "console" tries to lexically reference a variable, but "log" does
// not. So, to avoid that being an issue, we carefully decide when to recurse. For
// example, for member access expressions (i.e. A.B) we do not recurse down the right side.
ts.forEachChild(file, walk);
// Now just return all variables whose value is true. Filter out any that are part of the built-in
// Node.js global object, however, since those are implicitly availble on the other side of serialization.
const result: CapturedVariables = { required: new Map(), optional: new Map() };
for (const key of required.keys()) {
if (!isBuiltIn(key)) {
result.required.set(key, required.get(key)!.concat(
optional.has(key) ? optional.get(key)! : []));
}
}
for (const key of optional.keys()) {
if (!isBuiltIn(key) && !required.has(key)) {
result.optional.set(key, optional.get(key)!);
}
}
log.debug(`Found free variables: ${JSON.stringify(result)}`);
return result;
function isBuiltIn(ident: string): boolean {
// __awaiter and __rest are never considered built-in. We do this as async/await code will generate
// an __awaiter (so we will need it), but some libraries (like tslib) will add this to the 'global'
// object. The same is true for __rest when destructuring.
// If we think these are built-in, we won't serialize them, and the functions may not
// actually be available if the import that caused it to get attached isn't included in the
// final serialized code.
if (ident === "__awaiter" || ident === "__rest") {
return false;
}
// Anything in the global dictionary is a built-in. So is anything that's a global Node.js object;
// note that these only exist in the scope of modules, and so are not truly global in the usual sense.
// See https://nodejs.org/api/globals.html for more details.
return global.hasOwnProperty(ident) || nodeModuleGlobals[ident];
}
function currentScope(): Set<string> {
return scopes[scopes.length - 1];
}
function visitIdentifier(node: ts.Identifier): void {
// Remember undeclared identifiers during the walk, as they are possibly free.
const name = node.text;
for (let i = scopes.length - 1; i >= 0; i--) {
if (scopes[i].has(name)) {
// This is currently known in the scope chain, so do not add it as free.
return;
}
}
// We reached the top of the scope chain and this wasn't found; it's captured.
const capturedPropertyChain = determineCapturedPropertyChain(node);
if (node.parent!.kind === ts.SyntaxKind.TypeOfExpression) {
// "typeof undeclared_id" is legal in JS (and is actually used in libraries). So keep
// track that we would like to capture this variable, but mark that capture as optional
// so we will not throw if we aren't able to find it in scope.
optional.set(name, combineProperties(optional.get(name), capturedPropertyChain));
} else {
required.set(name, combineProperties(required.get(name), capturedPropertyChain));
}
}
function walk(node: ts.Node | undefined) {
if (!node) {
return;
}
switch (node.kind) {
case ts.SyntaxKind.Identifier:
return visitIdentifier(<ts.Identifier>node);
case ts.SyntaxKind.ThisKeyword:
return visitThisExpression(<ts.ThisExpression>node);
case ts.SyntaxKind.Block:
return visitBlockStatement(<ts.Block>node);
case ts.SyntaxKind.CallExpression:
return visitCallExpression(<ts.CallExpression>node);
case ts.SyntaxKind.CatchClause:
return visitCatchClause(<ts.CatchClause>node);
case ts.SyntaxKind.MethodDeclaration:
return visitMethodDeclaration(<ts.MethodDeclaration>node);
case ts.SyntaxKind.MetaProperty:
// don't walk down an es6 metaproperty (i.e. "new.target"). It doesn't
// capture anything.
return;
case ts.SyntaxKind.PropertyAssignment:
return visitPropertyAssignment(<ts.PropertyAssignment>node);
case ts.SyntaxKind.PropertyAccessExpression:
return visitPropertyAccessExpression(<ts.PropertyAccessExpression>node);
case ts.SyntaxKind.FunctionDeclaration:
case ts.SyntaxKind.FunctionExpression:
return visitFunctionDeclarationOrExpression(<ts.FunctionDeclaration>node);
case ts.SyntaxKind.ArrowFunction:
return visitBaseFunction(<ts.ArrowFunction>node, /*isArrowFunction:*/true, /*name:*/ undefined);
case ts.SyntaxKind.VariableDeclaration:
return visitVariableDeclaration(<ts.VariableDeclaration>node);
default:
break;
}
ts.forEachChild(node, walk);
}
function visitThisExpression(node: ts.ThisExpression): void {
required.set(
"this", combineProperties(required.get("this"), determineCapturedPropertyChain(node)));
}
function combineProperties(existing: CapturedPropertyChain[] | undefined,
current: CapturedPropertyChain | undefined) {
if (existing && existing.length === 0) {
// We already want to capture everything. Keep things that way.
return existing;
}
if (current === undefined) {
// We want to capture everything. So ignore any properties we've filtered down
// to and just capture them all.
return [];
}
// We want to capture a specific set of properties. Add this set of properties
// into the existing set.
const combined = existing || [];
combined.push(current);
return combined;
}
// Finds nodes of the form `(...expr...).PropName` or `(...expr...)["PropName"]`
// For element access expressions, the argument must be a string literal.
function isPropertyOrElementAccessExpression(node: ts.Node): node is (ts.PropertyAccessExpression | ts.ElementAccessExpression) {
if (ts.isPropertyAccessExpression(node)) {
return true;
}
if (ts.isElementAccessExpression(node) && ts.isStringLiteral(node.argumentExpression)) {
return true;
}
return false;
}
function determineCapturedPropertyChain(node: ts.Node): CapturedPropertyChain | undefined {
let infos: CapturedPropertyInfo[] | undefined;
// Walk up a sequence of property-access'es, recording the names we hit, until we hit
// something that isn't a property-access.
while (node &&
node.parent &&
isPropertyOrElementAccessExpression(node.parent) &&
node.parent.expression === node) {
if (!infos) {
infos = [];
}
const propOrElementAccess = node.parent;
const name = ts.isPropertyAccessExpression(propOrElementAccess)
? propOrElementAccess.name.text
: (<ts.StringLiteral>propOrElementAccess.argumentExpression).text;
const invoked = propOrElementAccess.parent !== undefined &&
ts.isCallExpression(propOrElementAccess.parent) &&
propOrElementAccess.parent.expression === propOrElementAccess;
// Keep track if this name was invoked. If so, we'll have to analyze it later
// to see if it captured 'this'
infos.push({ name, invoked });
node = propOrElementAccess;
}
if (infos) {
// Invariant checking.
if (infos.length === 0) {
throw new Error("How did we end up with an empty list?");
}
for (let i = 0; i < infos.length - 1; i++) {
if (infos[i].invoked) {
throw new Error("Only the last item in the dotted chain is allowed to be invoked.");
}
}
return { infos };
}
// For all other cases, capture everything.
return undefined;
}
function visitBlockStatement(node: ts.Block): void {
// Push new scope, visit all block statements, and then restore the scope.
scopes.push(new Set());
ts.forEachChild(node, walk);
scopes.pop();
}
function visitFunctionDeclarationOrExpression(
node: ts.FunctionDeclaration | ts.FunctionExpression): void {
// A function declaration is special in one way: its identifier is added to the current function's
// var-style variables, so that its name is in scope no matter the order of surrounding references to it.
if (node.name) {
functionVars.add(node.name.text);
}
visitBaseFunction(node, /*isArrowFunction:*/false, node.name);
}
function visitBaseFunction(
node: ts.FunctionLikeDeclarationBase,
isArrowFunction: boolean,
functionName: ts.Identifier | undefined): void {
// First, push new free vars list, scope, and function vars
const savedRequired = required;
const savedOptional = optional;
const savedFunctionVars = functionVars;
required = new Map();
optional = new Map();
functionVars = new Set();
scopes.push(new Set());
// If this is a named function, it's name is in scope at the top level of itself.
if (functionName) {
functionVars.add(functionName.text);
}
// this/arguments are in scope inside any non-arrow function.
if (!isArrowFunction) {
functionVars.add("this");
functionVars.add("arguments");
}
// The parameters of any function are in scope at the top level of the function.
for (const param of node.parameters) {
nameWalk(param.name, /*isVar:*/ true);
// Parse default argument expressions
if (param.initializer) {
walk(param.initializer);
}
}
// Next, visit the body underneath this new context.
walk(node.body);
// Remove any function-scoped variables that we encountered during the walk.
for (const v of functionVars) {
required.delete(v);
optional.delete(v);
}
// Restore the prior context and merge our free list with the previous one.
scopes.pop();
mergeMaps(savedRequired, required);
mergeMaps(savedOptional, optional);
functionVars = savedFunctionVars;
required = savedRequired;
optional = savedOptional;
}
function mergeMaps(target: CapturedVariableMap, source: CapturedVariableMap) {
for (const key of source.keys()) {
const sourcePropInfos = source.get(key)!;
let targetPropInfos = target.get(key)!;
if (sourcePropInfos.length === 0) {
// we want to capture everything. Make sure that's reflected in the target.
targetPropInfos = [];
}
else {
// we want to capture a subet of properties. merge that subset into whatever
// subset we've recorded so far.
for (const sourceInfo of sourcePropInfos) {
targetPropInfos = combineProperties(targetPropInfos, sourceInfo);
}
}
target.set(key, targetPropInfos);
}
}
function visitCatchClause(node: ts.CatchClause): void {
scopes.push(new Set());
// Add the catch pattern to the scope as a variable. Note that it is scoped to our current
// fresh scope (so it can't be seen by the rest of the function).
if (node.variableDeclaration) {
nameWalk(node.variableDeclaration.name, /*isVar:*/ false);
}
// And then visit the block without adding them as free variables.
walk(node.block);
// Relinquish the scope so the error patterns aren't available beyond the catch.
scopes.pop();
}
function visitCallExpression(node: ts.CallExpression): void {
// Most call expressions are normal. But we must special case one kind of function:
// TypeScript's __awaiter functions. They are of the form `__awaiter(this, void 0, void 0, function* (){})`,
// The first 'this' argument is passed along in case the expression awaited uses 'this'.
// However, doing that can be very bad for us as in many cases the 'this' just refers to the
// surrounding module, and the awaited expression won't be using that 'this' at all.
//
// However, there are cases where 'this' may be legitimately lexically used in the awaited
// expression and should be captured properly. We'll figure this out by actually descending
// explicitly into the "function*(){}" argument, asking it to be treated as if it was
// actually a lambda and not a JS function (with the standard js 'this' semantics). By
// doing this, if 'this' is used inside the function* we'll act as if it's a real lexical
// capture so that we pass 'this' along.
walk(node.expression);
if (isAwaiterCall(node)) {
return visitBaseFunction(
<ts.FunctionLikeDeclarationBase><ts.FunctionExpression>node.arguments[3],
/*isArrowFunction*/ true,
/*name*/ undefined);
}
// For normal calls, just walk all arguments normally.
for (const arg of node.arguments) {
walk(arg);
}
}
function visitMethodDeclaration(node: ts.MethodDeclaration): void {
if (ts.isComputedPropertyName(node.name)) {
// Don't walk down the 'name' part of the property assignment if it is an identifier. It
// does not capture any variables. However, if it is a computed property name, walk it
// as it may capture variables.
walk(node.name);
}
// Always walk the method. Pass 'undefined' for the name as a method's name is not in scope
// inside itself.
visitBaseFunction(node, /*isArrowFunction:*/ false, /*name:*/ undefined);
}
function visitPropertyAssignment(node: ts.PropertyAssignment): void {
if (ts.isComputedPropertyName(node.name)) {
// Don't walk down the 'name' part of the property assignment if it is an identifier. It
// is not capturing any variables. However, if it is a computed property name, walk it
// as it may capture variables.
walk(node.name);
}
// Always walk the property initializer.
walk(node.initializer);
}
function visitPropertyAccessExpression(node: ts.PropertyAccessExpression): void {
// Don't walk down the 'name' part of the property access. It could not capture a free variable.
// i.e. if you have "A.B", we should analyze the "A" part and not the "B" part.
walk(node.expression);
}
function nameWalk(n: ts.BindingName | undefined, isVar: boolean): void {
if (!n) {
return;
}
switch (n.kind) {
case ts.SyntaxKind.Identifier:
return visitVariableDeclarationIdentifier(<ts.Identifier>n, isVar);
case ts.SyntaxKind.ObjectBindingPattern:
case ts.SyntaxKind.ArrayBindingPattern:
const bindingPattern = <ts.BindingPattern>n;
for (const element of bindingPattern.elements) {
if (ts.isBindingElement(element)) {
visitBindingElement(element, isVar);
}
}
return;
default:
return;
}
}
function visitVariableDeclaration(node: ts.VariableDeclaration): void {
// tslint:disable-next-line:max-line-length
const isLet = node.parent !== undefined && ts.isVariableDeclarationList(node.parent) && (node.parent.flags & ts.NodeFlags.Let) !== 0;
// tslint:disable-next-line:max-line-length
const isConst = node.parent !== undefined && ts.isVariableDeclarationList(node.parent) && (node.parent.flags & ts.NodeFlags.Const) !== 0;
const isVar = !isLet && !isConst;
// Walk the declaration's `name` property (which may be an Identifier or Pattern) placing
// any variables we encounter into the right scope.
nameWalk(node.name, isVar);
// Also walk into the variable initializer with the original walker to make sure we see any
// captures on the right hand side.
walk(node.initializer);
}
function visitVariableDeclarationIdentifier(node: ts.Identifier, isVar: boolean): void {
// If the declaration is an identifier, it isn't a free variable, for whatever scope it
// pertains to (function-wide for var and scope-wide for let/const). Track it so we can
// remove any subseqeunt references to that variable, so we know it isn't free.
if (isVar) {
functionVars.add(node.text);
} else {
currentScope().add(node.text);
}
}
function visitBindingElement(node: ts.BindingElement, isVar: boolean): void {
// array and object patterns can be quite complex. You can have:
//
// var {t} = val; // lookup a property in 'val' called 't' and place into a variable 't'.
// var {t: m} = val; // lookup a property in 'val' called 't' and place into a variable 'm'.
// var {t: <pat>} = val; // lookup a property in 'val' called 't' and decompose further into the pattern.
//
// And, for all of the above, you can have:
//
// var {t = def} = val;
// var {t: m = def} = val;
// var {t: <pat> = def} = val;
//
// These are the same as the above, except that if there is no property 't' in 'val',
// then the default value will be used.
//
// You can also have at the end of the literal: { ...rest}
// Walk the name portion, looking for names to add. for
//
// var {t} // this will be 't'.
//
// for
//
// var {t: m} // this will be 'm'
//
// and for
//
// var {t: <pat>} // this will recurse into the pattern.
//
// and for
//
// ...rest // this will be 'rest'
nameWalk(node.name, isVar);
// if there is a default value, walk it as well, looking for captures.
walk(node.initializer);
// importantly, we do not walk into node.propertyName
// This Name defines what property will be retrieved from the value being pattern
// matched against. Importantly, it does not define a new name put into scope,
// nor does it reference a variable in scope.
}
}
function isAwaiterCall(node: ts.CallExpression) {
const result =
ts.isIdentifier(node.expression) &&
node.expression.text === "__awaiter" &&
node.arguments.length === 4 &&
node.arguments[0].kind === ts.SyntaxKind.ThisKeyword &&
ts.isFunctionLike(node.arguments[3]);
return result;
}

View file

@ -1,121 +0,0 @@
// Copyright 2016-2018, Pulumi Corporation.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import * as ts from "typescript";
import * as utils from "./utils";
/** @internal */
export function rewriteSuperReferences(code: string, isStatic: boolean): string {
const sourceFile = ts.createSourceFile(
"", code, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
// Transform any usages of "super(...)" into "__super.call(this, ...)", any
// instance usages of "super.xxx" into "__super.prototype.xxx" and any static
// usages of "super.xxx" into "__super.xxx"
const transformed = ts.transform(sourceFile, [rewriteSuperCallsWorker]);
const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
const output = printer.printNode(ts.EmitHint.Unspecified, transformed.transformed[0], sourceFile).trim();
return output;
function rewriteSuperCallsWorker(transformationContext: ts.TransformationContext) {
const newNodes = new Set<ts.Node>();
let firstFunctionDeclaration = true;
function visitor(node: ts.Node): ts.Node {
// Convert the top level function so it doesn't have a name. We want to convert the user
// function to an anonymous function so that interior references to the same function
// bind properly. i.e. if we start with "function f() { f(); }" then this gets converted to
//
// function __f() {
// with ({ f: __f }) {
// return /*f*/() { f(); }
//
// This means the inner call properly binds to the *outer* function we create.
if (firstFunctionDeclaration && ts.isFunctionDeclaration(node)) {
firstFunctionDeclaration = false;
const funcDecl = ts.visitEachChild(node, visitor, transformationContext);
const text = utils.isLegalMemberName(funcDecl.name!.text)
? "/*" + funcDecl.name!.text + "*/" : "";
return ts.updateFunctionDeclaration(
funcDecl,
funcDecl.decorators,
funcDecl.modifiers,
funcDecl.asteriskToken,
ts.createIdentifier(text),
funcDecl.typeParameters,
funcDecl.parameters,
funcDecl.type,
funcDecl.body);
}
if (node.kind === ts.SyntaxKind.SuperKeyword) {
const newNode = ts.createIdentifier("__super");
newNodes.add(newNode);
return newNode;
}
else if (ts.isPropertyAccessExpression(node) &&
node.expression.kind === ts.SyntaxKind.SuperKeyword) {
const expr = isStatic
? ts.createIdentifier("__super")
: ts.createPropertyAccess(ts.createIdentifier("__super"), "prototype");
const newNode = ts.updatePropertyAccess(node, expr, node.name);
newNodes.add(newNode);
return newNode;
}
else if (ts.isElementAccessExpression(node) &&
node.argumentExpression &&
node.expression.kind === ts.SyntaxKind.SuperKeyword) {
const expr = isStatic
? ts.createIdentifier("__super")
: ts.createPropertyAccess(ts.createIdentifier("__super"), "prototype");
const newNode = ts.updateElementAccess(
node, expr, node.argumentExpression);
newNodes.add(newNode);
return newNode;
}
// for all other nodes, recurse first (so we update any usages of 'super')
// below them
const rewritten = ts.visitEachChild(node, visitor, transformationContext);
if (ts.isCallExpression(rewritten) &&
newNodes.has(rewritten.expression)) {
// this was a call to super() or super.x() or super["x"]();
// the super will already have been transformed to __super or
// __super.prototype.x or __super.prototype["x"].
//
// to that, we have to add the .call(this, ...) call.
const argumentsCopy = rewritten.arguments.slice();
argumentsCopy.unshift(ts.createThis());
return ts.updateCall(
rewritten,
ts.createPropertyAccess(rewritten.expression, "call"),
rewritten.typeArguments,
argumentsCopy);
}
return rewritten;
}
return (node: ts.Node) => ts.visitNode(node, visitor);
}
}

View file

@ -1,530 +0,0 @@
// Copyright 2016-2018, Pulumi Corporation.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { log } from "../..";
import { Resource } from "../../resource";
import * as closure from "./createClosure";
import * as utils from "./utils";
/**
* SerializeFunctionArgs are arguments used to serialize a JavaScript function
*/
export interface SerializeFunctionArgs {
/**
* The name to export from the module defined by the generated module text. Defaults to 'handler'.
*/
exportName?: string;
/**
* A function to prevent serialization of certain objects captured during the serialization. Primarily used to
* prevent potential cycles.
*/
serialize?: (o: any) => boolean;
/**
* If this is a function which, when invoked, will produce the actual entrypoint function.
* Useful for when serializing a function that has high startup cost that only wants to be
* run once. The signature of this function should be: () => (provider_handler_args...) => provider_result
*
* This will then be emitted as: `exports.[exportName] = serialized_func_name();`
*
* In other words, the function will be invoked (once) and the resulting inner function will
* be what is exported.
*/
isFactoryFunction?: boolean;
/**
* The resource to log any errors we encounter against.
*/
logResource?: Resource;
/**
* If true, allow secrets to be serialized into the function. This should only be set to true if the calling
* code will handle this and propoerly wrap the resulting text in a Secret before passing it into any Resources
* or serializing it to any other output format. If set, the `containsSecrets` property on the returned
* SerializedFunction object will indicate whether secrets were serialized into the function text.
*/
allowSecrets?: boolean;
}
/**
* SerializeFunction is a representation of a serialized JavaScript function.
*/
export interface SerializedFunction {
/**
* The text of a JavaScript module which exports a single name bound to an appropriate value.
* In the case of a normal function, this value will just be serialized function. In the case
* of a factory function this value will be the result of invoking the factory function.
*/
text: string;
/**
* The name of the exported module member.
*/
exportName: string;
/**
* True if the serialized function text includes serialization of secret
*/
containsSecrets: boolean;
}
/**
* serializeFunction serializes a JavaScript function into a text form that can be loaded in another execution context,
* for example as part of a function callback associated with an AWS Lambda. The function serialization captures any
* variables captured by the function body and serializes those values into the generated text along with the function
* body. This process is recursive, so that functions referenced by the body of the serialized function will themselves
* be serialized as well. This process also deeply serializes captured object values, including prototype chains and
* property descriptors, such that the semantics of the function when deserialized should match the original function.
*
* There are several known limitations:
* - If a native function is captured either directly or indirectly, closure serialization will return an error.
* - Captured values will be serialized based on their values at the time that `serializeFunction` is called. Mutations
* to these values after that (but before the deserialized function is used) will not be observed by the deserialized
* function.
*
* @param func The JavaScript function to serialize.
* @param args Arguments to use to control the serialization of the JavaScript function.
*/
export async function serializeFunction(
func: Function,
args: SerializeFunctionArgs = {}): Promise<SerializedFunction> {
const exportName = args.exportName || "handler";
const serialize = args.serialize || (_ => true);
const isFactoryFunction = args.isFactoryFunction === undefined ? false : args.isFactoryFunction;
const closureInfo = await closure.createClosureInfoAsync(func, serialize, args.logResource);
if (!args.allowSecrets && closureInfo.containsSecrets) {
throw new Error("Secret outputs cannot be captured by a closure.");
}
return serializeJavaScriptText(closureInfo, exportName, isFactoryFunction);
}
/**
* @deprecated Please use 'serializeFunction' instead.
*/
export async function serializeFunctionAsync(
func: Function,
serialize?: (o: any) => boolean): Promise<string> {
log.warn("'function serializeFunctionAsync' is deprecated. Please use 'serializeFunction' instead.");
serialize = serialize || (_ => true);
const closureInfo = await closure.createClosureInfoAsync(func, serialize, /*logResource:*/ undefined);
if (closureInfo.containsSecrets) {
throw new Error("Secret outputs cannot be captured by a closure.");
}
return serializeJavaScriptText(closureInfo, "handler", /*isFactoryFunction*/ false).text;
}
/**
* serializeJavaScriptText converts a FunctionInfo object into a string representation of a Node.js module body which
* exposes a single function `exports.handler` representing the serialized function.
*
* @param c The FunctionInfo to be serialized into a module string.
*/
function serializeJavaScriptText(
outerClosure: closure.ClosureInfo,
exportName: string,
isFactoryFunction: boolean): SerializedFunction {
// Now produce a textual representation of the closure and its serialized captured environment.
// State used to build up the environment variables for all the funcs we generate.
// In general, we try to create idiomatic code to make the generated code not too
// hideous. For example, we will try to generate code like:
//
// var __e1 = [1, 2, 3] // or
// var __e2 = { a: 1, b: 2, c: 3 }
//
// However, for non-common cases (i.e. sparse arrays, objects with configured properties,
// etc. etc.) we will spit things out in a much more verbose fashion that eschews
// prettyness for correct semantics.
const envEntryToEnvVar = new Map<closure.Entry, string>();
const envVarNames = new Set<string>();
const functionInfoToEnvVar = new Map<closure.FunctionInfo, string>();
let environmentText = "";
let functionText = "";
const outerFunctionName = emitFunctionAndGetName(outerClosure.func);
if (environmentText) {
environmentText = "\n" + environmentText;
}
// Export the appropriate value. For a normal function, this will just be exporting the name of
// the module function we created by serializing it. For a factory function this will export
// the function produced by invoking the factory function once.
let text: string;
const exportText = `exports.${exportName} = ${outerFunctionName}${isFactoryFunction ? "()" : ""};`;
if (isFactoryFunction) {
// for a factory function, we need to call the function at the end. That way all the logic
// to set up the environment has run.
text = environmentText + functionText + "\n" + exportText;
}
else {
text = exportText + "\n" + environmentText + functionText;
}
return { text, exportName, containsSecrets: outerClosure.containsSecrets };
function emitFunctionAndGetName(functionInfo: closure.FunctionInfo): string {
// If this is the first time seeing this function, then actually emit the function code for
// it. Otherwise, just return the name of the emitted function for anyone that wants to
// reference it from their own code.
let functionName = functionInfoToEnvVar.get(functionInfo);
if (!functionName) {
functionName = functionInfo.name
? createEnvVarName(functionInfo.name, /*addIndexAtEnd:*/ false)
: createEnvVarName("f", /*addIndexAtEnd:*/ true);
functionInfoToEnvVar.set(functionInfo, functionName);
emitFunctionWorker(functionInfo, functionName);
}
return functionName;
}
function emitFunctionWorker(functionInfo: closure.FunctionInfo, varName: string) {
const capturedValues = envFromEnvObj(functionInfo.capturedValues);
const thisCapture = capturedValues.this;
const argumentsCapture = capturedValues.arguments;
delete capturedValues.this;
delete capturedValues.arguments;
const parameters = [...Array(functionInfo.paramCount)].map((_, index) => `__${index}`).join(", ");
functionText += "\n" +
"function " + varName + "(" + parameters + ") {\n" +
" return (function() {\n" +
" with(" + envObjToString(capturedValues) + ") {\n\n" +
"return " + functionInfo.code + ";\n\n" +
" }\n" +
" }).apply(" + thisCapture + ", " + argumentsCapture + ").apply(this, arguments);\n" +
"}\n";
// If this function is complex (i.e. non-default __proto__, or has properties, etc.)
// then emit those as well.
emitComplexObjectProperties(varName, varName, functionInfo);
if (functionInfo.proto !== undefined) {
const protoVar = envEntryToString(functionInfo.proto, `${varName}_proto`);
environmentText += `Object.setPrototypeOf(${varName}, ${protoVar});\n`;
}
}
function envFromEnvObj(env: closure.PropertyMap): Record<string, string> {
const envObj: Record<string, string> = {};
for (const [keyEntry, { entry: valEntry }] of env) {
if (typeof keyEntry.json !== "string") {
throw new Error("PropertyMap key was not a string.");
}
const key = keyEntry.json;
const val = envEntryToString(valEntry, key);
envObj[key] = val;
}
return envObj;
}
function envEntryToString(envEntry: closure.Entry, varName: string): string {
const envVar = envEntryToEnvVar.get(envEntry);
if (envVar !== undefined) {
return envVar;
}
// Complex objects may also be referenced from multiple functions. As such, we have to
// create variables for them in the environment so that all references to them unify to the
// same reference to the env variable. Effectively, we need to do this for any object that
// could be compared for reference-identity. Basic types (strings, numbers, etc.) have
// value semantics and this can be emitted directly into the code where they are used as
// there is no way to observe that you are getting a different copy.
if (isObjOrArrayOrRegExp(envEntry)) {
return complexEnvEntryToString(envEntry, varName);
}
else {
// Other values (like strings, bools, etc.) can just be emitted inline.
return simpleEnvEntryToString(envEntry, varName);
}
}
function simpleEnvEntryToString(
envEntry: closure.Entry, varName: string): string {
if (envEntry.hasOwnProperty("json")) {
return JSON.stringify(envEntry.json);
}
else if (envEntry.function !== undefined) {
return emitFunctionAndGetName(envEntry.function);
}
else if (envEntry.module !== undefined) {
return `require("${envEntry.module}")`;
}
else if (envEntry.output !== undefined) {
return envEntryToString(envEntry.output, varName);
}
else if (envEntry.expr) {
// Entry specifies exactly how it should be emitted. So just use whatever
// it wanted.
return envEntry.expr;
}
else if (envEntry.promise) {
return `Promise.resolve(${envEntryToString(envEntry.promise, varName)})`;
}
else {
throw new Error("Malformed: " + JSON.stringify(envEntry));
}
}
function complexEnvEntryToString(
envEntry: closure.Entry, varName: string): string {
// Call all environment variables __e<num> to make them unique. But suffix
// them with the original name of the property to help provide context when
// looking at the source.
const envVar = createEnvVarName(varName, /*addIndexAtEnd:*/ false);
envEntryToEnvVar.set(envEntry, envVar);
if (envEntry.object) {
emitObject(envVar, envEntry.object, varName);
}
else if (envEntry.array) {
emitArray(envVar, envEntry.array, varName);
}
else if (envEntry.regexp) {
const { source, flags } = envEntry.regexp;
const regexVal = `new RegExp(${JSON.stringify(source)}, ${JSON.stringify(flags)})`;
const entryString = `var ${envVar} = ${regexVal};\n`;
environmentText += entryString;
}
return envVar;
}
function createEnvVarName(baseName: string, addIndexAtEnd: boolean): string {
const trimLeadingUnderscoreRegex = /^_*/g;
const legalName = makeLegalJSName(baseName).replace(trimLeadingUnderscoreRegex, "");
let index = 0;
let currentName = addIndexAtEnd
? "__" + legalName + index
: "__" + legalName;
while (envVarNames.has(currentName)) {
currentName = addIndexAtEnd
? "__" + legalName + index
: "__" + index + "_" + legalName;
index++;
}
envVarNames.add(currentName);
return currentName;
}
function emitObject(envVar: string, obj: closure.ObjectInfo, varName: string): void {
const complex = isComplex(obj);
if (complex) {
// we have a complex child. Because of the possibility of recursion in
// the object graph, we have to spit out this variable uninitialized first.
// Then we can walk our children, creating a single assignment per child.
// This way, if the child ends up referencing us, we'll have already emitted
// the **initialized** variable for them to reference.
if (obj.proto) {
const protoVar = envEntryToString(obj.proto, `${varName}_proto`);
environmentText += `var ${envVar} = Object.create(${protoVar});\n`;
}
else {
environmentText += `var ${envVar} = {};\n`;
}
emitComplexObjectProperties(envVar, varName, obj);
}
else {
// All values inside this obj are simple. We can just emit the object
// directly as an object literal with all children embedded in the literal.
const props: string[] = [];
for (const [keyEntry, { entry: valEntry }] of obj.env) {
const keyName = typeof keyEntry.json === "string" ? keyEntry.json : "sym";
const propName = envEntryToString(keyEntry, keyName);
const propVal = simpleEnvEntryToString(valEntry, keyName);
if (typeof keyEntry.json === "string" && utils.isLegalMemberName(keyEntry.json)) {
props.push(`${keyEntry.json}: ${propVal}`);
}
else {
props.push(`[${propName}]: ${propVal}`);
}
}
const allProps = props.join(", ");
const entryString = `var ${envVar} = {${allProps}};\n`;
environmentText += entryString;
}
function isComplex(o: closure.ObjectInfo) {
if (obj.proto !== undefined) {
return true;
}
for (const v of o.env.values()) {
if (entryIsComplex(v)) {
return true;
}
}
return false;
}
function entryIsComplex(v: closure.PropertyInfoAndValue) {
return !isSimplePropertyInfo(v.info) || deepContainsObjOrArrayOrRegExp(v.entry);
}
}
function isSimplePropertyInfo(info: closure.PropertyInfo | undefined): boolean {
if (!info) {
return true;
}
return info.enumerable === true &&
info.writable === true &&
info.configurable === true &&
!info.get && !info.set;
}
function emitComplexObjectProperties(
envVar: string, varName: string, objEntry: closure.ObjectInfo): void {
for (const [keyEntry, { info, entry: valEntry }] of objEntry.env) {
const subName = typeof keyEntry.json === "string" ? keyEntry.json : "sym";
const keyString = envEntryToString(keyEntry, varName + "_" + subName);
const valString = envEntryToString(valEntry, varName + "_" + subName);
if (isSimplePropertyInfo(info)) {
// normal property. Just emit simply as a direct assignment.
if (typeof keyEntry.json === "string" && utils.isLegalMemberName(keyEntry.json)) {
environmentText += `${envVar}.${keyEntry.json} = ${valString};\n`;
}
else {
environmentText += `${envVar}${`[${keyString}]`} = ${valString};\n`;
}
}
else {
// complex property. emit as Object.defineProperty
emitDefineProperty(info!, valString, keyString);
}
}
function emitDefineProperty(
desc: closure.PropertyInfo, entryValue: string, propName: string) {
const copy: any = {};
if (desc.configurable) {
copy.configurable = desc.configurable;
}
if (desc.enumerable) {
copy.enumerable = desc.enumerable;
}
if (desc.writable) {
copy.writable = desc.writable;
}
if (desc.get) {
copy.get = envEntryToString(desc.get, `${varName}_get`);
}
if (desc.set) {
copy.set = envEntryToString(desc.set, `${varName}_set`);
}
if (desc.hasValue) {
copy.value = entryValue;
}
const line = `Object.defineProperty(${envVar}, ${propName}, ${ envObjToString(copy) });\n`;
environmentText += line;
}
}
function emitArray(
envVar: string, arr: closure.Entry[], varName: string): void {
if (arr.some(deepContainsObjOrArrayOrRegExp) || isSparse(arr) || hasNonNumericIndices(arr)) {
// we have a complex child. Because of the possibility of recursion in the object
// graph, we have to spit out this variable initialized (but empty) first. Then we can
// walk our children, knowing we'll be able to find this variable if they reference it.
environmentText += `var ${envVar} = [];\n`;
// Walk the names of the array properties directly. This ensures we work efficiently
// with sparse arrays. i.e. if the array has length 1k, but only has one value in it
// set, we can just set htat value, instead of setting 999 undefineds.
let length = 0;
for (const key of Object.getOwnPropertyNames(arr)) {
if (key !== "length") {
const entryString = envEntryToString(arr[<any>key], `${varName}_${key}`);
environmentText += `${envVar}${
isNumeric(key) ? `[${key}]` : `.${key}`} = ${entryString};\n`;
length++;
}
}
}
else {
// All values inside this array are simple. We can just emit the array elements in
// place. i.e. we can emit as ``var arr = [1, 2, 3]`` as that's far more preferred than
// having four individual statements to do the same.
const strings: string[] = [];
for (let i = 0, n = arr.length; i < n; i++) {
strings.push(simpleEnvEntryToString(arr[i], `${varName}_${i}`));
}
const entryString = `var ${envVar} = [${strings.join(", ")}];\n`;
environmentText += entryString;
}
}
}
(<any>serializeJavaScriptText).doNotCapture = true;
const makeLegalRegex = /[^0-9a-zA-Z_]/g;
function makeLegalJSName(n: string) {
return n.replace(makeLegalRegex, x => "");
}
function isSparse<T>(arr: Array<T>) {
// getOwnPropertyNames for an array returns all the indices as well as 'length'.
// so we subtract one to get all the real indices. If that's not the same as
// the array length, then we must have missing properties and are thus sparse.
return arr.length !== (Object.getOwnPropertyNames(arr).length - 1);
}
function hasNonNumericIndices<T>(arr: Array<T>) {
return Object.keys(arr).some(k => k !== "length" && !isNumeric(k));
}
function isNumeric(n: string) {
return !isNaN(parseFloat(n)) && isFinite(+n);
}
function isObjOrArrayOrRegExp(env: closure.Entry): boolean {
return env.object !== undefined || env.array !== undefined || env.regexp !== undefined;
}
function deepContainsObjOrArrayOrRegExp(env: closure.Entry): boolean {
return isObjOrArrayOrRegExp(env) ||
(env.output !== undefined && deepContainsObjOrArrayOrRegExp(env.output)) ||
(env.promise !== undefined && deepContainsObjOrArrayOrRegExp(env.promise));
}
/**
* Converts an environment object into a string which can be embedded into a serialized function
* body. Note that this is not JSON serialization, as we may have property values which are
* variable references to other global functions. In other words, there can be free variables in the
* resulting object literal.
*
* @param envObj The environment object to convert to a string.
*/
function envObjToString(envObj: Record<string, string>): string {
return `{ ${Object.keys(envObj).map(k => `${k}: ${envObj[k]}`).join(", ")} }`;
}

View file

@ -1,39 +0,0 @@
// Copyright 2016-2018, Pulumi Corporation.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import * as ts from "typescript";
const legalNameRegex = /^[a-zA-Z_][0-9a-zA-Z_]*$/;
/** @internal */
export function isLegalMemberName(n: string) {
return legalNameRegex.test(n);
}
/** @internal */
export function isLegalFunctionName(n: string) {
if (!isLegalMemberName(n)) {
return false;
}
const scanner = ts.createScanner(
ts.ScriptTarget.Latest, /*skipTrivia:*/false, ts.LanguageVariant.Standard, n);
const tokenKind = scanner.scan();
if (tokenKind !== ts.SyntaxKind.Identifier &&
tokenKind !== ts.SyntaxKind.ConstructorKeyword) {
return false;
}
return true;
}

View file

@ -12,13 +12,6 @@
// See the License for the specific language governing permissions and
// limitations under the License.
export {
serializeFunctionAsync,
serializeFunction,
SerializedFunction,
SerializeFunctionArgs,
} from "./closure/serializeClosure";
export { CodePathOptions, computeCodePaths } from "./closure/codePaths";
export { leakedPromises } from "./debuggable";
export { Mocks, setMocks, MockResourceArgs, MockCallArgs } from "./mocks";

File diff suppressed because it is too large Load diff