-
Notifications
You must be signed in to change notification settings - Fork 137
/
macros-babel-plugin.ts
258 lines (245 loc) · 10.6 KB
/
macros-babel-plugin.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
import type { NodePath } from '@babel/traverse';
import type { types as t } from '@babel/core';
import type State from './state';
import { initState } from './state';
import type { Mode as GetConfigMode } from './get-config';
import { inlineRuntimeConfig, insertConfig } from './get-config';
import macroCondition, { isMacroConditionPath } from './macro-condition';
import { isEachPath, insertEach } from './each';
import error from './error';
import failBuild from './fail-build';
import { Evaluator, buildLiterals } from './evaluate-json';
import type * as Babel from '@babel/core';
import { existsSync, readdirSync } from 'fs';
import { resolve, dirname, join } from 'path';
export default function main(context: typeof Babel): unknown {
let t = context.types;
let visitor = {
Program: {
enter(path: NodePath<t.Program>, state: State) {
initState(t, path, state);
},
exit(_: NodePath<t.Program>, state: State) {
// @embroider/macros itself has no runtime behaviors and should always be removed
state.importUtil.removeAllImports('@embroider/macros');
for (let handler of state.jobs) {
handler();
}
},
},
'IfStatement|ConditionalExpression': {
enter(path: NodePath<t.IfStatement | t.ConditionalExpression>, state: State) {
if (isMacroConditionPath(path)) {
state.calledIdentifiers.add(path.get('test').get('callee').node);
macroCondition(path, state);
}
},
},
ForOfStatement: {
enter(path: NodePath<t.ForOfStatement>, state: State) {
if (isEachPath(path)) {
state.calledIdentifiers.add(path.get('right').get('callee').node);
insertEach(path, state, context);
}
},
},
FunctionDeclaration: {
enter(path: NodePath<t.FunctionDeclaration>, state: State) {
let id = path.get('id');
if (id.isIdentifier() && id.node.name === 'initializeRuntimeMacrosConfig' && state.opts.mode === 'run-time') {
let pkg = state.owningPackage();
if (pkg && pkg.name === '@embroider/macros') {
inlineRuntimeConfig(path, state, context);
}
}
},
},
CallExpression: {
enter(path: NodePath<t.CallExpression>, state: State) {
let callee = path.get('callee');
if (!callee.isIdentifier()) {
return;
}
// failBuild is implemented for side-effect, not value, so it's not
// handled by evaluateMacroCall.
if (callee.referencesImport('@embroider/macros', 'failBuild')) {
state.calledIdentifiers.add(callee.node);
failBuild(path, state);
return;
}
if (callee.referencesImport('@embroider/macros', 'importSync')) {
// we handle importSync in the exit hook
return;
}
// getOwnConfig/getGlobalConfig/getConfig needs special handling, so
// even though it also emits values via evaluateMacroCall when they're
// needed recursively by other macros, it has its own insertion-handling
// code that we invoke here.
//
// The things that are special include:
// - automatic collapsing of chained properties, etc
// - these macros have runtime implementations sometimes, which changes
// how we rewrite them
let mode: GetConfigMode | false = callee.referencesImport('@embroider/macros', 'getOwnConfig')
? 'own'
: callee.referencesImport('@embroider/macros', 'getGlobalConfig')
? 'getGlobalConfig'
: callee.referencesImport('@embroider/macros', 'getConfig')
? 'package'
: false;
if (mode) {
state.calledIdentifiers.add(callee.node);
insertConfig(path, state, mode, context);
return;
}
// isTesting can have a runtime implementation. At compile time it
// instead falls through to evaluateMacroCall.
if (callee.referencesImport('@embroider/macros', 'isTesting') && state.opts.mode === 'run-time') {
state.calledIdentifiers.add(callee.node);
callee.replaceWith(state.importUtil.import(callee, state.pathToOurAddon('runtime'), 'isTesting'));
return;
}
let result = new Evaluator({ state }).evaluateMacroCall(path);
if (result.confident) {
state.calledIdentifiers.add(callee.node);
path.replaceWith(buildLiterals(result.value, context));
}
},
exit(path: NodePath<t.CallExpression>, state: State) {
let callee = path.get('callee');
if (!callee.isIdentifier()) {
return;
}
// importSync doesn't evaluate to a static value, so it's implemented
// directly here, not in evaluateMacroCall.
// We intentionally do this on exit here, to allow other transforms to handle importSync before we do
// For example ember-auto-import needs to do some custom transforms to enable use of dynamic template strings,
// so its babel plugin needs to see and handle the importSync call first!
if (callee.referencesImport('@embroider/macros', 'importSync')) {
let specifier = path.node.arguments[0];
if (specifier?.type !== 'StringLiteral') {
let relativePath = '';
let property;
if (specifier.type === 'TemplateLiteral') {
relativePath = specifier.quasis[0].value.cooked!;
property = specifier.expressions[0] as t.Expression;
}
// babel might transform template form `../my-path/${id}` to '../my-path/'.concat(id)
if (
specifier.type === 'CallExpression' &&
specifier.callee.type === 'MemberExpression' &&
specifier.callee.property.type === 'Identifier' &&
specifier.callee.property.name === 'concat' &&
specifier.callee.object.type === 'StringLiteral'
) {
relativePath = specifier.callee.object.value;
property = specifier.arguments[0] as t.Expression;
}
if (property && relativePath && relativePath.startsWith('.')) {
const resolvedPath = resolve(dirname((state as any).filename), relativePath);
let entries: string[] = [];
if (existsSync(resolvedPath)) {
entries = readdirSync(resolvedPath).filter(e => !e.startsWith('.'));
}
const obj = t.objectExpression(
entries.map(e => {
let key = e.split('.')[0];
const rest = e.split('.').slice(1, -1);
if (rest.length) {
key += `.${rest}`;
}
const id = t.callExpression(
state.importUtil.import(path, state.pathToOurAddon('es-compat2'), 'default', 'esc'),
[state.importUtil.import(path, join(relativePath, key).replace(/\\/g, '/'), '*')]
);
return t.objectProperty(t.stringLiteral(key), id);
})
);
const memberExpr = t.memberExpression(obj, property, true);
path.replaceWith(memberExpr);
state.calledIdentifiers.add(callee.node);
return;
} else {
throw new Error(
`importSync eager mode only supports dynamic paths which are relative, must start with a '.', had ${specifier.type}`
);
}
}
path.replaceWith(
t.callExpression(state.importUtil.import(path, state.pathToOurAddon('es-compat2'), 'default', 'esc'), [
state.importUtil.import(path, specifier.value, '*'),
])
);
state.calledIdentifiers.add(callee.node);
return;
}
},
},
ReferencedIdentifier(path: NodePath<t.Identifier>, state: State) {
for (let candidate of [
'dependencySatisfies',
'moduleExists',
'getConfig',
'getOwnConfig',
'failBuild',
// we cannot check importSync, as the babel transform runs on exit, so *after* this check
// 'importSync',
'isDevelopingApp',
'isDevelopingThisPackage',
'isTesting',
]) {
if (path.referencesImport('@embroider/macros', candidate) && !state.calledIdentifiers.has(path.node)) {
throw error(path, `You can only use ${candidate} as a function call`);
}
}
if (path.referencesImport('@embroider/macros', 'macroCondition') && !state.calledIdentifiers.has(path.node)) {
throw error(path, `macroCondition can only be used as the predicate of an if statement or ternary expression`);
}
if (path.referencesImport('@embroider/macros', 'each') && !state.calledIdentifiers.has(path.node)) {
throw error(
path,
`the each() macro can only be used within a for ... of statement, like: for (let x of each(thing)){}`
);
}
if (state.opts.owningPackageRoot) {
// there is only an owningPackageRoot when we are running inside a
// classic ember-cli build. In the embroider stage3 build, there is no
// owning package root because we're compiling *all* packages
// simultaneously.
//
// given that we're inside classic ember-cli, stop here without trying
// to rewrite bare `require`. It's not needed, because both our
// `importSync` and any user-written bare `require` can both mean the
// same thing: runtime AMD `require`.
return;
}
if (
state.opts.hideRequires &&
path.node.name === 'require' &&
!path.scope.hasBinding('require') &&
state.owningPackage().isEmberAddon()
) {
// Our importSync macro has been compiled to `require`. But we want to
// distinguish that from any pre-existing, user-written `require` in an
// Ember addon, which should retain its *runtime* meaning.
path.replaceWith(t.memberExpression(t.identifier('window'), path.node));
}
},
};
if ((context as any).types.OptionalMemberExpression) {
// our getConfig and getOwnConfig macros are supposed to be able to absorb
// optional chaining. To make that work we need to see the optional chaining
// before preset-env compiles them away.
(visitor as any).OptionalMemberExpression = {
enter(path: NodePath<t.OptionalMemberExpression>, state: State) {
if (state.opts.mode === 'compile-time') {
let result = new Evaluator({ state }).evaluate(path);
if (result.confident) {
path.replaceWith(buildLiterals(result.value, context));
}
}
},
};
}
return { visitor };
}