Skip to content

Commit 30ddfa0

Browse files
committed
Extract transforms and utils
Preparation work for #3: separated existing (classic component and template) transforms and utils into separate modules, so we can later add a native class transform easier.
1 parent 8b0e980 commit 30ddfa0

6 files changed

Lines changed: 346 additions & 282 deletions

File tree

lib/__tests__/find-properties.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ const {
66
findAttributeBindings,
77
findClassNames,
88
findClassNameBindings,
9-
} = require('../transform');
9+
} = require('../utils');
1010

1111
describe('findTagName()', () => {
1212
const TESTS = [

lib/__tests__/transform.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ test('throws if `Component.extend({ ... })` is not found', () => {
106106
`;
107107

108108
expect(() => transform(source, '')).toThrowErrorMatchingInlineSnapshot(
109-
`"Could not find \`export default Component.extend({ ... });\`"`
109+
`"Unsupported component type. Only classic components (\`Component.extend({ ... }\`) are supported currently."`
110110
);
111111
});
112112

lib/transform.js

Lines changed: 22 additions & 280 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,11 @@
11
const fs = require('fs');
22
const path = require('path');
3-
const templateRecast = require('ember-template-recast');
43
const j = require('jscodeshift').withParser('ts');
54
const _debug = require('debug')('tagless-ember-components-codemod');
65

76
const SilentError = require('./silent-error');
8-
9-
const b = templateRecast.builders;
10-
11-
const EVENT_HANDLER_METHODS = [
12-
// Touch events
13-
'touchStart',
14-
'touchMove',
15-
'touchEnd',
16-
'touchCancel',
17-
18-
// Keyboard events
19-
'keyDown',
20-
'keyUp',
21-
'keyPress',
22-
23-
// Mouse events
24-
'mouseDown',
25-
'mouseUp',
26-
'contextMenu',
27-
'click',
28-
'doubleClick',
29-
'focusIn',
30-
'focusOut',
31-
32-
// Form events
33-
'submit',
34-
'change',
35-
'focusIn',
36-
'focusOut',
37-
'input',
38-
39-
// Drag and drop events
40-
'dragStart',
41-
'drag',
42-
'dragEnter',
43-
'dragLeave',
44-
'dragOver',
45-
'dragEnd',
46-
'drop',
47-
];
48-
49-
const PLACEHOLDER = '@@@PLACEHOLDER@@@';
7+
const transformClassicComponent = require('./transform/classic');
8+
const transformTemplate = require('./transform/template');
509

5110
function transformPath(componentPath, options) {
5211
let debug = (fmt, ...args) => _debug(`${componentPath}: ${fmt}`, ...args);
@@ -70,11 +29,7 @@ function transformPath(componentPath, options) {
7029
return result.tagName;
7130
}
7231

73-
function transform(source, template, options = {}) {
74-
let debug = options.debug || _debug;
75-
76-
let root = j(source);
77-
32+
function checkComponentType(root) {
7833
// find `export default Component.extend({ ... });` AST node
7934
let exportDefaultDeclarations = root.find(j.ExportDefaultDeclaration, {
8035
declaration: {
@@ -87,235 +42,34 @@ function transform(source, template, options = {}) {
8742
},
8843
});
8944

90-
if (exportDefaultDeclarations.length !== 1) {
91-
throw new SilentError(`Could not find \`export default Component.extend({ ... });\``);
92-
}
93-
94-
let exportDefaultDeclaration = exportDefaultDeclarations.get();
95-
96-
// find first `{ ... }` inside `Component.extend()` arguments
97-
let extendObjectArgs = exportDefaultDeclaration
98-
.get('declaration', 'arguments')
99-
.filter(path => path.value.type === 'ObjectExpression');
100-
101-
let objectArg = extendObjectArgs[0];
102-
if (!objectArg) {
103-
throw new SilentError(
104-
`Could not find object argument in \`export default Component.extend({ ... });\``
105-
);
106-
}
107-
108-
// find `tagName` property if it exists
109-
let properties = objectArg.get('properties');
110-
let tagName = findTagName(properties);
111-
112-
// skip tagless components (silent)
113-
if (tagName === '') {
114-
debug('tagName: %o -> skip', tagName);
115-
return { source, template };
116-
}
117-
118-
debug('tagName: %o', tagName);
119-
120-
// skip components that use `this.element`
121-
let thisElementPaths = j(objectArg).find(j.MemberExpression, {
122-
object: { type: 'ThisExpression' },
123-
property: { name: 'element' },
124-
});
125-
if (thisElementPaths.length !== 0) {
126-
throw new SilentError(`Using \`this.element\` is not supported in tagless components`);
127-
}
128-
129-
// skip components that use `this.elementId`
130-
let thisElementIdPaths = j(objectArg).find(j.MemberExpression, {
131-
object: { type: 'ThisExpression' },
132-
property: { name: 'elementId' },
133-
});
134-
if (thisElementIdPaths.length !== 0) {
135-
throw new SilentError(`Using \`this.elementId\` is not supported in tagless components`);
136-
}
137-
138-
// skip components that use `click()` etc.
139-
for (let methodName of EVENT_HANDLER_METHODS) {
140-
let handlerMethod = properties.filter(path => isMethod(path, methodName))[0];
141-
if (handlerMethod) {
142-
throw new SilentError(`Using \`${methodName}()\` is not supported in tagless components`);
143-
}
144-
}
145-
146-
// analyze `elementId`, `attributeBindings`, `classNames` and `classNameBindings`
147-
let elementId = findElementId(properties);
148-
debug('elementId: %o', elementId);
149-
150-
let attributeBindings = findAttributeBindings(properties);
151-
debug('attributeBindings: %o', attributeBindings);
152-
153-
let classNames = findClassNames(properties);
154-
debug('classNames: %o', classNames);
155-
156-
let classNameBindings = findClassNameBindings(properties);
157-
debug('classNameBindings: %o', classNameBindings);
158-
159-
let templateAST = templateRecast.parse(template);
160-
161-
// set `tagName: ''`
162-
let tagNamePath = j(properties)
163-
.find(j.ObjectProperty)
164-
.filter(path => path.parentPath === properties)
165-
.filter(path => isProperty(path, 'tagName'));
166-
167-
if (tagNamePath.length === 1) {
168-
j(tagNamePath.get('value')).replaceWith(j.stringLiteral(''));
169-
} else {
170-
properties.unshift(j.objectProperty(j.identifier('tagName'), j.stringLiteral('')));
171-
}
172-
173-
// remove `elementId`, `attributeBindings`, `classNames` and `classNameBindings`
174-
j(properties)
175-
.find(j.ObjectProperty)
176-
.filter(path => path.parentPath === properties)
177-
.filter(
178-
path =>
179-
isProperty(path, 'elementId') ||
180-
isProperty(path, 'attributeBindings') ||
181-
isProperty(path, 'classNames') ||
182-
isProperty(path, 'classNameBindings')
183-
)
184-
.remove();
185-
186-
let newSource = root.toSource();
187-
188-
// wrap existing template with root element
189-
let classNodes = [];
190-
if (options.hasComponentCSS) {
191-
classNodes.push(b.mustache('styleNamespace'));
192-
}
193-
for (let className of classNames) {
194-
classNodes.push(b.text(className));
195-
}
196-
classNameBindings.forEach(([truthy, falsy], property) => {
197-
if (!truthy) {
198-
classNodes.push(b.mustache(`unless this.${property} "${falsy}"`));
199-
} else {
200-
classNodes.push(b.mustache(`if this.${property} "${truthy}"${falsy ? ` "${falsy}"` : ''}`));
201-
}
202-
});
203-
204-
let attrs = [];
205-
if (elementId) {
206-
attrs.push(b.attr('id', b.text(elementId)));
207-
}
208-
attributeBindings.forEach((value, key) => {
209-
attrs.push(b.attr(key, b.mustache(`this.${value}`)));
210-
});
211-
if (classNodes.length === 1) {
212-
attrs.push(b.attr('class', classNodes[0]));
213-
} else if (classNodes.length !== 0) {
214-
let parts = [];
215-
classNodes.forEach((node, i) => {
216-
if (i !== 0) parts.push(b.text(' '));
217-
parts.push(node);
218-
});
219-
220-
attrs.push(b.attr('class', b.concat(parts)));
221-
}
222-
attrs.push(b.attr('...attributes', b.text('')));
223-
224-
templateAST.body = [
225-
b.element(tagName, {
226-
attrs,
227-
children: [b.text(`\n${PLACEHOLDER}\n`)],
228-
}),
229-
];
230-
231-
let newTemplate = templateRecast.print(templateAST).replace(PLACEHOLDER, indentLines(template));
232-
233-
return { source: newSource, template: newTemplate, tagName };
234-
}
235-
236-
function isProperty(path, name) {
237-
let node = path.value;
238-
return node.type === 'ObjectProperty' && node.key.type === 'Identifier' && node.key.name === name;
239-
}
240-
241-
function isMethod(path, name) {
242-
let node = path.value;
243-
return node.type === 'ObjectMethod' && node.key.type === 'Identifier' && node.key.name === name;
244-
}
245-
246-
function findStringProperty(properties, name, defaultValue = null) {
247-
let propertyPath = properties.filter(path => isProperty(path, name))[0];
248-
if (!propertyPath) {
249-
return defaultValue;
45+
if (exportDefaultDeclarations.length === 1) {
46+
return 'classic';
25047
}
251-
252-
let valuePath = propertyPath.get('value');
253-
if (valuePath.value.type !== 'StringLiteral') {
254-
throw new SilentError(`Unexpected \`${name}\` value: ${j(valuePath).toSource()}`);
255-
}
256-
257-
return valuePath.value.value;
25848
}
25949

260-
function findTagName(properties) {
261-
return findStringProperty(properties, 'tagName', 'div');
262-
}
263-
264-
function findElementId(properties) {
265-
return findStringProperty(properties, 'elementId');
266-
}
267-
268-
function findStringArrayProperties(properties, name) {
269-
let propertyPath = properties.filter(path => isProperty(path, name))[0];
270-
if (!propertyPath) {
271-
return [];
272-
}
273-
274-
let arrayPath = propertyPath.get('value');
275-
if (arrayPath.value.type !== 'ArrayExpression') {
276-
throw new SilentError(`Unexpected \`${name}\` value: ${j(arrayPath).toSource()}`);
277-
}
278-
279-
return arrayPath.get('elements').value.map(element => {
280-
if (element.type !== 'StringLiteral') {
281-
throw new SilentError(`Unexpected \`${name}\` value: ${j(arrayPath).toSource()}`);
282-
}
283-
284-
return element.value;
285-
});
286-
}
50+
function transform(source, template, options = {}) {
51+
let root = j(source);
52+
let type = checkComponentType(root);
53+
let result;
28754

288-
function findAttributeBindings(properties) {
289-
let attrBindings = new Map();
290-
for (let binding of findStringArrayProperties(properties, 'attributeBindings')) {
291-
let [value, attr] = binding.split(':');
292-
attrBindings.set(attr || value, value);
55+
switch (type) {
56+
case 'classic':
57+
result = transformClassicComponent(root, options);
58+
break;
59+
default:
60+
throw new Error(
61+
`Unsupported component type. Only classic components (\`Component.extend({ ... }\`) are supported currently.`
62+
);
29363
}
29464

295-
return attrBindings;
296-
}
65+
if (result) {
66+
let { newSource, attrs, tagName } = result;
67+
let newTemplate = transformTemplate(template, tagName, attrs);
29768

298-
function findClassNames(properties) {
299-
return findStringArrayProperties(properties, 'classNames');
300-
}
301-
302-
function findClassNameBindings(properties) {
303-
let classNameBindings = new Map();
304-
for (let binding of findStringArrayProperties(properties, 'classNameBindings')) {
305-
let parts = binding.split(':');
306-
307-
if (parts.length === 1) {
308-
throw new SilentError(`Unsupported non-boolean \`classNameBindings\` value: ${binding}`);
309-
} else if (parts.length === 2) {
310-
classNameBindings.set(parts[0], [parts[1], null]);
311-
} else if (parts.length === 3) {
312-
classNameBindings.set(parts[0], [parts[1] || null, parts[2]]);
313-
} else {
314-
throw new SilentError(`Unexpected \`classNameBindings\` value: ${binding}`);
315-
}
69+
return { source: newSource, template: newTemplate, tagName };
31670
}
31771

318-
return classNameBindings;
72+
return { source, template };
31973
}
32074

32175
function guessTemplatePath(componentPath) {
@@ -327,20 +81,8 @@ function guessTemplatePath(componentPath) {
32781
return componentPath.replace('/components/', '/templates/components/').replace(/\.js$/, '.hbs');
32882
}
32983

330-
function indentLines(content) {
331-
return content
332-
.split('\n')
333-
.map(it => ` ${it}`)
334-
.join('\n');
335-
}
336-
33784
module.exports = {
33885
transform,
33986
transformPath,
340-
findTagName,
341-
findElementId,
342-
findAttributeBindings,
343-
findClassNames,
344-
findClassNameBindings,
34587
guessTemplatePath,
34688
};

0 commit comments

Comments
 (0)