Skip to content

Commit

Permalink
Merge pull request #75 from mydea/fn/use-preprocessor
Browse files Browse the repository at this point in the history
Ensure polyfill works properly with Ember 3.27+
  • Loading branch information
rwjblue authored May 19, 2021
2 parents 5759283 + b18c9a7 commit ed8a3e9
Show file tree
Hide file tree
Showing 10 changed files with 1,234 additions and 267 deletions.
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

0 comments on commit ed8a3e9

Please sign in to comment.