Compare commits
1 commit
master
...
evan/remov
Author | SHA1 | Date | |
---|---|---|---|
5310ecbdfc |
|
@ -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));
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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
|
@ -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;
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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(", ")} }`;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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
Loading…
Reference in a new issue