Support module capture without serialization (#375)
This change adds first class support for capturing objects which are references to loaded Node modules. If an object to be serialized is found as a loaded module which can be referenced as `require(<name>)`, then is is not serialized and is passed as a new kind of environment entry - `module` which will be de-serialized as a `require` statement. Supports three cases: 1. built-in modules such as `http` and `path` 2. dependencies in the `node_modules` folder 3. other user-defined modules in the source folder This allows natural use of `import`s with "inside" code. For example - note the use of `$` in the outside scope only on the "inside". ```typescript import * as cloud from "@pulumi/cloud"; import * as $ from "cheerio"; let queue = new pulumi.Topic<string>("sites_to_process"); queue.subscribe("foreachurl", async (url) => { let x = $("a", "<a href='foo'>hello</a>"); }); ``` Also fixes free variable capture of `this` in arrow functions. Fixes #342.
This commit is contained in:
parent
69d8e38dff
commit
ad5ee5bc04
2 changed files with 102 additions and 4 deletions
|
@ -1,9 +1,10 @@
|
||||||
// Copyright 2016-2017, Pulumi Corporation. All rights reserved.
|
// Copyright 2016-2017, Pulumi Corporation. All rights reserved.
|
||||||
|
|
||||||
import { debuggablePromise } from "./debuggable";
|
|
||||||
import { Log } from "./log";
|
|
||||||
import * as acorn from "acorn";
|
import * as acorn from "acorn";
|
||||||
import * as estree from "estree";
|
import * as estree from "estree";
|
||||||
|
import { relative as pathRelative } from "path";
|
||||||
|
import { debuggablePromise } from "./debuggable";
|
||||||
|
import { Log } from "./log";
|
||||||
|
|
||||||
const acornwalk = require("acorn/dist/walk");
|
const acornwalk = require("acorn/dist/walk");
|
||||||
const nativeruntime = require("./native/build/Release/nativeruntime.node");
|
const nativeruntime = require("./native/build/Release/nativeruntime.node");
|
||||||
|
@ -30,6 +31,7 @@ export interface EnvironmentEntry {
|
||||||
closure?: Closure; // a closure we are dependent on.
|
closure?: Closure; // a closure we are dependent on.
|
||||||
obj?: Environment; // an object which may contain nested closures.
|
obj?: Environment; // an object which may contain nested closures.
|
||||||
arr?: EnvironmentEntry[]; // an array which may contain nested closures.
|
arr?: EnvironmentEntry[]; // an array which may contain nested closures.
|
||||||
|
module?: string; // a reference to a requirable module name.
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -84,6 +86,9 @@ async function flattenEnvironmentEntry(entry: Promise<AsyncEnvironmentEntry>,
|
||||||
if (e.hasOwnProperty("json")) {
|
if (e.hasOwnProperty("json")) {
|
||||||
result.json = e.json;
|
result.json = e.json;
|
||||||
}
|
}
|
||||||
|
else if (e.module) {
|
||||||
|
result.module = e.module;
|
||||||
|
}
|
||||||
else if (e.closure) {
|
else if (e.closure) {
|
||||||
result.closure = await flattenClosure(e.closure, flatCache);
|
result.closure = await flattenClosure(e.closure, flatCache);
|
||||||
}
|
}
|
||||||
|
@ -125,6 +130,7 @@ export interface AsyncEnvironmentEntry {
|
||||||
closure?: AsyncClosure; // a closure we are dependent on.
|
closure?: AsyncClosure; // a closure we are dependent on.
|
||||||
obj?: AsyncEnvironment; // an object which may contain nested closures.
|
obj?: AsyncEnvironment; // an object which may contain nested closures.
|
||||||
arr?: Promise<AsyncEnvironmentEntry>[]; // an array which may contain nested closures.
|
arr?: Promise<AsyncEnvironmentEntry>[]; // an array which may contain nested closures.
|
||||||
|
module?: string; // a reference to a requirable module name.
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -168,11 +174,16 @@ function serializeCapturedObject(obj: any): Promise<AsyncEnvironmentEntry> {
|
||||||
* serializeCapturedObjectAsync is the work-horse that actually performs object serialization.
|
* serializeCapturedObjectAsync is the work-horse that actually performs object serialization.
|
||||||
*/
|
*/
|
||||||
function serializeCapturedObjectAsync(obj: any, resolve: (v: AsyncEnvironmentEntry) => void): void {
|
function serializeCapturedObjectAsync(obj: any, resolve: (v: AsyncEnvironmentEntry) => void): void {
|
||||||
|
let moduleName = findRequirableModuleName(obj);
|
||||||
if (obj === undefined || obj === null ||
|
if (obj === undefined || obj === null ||
|
||||||
typeof obj === "boolean" || typeof obj === "number" || typeof obj === "string") {
|
typeof obj === "boolean" || typeof obj === "number" || typeof obj === "string") {
|
||||||
// Serialize primitives as-is.
|
// Serialize primitives as-is.
|
||||||
resolve({ json: obj });
|
resolve({ json: obj });
|
||||||
}
|
}
|
||||||
|
else if (moduleName) {
|
||||||
|
// Serialize any value which was found as a requirable module name as a reference to the module
|
||||||
|
resolve({module: moduleName});
|
||||||
|
}
|
||||||
else if (obj instanceof Array) {
|
else if (obj instanceof Array) {
|
||||||
// Recursively serialize elements of an array.
|
// Recursively serialize elements of an array.
|
||||||
let arr: Promise<AsyncEnvironmentEntry>[] = [];
|
let arr: Promise<AsyncEnvironmentEntry>[] = [];
|
||||||
|
@ -199,6 +210,46 @@ function serializeCapturedObjectAsync(obj: any, resolve: (v: AsyncEnvironmentEnt
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// These modules are built-in to Node.js, and are available via `require(...)`
|
||||||
|
// but are not stored in the `require.cache`. They are guaranteed to be
|
||||||
|
// available at the unqualified names listed below. _Note_: This list is derived
|
||||||
|
// based on Node.js 6.x tree at: https://github.com/nodejs/node/tree/v6.x/lib
|
||||||
|
let builtInModuleNames = [
|
||||||
|
"assert", "buffer", "child_process", "cluster", "console", "constants", "crypto",
|
||||||
|
"dgram", "dns", "domain", "events", "fs", "http", "https", "module", "net", "os",
|
||||||
|
"path", "process", "punycode", "querystring", "readline", "repl", "stream", "string_decoder",
|
||||||
|
/* "sys" deprecated ,*/ "timers", "tls", "tty", "url", "util", "v8", "vm", "zlib",
|
||||||
|
];
|
||||||
|
let builtInModules = new Map<any, string>();
|
||||||
|
for (let name of builtInModuleNames) {
|
||||||
|
builtInModules.set(require(name), name);
|
||||||
|
}
|
||||||
|
|
||||||
|
// findRequirableModuleName attempts to find a global name bound to the object, which can
|
||||||
|
// be used as a stable reference across serialization.
|
||||||
|
function findRequirableModuleName(obj: any): string | undefined {
|
||||||
|
// First, check the built-in modules
|
||||||
|
let key = builtInModules.get(obj);
|
||||||
|
if (key) {
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
// Next, check the Node module require cache, which will store cached values
|
||||||
|
// of all non-built-in Node modules loaded by the program so far. _Note_: We
|
||||||
|
// don't pre-compute this because the require cache will get populated
|
||||||
|
// dynamically during execution.
|
||||||
|
for (let path of Object.keys(require.cache)) {
|
||||||
|
if (require.cache[path].exports === obj) {
|
||||||
|
// Rewrite the path to be a local module reference relative to the
|
||||||
|
// current working directory
|
||||||
|
let modPath = pathRelative(process.cwd(), path);
|
||||||
|
return "./" + modPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Else, return that no global name is available for this object.
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* computeFreeVariables computes the set of free variables in a given function string. Note that this string is
|
* computeFreeVariables 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.
|
* expected to be the usual V8-serialized function expression text.
|
||||||
|
@ -293,7 +344,7 @@ class FreeVariableComputer {
|
||||||
}
|
}
|
||||||
|
|
||||||
private visitThisExpression(node: estree.Identifier, state: any, cb: walkCallback): void {
|
private visitThisExpression(node: estree.Identifier, state: any, cb: walkCallback): void {
|
||||||
// Mar references to the built-in 'this' variable as free.
|
// Mark references to the built-in 'this' variable as free.
|
||||||
this.frees["this"] = true;
|
this.frees["this"] = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -341,6 +392,12 @@ class FreeVariableComputer {
|
||||||
this.frees[v] = false;
|
this.frees[v] = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If the function is not an arrow, then its `this` is also a
|
||||||
|
// function-scoped variable and should be removed.
|
||||||
|
if ((node as estree.ArrowFunctionExpression).type !== "ArrowFunctionExpression") {
|
||||||
|
this.frees["this"] = false;
|
||||||
|
}
|
||||||
|
|
||||||
// Restore the prior context and merge our free list with the previous one.
|
// Restore the prior context and merge our free list with the previous one.
|
||||||
this.scope.pop();
|
this.scope.pop();
|
||||||
this.functionVars = oldFunctionVars;
|
this.functionVars = oldFunctionVars;
|
||||||
|
|
|
@ -177,6 +177,38 @@ describe("closure", () => {
|
||||||
runtime: "nodejs",
|
runtime: "nodejs",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
{
|
||||||
|
let os = require("os");
|
||||||
|
cases.push({
|
||||||
|
title: "Capture built-in modules as stable references, not serialized values",
|
||||||
|
func: () => os,
|
||||||
|
expect: {
|
||||||
|
code: `(() => os)`,
|
||||||
|
environment: {
|
||||||
|
os: {
|
||||||
|
module: "os",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
runtime: "nodejs",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
{
|
||||||
|
let util = require("../util");
|
||||||
|
cases.push({
|
||||||
|
title: "Capture user-defined modules as stable references, not serialized values",
|
||||||
|
func: () => util,
|
||||||
|
expect: {
|
||||||
|
code: `(() => util)`,
|
||||||
|
environment: {
|
||||||
|
util: {
|
||||||
|
module: "./bin/tests/util.js",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
runtime: "nodejs",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
cases.push({
|
cases.push({
|
||||||
title: "Don't capture catch variables",
|
title: "Don't capture catch variables",
|
||||||
// tslint:disable-next-line
|
// tslint:disable-next-line
|
||||||
|
@ -276,7 +308,7 @@ describe("closure", () => {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
cases.push({
|
cases.push({
|
||||||
title: "Serializes `this` capturing closures",
|
title: "Serializes `this` capturing arrow functions",
|
||||||
func: cap.f,
|
func: cap.f,
|
||||||
expect: {
|
expect: {
|
||||||
code: "(() => { console.log(this.x); })",
|
code: "(() => { console.log(this.x); })",
|
||||||
|
@ -285,6 +317,15 @@ describe("closure", () => {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
cases.push({
|
||||||
|
title: "Don't serialize `this` in function expressions",
|
||||||
|
func: function() { return this; },
|
||||||
|
expect: {
|
||||||
|
code: `(function () { return this; })`,
|
||||||
|
environment: {},
|
||||||
|
runtime: "nodejs",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
// Now go ahead and run the test cases, each as its own case.
|
// Now go ahead and run the test cases, each as its own case.
|
||||||
for (let test of cases) {
|
for (let test of cases) {
|
||||||
|
|
Loading…
Reference in a new issue