Skip to content

Commit ed8a3e9

Browse files
author
Robert Jackson
authored
Merge pull request #75 from mydea/fn/use-preprocessor
Ensure polyfill works properly with Ember 3.27+
2 parents 5759283 + b18c9a7 commit ed8a3e9

File tree

10 files changed

+1234
-267
lines changed

10 files changed

+1234
-267
lines changed

addon/index.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { createCache, getValue } from '@glimmer/tracking/primitives/cache';
2+
import { assert } from '@ember/debug';
3+
4+
export function cached(...args) {
5+
const [target, key, descriptor] = args;
6+
7+
// Error on `@cached()`, `@cached(...args)`, and `@cached propName = value;`
8+
assert(
9+
'You attempted to use @cached(), which is not necessary nor supported. Remove the parentheses and you will be good to go!',
10+
target !== undefined
11+
);
12+
assert(
13+
`You attempted to use @cached on with ${
14+
args.length > 1 ? 'arguments' : 'an argument'
15+
} ( @cached(${args
16+
.map(d => `'${d}'`)
17+
.join(
18+
', '
19+
)}), which is not supported. Dependencies are automatically tracked, so you can just use ${'`@cached`'}`,
20+
typeof target === 'object' &&
21+
typeof key === 'string' &&
22+
typeof descriptor === 'object' &&
23+
args.length === 3
24+
);
25+
assert(
26+
`The @cached decorator must be applied to getters. '${key}' is not a getter.`,
27+
typeof descriptor.get == 'function'
28+
);
29+
30+
const caches = new WeakMap();
31+
const getter = descriptor.get;
32+
descriptor.get = function () {
33+
if (!caches.has(this)) caches.set(this, createCache(getter.bind(this)));
34+
return getValue(caches.get(this));
35+
};
36+
}

index.js

Lines changed: 6 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,22 @@
11
'use strict';
22

33
const { resolve } = require('path');
4+
const { hasPlugin, addPlugin } = require('ember-cli-babel-plugin-helpers');
45

56
module.exports = {
67
name: require('./package').name,
78

89
included() {
910
this._super.included.apply(this, arguments);
10-
this._ensureThisImport();
1111

12-
this.import('vendor/ember-cached-decorator-polyfill/index.js');
13-
this.patchEmberModulesAPIPolyfill();
12+
this.addBabelPlugin();
1413
},
1514

16-
treeForVendor(tree) {
17-
const babel = this.addons.find(a => a.name === 'ember-cli-babel');
15+
addBabelPlugin() {
16+
let app = this._findHost();
1817

19-
return babel.transpileTree(tree, {
20-
babel: this.options.babel,
21-
22-
'ember-cli-babel': {
23-
compileModules: false
24-
}
25-
});
26-
},
27-
28-
_ensureThisImport() {
29-
if (!this.import) {
30-
this._findHost = function findHostShim() {
31-
let current = this;
32-
let app;
33-
do {
34-
app = current.app || app;
35-
// eslint-disable-next-line no-cond-assign
36-
} while (current.parent.parent && (current = current.parent));
37-
return app;
38-
};
39-
this.import = function importShim(asset, options) {
40-
const app = this._findHost();
41-
app.import(asset, options);
42-
};
18+
if (!hasPlugin(app, 'ember-cache-decorator-polyfill')) {
19+
addPlugin(app, resolve(__dirname, './lib/transpile-modules.js'));
4320
}
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-
};
6121
}
6222
};

lib/transpile-modules.js

Lines changed: 10 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -11,20 +11,7 @@ module.exports = function (babel) {
1111

1212
const MODULE = '@glimmer/tracking';
1313
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);
14+
const REPLACED_MODULE = 'ember-cached-decorator-polyfill';
2815

2916
return {
3017
name: 'ember-cache-decorator-polyfill',
@@ -74,30 +61,17 @@ module.exports = function (babel) {
7461

7562
removals.push(specifierPath);
7663

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(
64+
declarations.push(
65+
t.importDeclaration(
66+
[
67+
t.importSpecifier(
8668
t.identifier(local.name),
87-
t.identifier(GLOBAL)
69+
t.identifier(IMPORT)
8870
)
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-
}
71+
],
72+
t.stringLiteral(REPLACED_MODULE)
73+
)
74+
);
10175
});
10276
}
10377

@@ -109,69 +83,6 @@ module.exports = function (babel) {
10983
path.insertAfter(declarations);
11084
}
11185
}
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-
}
17586
}
17687
}
17788
};

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,14 @@
3939
"test:ember-compatibility": "ember try:each"
4040
},
4141
"dependencies": {
42+
"@glimmer/tracking": "^1.0.4",
4243
"ember-cache-primitive-polyfill": "^1.0.1",
43-
"ember-cli-babel": "^7.21.0"
44+
"ember-cli-babel": "^7.21.0",
45+
"ember-cli-babel-plugin-helpers": "^1.1.1"
4446
},
4547
"devDependencies": {
4648
"@ember/optional-features": "^2.0.0",
4749
"@glimmer/component": "^1.0.1",
48-
"@glimmer/tracking": "^1.0.0",
4950
"@types/ember": "^3.16.0",
5051
"@types/ember-qunit": "^3.4.9",
5152
"@types/ember-resolver": "^5.0.9",

tests/unit/followed-import-test.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { module, test } from 'qunit';
2+
import { cached, tracked } from '@glimmer/tracking';
3+
4+
module('Unit | Import | followed import', function () {
5+
test('it works', function (assert) {
6+
class Person {
7+
@tracked firstName = 'Jen';
8+
lastName = 'Weber';
9+
10+
@cached
11+
get fullName() {
12+
const fullName = `${this.firstName} ${this.lastName}`;
13+
assert.step(fullName);
14+
return fullName;
15+
}
16+
}
17+
18+
const person = new Person();
19+
assert.verifySteps([], 'getter is not called after class initialization');
20+
21+
assert.strictEqual(person.fullName, 'Jen Weber');
22+
assert.verifySteps(
23+
['Jen Weber'],
24+
'getter was called after property access'
25+
);
26+
27+
assert.strictEqual(person.fullName, 'Jen Weber');
28+
assert.verifySteps(
29+
[],
30+
'getter was not called again after repeated property access'
31+
);
32+
});
33+
});
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { module, test } from 'qunit';
2+
// prettier-ignore
3+
import {
4+
cached,
5+
tracked
6+
} from "@glimmer/tracking";
7+
8+
module('Unit | Import | multi-line import', function () {
9+
test('it works', function (assert) {
10+
class Person {
11+
@tracked firstName = 'Jen';
12+
lastName = 'Weber';
13+
14+
@cached
15+
get fullName() {
16+
const fullName = `${this.firstName} ${this.lastName}`;
17+
assert.step(fullName);
18+
return fullName;
19+
}
20+
}
21+
22+
const person = new Person();
23+
assert.verifySteps([], 'getter is not called after class initialization');
24+
25+
assert.strictEqual(person.fullName, 'Jen Weber');
26+
assert.verifySteps(
27+
['Jen Weber'],
28+
'getter was called after property access'
29+
);
30+
31+
assert.strictEqual(person.fullName, 'Jen Weber');
32+
assert.verifySteps(
33+
[],
34+
'getter was not called again after repeated property access'
35+
);
36+
});
37+
});

tests/unit/renamed-import-test.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { module, test } from 'qunit';
2+
import { tracked, cached as localCached } from '@glimmer/tracking';
3+
4+
module('Unit | Import | renamed import', function () {
5+
test('it works', function (assert) {
6+
class Person {
7+
@tracked firstName = 'Jen';
8+
lastName = 'Weber';
9+
10+
@localCached
11+
get fullName() {
12+
const fullName = `${this.firstName} ${this.lastName}`;
13+
assert.step(fullName);
14+
return fullName;
15+
}
16+
}
17+
18+
const person = new Person();
19+
assert.verifySteps([], 'getter is not called after class initialization');
20+
21+
assert.strictEqual(person.fullName, 'Jen Weber');
22+
assert.verifySteps(
23+
['Jen Weber'],
24+
'getter was called after property access'
25+
);
26+
27+
assert.strictEqual(person.fullName, 'Jen Weber');
28+
assert.verifySteps(
29+
[],
30+
'getter was not called again after repeated property access'
31+
);
32+
});
33+
});

tests/unit/single-import-test.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { module, test } from 'qunit';
2+
import { cached } from '@glimmer/tracking';
3+
4+
module('Unit | Import | single import', function () {
5+
test('it works', function (assert) {
6+
class Person {
7+
firstName = 'Jen';
8+
lastName = 'Weber';
9+
10+
@cached
11+
get fullName() {
12+
const fullName = `${this.firstName} ${this.lastName}`;
13+
assert.step(fullName);
14+
return fullName;
15+
}
16+
}
17+
18+
const person = new Person();
19+
assert.verifySteps([], 'getter is not called after class initialization');
20+
21+
assert.strictEqual(person.fullName, 'Jen Weber');
22+
assert.verifySteps(
23+
['Jen Weber'],
24+
'getter was called after property access'
25+
);
26+
27+
assert.strictEqual(person.fullName, 'Jen Weber');
28+
assert.verifySteps(
29+
[],
30+
'getter was not called again after repeated property access'
31+
);
32+
});
33+
});

0 commit comments

Comments
 (0)