Skip to content

Commit d556832

Browse files
ssutarpzuraq
authored andcommitted
Add support for missing decorator imports (#27)
#26 #28
1 parent 5933c24 commit d556832

6 files changed

Lines changed: 199 additions & 108 deletions

File tree

transforms/ember-object/__testfixtures__/decorators.output.js

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
1+
import { layout, className, classNames, tagName, attribute } from "@ember-decorators/component";
12
import { sum as add, overridableReads as enoWay, overridableReads, reads, alias } from "@ember-decorators/object/computed";
23
import { get, set } from "@ember/object";
3-
import { computed, observes as watcher } from "@ember-decorators/object";
4+
import { action, readOnly, volatile, computed, observes as watcher } from "@ember-decorators/object";
45
import { controller } from "@ember-decorators/controller";
56
import { service } from "@ember-decorators/service";
67
import { on } from "@ember-decorators/object/evented";
7-
import layout from "components/templates/foo";
8-
9-
import { volatile, readOnly } from "@ember-decorators/object";
8+
import templateLayout from "components/templates/foo";
109

1110
@tagName("div")
1211
@classNames(["test-class", "custom-class"])
@@ -159,5 +158,5 @@ class Foo extends EmberObject {
159158
lName5;
160159
}
161160

162-
@layout(layout)
161+
@layout(templateLayout)
163162
class Foo extends EmberObject {}

transforms/ember-object/index.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
const { getOptions } = require("codemod-cli");
22
const { replaceEmberObjectExpressions } = require("../helpers/parse-helper");
33

4-
module.exports = function transformer(file, api) {
4+
module.exports = function transformer(file, api, options) {
55
const j = api.jscodeshift;
66
const root = j(file.source);
77

8-
replaceEmberObjectExpressions(j, root, file.path, getOptions());
8+
replaceEmberObjectExpressions(
9+
j,
10+
root,
11+
file.path,
12+
Object.assign({}, options, getOptions())
13+
);
914

1015
return root.toSource();
1116
};

transforms/helpers/EOProp.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,30 @@ class EOProp {
9494
return this.modifiers.length === 2 && this.hasVolatile && this.hasReadOnly;
9595
}
9696

97+
get isLayout() {
98+
return this.name === "layout";
99+
}
100+
101+
get isTagName() {
102+
return this.name === "tagName";
103+
}
104+
105+
get isClassNames() {
106+
return this.name === "classNames";
107+
}
108+
109+
get isAction() {
110+
return this.name === "actions";
111+
}
112+
113+
get hasClassNameDecorator() {
114+
return this.decoratorNames.includes("className");
115+
}
116+
117+
get hasAttributeDecorator() {
118+
return this.decoratorNames.includes("attribute");
119+
}
120+
97121
setCallExpressionProps() {
98122
let calleeObject = get(this._prop, "value");
99123
const modifiers = [getModifier(calleeObject)];
@@ -128,6 +152,12 @@ class EOProp {
128152
this.propList = classNameBindingsProps[this.name];
129153
}
130154
}
155+
156+
setLayoutValue(value) {
157+
if (this.type === "Identifier") {
158+
this.value.name = value;
159+
}
160+
}
131161
}
132162

133163
module.exports = EOProp;

transforms/helpers/parse-helper.js

Lines changed: 110 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,20 @@ const camelCase = require("camelcase");
33
const {
44
get,
55
getOptions,
6-
shouldImportVolatile,
7-
shouldImportReadOnly,
86
capitalizeFirstLetter,
97
startsWithUpperCaseLetter,
108
DECORATOR_PATHS,
9+
LAYOUT_IMPORT_SPECIFIER,
1110
METHOD_DECORATORS,
12-
META_DECORATORS
11+
META_DECORATORS,
12+
EMBER_DECORATOR_SPECIFIERS
1313
} = require("./util");
1414
const { hasValidProps, isFileOfType } = require("./validation-helper");
1515
const {
1616
withComments,
1717
createClass,
18-
createImportDeclaration
18+
createImportDeclaration,
19+
createEmberDecoratorSpecifiers
1920
} = require("./transform-helper");
2021
const EOProp = require("./EOProp");
2122
const logger = require("./log-helper");
@@ -56,6 +57,9 @@ function getEmberObjectProps(j, eoExpression, importedDecoratedProps = {}) {
5657
prop.setDecorators(importedDecoratedProps);
5758
instanceProps.push(prop);
5859
}
60+
if (prop.isLayout) {
61+
prop.setLayoutValue(LAYOUT_IMPORT_SPECIFIER);
62+
}
5963
});
6064

6165
// Assign decoator names to the binding props if any
@@ -204,55 +208,25 @@ function getDecoratorImports(j, root) {
204208
}
205209

206210
/**
207-
* Create the import declarations for `readOnly` in `computed(...).readOnly()` and `volatile` in `computed(...).volatile()`.
208-
* It will import from these specifiers from `@ember-decorators/object`
209-
*
210-
* @param {Object} j - jscodeshift lib reference
211-
* @param {Boolean} param.importVolatile if true import `volatile`
212-
* @param {Boolean} param.importReadOnly if true import `readOnly`
213-
*/
214-
function createVolatileReadOnlyImportDeclarations(
215-
j,
216-
{ importVolatile = false, importReadOnly = false } = {}
217-
) {
218-
const specifiers = [];
219-
if (importVolatile) {
220-
specifiers.push(j.importSpecifier(j.identifier("volatile")));
221-
}
222-
if (importReadOnly) {
223-
specifiers.push(j.importSpecifier(j.identifier("readOnly")));
224-
}
225-
if (specifiers.length) {
226-
return createImportDeclaration(j, specifiers, "@ember-decorators/object");
227-
}
228-
}
229-
230-
/**
231-
* Create and insert the import declarations for `computed(...).volatile()` and `computed(...).readOnly()`
211+
* Get the map of decorators to import other than the computed props, services etc
212+
* which already have imports in the code
232213
*
233-
* @param {Object} j - jscodeshift lib reference
234-
* @param {File} root
235-
* @param {Boolean} param.importVolatile if true import `volatile`
236-
* @param {Boolean} param.importReadOnly if true import `readOnly`
214+
* @param {EOProp[]} instanceProps
215+
* @param {Object} decoratorsMap
237216
*/
238-
function insertVolatileReadOnlyImportDeclarations(
239-
j,
240-
root,
241-
{ importVolatile, importReadOnly }
242-
) {
243-
const vroImportDeclaration = createVolatileReadOnlyImportDeclarations(j, {
244-
importVolatile,
245-
importReadOnly
246-
});
247-
if (!vroImportDeclaration) {
248-
return;
249-
}
250-
const importDeclarations = root.find(j.ImportDeclaration);
251-
if (importDeclarations.length) {
252-
importDeclarations.at(-1).insertAfter(vroImportDeclaration);
253-
} else {
254-
root.get().node.program.body.unshift(vroImportDeclaration);
255-
}
217+
function getDecoratorsToImport(instanceProps, decoratorsMap = {}) {
218+
return instanceProps.reduce((specs, prop) => {
219+
return {
220+
attribute: specs.attribute || prop.hasAttributeDecorator,
221+
readOnly: specs.readOnly || prop.hasReadOnly,
222+
action: specs.action || prop.isAction,
223+
layout: specs.layout || prop.isLayout,
224+
tagName: specs.tagName || prop.isTagName,
225+
className: specs.className || prop.hasClassNameDecorator,
226+
classNames: specs.classNames || prop.isClassNames,
227+
volatile: specs.volatile || prop.hasVolatile
228+
};
229+
}, decoratorsMap);
256230
}
257231

258232
/**
@@ -262,17 +236,24 @@ function insertVolatileReadOnlyImportDeclarations(
262236
* @param {File} root
263237
* @returns {Object}
264238
*/
265-
function createDecoratorImportDeclarations(
266-
j,
267-
root,
268-
{ importVolatile = false, importReadOnly = false } = {}
269-
) {
239+
function createDecoratorImportDeclarations(j, root, decoratorsToImport = []) {
240+
// create a copy - we need to mutate the object later
241+
const edSpecifiers = Object.assign({}, EMBER_DECORATOR_SPECIFIERS);
270242
getDecoratorImports(j, root).forEach(decoratorImport => {
271243
const { importPropDecoratorMap, decoratorPath } = DECORATOR_PATHS[
272244
get(decoratorImport, "value.source.value")
273245
];
246+
const pathSpecifiers = edSpecifiers[decoratorPath] || [];
247+
if (pathSpecifiers.length) {
248+
// delete the visited path to avoid duplicate imports
249+
delete edSpecifiers[decoratorPath];
250+
}
251+
const decoratedSpecifiers = createEmberDecoratorSpecifiers(
252+
j,
253+
pathSpecifiers,
254+
decoratorsToImport
255+
);
274256

275-
const decoratedSpecifiers = [];
276257
const specifiers = get(decoratorImport, "value.specifiers") || [];
277258

278259
for (let i = specifiers.length - 1; i >= 0; i -= 1) {
@@ -311,10 +292,25 @@ function createDecoratorImportDeclarations(
311292
}
312293
});
313294

314-
insertVolatileReadOnlyImportDeclarations(j, root, {
315-
importVolatile,
316-
importReadOnly
317-
});
295+
const edSpecifierPaths = Object.keys(edSpecifiers);
296+
if (edSpecifierPaths.length) {
297+
edSpecifierPaths.forEach(path => {
298+
const specifiers = createEmberDecoratorSpecifiers(
299+
j,
300+
edSpecifiers[path],
301+
decoratorsToImport
302+
);
303+
304+
if (specifiers.length) {
305+
j(
306+
root
307+
.find(j.Declaration)
308+
.at(0)
309+
.get()
310+
).insertBefore(createImportDeclaration(j, specifiers, path));
311+
}
312+
});
313+
}
318314
}
319315

320316
/**
@@ -366,6 +362,45 @@ function getEmberObjectCallExpressions(j, root) {
366362
);
367363
}
368364

365+
/**
366+
* Extracts the layout property name
367+
*
368+
* @param {Object} j - jscodeshift lib reference
369+
* @param {File} root
370+
* @returns {String} Name of the layout property
371+
*/
372+
function getLayoutPropertyName(j, root) {
373+
const layoutPropCollection = root.find(j.Property, {
374+
key: {
375+
type: "Identifier",
376+
name: "layout"
377+
}
378+
});
379+
if (layoutPropCollection.length) {
380+
const layoutProp = layoutPropCollection.get();
381+
return get(layoutProp, "value.value.name");
382+
}
383+
}
384+
385+
/**
386+
* Update the layout import name
387+
*
388+
* @param {Object} j - jscodeshift lib reference
389+
* @param {File} root
390+
*/
391+
function updateLayoutImportDeclaration(j, root, layoutName) {
392+
if (!layoutName) {
393+
return;
394+
}
395+
const layoutIdentifier = root
396+
.find(j.ImportDefaultSpecifier, { local: { name: layoutName } })
397+
.find(j.Identifier);
398+
399+
if (layoutIdentifier.length) {
400+
layoutIdentifier.get().value.name = LAYOUT_IMPORT_SPECIFIER;
401+
}
402+
}
403+
369404
/**
370405
* Returns the variable name
371406
*
@@ -466,9 +501,9 @@ function replaceEmberObjectExpressions(j, root, filePath, options = {}) {
466501
}
467502
// Parse the import statements
468503
const importedDecoratedProps = getImportedDecoratedProps(j, root);
504+
const layoutName = getLayoutPropertyName(j, root);
469505
let transformed = false;
470-
let importVolatile = false;
471-
let importReadOnly = false;
506+
let decoratorsToImportMap = {};
472507

473508
getEmberObjectCallExpressions(j, root).forEach(eoCallExpression => {
474509
const { eoExpression, mixins } = parseEmberObjectCallExpression(
@@ -480,6 +515,7 @@ function replaceEmberObjectExpressions(j, root, filePath, options = {}) {
480515
eoExpression,
481516
importedDecoratedProps
482517
);
518+
483519
const errors = hasValidProps(eoProps, getOptions(options));
484520
if (errors.length) {
485521
logger.warn(
@@ -504,20 +540,21 @@ function replaceEmberObjectExpressions(j, root, filePath, options = {}) {
504540

505541
transformed = true;
506542

507-
importVolatile =
508-
importVolatile || shouldImportVolatile(eoProps.instanceProps);
509-
importReadOnly =
510-
importReadOnly || shouldImportReadOnly(eoProps.instanceProps);
511-
512-
logger.info(`[${filePath}]: SUCCESS`);
543+
decoratorsToImportMap = getDecoratorsToImport(
544+
eoProps.instanceProps,
545+
decoratorsToImportMap
546+
);
513547
});
548+
514549
// Need to find another way, as there might be a case where
515550
// one object from a file is transformed and other is not
516551
if (transformed) {
517-
createDecoratorImportDeclarations(j, root, {
518-
importVolatile,
519-
importReadOnly
520-
});
552+
const decoratorsToImport = Object.keys(decoratorsToImportMap).filter(
553+
key => decoratorsToImportMap[key]
554+
);
555+
createDecoratorImportDeclarations(j, root, decoratorsToImport);
556+
updateLayoutImportDeclaration(j, root, layoutName);
557+
logger.info(`[${filePath}]: SUCCESS`);
521558
}
522559
}
523560

transforms/helpers/transform-helper.js

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -312,11 +312,36 @@ function createImportDeclaration(j, specifiers, path) {
312312
return j.importDeclaration(specifiers, j.literal(path));
313313
}
314314

315+
/**
316+
* Matches the decorators for the current path with the `decoratorsToImport`,
317+
* and creates import specifiers for the matching decorators
318+
*
319+
* @param {Object} j - jscodeshift lib reference
320+
* @param {String[]} pathSpecifiers
321+
* @param {String[]} decoratorsToImport
322+
* @returns {ImportSpecifier}
323+
*/
324+
function createEmberDecoratorSpecifiers(
325+
j,
326+
pathSpecifiers = [],
327+
decoratorsToImport = []
328+
) {
329+
return pathSpecifiers
330+
.filter(specifier => decoratorsToImport.includes(specifier))
331+
.map(specifier => {
332+
return j.importSpecifier(
333+
j.identifier(specifier),
334+
j.identifier(specifier)
335+
);
336+
});
337+
}
338+
315339
module.exports = {
316340
withComments,
317341
instancePropsToExpressions,
318342
createSuperExpressionStatement,
319343
createConstructor,
320344
createClass,
321-
createImportDeclaration
345+
createImportDeclaration,
346+
createEmberDecoratorSpecifiers
322347
};

0 commit comments

Comments
 (0)