hoisting and pattern matching
ADD
REWRITE.md
+## export const
+
+```js
+export const { abc: abc2, nest: [def2] } = obj;
+```
+
+```js
+// temporal dead zone for const decls until...
+const { abc: abc2, nest: [def2] } = obj;
+$h_once.abc2(abc2); $h_once.def2(def2); // ... here.
+```
+
+## export let
+
+```js
+export let { abc: abc2, nest: [def2] } = obj;
+```
+
+```js
+// temporal dead zone for let decls until...
+{
+ // Private scope so that the let expression can be
+ // reused without interfering with the proxy traps.
+ let { abc: abc2, nest: [def2] } = obj;
+ $h_live.abc2(abc2); $h_live.def2(def2); // ... here.
+ undefined;
+};
+```
+
+## export var
+
+```js
+export var { abc: abc2, nest: [def2] } = obj;
+```
+
+```js
+$h_live.abc2(); $h_live.def2(); // hoisted decls (no tdz)
+...
+{
+ // Same as `let`, since our proxy traps will treat the
+ // identifier as a hoisted declaration.
+ let { abc: abc2, nest: [def2] } = obj;
+ $h_live.abc2(abc2); $h_live.def2(def2);
+ undefined;
+}
+```
+
+## export function
+
+```js
+export function fn() {
+ ...
+};
+```
+
+```js
+$h_live.fn(); // hoisted decl (no tdz)
+...
+(() => {
+ // Use an IIFE to preserve function semantics of `fn`
+ // without polluting the global scope (we need the proxy
+ // traps for fn)
+ function fn() {
+ ...
+ }
+ $h_live.fn(fn);
+})();
+```

MODIFY
src/index.js
src/index.js
@@ -61,25 +61,123 @@ const makeModulePlugin = options =>
const specs = path.node.specifiers;
const replace = [];
if (decl) {
- replace.push(
- ...decl.declarations.map(dec => {
- // TODO: Detect if a non-const declaration is actually fixed.
+ const collectPatternIdentifiers = pattern => {
+ if (pattern === null) {
+ // Elided array element.
+ return [];
+ }
+ switch (pattern.type) {
+ case 'Identifier':
+ return [pattern];
+ case 'ObjectPattern':
+ return pattern.properties.reduce((prior, { value }) => {
+ prior.push(...collectPatternIdentifiers(value));
+ return prior;
+ }, []);
+ case 'ArrayPattern':
+ return pattern.elements.reduce((prior, pat) => {
+ prior.push(...collectPatternIdentifiers(pat));
+ return prior;
+ }, []);
+ default:
+ throw path.buildCodeFrameError(
+ `Pattern type ${pattern.type} is not recognized`,
+ );
+ }
+ };
+
+ // Find all the declared variable identifiers.
+ const vids = (decl.declarations || [decl]).reduce(
+ (prior, { id: pat }) => {
+ prior.push(...collectPatternIdentifiers(pat));
+ return prior;
+ },
+ [],
+ );
+ const vnames = vids.map(({ name }) => name);
+
+ // TODO: Detect if a non-const declaration is actually fixed
+ // (no other assignments to it).
const isLive = decl.kind !== 'const';
- const { name } = dec.id;
if (isLive) {
- liveExportMap[name] = [name];
+ // These exports may potentially change.
+ vnames.forEach(vname => (liveExportMap[vname] = [vname]));
} else {
- fixedExports.push(name);
+ // Fixed exports (i.e. const or non-mutated)
+ fixedExports.push(...vnames);
}
- const callee = t.memberExpression(
- hiddenIdentifier(isLive ? HIDDEN_LIVE : HIDDEN_ONCE),
- dec.id,
+
+ // Create the export calls.
+ const exportFname = hiddenIdentifier(
+ isLive ? HIDDEN_LIVE : HIDDEN_ONCE,
);
- return t.expressionStatement(
- t.callExpression(callee, dec.init ? [dec.init] : []),
+ const exportCalls = vids.map(id =>
+ t.expressionStatement(
+ t.callExpression(t.memberExpression(exportFname, id), [id]),
+ ),
);
- }),
+
+ let rewriteKind;
+ if (decl.type === 'FunctionDeclaration') {
+ // Hoist the name, and rewrite as an IIFE.
+ options.hoistedDecls.push(...vnames);
+ rewriteKind = 'function';
+ } else if (decl.kind === 'var') {
+ // Save the hoistedDecls so that we don't have a
+ // temporal dead zone for them.
+ options.hoistedDecls.push(...vnames);
+
+ // Use a let declaration in our rewrite.
+ rewriteKind = 'let';
+ } else {
+ rewriteKind = decl.kind;
+ }
+
+ switch (rewriteKind) {
+ case 'const':
+ // Just put the declaration and export calls next to each other.
+ // This optimizes by shadowing the endowed proxy trap.
+ replace.push(decl, ...exportCalls);
+ break;
+ case 'let':
+ // Replace with a block so that the let declarations
+ // don't shadow the endowed proxy trap.
+ replace.push(
+ t.blockStatement([
+ // The let declaration,
+ { ...decl, kind: rewriteKind },
+ // all the export calls,
+ ...exportCalls,
+ ]),
+ );
+ break;
+
+ case 'function':
+ // Replace with an IIFE to preserve the function
+ // semantics without shadowing the endowed proxy trap.
+ replace.push(
+ t.expressionStatement(
+ t.callExpression(
+ t.arrowFunctionExpression(
+ [], // no params
+ t.blockStatement([
+ // The function declaration.
+ decl,
+ // all the export calls,
+ ...exportCalls,
+ ]),
+ ),
+ [], // no args
+ ),
+ ),
);
+ break;
+
+ default:
+ throw path.buildCodeFrameError(
+ `Unrecognized rewrite kind ${rewriteKind}`,
+ );
+ }
}
replace.push(
...specs.map(spec => {
@@ -91,6 +189,8 @@ const makeModulePlugin = options =>
);
return t.expressionStatement(t.callExpression(callee, []));
}),
+ // and don't evaluate to anything.
+ t.expressionStatement(t.identifier('undefined')),
);
path.replaceWithMultiple(replace);
},
@@ -183,6 +283,7 @@ const makeModuleTransformer = babelCore => {
fixedExports: [],
imports: {},
liveExportMap: {},
+ hoistedDecls: [],
importSources: {},
importDecls: [],
};
@@ -202,6 +303,9 @@ const makeModuleTransformer = babelCore => {
.join(',')}`,
)
.join(',')}});`;
+ preamble += sourceOptions.hoistedDecls
+ .map(vname => `${HIDDEN_LIVE}.${vname}();`)
+ .join('');
const functorSource = `\
(function moduleFunctor(${HIDDEN_IMPORTS}, ${HIDDEN_ONCE}, ${HIDDEN_LIVE}) { \
@@ -337,6 +441,7 @@ const makeModuleTransformer = babelCore => {
importHandleData[importHandle].moduleEndowments = endowments;
return {
...ss,
+ moduleRewritten: true,
src: `${HIDDEN_IMPORT}(${JSON.stringify(importHandle)})(true)`,
};
}

MODIFY
test/test-export-named.js
test/test-export-named.js
@@ -15,8 +15,7 @@ test(`export named`, async t => {
t.deepEqual(
await evaluateModule(`\
export const abc = 123;
-// export const [ def, ghi ] = [ 456, 789 ];
-export const def = 456, ghi = 789;
+export const { def, nest: [, ghi] } = { def: 456, nest: [ 'skip', 789 ] };
`),
{ abc: 123, def: 456, ghi: 789 },
`const exports`,
@@ -26,10 +25,11 @@ export const def = 456, ghi = 789;
await evaluateModule(`\
export let abc = 123;
export let def = 456;
+export let def2 = def;
def ++;
export const ghi = 789;
`),
- { abc: 123, def: 457, ghi: 789 },
+ { abc: 123, def: 457, def2: 456, ghi: 789 },
`let exports`,
);
} catch (e) {
@@ -39,3 +39,55 @@ export const ghi = 789;
t.end();
}
});
+
+test(`export hoisting`, async t => {
+ try {
+ const transforms = [makeModuleTransformer(babelCore)];
+ const { evaluateModule } = makeEvaluators({
+ transforms,
+ });
+
+ await t.rejects(
+ evaluateModule(`\
+const abc2 = abc;
+export const abc = 123;
+`),
+ ReferenceError,
+ `const exports without hoisting`,
+ );
+
+ await t.rejects(
+ evaluateModule(`\
+const abc2 = abc;
+export let abc = 123;
+`),
+ ReferenceError,
+ `let exports without hoisting`,
+ );
+
+ const { abc, abc2, abc3 } = await evaluateModule(`\
+export const abc2 = abc;
+export var abc = 123;
+export const abc3 = abc;
+`);
+ t.equal(abc2, undefined, `undefined instead of tdz`);
+ t.equal(abc, abc3, `var exports with hoisting`);
+ t.equal(abc, 123, `abc evaluates`);
+
+ const { fn, fn2, fn3 } = await evaluateModule(`\
+export const fn2 = fn;
+export function fn() {
+ return 'foo';
+}
+export const fn3 = fn;
+`);
+ t.equal(fn2, undefined, `undefined instead of tdz`);
+ t.equal(fn, fn3, `function exports with hoisting`);
+ t.equal(fn(), 'foo', `fn evaluates`);
+ } catch (e) {
+ console.log('unexpected exception', e);
+ t.assert(false, e);
+ } finally {
+ t.end();
+ }
+});

hoisting and pattern matching