Skip to content

Commit

Permalink
split the runtime macro implementations from the type-signatures-only…
Browse files Browse the repository at this point in the history
… module
  • Loading branch information
ef4 committed May 5, 2020
1 parent ab6d479 commit 2765aa5
Show file tree
Hide file tree
Showing 13 changed files with 155 additions and 87 deletions.
2 changes: 1 addition & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"name": "Run tests",
"program": "${workspaceFolder}/node_modules/.bin/jest",
"cwd": "${workspaceFolder}/packages/macros",
"args": ["--runInBand", "--testPathPattern", "tests/babel/fail-build.test.js"]
"args": ["--runInBand", "--testPathPattern", "tests/babel/each.test.js"]
},
{
"type": "node",
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/template-colocation-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ interface State {
function unusedNameLike(name: string, path: NodePath<unknown>) {
let candidate = name;
let counter = 0;
while (candidate in path.scope.bindings) {
while (path.scope.getBinding(candidate)) {
candidate = `${name}${counter++}`;
}
return candidate;
Expand Down
25 changes: 15 additions & 10 deletions packages/macros/src/babel/each.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,12 @@ import { CallExpression, ForOfStatement, identifier, File, ExpressionStatement,
import error from './error';
import State, { cloneDeep } from './state';

type CallEachExpression = NodePath<CallExpression> & {
get(callee: 'callee'): NodePath<Identifier>;
};

export type EachPath = NodePath<ForOfStatement> & {
get(right: 'right'): NodePath<CallExpression>;
get(right: 'right'): CallEachExpression;
};

export function isEachPath(path: NodePath<ForOfStatement>): path is EachPath {
Expand Down Expand Up @@ -46,17 +50,18 @@ export function insertEach(path: EachPath, state: State) {
}

if (state.opts.mode === 'run-time') {
return;
}

for (let element of array.value) {
let literalElement = asLiteral(element);
for (let target of nameRefs) {
target.replaceWith(literalElement);
let callee = path.get('right').get('callee');
state.neededRuntimeImports.set(callee.node.name, 'each');
} else {
for (let element of array.value) {
let literalElement = asLiteral(element);
for (let target of nameRefs) {
target.replaceWith(literalElement);
}
path.insertBefore(cloneDeep(path.get('body').node, state));
}
path.insertBefore(cloneDeep(path.get('body').node, state));
path.remove();
}
path.remove();
}

function asLiteral(value: unknown | undefined) {
Expand Down
10 changes: 4 additions & 6 deletions packages/macros/src/babel/get-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,10 @@ import {
CallExpression,
callExpression,
stringLiteral,
memberExpression,
FunctionDeclaration,
returnStatement,
Identifier,
} from '@babel/types';
import State, { sourceFile } from './state';
import State, { sourceFile, unusedNameLike } from './state';
import { PackageCache, Package } from '@embroider/core';
import error from './error';
import { Evaluator, assertArray, buildLiterals, ConfidentResult } from './evaluate-json';
Expand Down Expand Up @@ -63,9 +61,9 @@ export function insertConfig(path: NodePath<CallExpression>, state: State, own:
} else {
pkgRoot = identifier('undefined');
}
path.replaceWith(
callExpression(memberExpression(path.get('callee').node as Identifier, identifier('_runtimeGet')), [pkgRoot])
);
let name = unusedNameLike('config', path);
path.replaceWith(callExpression(identifier(name), [pkgRoot]));
state.neededRuntimeImports.set(name, 'config');
}
}

Expand Down
25 changes: 13 additions & 12 deletions packages/macros/src/babel/macro-condition.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { NodePath } from '@babel/traverse';
import { Evaluator } from './evaluate-json';
import { IfStatement, ConditionalExpression, CallExpression } from '@babel/types';
import { IfStatement, ConditionalExpression, CallExpression, Identifier } from '@babel/types';
import error from './error';
import State from './state';

export type MacroConditionPath = NodePath<IfStatement | ConditionalExpression> & {
get(test: 'test'): NodePath<CallExpression>;
get(test: 'test'): NodePath<CallExpression> & { get(callee: 'callee'): NodePath<Identifier> };
};

export function isMacroConditionPath(path: NodePath<IfStatement | ConditionalExpression>): path is MacroConditionPath {
Expand Down Expand Up @@ -35,16 +35,17 @@ export default function macroCondition(conditionalPath: MacroConditionPath, stat
let alternate = conditionalPath.get('alternate');

if (state.opts.mode === 'run-time') {
return;
}

let [kept, removed] = predicate.value ? [consequent.node, alternate.node] : [alternate.node, consequent.node];
if (kept) {
conditionalPath.replaceWith(kept);
let callee = conditionalPath.get('test').get('callee');
state.neededRuntimeImports.set(callee.node.name, 'macroCondition');
} else {
conditionalPath.remove();
}
if (removed) {
state.removed.add(removed);
let [kept, removed] = predicate.value ? [consequent.node, alternate.node] : [alternate.node, consequent.node];
if (kept) {
conditionalPath.replaceWith(kept);
} else {
conditionalPath.remove();
}
if (removed) {
state.removed.add(removed);
}
}
}
31 changes: 25 additions & 6 deletions packages/macros/src/babel/macros-babel-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,13 @@ import {
ForOfStatement,
FunctionDeclaration,
OptionalMemberExpression,
importSpecifier,
importDeclaration,
Program,
stringLiteral,
} from '@babel/types';
import { PackageCache } from '@embroider/core';
import State, { sourceFile } from './state';
import State, { sourceFile, relativePathToRuntime } from './state';
import { inlineRuntimeConfig, insertConfig } from './get-config';
import macroCondition, { isMacroConditionPath } from './macro-condition';
import { isEachPath, insertEach } from './each';
Expand All @@ -25,14 +29,16 @@ const packageCache = PackageCache.shared('embroider-stage3');
export default function main(context: unknown): unknown {
let visitor = {
Program: {
enter(_: NodePath, state: State) {
enter(_: NodePath<Program>, state: State) {
state.generatedRequires = new Set();
state.jobs = [];
state.removed = new Set();
state.calledIdentifiers = new Set();
state.neededRuntimeImports = new Map();
},
exit(path: NodePath, state: State) {
pruneMacroImports(path, state);
exit(path: NodePath<Program>, state: State) {
pruneMacroImports(path);
addRuntimeImports(path, state);
for (let handler of state.jobs) {
handler();
}
Expand Down Expand Up @@ -178,8 +184,8 @@ export default function main(context: unknown): unknown {

// This removes imports from "@embroider/macros" itself, because we have no
// runtime behavior at all.
function pruneMacroImports(path: NodePath, state: State) {
if (!path.isProgram() || state.opts.mode === 'run-time') {
function pruneMacroImports(path: NodePath) {
if (!path.isProgram()) {
return;
}
for (let topLevelPath of path.get('body')) {
Expand All @@ -189,6 +195,19 @@ function pruneMacroImports(path: NodePath, state: State) {
}
}

function addRuntimeImports(path: NodePath<Program>, state: State) {
if (state.neededRuntimeImports.size > 0) {
path.node.body.push(
importDeclaration(
[...state.neededRuntimeImports].map(([local, imported]) =>
importSpecifier(identifier(local), identifier(imported))
),
stringLiteral(relativePathToRuntime(path, state))
)
);
}
}

function ownedByEmberPackage(path: NodePath, state: State) {
let filename = sourceFile(path, state);
let pkg = packageCache.ownerOfFile(filename);
Expand Down
44 changes: 44 additions & 0 deletions packages/macros/src/babel/runtime.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
These are the runtime implementations for the javascript macros that have
runtime implementations.
Not every macro has a runtime implementations, some only make sense in the
build and always run there.
Even when we have runtime implementations, we are still careful to emit static
errors during the build wherever possible, and runtime errors when necessary,
so that you're not surprised when you switch from runtime-mode to compile-time
mode.
*/

export function each<T>(array: T[]): T[] {
if (!Array.isArray(array)) {
throw new Error(`the argument to the each() macro must be an array`);
}
return array;
}

export function macroCondition(predicate: boolean): boolean {
return predicate;
}

// This is here as a compile target for `getConfig` and `getOwnConfig` when
// we're in runtime mode. This is not public API to call from your own code.
export function config<T>(packageRoot: string | undefined): T | undefined {
if (packageRoot) {
return runtimeConfig[packageRoot] as T;
}
}

export function setConfig<T>(packageRoot: string | undefined, config: T): void {
if (packageRoot) {
runtimeConfig[packageRoot] = config;
}
}

const runtimeConfig: { [packageRoot: string]: unknown } = initializeRuntimeMacrosConfig();

// this exists to be targeted by our babel plugin
function initializeRuntimeMacrosConfig() {
return {};
}
21 changes: 21 additions & 0 deletions packages/macros/src/babel/state.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
import { NodePath, Node } from '@babel/traverse';
import cloneDeepWith from 'lodash/cloneDeepWith';
import lodashCloneDeep from 'lodash/cloneDeep';
import { join, dirname } from 'path';
import { explicitRelative } from '@embroider/core';

export default interface State {
generatedRequires: Set<Node>;
removed: Set<Node>;
calledIdentifiers: Set<Node>;
jobs: (() => void)[];

// map from local name to imported name
neededRuntimeImports: Map<string, string>;

opts: {
userConfigs: {
[pkgRoot: string]: unknown;
Expand All @@ -25,6 +30,13 @@ export default interface State {
};
}

const runtimePath = join(__dirname, 'runtime');

export function relativePathToRuntime(path: NodePath, state: State): string {
let source = sourceFile(path, state);
return explicitRelative(dirname(source), runtimePath);
}

export function sourceFile(path: NodePath, state: State): string {
return state.opts.owningPackageRoot || path.hub.file.opts.filename;
}
Expand All @@ -38,3 +50,12 @@ export function cloneDeep(node: Node, state: State): Node {
}
});
}

export function unusedNameLike(name: string, path: NodePath<unknown>) {
let candidate = name;
let counter = 0;
while (path.scope.getBinding(candidate)) {
candidate = `${name}${counter++}`;
}
return candidate;
}
51 changes: 10 additions & 41 deletions packages/macros/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,35 +4,28 @@
CAUTION: this code is not necessarily what you are actually running. In
general, the macros are implemented at build time using babel, and so calls to
these functions get compiled away before they ever run. However, this code is
here because:
here because it provides types to typescript users of the macros.
1. It provides types to typescript users of the macros.
Some macros also have runtime implementations that are useful in development
mode, in addition to their build-time implementations in babel. You can find
the runtime implementations in ./runtime.ts.
2. Some macros have runtime implementations that are useful in development
mode, in addition to their build-time implementations in babel. This lets
us do things like produce a single build in development that works for both
fastboot and browser, using the macros to switch between modes. For
production, you would switch to the build-time macro implementation to get
two optimized builds instead.
Having a runtime mode lets us do things like produce a single build in
development that works for both fastboot and browser, using the macros to
switch between modes. For production, you would switch to the build-time macro
implementation to get two optimized builds instead.
*/

export function dependencySatisfies(packageName: string, semverRange: string): boolean {
// this has no runtime implementation, it's always evaluated at build time
// because only at build time can we see what set of dependencies are
// resolvable on disk, and there's really no way to change your set of
// dependencies on the fly anyway.
throw new Oops(packageName, semverRange);
}

export function macroCondition(predicate: boolean): boolean {
return predicate;
throw new Oops(predicate);
}

export function each<T>(array: T[]): T[] {
if (!Array.isArray(array)) {
throw new Error(`the argument to the each() macro must be an array`);
}
return array;
throw new Oops(array);
}

// We would prefer to write:
Expand Down Expand Up @@ -68,30 +61,6 @@ class Oops extends Error {
}
}

// This is here as a compile target for `getConfig` and `getOwnConfig` when
// we're in runtime mode. This is not public API to call from your own code.
function _runtimeGetConfig<T>(packageRoot: string | undefined): T | undefined {
if (packageRoot) {
return runtimeConfig[packageRoot] as T;
}
}
function _runtimeSetConfig<T>(packageRoot: string | undefined, config: T): void {
if (packageRoot) {
runtimeConfig[packageRoot] = config;
}
}
getOwnConfig._runtimeGet = _runtimeGetConfig;
getOwnConfig._runtimeSet = _runtimeSetConfig;
getConfig._runtimeGet = _runtimeGetConfig;
getConfig._runtimeSet = _runtimeSetConfig;

const runtimeConfig: { [packageRoot: string]: unknown } = initializeRuntimeMacrosConfig();

// this exists to be targeted by our babel plugin in runtime mode.
function initializeRuntimeMacrosConfig() {
return {};
}

// TODO: beyond this point should only ever be used within the build system. We
// need to guard it so it never ships in apps.

Expand Down
4 changes: 2 additions & 2 deletions packages/macros/tests/babel/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export function makeRunner(transform: Transform) {

return function run(code: string) {
if (!cachedMacrosPackage) {
let filename = join(__dirname, '../../src/index.ts');
let filename = join(__dirname, '../../src/babel/runtime.ts');
let tsSrc = readFileSync(filename, 'utf8');
let jsSrc = toJS(tsSrc);
let withInlinedConfig = transform(jsSrc, { filename });
Expand All @@ -32,7 +32,7 @@ export function makeRunner(transform: Transform) {
script.runInContext(context);
cachedMacrosPackage = context.exports;
}
return runDefault(code, { dependencies: { '@embroider/macros': cachedMacrosPackage } });
return runDefault(code, { dependencies: { '../../src/babel/runtime': cachedMacrosPackage } });
};
}

Expand Down

0 comments on commit 2765aa5

Please sign in to comment.