Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ensure polyfill works properly with Ember 3.27+ #75

Merged
merged 3 commits into from
May 19, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions addon/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { createCache, getValue } from '@glimmer/tracking/primitives/cache';
import { assert } from '@ember/debug';

export function cached(...args) {
const [target, key, descriptor] = args;

// Error on `@cached()`, `@cached(...args)`, and `@cached propName = value;`
assert(
'You attempted to use @cached(), which is not necessary nor supported. Remove the parentheses and you will be good to go!',
target !== undefined
);
assert(
`You attempted to use @cached on with ${
args.length > 1 ? 'arguments' : 'an argument'
} ( @cached(${args
.map(d => `'${d}'`)
.join(
', '
)}), which is not supported. Dependencies are automatically tracked, so you can just use ${'`@cached`'}`,
typeof target === 'object' &&
typeof key === 'string' &&
typeof descriptor === 'object' &&
args.length === 3
);
assert(
`The @cached decorator must be applied to getters. '${key}' is not a getter.`,
typeof descriptor.get == 'function'
);

const caches = new WeakMap();
const getter = descriptor.get;
descriptor.get = function () {
if (!caches.has(this)) caches.set(this, createCache(getter.bind(this)));
return getValue(caches.get(this));
};
}
52 changes: 6 additions & 46 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,62 +1,22 @@
'use strict';

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

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

included() {
this._super.included.apply(this, arguments);
this._ensureThisImport();

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

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

return babel.transpileTree(tree, {
babel: this.options.babel,

'ember-cli-babel': {
compileModules: false
}
});
},

_ensureThisImport() {
if (!this.import) {
this._findHost = function findHostShim() {
let current = this;
let app;
do {
app = current.app || app;
// eslint-disable-next-line no-cond-assign
} while (current.parent.parent && (current = current.parent));
return app;
};
this.import = function importShim(asset, options) {
const app = this._findHost();
app.import(asset, options);
};
if (!hasPlugin(app, 'ember-cache-decorator-polyfill')) {
addPlugin(app, resolve(__dirname, './lib/transpile-modules.js'));
}
},

patchEmberModulesAPIPolyfill() {
const babel = this.parent.findOwnAddonByName
? this.parent.findOwnAddonByName('ember-cli-babel') // parent is an addon
: this.parent.findAddonByName('ember-cli-babel'); // parent is an app

if (babel.__CachedDecoratorPolyfillApplied) return;
babel.__CachedDecoratorPolyfillApplied = true;

const { _getEmberModulesAPIPolyfill } = babel;
babel._getEmberModulesAPIPolyfill = function (...args) {
const plugins = _getEmberModulesAPIPolyfill.apply(this, args);
if (!plugins) return;

return [[resolve(__dirname, './lib/transpile-modules.js')], ...plugins];
};
}
};
109 changes: 10 additions & 99 deletions lib/transpile-modules.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,7 @@ module.exports = function (babel) {

const MODULE = '@glimmer/tracking';
const IMPORT = 'cached';
const GLOBAL = 'Ember._cached';
const MEMBER_EXPRESSION = t.MemberExpression(
t.identifier('Ember'),
t.identifier('_cached')
);

const TSTypesRequiringModification = [
'TSAsExpression',
'TSTypeAssertion',
'TSNonNullExpression'
];
const isTypescriptNode = node =>
node.type.startsWith('TS') &&
!TSTypesRequiringModification.includes(node.type);
const REPLACED_MODULE = 'ember-cached-decorator-polyfill';

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

removals.push(specifierPath);

if (
path.scope.bindings[local.name].referencePaths.find(
rp => rp.parent.type === 'ExportSpecifier'
)
) {
// not safe to use path.scope.rename directly
declarations.push(
t.variableDeclaration('var', [
t.variableDeclarator(
declarations.push(
t.importDeclaration(
[
t.importSpecifier(
t.identifier(local.name),
t.identifier(GLOBAL)
t.identifier(IMPORT)
)
])
);
} else {
// Replace the occurences of the imported name with the global name.
let binding = path.scope.getBinding(local.name);

binding.referencePaths.forEach(referencePath => {
if (!isTypescriptNode(referencePath.parentPath)) {
referencePath.replaceWith(MEMBER_EXPRESSION);
}
});
}
],
t.stringLiteral(REPLACED_MODULE)
)
);
});
}

Expand All @@ -109,69 +83,6 @@ module.exports = function (babel) {
path.insertAfter(declarations);
}
}
},

ExportNamedDeclaration(path) {
let node = path.node;
if (!node.source) {
return;
}

let replacements = [];
let removals = [];
let specifiers = path.get('specifiers');
let importPath = node.source.value;

// Only walk specifiers if this is a module we have a mapping for
if (importPath === MODULE) {
// Iterate all the specifiers and attempt to locate their mapping
specifiers.forEach(specifierPath => {
let specifier = specifierPath.node;

// exported is the name of the module being export,
// e.g. `foo` in `export { computed as foo } from '@ember/object';`
const exported = specifier.exported;

// local is the original name of the module, this is usually the same
// as the exported value, unless the module is aliased
const local = specifier.local;

// We only care about the ExportSpecifier
if (specifier.type !== 'ExportSpecifier') {
return;
}

// Determine the import name, either default or named
let importName = local.name;

if (importName !== IMPORT) return;

removals.push(specifierPath);

let declaration;
const globalAsIdentifier = t.identifier(GLOBAL);
if (exported.name === 'default') {
declaration = t.exportDefaultDeclaration(globalAsIdentifier);
} else {
// Replace the node with a new `var name = Ember.something`
declaration = t.exportNamedDeclaration(
t.variableDeclaration('var', [
t.variableDeclarator(exported, globalAsIdentifier)
]),
[],
null
);
}
replacements.push(declaration);
});
}

if (removals.length > 0 && removals.length === node.specifiers.length) {
path.replaceWithMultiple(replacements);
} else if (replacements.length > 0) {
removals.forEach(specifierPath => specifierPath.remove());
path.insertAfter(replacements);
}
}
}
};
Expand Down
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,14 @@
"test:ember-compatibility": "ember try:each"
},
"dependencies": {
"@glimmer/tracking": "^1.0.4",
"ember-cache-primitive-polyfill": "^1.0.1",
"ember-cli-babel": "^7.21.0"
"ember-cli-babel": "^7.21.0",
"ember-cli-babel-plugin-helpers": "^1.1.1"
},
"devDependencies": {
"@ember/optional-features": "^2.0.0",
"@glimmer/component": "^1.0.1",
"@glimmer/tracking": "^1.0.0",
"@types/ember": "^3.16.0",
"@types/ember-qunit": "^3.4.9",
"@types/ember-resolver": "^5.0.9",
Expand Down
33 changes: 33 additions & 0 deletions tests/unit/followed-import-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { module, test } from 'qunit';
import { cached, tracked } from '@glimmer/tracking';

module('Unit | Import | followed import', function () {
test('it works', function (assert) {
class Person {
@tracked firstName = 'Jen';
lastName = 'Weber';

@cached
get fullName() {
const fullName = `${this.firstName} ${this.lastName}`;
assert.step(fullName);
return fullName;
}
}

const person = new Person();
assert.verifySteps([], 'getter is not called after class initialization');

assert.strictEqual(person.fullName, 'Jen Weber');
assert.verifySteps(
['Jen Weber'],
'getter was called after property access'
);

assert.strictEqual(person.fullName, 'Jen Weber');
assert.verifySteps(
[],
'getter was not called again after repeated property access'
);
});
});
37 changes: 37 additions & 0 deletions tests/unit/multi-line-import-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { module, test } from 'qunit';
// prettier-ignore
import {
cached,
tracked
} from "@glimmer/tracking";

module('Unit | Import | multi-line import', function () {
test('it works', function (assert) {
class Person {
@tracked firstName = 'Jen';
lastName = 'Weber';

@cached
get fullName() {
const fullName = `${this.firstName} ${this.lastName}`;
assert.step(fullName);
return fullName;
}
}

const person = new Person();
assert.verifySteps([], 'getter is not called after class initialization');

assert.strictEqual(person.fullName, 'Jen Weber');
assert.verifySteps(
['Jen Weber'],
'getter was called after property access'
);

assert.strictEqual(person.fullName, 'Jen Weber');
assert.verifySteps(
[],
'getter was not called again after repeated property access'
);
});
});
33 changes: 33 additions & 0 deletions tests/unit/renamed-import-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { module, test } from 'qunit';
import { tracked, cached as localCached } from '@glimmer/tracking';

module('Unit | Import | renamed import', function () {
test('it works', function (assert) {
class Person {
@tracked firstName = 'Jen';
lastName = 'Weber';

@localCached
get fullName() {
const fullName = `${this.firstName} ${this.lastName}`;
assert.step(fullName);
return fullName;
}
}

const person = new Person();
assert.verifySteps([], 'getter is not called after class initialization');

assert.strictEqual(person.fullName, 'Jen Weber');
assert.verifySteps(
['Jen Weber'],
'getter was called after property access'
);

assert.strictEqual(person.fullName, 'Jen Weber');
assert.verifySteps(
[],
'getter was not called again after repeated property access'
);
});
});
33 changes: 33 additions & 0 deletions tests/unit/single-import-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { module, test } from 'qunit';
import { cached } from '@glimmer/tracking';

module('Unit | Import | single import', function () {
test('it works', function (assert) {
class Person {
firstName = 'Jen';
lastName = 'Weber';

@cached
get fullName() {
const fullName = `${this.firstName} ${this.lastName}`;
assert.step(fullName);
return fullName;
}
}

const person = new Person();
assert.verifySteps([], 'getter is not called after class initialization');

assert.strictEqual(person.fullName, 'Jen Weber');
assert.verifySteps(
['Jen Weber'],
'getter was called after property access'
);

assert.strictEqual(person.fullName, 'Jen Weber');
assert.verifySteps(
[],
'getter was not called again after repeated property access'
);
});
});
Loading