├── .editorconfig ├── .gitattributes ├── .gitignore ├── .travis.yml ├── .vscode └── launch.json ├── README.md ├── __testfixtures__ ├── cleanup-unused-namespaces.input.js ├── cleanup-unused-namespaces.output.js ├── destructure-inject-and-computed.input.js ├── destructure-inject-and-computed.output.js ├── destructure-nested-namespace.input.js ├── destructure-nested-namespace.output.js ├── final-boss.input.js ├── final-boss.output.js ├── fix-destructure-inject-and-computed.input.js ├── fix-destructure-inject-and-computed.output.js ├── helper.input.js ├── helper.output.js ├── leave-destructured-name.input.js ├── leave-destructured-name.output.js ├── leave-ember-test.crlf.input.js ├── leave-ember-test.crlf.output.js ├── leave-ember-test.input.js ├── leave-ember-test.output.js ├── leave-emberk.input.js ├── leave-emberk.output.js ├── leave-unmatched-destructuring.input.js ├── leave-unmatched-destructuring.output.js ├── multi-level-destructuring.input.js ├── multi-level-destructuring.output.js ├── named-then-default-imports.input.js ├── named-then-default-imports.output.js ├── remove-ember-import.input.js ├── remove-ember-import.output.js ├── top-comment.input.js └── top-comment.output.js ├── __tests__ ├── bin-test.js ├── expected │ └── MODULE_REPORT.md └── transform-test.js ├── appveyor.yml ├── bin └── ember-modules-codemod.js ├── package.json ├── transform.js └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | trim_trailing_whitespace = true 5 | insert_final_newline = true 6 | indent_style = space 7 | indent_size = 2 8 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text eol=lf 2 | __testfixtures__/*.crlf.input.js text eol=crlf 3 | __testfixtures__/*.crlf.output.js text eol=crlf -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .idea -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "6" 4 | - "8" 5 | 6 | os: 7 | - linux 8 | - osx 9 | 10 | before_deploy: 11 | - npm install -g auto-dist-tag 12 | - auto-dist-tag --write 13 | 14 | deploy: 15 | provider: npm 16 | email: stefan.penner+ember-cli@gmail.com 17 | api_key: 18 | secure: n5Zt1d3Pw2o0E2WP/VAFqJvsq/rK4oJP+2ixrlBAf4WXm1cBQrpoc6+09YuMkqn7Z3S3i3N1WYq7Ebk8If/U3XJ9kdOZSHpsOAU3Wb5e9mWZTv1rdW7A77HLTMcqyURltkpPryHbToUvazfTOT4RUcFPKRqxzQ7LpU3Nj5jpbAylS959aCfdGKAG3MHy4kLqoMQj+x7UxunkL1/hyTCfHAWShwprnxodU52ywdklNErGnvi/giXxCpXDSn9mV3aJ+c//6wshG0yhasmsuZZ14O894qge4OUTkqZwoRL7EGSfRxK1P/0CIWQS3ypMqS7KjdokaPlraZ66GDTpopI2qHpja93DH3b/xcmmntnQ1sfhBtZpY9+5tR2EqabWN6dEBguj+0RaP+3VZuXQrxwtuQ5xND5ib4wM1VZKSbuprwTVqSzIPO3ZW9XH/f4I3GXh44VaFMSU360EGrbxg8VWihHOlurCrxuClsyOME69C1YYXMPMlBUGCTHhTlUZJLItQweLh6JYI7GDha/GPn04VqtHwvMJ4Rz0un+ipJQ9Lj9q6GQo9vHybCdWwxZsl3Rz7yv2q1bY54o9pxCGHT8rAJviEyVLgZk53gRCZ+oxu4a3tRGoLUP/y6uSQG1yaSopRRQwjIxwtYKvq8cK+uKFNciOglf+2LZiBoPf7yDVS54= 19 | on: 20 | tags: true 21 | repo: ember-cli/ember-modules-codemod 22 | 23 | install: 24 | - yarn install --no-lockfile 25 | 26 | script: 27 | - yarn test 28 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Launch", 6 | "type": "node", 7 | "request": "launch", 8 | "program": "${workspaceRoot}/node_modules/jscodeshift/bin/jscodeshift.sh", 9 | "stopOnEntry": false, 10 | "args": ["-t", "transform.js", "-d", "--run-in-band", "src"], 11 | "cwd": "${workspaceRoot}", 12 | "preLaunchTask": null, 13 | "runtimeExecutable": null, 14 | "runtimeArgs": [ 15 | "--nolazy" 16 | ], 17 | "env": { 18 | "NODE_ENV": "development" 19 | }, 20 | "console": "internalConsole", 21 | "sourceMaps": true, 22 | "outFiles": [] 23 | }, 24 | { 25 | "name": "Attach", 26 | "type": "node", 27 | "request": "attach", 28 | "port": 5858, 29 | "address": "localhost", 30 | "restart": false, 31 | "sourceMaps": false, 32 | "outFiles": [], 33 | "localRoot": "${workspaceRoot}", 34 | "remoteRoot": null 35 | }, 36 | { 37 | "name": "Attach to Process", 38 | "type": "node", 39 | "request": "attach", 40 | "processId": "${command.PickProcess}", 41 | "port": 5858, 42 | "sourceMaps": false, 43 | "outFiles": [] 44 | } 45 | ] 46 | } 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ember Modules Codemod 2 | 3 | [![npm version](https://badge.fury.io/js/ember-modules-codemod.svg)](https://badge.fury.io/js/ember-modules-codemod) 4 | [![Build Status](https://travis-ci.org/ember-cli/ember-modules-codemod.svg?branch=master)](https://travis-ci.org/ember-cli/ember-modules-codemod) 5 | [![Build status](https://ci.appveyor.com/api/projects/status/q0tmqlbgdtfnnss7/branch/master?svg=true)](https://ci.appveyor.com/project/embercli/ember-modules-codemod/branch/master) 6 | 7 | This codemod uses [`jscodeshift`](https://github.com/facebook/jscodeshift) to update an Ember application to 8 | import framework code using module syntax, as proposed in [RFC 176: JavaScript Module API](https://github.com/emberjs/rfcs/pull/176). It can update apps that use the global `Ember`, and will eventually also support 9 | apps using [ember-cli-shims][shims]. 10 | 11 | [shims]: https://github.com/ember-cli/ember-cli-shims 12 | 13 | For example, it will rewrite code that looks like this: 14 | 15 | ```js 16 | export default Ember.Component.extend({ 17 | isAnimal: Ember.computed.or('isDog', 'isCat') 18 | }); 19 | ``` 20 | 21 | Into this: 22 | 23 | ```js 24 | import Component from '@ember/component'; 25 | import { or } from '@ember/object/computed' 26 | 27 | export default Component.extend({ 28 | isAnimal: or('isDog', 'isCat') 29 | }); 30 | ``` 31 | 32 | ## Usage 33 | 34 | **WARNING**: `jscodeshift`, and thus this codemod, **edit your files in place**. 35 | It does not make a copy. Make sure your code is checked into a source control 36 | repository like Git and that you have no outstanding changes to commit before 37 | running this tool. 38 | 39 | The simplest way to use the codemod is like this: 40 | 41 | ```sh 42 | npm install ember-modules-codemod -g 43 | cd my-ember-app 44 | ember-modules-codemod 45 | ``` 46 | 47 | Or using `npx`: 48 | 49 | ```sh 50 | npx ember-modules-codemod 51 | ``` 52 | 53 | ### Paths 54 | 55 | By default, `ember-modules-codemod` will apply changes on files in the following folders: 56 | 57 | ``` 58 | app/ 59 | addon/ 60 | addon-test-support/ 61 | lib/ 62 | tests/ 63 | test-support/ 64 | ``` 65 | 66 | You can also execute the codemod in a specific folder: 67 | 68 | ```sh 69 | ember-modules-codemod my-folder-to-modify 70 | ``` 71 | 72 | #### Unknown Globals 73 | 74 | If the codemod finds a use of the `Ember` global it doesn't know how to 75 | translate, it will write a report to `MODULE_REPORT.md`. You can use this report 76 | as the basis for filing support issues or contributing to the RFC. 77 | 78 | #### Standalone 79 | 80 | This package includes an `ember-modules-codemod` binary that wraps `jscodeshift` 81 | and invokes it with the correct configuration when inside the root directory of 82 | an Ember app. 83 | 84 | If you're comfortable with `jscodeshift` already or would rather use it 85 | directly, you can clone this repository and invoke the transform manually: 86 | 87 | ```sh 88 | npm install jscodeshift -g 89 | git clone https://github.com/ember-cli/ember-modules-codemod 90 | cd my-ember-app 91 | jscodeshift -t ../ember-modules-codemod/transform.js app 92 | ``` 93 | 94 | Note that invoking the transform directly disables the generation of the 95 | Markdown report if any unknown globals are discovered. 96 | 97 | ## Contributing 98 | 99 | ### Running Tests 100 | 101 | ```sh 102 | yarn test // run all tests once 103 | yarn test -- --watchAll // continuously run tests 104 | yarn test:debug // run tests in debug mode (using Chrome's chrome://inspect tab) 105 | ``` 106 | 107 | Tests for this codemod work by comparing a paired input and output file in the `__testfixtures__` directory. Pre-transform files should be of format `.input.js`, expected output after the transform should be named `.output.js`. Files must use the same `` in their names so they can be compared. 108 | 109 | ### Transform Bugs 110 | 111 | If you discover a file in your app that the codemod doesn't handle well, please 112 | consider submitting either a fix or a failing test case. 113 | 114 | First, add the file to the `test/input/` directory. Then, make another file with 115 | the identical name and put it in `test/expected-output/`. This file should 116 | contain the JavaScript output you would expected after running the codemod. 117 | 118 | For example, if the codemod fails on a file in my app called 119 | `app/components/my-component.js`, I would copy that file into this repository as 120 | `test/input/my-component.js`. Ideally, I will edit the file to the smallest 121 | possible test case to reproduce the problem (and, obviously, remove any 122 | proprietary code!). I might also wish to give it a more descriptive name, like 123 | `preserve-leading-comment.js`. 124 | 125 | Next, I would copy *that* file into `test/input/my-component.js`, and hand apply 126 | the transformations I'm expecting. 127 | 128 | Then, run `npm test` to run the tests using Mocha. The tests will automatically 129 | compare identically named files in each directory and provide a diff of the 130 | output if they don't match. 131 | 132 | Lastly, make changes to `transform.js` until the tests report they are passing. 133 | 134 | If you are submitting changes to the transform, please include a test case so we 135 | can ensure that future changes do not cause a regression. 136 | 137 | ### Module Changes 138 | 139 | If you want to change how globals are mapped into modules, you will find 140 | the data structure that controls that in the `ember-rfc176-data` npm package. 141 | The structure is: 142 | 143 | ```js 144 | { 145 | "globalPath": ["moduleName", "namedExport"?, "localName"?] 146 | } 147 | ``` 148 | 149 | Only the first item in the array is mandatory. The second item is only needed 150 | for named exports. The third item is only necessary if the local identifier the 151 | import is bound to should be different than named export (or the previous global 152 | version, in the case of default exports). 153 | 154 | A few examples: 155 | 156 | 1. `Ember.Application` ⟹ `'Application': ['ember-application']` ⟹ `import Application from 'ember-application'` 157 | 1. `Ember.computed.or` ⟹ `'computed.or': ['ember-object/computed', 'or']` ⟹ `import { or } from 'ember-object/computed'` 158 | 1. `Ember.DefaultResolver` ⟹ `'DefaultResolver': ['ember-application/globals-resolver', null, 'GlobalsResolver']` ⟹ `import GlobalsResolver from 'ember-application/globals-resolver'` 159 | 160 | ### Known Issues 161 | 162 | There are some limitations in the current implementation that can hopefully be 163 | addressed in the future. PRs welcome! 164 | 165 | * Apps using `ember-cli-shims` are not updated. 166 | * All long imports are beautified, even non-Ember ones. 167 | * Namespace imports (`import * as bar from 'foo'`) are not supported. 168 | -------------------------------------------------------------------------------- /__testfixtures__/cleanup-unused-namespaces.input.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | const { inject, computed } = Ember; 3 | 4 | export default Ember.Controller.extend({ 5 | controller: inject.controller('application'), 6 | router: inject.service('router'), 7 | anotherRouter: computed.alias('router') 8 | }); 9 | -------------------------------------------------------------------------------- /__testfixtures__/cleanup-unused-namespaces.output.js: -------------------------------------------------------------------------------- 1 | import { inject as service } from '@ember/service'; 2 | import { alias } from '@ember/object/computed'; 3 | import Controller, { inject as controller } from '@ember/controller'; 4 | 5 | export default Controller.extend({ 6 | controller: controller('application'), 7 | router: service('router'), 8 | anotherRouter: alias('router') 9 | }); -------------------------------------------------------------------------------- /__testfixtures__/destructure-inject-and-computed.input.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | const { inject, computed } = Ember; 3 | 4 | export default Ember.Controller.extend({ 5 | controller: inject.controller('application'), 6 | router: inject.service('router'), 7 | anotherRouter: computed.alias('router'), 8 | someComputedProperty: computed(function() { return true; }), 9 | someInvalidMacro: computed.foo('bar') 10 | }); 11 | 12 | function notRelated() { 13 | const computed = new SomeThing(); 14 | 15 | return { 16 | foo: computed.not('bar') 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /__testfixtures__/destructure-inject-and-computed.output.js: -------------------------------------------------------------------------------- 1 | import { inject as service } from '@ember/service'; 2 | import { alias } from '@ember/object/computed'; 3 | import Controller, { inject as controller } from '@ember/controller'; 4 | import { computed } from '@ember/object'; 5 | 6 | export default Controller.extend({ 7 | controller: controller('application'), 8 | router: service('router'), 9 | anotherRouter: alias('router'), 10 | someComputedProperty: computed(function() { return true; }), 11 | someInvalidMacro: computed.foo('bar') 12 | }); 13 | 14 | function notRelated() { 15 | const computed = new SomeThing(); 16 | 17 | return { 18 | foo: computed.not('bar') 19 | }; 20 | } 21 | -------------------------------------------------------------------------------- /__testfixtures__/destructure-nested-namespace.input.js: -------------------------------------------------------------------------------- 1 | const { underscore } = Ember.String; 2 | underscore('hello-world'); 3 | -------------------------------------------------------------------------------- /__testfixtures__/destructure-nested-namespace.output.js: -------------------------------------------------------------------------------- 1 | import { underscore } from '@ember/string'; 2 | underscore('hello-world'); 3 | -------------------------------------------------------------------------------- /__testfixtures__/final-boss.input.js: -------------------------------------------------------------------------------- 1 | // Test cases: 2 | // * This comment gets preserved at the top of the file. 3 | // * Re-uses existing aliases if already specified 4 | // * Chooses appropriate alias if not specified 5 | // * Renames local name if it conflicts with a reserved word like Object 6 | // * Uses renamed local name if it already exists 7 | // * Adds default export to named exports if they already exist 8 | // * Adds named exports to default export if it already exists 9 | // * Handles ambiguous cases (computed.or _and_ computed) 10 | // * Variables named `Ember` are not considered 11 | // * Manual aliasing (`var Component = Ember.Component` is removed) 12 | // * `Ember` must be the root of property lookups (no `foo.Ember.bar`) 13 | // * Deep destructured aliases are resolved (`String.underscore`) 14 | // * Renamed destructured aliases are preserved (`get: myGet`) 15 | // * Fully modularized destructuring statements are removed 16 | import FemberObject from "@ember/object"; 17 | import { or as bore } from "@ember/object/computed"; 18 | import Ember from 'ember'; 19 | 20 | let bar = foo.Ember.computed.or; 21 | 22 | const Component = Ember.Component; 23 | 24 | const { 25 | get: myGet, 26 | String: { 27 | underscore 28 | } 29 | } = Ember; 30 | 31 | export default Ember.Object.extend({ 32 | postCountsPresent: Ember.computed.or('topic.unread', 'topic.displayNewPosts'), 33 | showBadges: Ember.computed.and('postBadgesEnabled', 'postCountsPresent') 34 | }); 35 | 36 | export default Component.extend({ 37 | topicExists: Ember.computed.or('topic.foo', 'topic.bar'), 38 | topicSlug: Ember.computed(function() { 39 | return underscore(myGet(this, 'topic.name')); 40 | }) 41 | }); 42 | 43 | (function() { 44 | let Ember = {}; 45 | Ember.Component = class Component { 46 | }; 47 | })(); 48 | 49 | export default Ember.Array.extend({ 50 | firstName: Ember.computed(function(foo) { 51 | }) 52 | }); 53 | -------------------------------------------------------------------------------- /__testfixtures__/final-boss.output.js: -------------------------------------------------------------------------------- 1 | // Test cases: 2 | // * This comment gets preserved at the top of the file. 3 | // * Re-uses existing aliases if already specified 4 | // * Chooses appropriate alias if not specified 5 | // * Renames local name if it conflicts with a reserved word like Object 6 | // * Uses renamed local name if it already exists 7 | // * Adds default export to named exports if they already exist 8 | // * Adds named exports to default export if it already exists 9 | // * Handles ambiguous cases (computed.or _and_ computed) 10 | // * Variables named `Ember` are not considered 11 | // * Manual aliasing (`var Component = Ember.Component` is removed) 12 | // * `Ember` must be the root of property lookups (no `foo.Ember.bar`) 13 | // * Deep destructured aliases are resolved (`String.underscore`) 14 | // * Renamed destructured aliases are preserved (`get: myGet`) 15 | // * Fully modularized destructuring statements are removed 16 | import EmberArray from '@ember/array'; 17 | 18 | import { underscore } from '@ember/string'; 19 | import Component from '@ember/component'; 20 | import FemberObject, { 21 | get as myGet, 22 | computed 23 | } from "@ember/object"; 24 | import { or as bore, and } from "@ember/object/computed"; 25 | 26 | let bar = foo.Ember.computed.or; 27 | 28 | export default FemberObject.extend({ 29 | postCountsPresent: bore('topic.unread', 'topic.displayNewPosts'), 30 | showBadges: and('postBadgesEnabled', 'postCountsPresent') 31 | }); 32 | 33 | export default Component.extend({ 34 | topicExists: bore('topic.foo', 'topic.bar'), 35 | topicSlug: computed(function() { 36 | return underscore(myGet(this, 'topic.name')); 37 | }) 38 | }); 39 | 40 | (function() { 41 | let Ember = {}; 42 | Ember.Component = class Component { 43 | }; 44 | })(); 45 | 46 | export default EmberArray.extend({ 47 | firstName: computed(function(foo) { 48 | }) 49 | }); 50 | -------------------------------------------------------------------------------- /__testfixtures__/fix-destructure-inject-and-computed.input.js: -------------------------------------------------------------------------------- 1 | import Controller from '@ember/controller'; 2 | import { computed } from '@ember/object'; 3 | import Ember from 'ember'; 4 | const { 5 | inject 6 | } = Ember; 7 | 8 | export default Controller.extend({ 9 | controller: inject.controller('application'), 10 | router: inject.service('router'), 11 | anotherRouter: computed.alias('router'), 12 | someComputedProperty: computed(function() { return true; }), 13 | someInvalidMacro: computed.foo('bar') 14 | }); 15 | 16 | function notRelated() { 17 | const computed = new SomeThing(); 18 | 19 | return { 20 | foo: computed.not('bar') 21 | }; 22 | } 23 | -------------------------------------------------------------------------------- /__testfixtures__/fix-destructure-inject-and-computed.output.js: -------------------------------------------------------------------------------- 1 | import { inject as service } from '@ember/service'; 2 | import { alias } from '@ember/object/computed'; 3 | import Controller, { inject as controller } from '@ember/controller'; 4 | import { computed } from '@ember/object'; 5 | 6 | export default Controller.extend({ 7 | controller: controller('application'), 8 | router: service('router'), 9 | anotherRouter: alias('router'), 10 | someComputedProperty: computed(function() { return true; }), 11 | someInvalidMacro: computed.foo('bar') 12 | }); 13 | 14 | function notRelated() { 15 | const computed = new SomeThing(); 16 | 17 | return { 18 | foo: computed.not('bar') 19 | }; 20 | } 21 | -------------------------------------------------------------------------------- /__testfixtures__/helper.input.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export function myHelper() { 4 | } 5 | 6 | export default Ember.Helper.helper(myHelper); 7 | -------------------------------------------------------------------------------- /__testfixtures__/helper.output.js: -------------------------------------------------------------------------------- 1 | import { helper as buildHelper } from '@ember/component/helper'; 2 | 3 | export function myHelper() { 4 | } 5 | 6 | export default buildHelper(myHelper); 7 | -------------------------------------------------------------------------------- /__testfixtures__/leave-destructured-name.input.js: -------------------------------------------------------------------------------- 1 | const { get: pleaseGet } = Ember; 2 | 3 | pleaseGet(x, y); 4 | Ember.get(x, y); 5 | -------------------------------------------------------------------------------- /__testfixtures__/leave-destructured-name.output.js: -------------------------------------------------------------------------------- 1 | import { get as pleaseGet } from '@ember/object'; 2 | 3 | pleaseGet(x, y); 4 | pleaseGet(x, y); 5 | -------------------------------------------------------------------------------- /__testfixtures__/leave-ember-test.crlf.input.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Component.extend({ 4 | a: Ember.testing ? 1 : 2, 5 | }); 6 | -------------------------------------------------------------------------------- /__testfixtures__/leave-ember-test.crlf.output.js: -------------------------------------------------------------------------------- 1 | import Component from '@ember/component'; 2 | import Ember from 'ember'; 3 | 4 | export default Component.extend({ 5 | a: Ember.testing ? 1 : 2, 6 | }); 7 | -------------------------------------------------------------------------------- /__testfixtures__/leave-ember-test.input.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Component.extend({ 4 | a: Ember.testing ? 1 : 2, 5 | }); 6 | -------------------------------------------------------------------------------- /__testfixtures__/leave-ember-test.output.js: -------------------------------------------------------------------------------- 1 | import Component from '@ember/component'; 2 | import Ember from 'ember'; 3 | 4 | export default Component.extend({ 5 | a: Ember.testing ? 1 : 2, 6 | }); 7 | -------------------------------------------------------------------------------- /__testfixtures__/leave-emberk.input.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Component.extend({ 4 | a: Ember.K 5 | }); 6 | -------------------------------------------------------------------------------- /__testfixtures__/leave-emberk.output.js: -------------------------------------------------------------------------------- 1 | import Component from '@ember/component'; 2 | import Ember from 'ember'; 3 | 4 | export default Component.extend({ 5 | a: Ember.K 6 | }); 7 | -------------------------------------------------------------------------------- /__testfixtures__/leave-unmatched-destructuring.input.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | const { 4 | String: { 5 | pluralize, 6 | camelize 7 | } 8 | } = Ember; 9 | 10 | pluralize('one'); 11 | camelize('two'); 12 | -------------------------------------------------------------------------------- /__testfixtures__/leave-unmatched-destructuring.output.js: -------------------------------------------------------------------------------- 1 | import { camelize } from '@ember/string'; 2 | import Ember from 'ember'; 3 | 4 | const { 5 | String: { 6 | pluralize 7 | } 8 | } = Ember; 9 | 10 | pluralize('one'); 11 | camelize('two'); 12 | -------------------------------------------------------------------------------- /__testfixtures__/multi-level-destructuring.input.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | const { computed, inject, String } = Ember; 4 | const { oneWay } = computed; 5 | const { service } = inject; 6 | const { camelize } = String; 7 | 8 | export default Ember.Component.extend({ 9 | barService: service('bar'), 10 | name: oneWay('userName'), 11 | foo() { 12 | camelize('bar'); 13 | } 14 | }); 15 | -------------------------------------------------------------------------------- /__testfixtures__/multi-level-destructuring.output.js: -------------------------------------------------------------------------------- 1 | import Component from '@ember/component'; 2 | import { camelize } from '@ember/string'; 3 | import { inject as service } from '@ember/service'; 4 | import { oneWay } from '@ember/object/computed'; 5 | 6 | export default Component.extend({ 7 | barService: service('bar'), 8 | name: oneWay('userName'), 9 | foo() { 10 | camelize('bar'); 11 | } 12 | }); 13 | -------------------------------------------------------------------------------- /__testfixtures__/named-then-default-imports.input.js: -------------------------------------------------------------------------------- 1 | Ember.computed; 2 | Ember.Object; 3 | -------------------------------------------------------------------------------- /__testfixtures__/named-then-default-imports.output.js: -------------------------------------------------------------------------------- 1 | import EmberObject, { computed } from '@ember/object'; 2 | computed; 3 | EmberObject; 4 | -------------------------------------------------------------------------------- /__testfixtures__/remove-ember-import.input.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Component.extend({ 4 | }); 5 | -------------------------------------------------------------------------------- /__testfixtures__/remove-ember-import.output.js: -------------------------------------------------------------------------------- 1 | import Component from '@ember/component'; 2 | 3 | export default Component.extend({ 4 | }); 5 | -------------------------------------------------------------------------------- /__testfixtures__/top-comment.input.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This comment should be preserved, and included before the inserted import statements. 3 | */ 4 | import Ember from 'ember'; 5 | 6 | export default Ember.Component.extend({ 7 | }); 8 | -------------------------------------------------------------------------------- /__testfixtures__/top-comment.output.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This comment should be preserved, and included before the inserted import statements. 3 | */ 4 | import Component from '@ember/component'; 5 | 6 | export default Component.extend({ 7 | }); 8 | -------------------------------------------------------------------------------- /__tests__/bin-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const fs = require('fs-extra'); 5 | const cp = require('child_process'); 6 | const tmp = require('tmp'); 7 | 8 | const originalCwd = process.cwd(); 9 | 10 | let tmpPath; 11 | 12 | function run(codemodArgs) { 13 | let stdout = ''; 14 | let stderr = ''; 15 | 16 | return new Promise(resolve => { 17 | let codemodCmd = [path.join(originalCwd, 'bin/ember-modules-codemod')]; 18 | if(codemodArgs) { 19 | codemodCmd = codemodCmd.concat(codemodArgs); 20 | } 21 | let ps = cp.spawn('node', codemodCmd, { 22 | cwd: tmpPath 23 | }); 24 | 25 | ps.stdout.on('data', data => { 26 | stdout += data.toString(); 27 | }); 28 | 29 | ps.stderr.on('data', data => { 30 | stderr += data.toString(); 31 | }); 32 | 33 | ps.on('exit', code => { 34 | resolve({ 35 | exitCode: code, 36 | stdout, 37 | stderr 38 | }); 39 | }); 40 | }); 41 | } 42 | 43 | describe('bin acceptance', function() { 44 | let tmpPackageJson; 45 | 46 | beforeEach(function() { 47 | tmpPath = tmp.dirSync().name; 48 | 49 | process.chdir(tmpPath); 50 | 51 | tmpPackageJson = path.join(process.cwd(), 'package.json'); 52 | }); 53 | 54 | afterAll(function() { 55 | process.chdir(originalCwd); 56 | }); 57 | 58 | it('handles non-ember projects', function() { 59 | return run().then(result => { 60 | let exitCode = result.exitCode; 61 | let stderr = result.stderr; 62 | 63 | expect(exitCode).not.toEqual(0); 64 | 65 | expect(stderr).toEqual(`It doesn't look like you're inside an Ember app. I couldn't find a package.json at ${tmpPackageJson}\n`); 66 | }); 67 | }); 68 | 69 | describe('with valid package.json', function() { 70 | beforeEach(function() { 71 | fs.writeJsonSync(tmpPackageJson, { 72 | devDependencies: { 73 | 'ember-cli': '' 74 | } 75 | }); 76 | }); 77 | 78 | it('exits gracefully when no files found', function() { 79 | return run().then(result => { 80 | let exitCode = result.exitCode; 81 | let stderr = result.stderr; 82 | 83 | expect(exitCode).toEqual(0); 84 | 85 | // jscodeshift can process in any order 86 | expect(stderr).toMatch('Skipping path app which does not exist.'); 87 | expect(stderr).toMatch('Skipping path addon which does not exist.'); 88 | expect(stderr).toMatch('Skipping path addon-test-support which does not exist.'); 89 | expect(stderr).toMatch('Skipping path tests which does not exist.'); 90 | expect(stderr).toMatch('Skipping path test-support which does not exist.'); 91 | expect(stderr).toMatch('Skipping path lib which does not exist.'); 92 | }); 93 | }); 94 | 95 | describe('with valid file', function() { 96 | let tmpFile; 97 | const inputFile = path.join(originalCwd, '__testfixtures__/final-boss.input.js'); 98 | const outputFile = path.join(originalCwd, '__testfixtures__/final-boss.output.js'); 99 | 100 | 101 | beforeEach(function() { 102 | fs.ensureDirSync(path.join(tmpPath, 'app')); 103 | 104 | tmpFile = path.join(tmpPath, 'app/final-boss.js'); 105 | 106 | fs.copySync( 107 | inputFile, 108 | tmpFile 109 | ); 110 | }); 111 | 112 | it('works', function() { 113 | return run().then(result => { 114 | let exitCode = result.exitCode; 115 | let stdout = result.stdout; 116 | 117 | expect(exitCode).toEqual(0); 118 | 119 | expect(stdout).toMatch('Done! All uses of the Ember global have been updated.\n'); 120 | 121 | expect(fs.readFileSync(tmpFile, 'utf8')).toEqual(fs.readFileSync(outputFile, 'utf8')); 122 | }); 123 | }); 124 | }); 125 | 126 | describe('with invalid file', function() { 127 | const inputFile = path.join(originalCwd, '__testfixtures__/leave-unmatched-destructuring.input.js'); 128 | 129 | beforeEach(function() { 130 | fs.ensureDirSync(path.join(tmpPath, 'app')); 131 | 132 | let tmpFile = path.join(tmpPath, 'app/leave-unmatched-destructuring.input.js'); 133 | 134 | fs.copySync( 135 | inputFile, 136 | tmpFile 137 | ); 138 | }); 139 | 140 | it('reports errors', function() { 141 | return run().then(result => { 142 | let exitCode = result.exitCode; 143 | let stdout = result.stdout; 144 | 145 | expect(exitCode).toEqual(0); 146 | 147 | expect(stdout).toMatch('Done! Some files could not be upgraded automatically. See MODULE_REPORT.md.\n'); 148 | 149 | let expectedOutput = fs.readFileSync(path.join(__dirname, 'expected', 'MODULE_REPORT.md'), 'utf8') 150 | .replace(/\//, path.sep) 151 | .replace(/\r?\n/g, require('os').EOL); 152 | let actualOutput = fs.readFileSync(path.join(tmpPath, 'MODULE_REPORT.md'), 'utf8'); 153 | 154 | expect(actualOutput).toEqual(expectedOutput); 155 | }); 156 | }); 157 | }); 158 | 159 | describe('with a custom path', function() { 160 | let tmpFile; 161 | const inputFile = path.join(originalCwd, '__testfixtures__/final-boss.input.js'); 162 | const outputFile = path.join(originalCwd, '__testfixtures__/final-boss.output.js'); 163 | 164 | 165 | beforeEach(function() { 166 | fs.ensureDirSync(path.join(tmpPath, 'custom-path')); 167 | 168 | tmpFile = path.join(tmpPath, 'custom-path/final-boss.js'); 169 | 170 | fs.copySync( 171 | inputFile, 172 | tmpFile 173 | ); 174 | }); 175 | 176 | it('works', function() { 177 | return run('custom-path').then(result => { 178 | let exitCode = result.exitCode; 179 | let stdout = result.stdout; 180 | 181 | expect(exitCode).toEqual(0); 182 | 183 | expect(stdout).toMatch('Done! All uses of the Ember global have been updated.\n'); 184 | 185 | expect(fs.readFileSync(tmpFile, 'utf8')).toEqual(fs.readFileSync(outputFile, 'utf8')); 186 | }); 187 | }); 188 | }); 189 | }); 190 | }); 191 | -------------------------------------------------------------------------------- /__tests__/expected/MODULE_REPORT.md: -------------------------------------------------------------------------------- 1 | ## Module Report 2 | ### Unknown Global 3 | 4 | **Global**: `Ember.String.pluralize` 5 | 6 | **Location**: `app/leave-unmatched-destructuring.input.js` at line 5 7 | 8 | ```js 9 | const { 10 | String: { 11 | pluralize, 12 | camelize 13 | } 14 | ``` 15 | -------------------------------------------------------------------------------- /__tests__/transform-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require("fs"); 4 | const defineTest = require('jscodeshift/dist/testUtils').defineTest; 5 | 6 | const fixturesPath = `${__dirname}/../__testfixtures__`; 7 | 8 | fs.readdirSync(fixturesPath).forEach(fixture => { 9 | let match = fixture.match(/(.*)\.input\.js$/); 10 | if (match) { 11 | defineTest(__dirname, 'transform', {}, match[1]); 12 | } 13 | }); 14 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | environment: 2 | matrix: 3 | - nodejs_version: "6" 4 | - nodejs_version: "8" 5 | 6 | # Fix line endings in Windows. (runs before repo cloning) 7 | init: 8 | - git config --global core.autocrlf true 9 | 10 | # Install scripts. (runs after repo cloning) 11 | install: 12 | - ps: Install-Product node $env:nodejs_version 13 | - appveyor-retry yarn 14 | 15 | # Post-install test scripts. 16 | test_script: 17 | - node --version 18 | - yarn test 19 | 20 | # http://help.appveyor.com/discussions/questions/1310-delete-cache 21 | cache: 22 | - '%LOCALAPPDATA%\Yarn' 23 | 24 | # Don't actually build. 25 | build: off 26 | -------------------------------------------------------------------------------- /bin/ember-modules-codemod.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict'; 3 | 4 | const fs = require("fs"); 5 | const execa = require('execa'); 6 | const chalk = require("chalk"); 7 | const path = require("path"); 8 | const glob = require("glob"); 9 | 10 | let cwd = process.cwd(); 11 | let pkgPath = path.join(cwd, 'package.json'); 12 | 13 | try { 14 | let pkg = JSON.parse(fs.readFileSync(pkgPath)); 15 | if (!isEmberApp(pkg)) { 16 | notAnEmberApp("I couldn't find ember-cli in the dependencies of " + pkgPath); 17 | } 18 | 19 | let binPath = path.dirname(require.resolve("jscodeshift")) + "/bin/jscodeshift.sh"; 20 | let transformPath = __dirname + "/../transform.js"; 21 | let env = Object.assign({ 22 | EMBER_MODULES_CODEMOD: true 23 | }, process.env); 24 | 25 | const args = process.argv; 26 | let jscodeshiftPaths; 27 | if (args[2]) { 28 | jscodeshiftPaths = [args[2]]; 29 | } else { 30 | jscodeshiftPaths = ["app", "addon", "addon-test-support", "tests", "test-support", "lib"]; 31 | } 32 | const jscodeshiftArgs = ["-t", transformPath].concat(jscodeshiftPaths); 33 | 34 | let transform = execa(binPath, jscodeshiftArgs, { 35 | stdio: "inherit", 36 | env: env 37 | }); 38 | 39 | // Generate MODULE_REPORT.md when jscodeshift is done running. 40 | transform.on("exit", buildReport); 41 | } catch (e) { 42 | if (e.code === "ENOENT") { 43 | notAnEmberApp("I couldn't find a package.json at " + pkgPath); 44 | } else { 45 | console.error(chalk.red(e.stack)); 46 | process.exit(-1); 47 | } 48 | } 49 | 50 | function isEmberApp(pkg) { 51 | return contains("ember-cli", pkg.devDependencies) || contains("ember-cli", pkg.dependencies); 52 | } 53 | 54 | function contains(key, object) { 55 | if (!object) { return false; } 56 | return key in object; 57 | } 58 | 59 | function notAnEmberApp(msg) { 60 | console.error(chalk.red("It doesn't look like you're inside an Ember app. " + msg)); 61 | process.exit(-1); 62 | } 63 | 64 | // Each worker process in jscodeshift will write to a file with its pid used to 65 | // make the path unique. This post-transform step aggregates all of those files 66 | // into a single Markdown report. 67 | function buildReport() { 68 | let report = []; 69 | 70 | // Find all of the temporary logs from the worker processes, which contain a 71 | // serialized JSON array on each line. 72 | glob("ember-modules-codemod.tmp.*", (err, logs) => { 73 | // If no worker found an unexpected value, nothing to report. 74 | if (!logs) { 75 | return; 76 | } 77 | 78 | // For each worker, split its log by line and eval each line 79 | // as JSON. 80 | logs.forEach(log => { 81 | let logText = fs.readFileSync(log); 82 | logText 83 | .toString() 84 | .split("\n") 85 | .forEach(line => { 86 | if (line) { 87 | try { 88 | report.push(JSON.parse(line)); 89 | } catch (e) { 90 | console.log("Error parsing " + line); 91 | } 92 | } 93 | }); 94 | 95 | // Delete the temporary log file 96 | fs.unlinkSync(log); 97 | }); 98 | 99 | // If there's anything to report, convert the JSON tuple into human-formatted 100 | // markdown and write it to MODULE_REPORT.md. 101 | if (report.length) { 102 | report = report.map(line => { 103 | let type = line[0]; 104 | if (type === 1) { 105 | return runtimeErrorWarning(line); 106 | } else { 107 | return unknownGlobalWarning(line); 108 | } 109 | }); 110 | 111 | let file = "## Module Report\n" + report.join("\n"); 112 | 113 | // normalize line endings, so we don't end up with mixed 114 | file = file.replace(/\r?\n/g, require('os').EOL); 115 | 116 | fs.writeFileSync("MODULE_REPORT.md", file); 117 | console.log(chalk.yellow("\nDone! Some files could not be upgraded automatically. See " + chalk.blue("MODULE_REPORT.md") + ".")); 118 | } else { 119 | console.log(chalk.green("\nDone! All uses of the Ember global have been updated.")); 120 | } 121 | }); 122 | } 123 | 124 | function runtimeErrorWarning(line) { 125 | let path = line[1]; 126 | let source = line[2]; 127 | let err = line[3]; 128 | 129 | return `### Runtime Error 130 | 131 | **Path**: \`${path}\` 132 | 133 | **Error**: 134 | 135 | \`\`\` 136 | ${err} 137 | \`\`\` 138 | 139 | **Source**: 140 | 141 | \`\`\`js 142 | ${source} 143 | \`\`\` 144 | `; 145 | } 146 | 147 | function unknownGlobalWarning(line) { 148 | let global = line[1]; 149 | let lineNumber = line[2]; 150 | let path = line[3]; 151 | let context = line[4]; 152 | 153 | return `### Unknown Global 154 | 155 | **Global**: \`Ember.${global}\` 156 | 157 | **Location**: \`${path}\` at line ${lineNumber} 158 | 159 | \`\`\`js 160 | ${context} 161 | \`\`\` 162 | `; 163 | } 164 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ember-modules-codemod", 3 | "version": "0.3.1", 4 | "author": "Tom Dale ", 5 | "license": "MIT", 6 | "repository": "https://github.com/ember-cli/ember-modules-codemod", 7 | "bin": "./bin/ember-modules-codemod.js", 8 | "scripts": { 9 | "test": "jest", 10 | "test:debug": "node --inspect node_modules/.bin/jest --runInBand" 11 | }, 12 | "dependencies": { 13 | "chalk": "^2.4.2", 14 | "ember-rfc176-data": "^0.3.16", 15 | "execa": "~1.0.0", 16 | "glob": "^7.1.6", 17 | "jscodeshift": "^0.3.29" 18 | }, 19 | "devDependencies": { 20 | "fs-extra": "^8.1.0", 21 | "jest": "^24.9.0", 22 | "tmp": "^0.1.0" 23 | }, 24 | "engines": { 25 | "node": "6.* || >= 8.*" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /transform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require("fs"); 4 | const RESERVED = require("ember-rfc176-data/reserved"); 5 | const MAPPINGS = require("ember-rfc176-data"); 6 | 7 | const LOG_FILE = "ember-modules-codemod.tmp." + process.pid; 8 | const ERROR_WARNING = 1; 9 | const MISSING_GLOBAL_WARNING = 2; 10 | 11 | const OPTS = { 12 | quote: 'single' 13 | }; 14 | 15 | const EMBER_NAMESPACES = ['computed', 'inject']; 16 | 17 | module.exports = transform; 18 | 19 | /** 20 | * This is the entry point for this jscodeshift transform. 21 | * It scans JavaScript files that use the Ember global and updates 22 | * them to use the module syntax from the proposed new RFC. 23 | */ 24 | function transform(file, api/*, options*/) { 25 | let source = file.source; 26 | 27 | const lineTerminator = source.indexOf('\r\n') > -1 ? '\r\n' : '\n'; 28 | 29 | let j = api.jscodeshift; 30 | 31 | let root = j(source); 32 | 33 | // Track any use of `Ember.*` that isn't accounted for in the mapping. We'll 34 | // use this at the end to generate a report. 35 | let warnings = []; 36 | 37 | let pendingGlobals = {}; 38 | 39 | try { 40 | // Discover existing module imports, if any, in the file. If the user has 41 | // already imported one or more exports that we rewrite a global with, we 42 | // won't import them again. We also try to be smart about not adding multiple 43 | // import statements to import from the same module, condensing default 44 | // exports and named exports into one line if necessary. 45 | let modules = findExistingModules(root); 46 | 47 | // Build a data structure that tells us how to map properties on the Ember 48 | // global into the module syntax. 49 | let mappings = buildMappings(modules); 50 | 51 | let globalEmber = getGlobalEmberName(root); 52 | 53 | // find all usages of namespaces like `computed.alias` 54 | // we have to do it here before `findGlobalEmberAliases`, as this might remove namespace destructurings like 55 | // `const { computed } = Ember` as `Ember.computed` is both a valid function with a named module import as well as 56 | // a namespace. And we need to check those variable declarations to prevent false positives 57 | let namespaces = EMBER_NAMESPACES; 58 | let namespaceUsages = EMBER_NAMESPACES.map(namespace => ({ 59 | namespace, 60 | usages: findNamespaceUsage(root, globalEmber, namespace) 61 | })); 62 | 63 | // Discover global aliases for Ember keys that are introduced via destructuring, 64 | // e.g. `const { String: { underscore } } = Ember;`. 65 | let globalAliases = findGlobalEmberAliases(root, globalEmber, mappings); 66 | 67 | // Go through all of the tracked pending Ember globals. The ones that have 68 | // been marked as missing should be added to the warnings. 69 | resolvePendingGlobals(); 70 | 71 | // Resolve the discovered aliases against the module registry. We intentionally do 72 | // this ahead of finding replacements for e.g. `Ember.String.underscore` usage in 73 | // order to reuse custom names for any fields referenced both ways. 74 | resolveAliasImports(globalAliases, mappings, modules, root); 75 | 76 | // Scan the source code, looking for any instances of the `Ember` identifier 77 | // used as the root of a property lookup. If they match one of the provided 78 | // mappings, save it off for replacement later. 79 | let replacements = findUsageOfEmberGlobal(root, globalEmber) 80 | .map(findReplacement(mappings)); 81 | // add the already found namespace replacements to our replacement array 82 | for (let ns of namespaceUsages) { 83 | let namespaceReplacements = ns.usages 84 | .map(findReplacement(mappings, ns.namespace)); 85 | 86 | replacements = replacements.concat(namespaceReplacements); 87 | } 88 | 89 | // Now that we've identified all of the replacements that we need to do, we'll 90 | // make sure to either add new `import` declarations, or update existing ones 91 | // to add new named exports or the default export. 92 | updateOrCreateImportDeclarations(root, modules); 93 | 94 | // Actually go through and replace each usage of `Ember.whatever` with the 95 | // imported binding (`whatever`). 96 | applyReplacements(replacements); 97 | 98 | // findGlobalEmberAliases might have removed destructured namespaces that are also valid functions themselves 99 | // like `Ember.computed`. But other namespaces like `Ember.inject` might have been left over, so remove them here 100 | removeNamespaces(root, globalEmber, namespaces); 101 | 102 | // Finally remove global Ember import if no globals left 103 | removeGlobalEmber(root, globalEmber); 104 | 105 | // jscodeshift is not so great about giving us control over the resulting whitespace. 106 | // We'll use a regular expression to try to improve the situation (courtesy of @rwjblue). 107 | source = beautifyImports(root.toSource(Object.assign({}, OPTS, { 108 | lineTerminator: lineTerminator 109 | }))); 110 | } catch(e) { 111 | if (process.env.EMBER_MODULES_CODEMOD) { 112 | warnings.push([ERROR_WARNING, file.path, source, e.stack]); 113 | } 114 | 115 | throw e; 116 | } finally { 117 | // If there were modules that we didn't know about, write them to a log file. 118 | // We only do this if invoked via the CLI tool, not jscodeshift directly, 119 | // because jscodeshift doesn't give us a cleanup hook when everything is done 120 | // to parse these files. (This is what the environment variable is checking.) 121 | if (warnings.length && process.env.EMBER_MODULES_CODEMOD) { 122 | warnings.forEach(warning => { 123 | fs.appendFileSync(LOG_FILE, JSON.stringify(warning) + "\n"); 124 | }); 125 | } 126 | } 127 | 128 | return source; 129 | 130 | /** 131 | * Loops through the raw JSON data in `mapping.json` and converts each entry 132 | * into a Mapping instance. The Mapping class lazily reifies its associated 133 | * module as they it is consumed. 134 | */ 135 | function buildMappings(registry) { 136 | let mappings = {}; 137 | 138 | for (let mapping of MAPPINGS) { 139 | if (!mapping.deprecated) { 140 | mappings[mapping.global.substr('Ember.'.length)] = new Mapping(mapping, registry); 141 | } 142 | } 143 | 144 | return mappings; 145 | } 146 | 147 | function getGlobalEmberImport(root) { 148 | return root.find(j.ImportDeclaration, { 149 | specifiers: [{ 150 | type: "ImportDefaultSpecifier", 151 | }], 152 | source: { 153 | value: "ember" 154 | } 155 | }); 156 | } 157 | 158 | function getGlobalEmberName(root) { 159 | const globalEmber = getGlobalEmberImport(root); 160 | 161 | let defaultImport = globalEmber.find(j.Identifier); 162 | let defaultMemberName = defaultImport.size() && defaultImport.get(0).node.name; 163 | 164 | return defaultMemberName || "Ember"; 165 | } 166 | 167 | /* 168 | * Finds all uses of a property looked up on the Ember global (i.e., 169 | * `Ember.something`). Makes sure that it is actually the Ember global 170 | * and not another variable that happens to be called `Ember`. 171 | */ 172 | function findUsageOfEmberGlobal(root, globalEmber) { 173 | let emberUsages = root.find(j.MemberExpression, { 174 | object: { 175 | name: globalEmber, 176 | }, 177 | }); 178 | 179 | return emberUsages.filter(isEmberGlobal(globalEmber)).paths(); 180 | } 181 | 182 | // Find destructured global aliases for fields on the Ember global 183 | function findGlobalEmberAliases(root, globalEmber, mappings) { 184 | let aliases = {}; 185 | let assignments = findUsageOfDestructuredEmber(root, globalEmber); 186 | for (let assignment of assignments) { 187 | let emberPath = joinEmberPath(assignment.get('init'), globalEmber); 188 | for (let alias of extractAliases(mappings, assignment.get('id'), emberPath)) { 189 | aliases[alias.identifier.node.name] = alias; 190 | } 191 | } 192 | return aliases; 193 | } 194 | 195 | function findUsageOfDestructuredEmber(root, globalEmber) { 196 | // Keep track of the nested properties off of the Ember namespace, 197 | // to support multi-statement destructuring, i.e.: 198 | // const { computed } = Ember; 199 | // const { oneWay } = computed; 200 | let globalEmberWithNestedProperties = [globalEmber]; 201 | let uses = root.find(j.VariableDeclarator, (node) => { 202 | if (j.Identifier.check(node.init)) { 203 | if (includes(globalEmberWithNestedProperties, node.init.name)) { 204 | // We've found an Ember global, or one of its nested properties. 205 | // Add it to the uses, and add its properties to the list of nested properties 206 | const identifierProperties = getIdentifierProperties(node); 207 | globalEmberWithNestedProperties = globalEmberWithNestedProperties.concat(identifierProperties); 208 | return true; 209 | } 210 | } else if (j.MemberExpression.check(node.init)) { 211 | return node.init.object.name === globalEmber; 212 | } 213 | }); 214 | 215 | return uses.paths(); 216 | } 217 | 218 | function resolvePendingGlobals() { 219 | Object.keys(pendingGlobals).forEach((key) => { 220 | let pendingGlobal = pendingGlobals[key]; 221 | const parentPath = pendingGlobal.pattern.parentPath; 222 | if (!pendingGlobal.hasMissingGlobal) { 223 | parentPath.prune(); 224 | } else { 225 | warnMissingGlobal(parentPath, pendingGlobal.emberPath); 226 | } 227 | }) 228 | } 229 | 230 | function getIdentifierProperties(node) { 231 | let identifierProperties = []; 232 | node.id.properties.forEach((property) => { 233 | if (j.Identifier.check(property.value)) { 234 | identifierProperties.push(property.key.name); 235 | } 236 | }); 237 | 238 | return identifierProperties; 239 | } 240 | 241 | function joinEmberPath(nodePath, globalEmber) { 242 | if (j.Identifier.check(nodePath.node)) { 243 | if (nodePath.node.name !== globalEmber) { 244 | return nodePath.node.name; 245 | } 246 | } else if (j.MemberExpression.check(nodePath.node)) { 247 | let lhs = nodePath.node.object.name; 248 | let rhs = joinEmberPath(nodePath.get('property')); 249 | if (lhs === globalEmber) { 250 | return rhs; 251 | } else { 252 | return `${lhs}.${rhs}`; 253 | } 254 | } 255 | } 256 | 257 | // Determine aliases introduced by the given destructuring pattern, removing 258 | // items from the pattern when they're available via a module import instead. 259 | // Also tracks and flags pending globals for future patterns, 260 | // in case we have multi-statement destructuring, i.e: 261 | // const { computed } = Ember; 262 | // const { oneWay } = computed; 263 | function extractAliases(mappings, pattern, emberPath) { 264 | if (j.Identifier.check(pattern.node)) { 265 | if (emberPath in mappings) { 266 | pattern.parentPath.prune(); 267 | const pendingGlobalParent = findPendingGlobal(emberPath); 268 | if (pendingGlobalParent) { 269 | // A parent has been found. Mark it as no longer being missing. 270 | pendingGlobalParent.hasMissingGlobal = false; 271 | } 272 | 273 | return [new GlobalAlias(pattern, emberPath)]; 274 | } else { 275 | let thisPatternHasMissingGlobal = false; 276 | const pendingGlobalParent = findPendingGlobal(emberPath); 277 | if (pendingGlobalParent) { 278 | // A parent has been found. Mark it as a missing global. 279 | pendingGlobalParent.hasMissingGlobal = true; 280 | } else { 281 | // Otherwise, mark this pattern as a missing global. 282 | thisPatternHasMissingGlobal = true; 283 | } 284 | 285 | // Add this pattern to pendingGlobals 286 | pendingGlobals[pattern.node.name] = { 287 | pattern, 288 | emberPath, 289 | hasMissingGlobal: thisPatternHasMissingGlobal 290 | }; 291 | } 292 | } else if (j.ObjectPattern.check(pattern.node)) { 293 | let aliases = findObjectPatternAliases(mappings, pattern, emberPath); 294 | if (!pattern.node.properties.length) { 295 | pattern.parentPath.prune(); 296 | } 297 | return aliases; 298 | } 299 | 300 | return []; 301 | } 302 | 303 | function findPendingGlobal(emberPath) { 304 | if (!emberPath) { 305 | return; 306 | } 307 | const paths = emberPath.split('.'); 308 | for (let idx = 0; idx < paths.length; idx++) { 309 | const path = paths[idx]; 310 | if (pendingGlobals[path]) { 311 | return pendingGlobals[path]; 312 | } 313 | } 314 | } 315 | 316 | function findObjectPatternAliases(mappings, objectPattern, basePath) { 317 | let aliases = []; 318 | for (let i = objectPattern.node.properties.length - 1; i >= 0; i--) { 319 | let property = objectPattern.get('properties', i); 320 | let propertyName = property.node.key.name; 321 | let fullPath = basePath ? `${basePath}.${propertyName}` : propertyName; 322 | aliases = aliases.concat(extractAliases(mappings, property.get('value'), fullPath)); 323 | } 324 | return aliases; 325 | } 326 | 327 | function resolveAliasImports(aliases, mappings, registry, root) { 328 | for (let globalName of Object.keys(aliases)) { 329 | let alias = aliases[globalName]; 330 | let mapping = mappings[alias.emberPath]; 331 | // skip if this is (also) a namespace and it is nowhere used as a direct function call 332 | // In the case of `const { computed } = Ember` where `computed` is only used as a namespace (e.g. `computed.alias`) 333 | // and not as a direct function call (`computed(function(){ ... })`), resolving the module would leave an unused 334 | // module import 335 | if ( 336 | !includes(EMBER_NAMESPACES, globalName) 337 | || hasSimpleCallExpression(root, alias.identifier.node.name) 338 | ) { 339 | registry.get(mapping.source, mapping.imported, alias.identifier.node.name); 340 | } 341 | } 342 | } 343 | 344 | function hasSimpleCallExpression(root, name) { 345 | let paths = root.find(j.CallExpression, { 346 | callee: { 347 | name 348 | } 349 | }); 350 | return paths.length > 0; 351 | } 352 | 353 | /** 354 | * Returns a function that can be used to map an array of MemberExpression 355 | * nodes into Replacement instances. Does the actual work of verifying if the 356 | * `Ember` identifier used in the MemberExpression is actually replaceable. 357 | */ 358 | function findReplacement(mappings, namespace) { 359 | return function(path) { 360 | // Expand the full set of property lookups. For example, we don't want 361 | // just "Ember.computed"—we want "Ember.computed.or" as well. 362 | let candidates = expandMemberExpressions(path); 363 | if (namespace) { 364 | candidates = candidates.map(expression => { 365 | let path = expression[0]; 366 | let propertyPath = expression[1]; 367 | return [path, `${namespace}.${propertyPath}`]; 368 | }); 369 | } 370 | 371 | // This will give us an array of tuples ([pathString, node]) that represent 372 | // the possible replacements, from most-specific to least-specific. For example: 373 | // 374 | // [Ember.computed.reads, Ember.computed], or 375 | // [Ember.Object.extend, Ember.Object] 376 | // 377 | // We'll go through these to find the most specific candidate that matches 378 | // our global->ES6 map. 379 | let found = candidates.find(expression => { 380 | let propertyPath = expression[1]; 381 | return propertyPath in mappings; 382 | }); 383 | 384 | // If we got this far but didn't find a viable candidate, that means the user is 385 | // using something on the `Ember` global that we don't have a module equivalent for. 386 | if (!found) { 387 | warnMissingGlobal(path, candidates[candidates.length - 1][1]); 388 | return null; 389 | } 390 | 391 | let nodePath = found[0]; 392 | let propertyPath = found[1]; 393 | let mapping = mappings[propertyPath]; 394 | 395 | let mod = mapping.getModule(); 396 | let local = mod.local; 397 | if (!local) { 398 | // Ember.computed.or => or 399 | local = propertyPath.split(".").slice(-1)[0]; 400 | } 401 | 402 | if (includes(RESERVED, local)) { 403 | local = `Ember${local}`; 404 | } 405 | mod.local = local; 406 | 407 | return new Replacement(nodePath, mod); 408 | }; 409 | } 410 | 411 | /** 412 | * Returns an array of paths that are MemberExpressions of the given namespace, e.g. `computed.alias` 413 | */ 414 | function findNamespaceUsage(root, globalEmber, namespace) { 415 | let namespaceUsages = root.find(j.MemberExpression, { 416 | object: { 417 | name: namespace, 418 | }, 419 | }); 420 | let destructureStatements = findUsageOfDestructuredEmber(root, globalEmber); 421 | 422 | // the namespace like `computed` could be coming from something other than `Ember.computed` 423 | // so we check the VariableDeclaration within the scope where it is defined and compare that to our 424 | // `destructureStatements` to make sure this is really coming from on of those 425 | return namespaceUsages.filter((path) => { 426 | let scope = path.scope.lookup(namespace); 427 | if (!scope) return false; 428 | let bindings = scope.getBindings()[namespace]; 429 | if (!bindings) return false; 430 | 431 | let parent = bindings[0].parent; 432 | while (parent) { 433 | // if the namespace is defined by a variable declaration, make sure this is one of our Ember destructure statements 434 | if (j.VariableDeclarator.check(parent.node)) { 435 | return includes(destructureStatements, parent); 436 | } 437 | // if the codemod has run before namespaces were supported, the `computed` namespace may already have been imported 438 | // through the new module API. So this is still using by a valid Ember namespace, so return true 439 | if (j.ImportDeclaration.check(parent.node)) { 440 | return parent.node.source.value.match(/@ember\//); 441 | } 442 | 443 | parent = parent.parent; 444 | } 445 | 446 | return false; 447 | }).paths(); 448 | } 449 | 450 | /** 451 | * Remove any destructuring of namespaces, like `const { inject } = Ember` 452 | */ 453 | function removeNamespaces(root, globalEmber, namespaces) { 454 | let assignments = findUsageOfDestructuredEmber(root, globalEmber); 455 | for (let assignment of assignments) { 456 | let emberPath = joinEmberPath(assignment.get('init'), globalEmber); 457 | 458 | if (!emberPath && j.ObjectPattern.check(assignment.node.id)) { 459 | assignment.get('id').get('properties').filter((path) => { 460 | let node = path.node; 461 | return j.Identifier.check(node.key) && includes(namespaces, node.key.name); 462 | }) 463 | .forEach(path => path.prune()); 464 | 465 | if (!assignment.node.id.properties.length) { 466 | assignment.prune(); 467 | } 468 | } 469 | } 470 | } 471 | 472 | function warnMissingGlobal(nodePath, emberPath) { 473 | let context = extractSourceContext(nodePath); 474 | let lineNumber = nodePath.value.loc.start.line; 475 | warnings.push([MISSING_GLOBAL_WARNING, emberPath, lineNumber, file.path, context]); 476 | } 477 | 478 | function extractSourceContext(path) { 479 | let start = path.node.loc.start.line; 480 | let end = path.node.loc.end.line; 481 | 482 | let lines = source.split("\n"); 483 | 484 | start = Math.max(start - 2, 1) - 1; 485 | end = Math.min(end + 2, lines.length); 486 | 487 | return lines.slice(start, end).join("\n"); 488 | } 489 | 490 | function applyReplacements(replacements) { 491 | replacements 492 | .filter(r => !!r) 493 | .forEach(replacement => { 494 | let local = replacement.mod.local; 495 | let nodePath = replacement.nodePath; 496 | 497 | if (isAliasVariableDeclarator(nodePath, local)) { 498 | nodePath.parent.prune(); 499 | } else { 500 | nodePath.replace(j.identifier(local)); 501 | } 502 | }); 503 | } 504 | 505 | function removeGlobalEmber(root, globalEmber) { 506 | let remainingGlobals = findUsageOfEmberGlobal(root, globalEmber); 507 | let remainingDestructuring = findUsageOfDestructuredEmber(root, globalEmber); 508 | 509 | if (!remainingGlobals.length && !remainingDestructuring.length) { 510 | getGlobalEmberImport(root).remove(); 511 | } 512 | } 513 | 514 | function isAliasVariableDeclarator(nodePath, local) { 515 | let parent = nodePath.parent; 516 | 517 | if (!parent) { return false; } 518 | if (!j.VariableDeclarator.check(parent.node)) { return false; } 519 | 520 | return parent.node.id.name === local; 521 | } 522 | 523 | function updateOrCreateImportDeclarations(root, registry) { 524 | let body = root.get().value.program.body; 525 | 526 | registry.modules.forEach(mod => { 527 | if (!mod.node) { 528 | let source = mod.source; 529 | let imported = mod.imported; 530 | let local = mod.local; 531 | 532 | let declaration = root.find(j.ImportDeclaration, { 533 | source: { value: mod.source } 534 | }); 535 | 536 | if (declaration.size() > 0) { 537 | let specifier; 538 | 539 | if (imported === 'default') { 540 | specifier = j.importDefaultSpecifier(j.identifier(local)); 541 | declaration.get("specifiers").unshift(specifier); 542 | } else { 543 | specifier = j.importSpecifier(j.identifier(imported), j.identifier(local)); 544 | declaration.get("specifiers").push(specifier); 545 | } 546 | 547 | mod.node = declaration.at(0); 548 | } else { 549 | let importStatement = createImportStatement(source, imported, local); 550 | body.unshift(importStatement); 551 | body[0].comments = body[1].comments; 552 | delete body[1].comments; 553 | mod.node = importStatement; 554 | } 555 | } 556 | }); 557 | } 558 | 559 | function findExistingModules(root) { 560 | let registry = new ModuleRegistry(); 561 | 562 | root 563 | .find(j.ImportDeclaration) 564 | .forEach(mod => { 565 | let node = mod.node; 566 | let source = node.source.value; 567 | 568 | node.specifiers.forEach(spec => { 569 | let isDefault = j.ImportDefaultSpecifier.check(spec); 570 | 571 | // Some cases like `import * as bar from "foo"` have neither a 572 | // default nor a named export, which we don't currently handle. 573 | let imported = isDefault ? "default" : 574 | (spec.imported ? spec.imported.name : null); 575 | 576 | if (!imported) { return; } 577 | 578 | if (!registry.find(source, imported)) { 579 | let mod = registry.create(source, imported, spec.local.name); 580 | mod.node = node; 581 | } 582 | }); 583 | }); 584 | 585 | return registry; 586 | } 587 | 588 | function expandMemberExpressions(path) { 589 | let propName = path.node.property.name; 590 | let expressions = [[path, propName]]; 591 | 592 | let currentPath = path; 593 | 594 | while (currentPath = currentPath.parent) { 595 | if (j.MemberExpression.check(currentPath.node)) { 596 | propName = propName + "." + currentPath.value.property.name; 597 | expressions.push([currentPath, propName]); 598 | } else { 599 | break; 600 | } 601 | } 602 | 603 | return expressions.reverse(); 604 | } 605 | 606 | // Flagrantly stolen from https://github.com/5to6/5to6-codemod/blob/master/utils/main.js 607 | function createImportStatement(source, imported, local) { 608 | let declaration, variable, idIdentifier, nameIdentifier; 609 | // console.log('variableName', variableName); 610 | // console.log('moduleName', moduleName); 611 | 612 | // if no variable name, return `import 'jquery'` 613 | if (!local) { 614 | declaration = j.importDeclaration([], j.literal(source)); 615 | return declaration; 616 | } 617 | 618 | // multiple variable names indicates a destructured import 619 | if (Array.isArray(local)) { 620 | let variableIds = local.map(function(v) { 621 | return j.importSpecifier(j.identifier(v), j.identifier(v)); 622 | }); 623 | 624 | declaration = j.importDeclaration(variableIds, j.literal(source)); 625 | } else { 626 | // else returns `import $ from 'jquery'` 627 | nameIdentifier = j.identifier(local); //import var name 628 | variable = j.importDefaultSpecifier(nameIdentifier); 629 | 630 | // if propName, use destructuring `import {pluck} from 'underscore'` 631 | if (imported && imported !== "default") { 632 | idIdentifier = j.identifier(imported); 633 | variable = j.importSpecifier(idIdentifier, nameIdentifier); // if both are same, one is dropped... 634 | } 635 | 636 | declaration = j.importDeclaration([variable], j.literal(source)); 637 | } 638 | 639 | return declaration; 640 | } 641 | 642 | function isEmberGlobal(name) { 643 | return function(path) { 644 | let localEmber = !path.scope.isGlobal && path.scope.declares(name); 645 | return !localEmber; 646 | }; 647 | } 648 | 649 | function beautifyImports(source) { 650 | return source.replace(/\bimport.+from/g, (importStatement) => { 651 | let openCurly = importStatement.indexOf('{'); 652 | 653 | // leave default only imports alone 654 | if (openCurly === -1) { 655 | return importStatement; 656 | } 657 | 658 | if (importStatement.length > 50) { 659 | // if the segment is > 50 chars make it multi-line 660 | let result = importStatement.slice(0, openCurly + 1); 661 | let named = importStatement 662 | .slice(openCurly + 1, -6).split(',') 663 | .map(name => `\n ${name.trim()}`); 664 | 665 | return result + named.join(',') + '\n} from'; 666 | } else { 667 | // if the segment is < 50 chars just make sure it has proper spacing 668 | return importStatement 669 | .replace(/,\s*/g, ', ') // ensure there is a space after commas 670 | .replace(/\{\s*/, '{ ') 671 | .replace(/\s*\}/, ' }'); 672 | } 673 | }); 674 | } 675 | } 676 | 677 | function includes(array, value) { 678 | return array.indexOf(value) > -1; 679 | } 680 | 681 | class ModuleRegistry { 682 | constructor() { 683 | this.bySource = {}; 684 | this.modules = []; 685 | } 686 | 687 | find(source, imported) { 688 | let byImported = this.bySource[source]; 689 | 690 | if (!byImported) { 691 | byImported = this.bySource[source] = {}; 692 | } 693 | 694 | return byImported[imported] || null; 695 | } 696 | 697 | create(source, imported, local) { 698 | if (this.find(source, imported)) { 699 | throw new Error(`Module { ${source}, ${imported} } already exists.`); 700 | } 701 | 702 | let byImported = this.bySource[source]; 703 | if (!byImported) { 704 | byImported = this.bySource[source] = {}; 705 | } 706 | 707 | let mod = new Module(source, imported, local); 708 | byImported[imported] = mod; 709 | this.modules.push(mod); 710 | 711 | return mod; 712 | } 713 | 714 | get(source, imported, local) { 715 | let mod = this.find(source, imported, local); 716 | if (!mod) { 717 | mod = this.create(source, imported, local); 718 | } 719 | 720 | return mod; 721 | } 722 | } 723 | 724 | class Module { 725 | constructor(source, imported, local) { 726 | this.source = source; 727 | this.imported = imported; 728 | this.local = local; 729 | this.node = null; 730 | } 731 | } 732 | 733 | class GlobalAlias { 734 | constructor(identifier, emberPath) { 735 | this.identifier = identifier; 736 | this.emberPath = emberPath; 737 | } 738 | } 739 | 740 | class Replacement { 741 | constructor(nodePath, mod) { 742 | this.nodePath = nodePath; 743 | this.mod = mod; 744 | } 745 | } 746 | 747 | class Mapping { 748 | constructor(options, registry) { 749 | this.source = options.module; 750 | this.imported = options.export; 751 | this.local = options.localName; 752 | this.registry = registry; 753 | } 754 | 755 | getModule() { 756 | return this.registry.get(this.source, this.imported, this.local); 757 | } 758 | } 759 | --------------------------------------------------------------------------------