Skip to content

Commit

Permalink
Merge pull request #184 from ember-polyfills/dependency-satisfies-check
Browse files Browse the repository at this point in the history
Macro-based ember version check
  • Loading branch information
ef4 authored Sep 16, 2022
2 parents 387f0bd + 5532da8 commit 5cfa048
Show file tree
Hide file tree
Showing 7 changed files with 1,977 additions and 3,960 deletions.
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ module.exports = {
rules: {
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-empty-function': 'off',
'prefer-const': 'off',
},

overrides: [
Expand Down
21 changes: 20 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,15 @@ ember install ember-cached-decorator-polyfill

For addons, pass the `-S` flag.

If you're working in an environment with an explicit Babel config (like a V2
addon or an app with `ember-cli-babel`'s `{ useBabelConfig: true }`
mode), see "Explicit Babel Config" below.

## Compatibility

- Ember.js v3.13 or above
- Ember CLI v2.13 or above
- Node.js v10 or above
- Node.js v14 or above

## Summary

Expand Down Expand Up @@ -65,3 +69,18 @@ import 'ember-cached-decorator-polyfill';

Once the upstream types have been updated to reflect RFC 566, this will no
longer be necessary.

## Explicit Babel Config

In environments where you have an explicit Babel config (like authoring a V2
addon) you will need to configure this polyfill's babel plugin. Add it to your
`babel.config.js` like:

```
{
"plugins": [
"ember-cached-decorator-polyfill/babel-plugin"
]
}
```

9 changes: 9 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,16 @@ module.exports = {
included(parent) {
this._super.included.apply(this, arguments);

// this adds our babel plugin to our parent package
this.addBabelPlugin(parent);

// this ensures our parent package can process macros, since our babel
// plugin emits macros
if (!this.parent.addons.find((a) => a.name === '@embroider/macros')) {
this.addons
.find((a) => a.name === '@embroider/macros')
.installBabelPlugin(parent);
}
},

addBabelPlugin(parent) {
Expand Down
96 changes: 53 additions & 43 deletions lib/transpile-modules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,65 +4,75 @@ import path from 'path';
import type * as Babel from '@babel/core';
import type { types as t } from '@babel/core';
import type { NodePath } from '@babel/traverse';
import { ImportUtil } from 'babel-import-util';

interface State {
importer: ImportUtil;
}

/**
* Based on `babel-plugin-ember-modules-api-polyfill`.
* @see https://github.com/ember-cli/babel-plugin-ember-modules-api-polyfill/blob/master/src/index.js
*/
export default function (babel: typeof Babel) {
const t = babel.types;

const MODULE = '@glimmer/tracking';
const REAL_MODULE = '@glimmer/tracking';
const IMPORT = 'cached';
const REPLACED_MODULE = 'ember-cached-decorator-polyfill';
const POLYFILL_MODULE = 'ember-cached-decorator-polyfill';
const AVAILABE_AT = '>= 4.1.0-alpha.0';

// Notice that the only name we introduce into scope here is %%local%%, and we
// know that name is safe to use because it's the name the user was already
// using in the ImportSpecifier that we're replacing.
let loader = babel.template(`
let %%local%% = %%macroCondition%%(%%dependencySatisfies%%('ember-source', '${AVAILABE_AT}')) ?
%%importSync%%('${REAL_MODULE}').${IMPORT} :
%%importSync%%('${POLYFILL_MODULE}').${IMPORT};
`);

return {
name: 'ember-cache-decorator-polyfill',
visitor: {
ImportDeclaration(path: NodePath<t.ImportDeclaration>) {
const node = path.node;
const declarations: t.ImportDeclaration[] = [];
const removals: NodePath<t.Node>[] = [];
const specifiers = path.get('specifiers');
const importPath = node.source.value;

// Only walk specifiers if this is a module we have a mapping for
if (importPath !== MODULE) {
Program(path: NodePath<t.Program>, state: State) {
state.importer = new ImportUtil(t, path);
},
ImportDeclaration(path: NodePath<t.ImportDeclaration>, state: State) {
if (path.node.source.value !== REAL_MODULE) {
return;
}

// Iterate all the specifiers and attempt to locate their mapping
for (const specifierPath of specifiers) {
const names = getNames(specifierPath);
if (!names) {
continue;
}
if (names.imported !== IMPORT) {
for (let specifierPath of path.get('specifiers')) {
let names = getNames(specifierPath);
if (names?.imported !== IMPORT) {
continue;
}

removals.push(specifierPath);

declarations.push(
t.importDeclaration(
[
t.importSpecifier(
t.identifier(names.local),
t.identifier(IMPORT)
),
],
t.stringLiteral(REPLACED_MODULE)
)
// using babel-import-util to gain access to these functions ensures
// that we will never smash any existing bindings (and we'll reuse
// existing imports for these if they exist)
let importSync = state.importer.import(
path,
'@embroider/macros',
'importSync'
);
let macroCondition = state.importer.import(
path,
'@embroider/macros',
'macroCondition'
);
let dependencySatisfies = state.importer.import(
path,
'@embroider/macros',
'dependencySatisfies'
);
}

if (removals.length > 0) {
if (removals.length === node.specifiers.length) {
path.replaceWithMultiple(declarations);
} else {
removals.forEach((specifierPath) => specifierPath.remove());
path.insertAfter(declarations);
}
specifierPath.remove();

path.insertAfter(
loader({
local: t.identifier(names.local),
macroCondition,
dependencySatisfies,
importSync,
})
);
}
},
},
Expand All @@ -81,7 +91,7 @@ function getNames(
if (specifierPath.isImportDefaultSpecifier()) {
return { imported: 'default', local: specifierPath.node.local.name };
} else if (specifierPath.isImportSpecifier()) {
const importedNode = specifierPath.node.imported;
let importedNode = specifierPath.node.imported;
if (importedNode.type === 'Identifier') {
return {
imported: importedNode.name,
Expand Down
11 changes: 10 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@
"memoized",
"memoization"
],
"exports": {
".": "./index.js",
"./babel-plugin": "./lib/transpile-modules.js"
},
"homepage": "https://github.com/ember-polyfills/ember-cached-decorator-polyfill#readme",
"bugs": {
"url": "https://github.com/ember-polyfills/ember-cached-decorator-polyfill/issues"
Expand Down Expand Up @@ -44,7 +48,9 @@
"test:ember-compatibility": "ember try:each"
},
"dependencies": {
"@embroider/macros": "^1.8.3",
"@glimmer/tracking": "^1.1.2",
"babel-import-util": "^1.2.2",
"ember-cache-primitive-polyfill": "^1.0.1",
"ember-cli-babel": "^7.26.11",
"ember-cli-babel-plugin-helpers": "^1.1.1"
Expand All @@ -56,7 +62,7 @@
"@glimmer/component": "^1.1.2",
"@types/babel__core": "^7.1.19",
"@types/babel__traverse": "^7.18.1",
"@types/ember": "^3.16.0",
"@types/ember": "^4.0.1",
"@types/ember-qunit": "^5.0.1",
"@types/ember-resolver": "^5.0.9",
"@types/ember__test-helpers": "^2.8.1",
Expand Down Expand Up @@ -112,6 +118,9 @@
"before": "ember-cli-babel",
"configPath": "tests/dummy/config"
},
"peerDependencies": {
"ember-source": "^3.13.0 | ^4.0.0"
},
"release-it": {
"plugins": {
"release-it-lerna-changelog": {
Expand Down
2 changes: 1 addition & 1 deletion tests/dummy/app/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import EmberRouter from '@ember/routing/router';
import config from 'dummy/config/environment';

export default class Router extends EmberRouter {
location = config.locationType;
location = config.locationType as 'auto';
rootURL = config.rootURL;
}

Expand Down
Loading

0 comments on commit 5cfa048

Please sign in to comment.