Skip to content

Commit fb057e0

Browse files
committed
feat: patch ember-cli-babel
1 parent 51872a4 commit fb057e0

File tree

4 files changed

+205
-1
lines changed

4 files changed

+205
-1
lines changed

.eslintrc.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ module.exports = {
2929
'testem.js',
3030
'blueprints/*/index.js',
3131
'config/**/*.js',
32-
'tests/dummy/config/**/*.js'
32+
'tests/dummy/config/**/*.js',
33+
'lib/**/*.js'
3334
],
3435
excludedFiles: [
3536
'addon/**',

index.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
'use strict';
22

3+
const { resolve } = require('path');
4+
35
module.exports = {
46
name: require('./package').name,
57

@@ -8,6 +10,7 @@ module.exports = {
810
this._ensureThisImport();
911

1012
this.import('vendor/ember-cached-decorator-polyfill/index.js');
13+
this.patchEmberModulesAPIPolyfill();
1114
},
1215

1316
treeForVendor(tree) {
@@ -38,5 +41,22 @@ module.exports = {
3841
app.import(asset, options);
3942
};
4043
}
44+
},
45+
46+
patchEmberModulesAPIPolyfill() {
47+
const babel = this.parent.findOwnAddonByName
48+
? this.parent.findOwnAddonByName('ember-cli-babel') // parent is an addon
49+
: this.parent.findAddonByName('ember-cli-babel'); // parent is an app
50+
51+
if (babel.__CachedDecoratorPolyfillApplied) return;
52+
babel.__CachedDecoratorPolyfillApplied = true;
53+
54+
const { _getEmberModulesAPIPolyfill } = babel;
55+
babel._getEmberModulesAPIPolyfill = function (...args) {
56+
const plugins = _getEmberModulesAPIPolyfill.apply(this, args);
57+
if (!plugins) return;
58+
59+
return [[resolve(__dirname, './lib/transpile-modules.js')], ...plugins];
60+
};
4161
}
4262
};

lib/transpile-modules.js

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
'use strict';
2+
3+
const path = require('path');
4+
5+
/**
6+
* Based on `babel-plugin-ember-modules-api-polyfill`.
7+
* @see https://github.com/ember-cli/babel-plugin-ember-modules-api-polyfill/blob/master/src/index.js
8+
*/
9+
module.exports = function (babel) {
10+
const t = babel.types;
11+
12+
const MODULE = '@glimmer/tracking';
13+
const IMPORT = 'cached';
14+
const GLOBAL = 'Ember._cached';
15+
const MEMBER_EXPRESSION = t.MemberExpression(
16+
t.identifier('Ember'),
17+
t.identifier('_cached')
18+
);
19+
20+
const TSTypesRequiringModification = [
21+
'TSAsExpression',
22+
'TSTypeAssertion',
23+
'TSNonNullExpression'
24+
];
25+
const isTypescriptNode = node =>
26+
node.type.startsWith('TS') &&
27+
!TSTypesRequiringModification.includes(node.type);
28+
29+
return {
30+
name: 'ember-cache-decorator-polyfill',
31+
visitor: {
32+
ImportDeclaration(path) {
33+
let node = path.node;
34+
let declarations = [];
35+
let removals = [];
36+
let specifiers = path.get('specifiers');
37+
let importPath = node.source.value;
38+
39+
// Only walk specifiers if this is a module we have a mapping for
40+
if (importPath === MODULE) {
41+
// Iterate all the specifiers and attempt to locate their mapping
42+
specifiers.forEach(specifierPath => {
43+
let specifier = specifierPath.node;
44+
let importName;
45+
46+
// imported is the name of the module being imported, e.g. import foo from bar
47+
const imported = specifier.imported;
48+
49+
// local is the name of the module in the current scope, this is usually the same
50+
// as the imported value, unless the module is aliased
51+
const local = specifier.local;
52+
53+
// We only care about these 2 specifiers
54+
if (
55+
specifier.type !== 'ImportDefaultSpecifier' &&
56+
specifier.type !== 'ImportSpecifier'
57+
) {
58+
if (specifier.type === 'ImportNamespaceSpecifier') {
59+
throw new Error(
60+
`Using \`import * as ${specifier.local.name} from '${importPath}'\` is not supported.`
61+
);
62+
}
63+
return;
64+
}
65+
66+
// Determine the import name, either default or named
67+
if (specifier.type === 'ImportDefaultSpecifier') {
68+
importName = 'default';
69+
} else {
70+
importName = imported.name;
71+
}
72+
73+
if (importName !== IMPORT) return;
74+
75+
removals.push(specifierPath);
76+
77+
if (
78+
path.scope.bindings[local.name].referencePaths.find(
79+
rp => rp.parent.type === 'ExportSpecifier'
80+
)
81+
) {
82+
// not safe to use path.scope.rename directly
83+
declarations.push(
84+
t.variableDeclaration('var', [
85+
t.variableDeclarator(
86+
t.identifier(local.name),
87+
t.identifier(GLOBAL)
88+
)
89+
])
90+
);
91+
} else {
92+
// Replace the occurences of the imported name with the global name.
93+
let binding = path.scope.getBinding(local.name);
94+
95+
binding.referencePaths.forEach(referencePath => {
96+
if (!isTypescriptNode(referencePath.parentPath)) {
97+
referencePath.replaceWith(MEMBER_EXPRESSION);
98+
}
99+
});
100+
}
101+
});
102+
}
103+
104+
if (removals.length > 0) {
105+
if (removals.length === node.specifiers.length) {
106+
path.replaceWithMultiple(declarations);
107+
} else {
108+
removals.forEach(specifierPath => specifierPath.remove());
109+
path.insertAfter(declarations);
110+
}
111+
}
112+
},
113+
114+
ExportNamedDeclaration(path) {
115+
let node = path.node;
116+
if (!node.source) {
117+
return;
118+
}
119+
120+
let replacements = [];
121+
let removals = [];
122+
let specifiers = path.get('specifiers');
123+
let importPath = node.source.value;
124+
125+
// Only walk specifiers if this is a module we have a mapping for
126+
if (importPath === MODULE) {
127+
// Iterate all the specifiers and attempt to locate their mapping
128+
specifiers.forEach(specifierPath => {
129+
let specifier = specifierPath.node;
130+
131+
// exported is the name of the module being export,
132+
// e.g. `foo` in `export { computed as foo } from '@ember/object';`
133+
const exported = specifier.exported;
134+
135+
// local is the original name of the module, this is usually the same
136+
// as the exported value, unless the module is aliased
137+
const local = specifier.local;
138+
139+
// We only care about the ExportSpecifier
140+
if (specifier.type !== 'ExportSpecifier') {
141+
return;
142+
}
143+
144+
// Determine the import name, either default or named
145+
let importName = local.name;
146+
147+
if (importName !== IMPORT) return;
148+
149+
removals.push(specifierPath);
150+
151+
let declaration;
152+
const globalAsIdentifier = t.identifier(GLOBAL);
153+
if (exported.name === 'default') {
154+
declaration = t.exportDefaultDeclaration(globalAsIdentifier);
155+
} else {
156+
// Replace the node with a new `var name = Ember.something`
157+
declaration = t.exportNamedDeclaration(
158+
t.variableDeclaration('var', [
159+
t.variableDeclarator(exported, globalAsIdentifier)
160+
]),
161+
[],
162+
null
163+
);
164+
}
165+
replacements.push(declaration);
166+
});
167+
}
168+
169+
if (removals.length > 0 && removals.length === node.specifiers.length) {
170+
path.replaceWithMultiple(replacements);
171+
} else if (replacements.length > 0) {
172+
removals.forEach(specifierPath => specifierPath.remove());
173+
path.insertAfter(replacements);
174+
}
175+
}
176+
}
177+
};
178+
};
179+
180+
// Provide the path to the package's base directory for caching with broccoli
181+
// Ref: https://github.com/babel/broccoli-babel-transpiler#caching
182+
module.exports.baseDir = () => path.resolve(__dirname, '..');

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@
9595
"edition": "octane"
9696
},
9797
"ember-addon": {
98+
"before": "ember-cli-babel",
9899
"configPath": "tests/dummy/config"
99100
},
100101
"release-it": {

0 commit comments

Comments
 (0)