From 7a577431682558259b6364e43abd319f043194d4 Mon Sep 17 00:00:00 2001 From: simonihmig Date: Sun, 12 Jan 2020 20:18:11 +0100 Subject: [PATCH 01/11] First basic support for native classes --- lib/__tests__/__snapshots__/transform.js.snap | 156 ++++++-- lib/__tests__/transform.js | 375 ++++++++++++++---- lib/transform.js | 40 +- lib/transform/native.js | 207 ++++++++++ lib/utils/native.js | 257 ++++++++++++ 5 files changed, 921 insertions(+), 114 deletions(-) create mode 100644 lib/transform/native.js create mode 100644 lib/utils/native.js diff --git a/lib/__tests__/__snapshots__/transform.js.snap b/lib/__tests__/__snapshots__/transform.js.snap index a3d3667..92b48d4 100644 --- a/lib/__tests__/__snapshots__/transform.js.snap +++ b/lib/__tests__/__snapshots__/transform.js.snap @@ -1,11 +1,11 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`basic 1`] = ` +exports[`classic components basic 1`] = ` "========== export default Component.extend({ }); - + ~~~~~~~~~~ foo ~~~~~~~~~~ @@ -15,7 +15,7 @@ foo export default Component.extend({ tagName: \\"\\" }); - + ~~~~~~~~~~
foo @@ -29,7 +29,7 @@ exports[`handles \`ariaRole\` correctly 1`] = ` export default Component.extend({ ariaRole: 'button', }); - + ~~~~~~~~~~ foo ~~~~~~~~~~ @@ -39,7 +39,7 @@ foo export default Component.extend({ tagName: \\"\\", }); - + ~~~~~~~~~~
foo @@ -53,7 +53,7 @@ exports[`handles \`attributeBindings\` correctly 1`] = ` export default Component.extend({ attributeBindings: ['foo', 'bar:baz'], }); - + ~~~~~~~~~~ foo ~~~~~~~~~~ @@ -63,7 +63,7 @@ foo export default Component.extend({ tagName: \\"\\", }); - + ~~~~~~~~~~
foo @@ -71,13 +71,13 @@ foo ==========" `; -exports[`handles \`classNameBindings\` correctly 1`] = ` +exports[`classic components handles \`classNameBindings\` correctly 1`] = ` "========== export default Component.extend({ classNameBindings: ['a:b', 'x:y:z', 'foo::bar'], }); - + ~~~~~~~~~~ foo ~~~~~~~~~~ @@ -87,7 +87,7 @@ foo export default Component.extend({ tagName: \\"\\", }); - + ~~~~~~~~~~
foo @@ -95,13 +95,13 @@ foo ==========" `; -exports[`handles \`classNames\` correctly 1`] = ` +exports[`classic components handles \`classNames\` correctly 1`] = ` "========== export default Component.extend({ classNames: ['foo', 'bar:baz'], }); - + ~~~~~~~~~~ foo ~~~~~~~~~~ @@ -111,7 +111,7 @@ foo export default Component.extend({ tagName: \\"\\", }); - + ~~~~~~~~~~
foo @@ -119,13 +119,13 @@ foo ==========" `; -exports[`handles \`elementId\` correctly 1`] = ` +exports[`classic components handles \`elementId\` correctly 1`] = ` "========== export default Component.extend({ elementId: 'qux', }); - + ~~~~~~~~~~ foo ~~~~~~~~~~ @@ -135,7 +135,7 @@ foo export default Component.extend({ tagName: \\"\\", }); - + ~~~~~~~~~~
foo @@ -143,13 +143,13 @@ foo ==========" `; -exports[`handles \`hasComponentCSS\` option correctly 1`] = ` +exports[`classic components handles \`hasComponentCSS\` option correctly 1`] = ` "========== export default Component.extend({ classNames: ['foo', 'bar:baz'], }); - + ~~~~~~~~~~ foo ~~~~~~~~~~ @@ -159,7 +159,7 @@ foo export default Component.extend({ tagName: \\"\\", }); - + ~~~~~~~~~~
foo @@ -167,13 +167,13 @@ foo ==========" `; -exports[`handles single \`classNames\` item correctly 1`] = ` +exports[`classic components handles single \`classNames\` item correctly 1`] = ` "========== export default Component.extend({ classNames: ['foo'], }); - + ~~~~~~~~~~ foo ~~~~~~~~~~ @@ -183,7 +183,7 @@ foo export default Component.extend({ tagName: \\"\\", }); - + ~~~~~~~~~~
foo @@ -191,7 +191,7 @@ foo ==========" `; -exports[`multi-line template 1`] = ` +exports[`classic components multi-line template 1`] = ` "========== export default Component.extend({}); ~~~~~~~~~~ @@ -217,13 +217,13 @@ export default Component.extend({ ==========" `; -exports[`replaces existing \`tagName\` 1`] = ` +exports[`classic components replaces existing \`tagName\` 1`] = ` "========== export default Component.extend({ tagName: 'span', }); - + ~~~~~~~~~~ foo ~~~~~~~~~~ @@ -233,7 +233,111 @@ foo export default Component.extend({ tagName: \\"\\", }); - + +~~~~~~~~~~ + + foo + +==========" +`; + +exports[`native components basic 1`] = ` +"========== + + export default class FooComponent extends Component { + } + +~~~~~~~~~~ +foo +~~~~~~~~~~ + => tagName: div +~~~~~~~~~~ + + import { tagName } from \\"@ember-decorators/component\\"; + @tagName(\\"\\") + export default class FooComponent extends Component { + } + +~~~~~~~~~~ +
+ foo +
+==========" +`; + +exports[`native components handles \`@attributeBindings\` correctly 1`] = ` +"========== + + import { attributeBindings } from '@ember-decorators/component'; + + @attributeBindings('foo', 'bar:baz') + export default class FooComponent extends Component { + } + +~~~~~~~~~~ +foo +~~~~~~~~~~ + => tagName: div +~~~~~~~~~~ + + import { tagName } from '@ember-decorators/component'; + + @tagName(\\"\\") + export default class FooComponent extends Component { + } + +~~~~~~~~~~ +
+ foo +
+==========" +`; + +exports[`native components handles \`elementId\` correctly 1`] = ` +"========== + + export default class FooComponent extends Component { + elementId = 'qux'; + } + +~~~~~~~~~~ +foo +~~~~~~~~~~ + => tagName: div +~~~~~~~~~~ + + import { tagName } from \\"@ember-decorators/component\\"; + @tagName(\\"\\") + export default class FooComponent extends Component {} + +~~~~~~~~~~ +
+ foo +
+==========" +`; + +exports[`native components replaces existing \`tagName\` 1`] = ` +"========== + + import { tagName } from '@ember-decorators/component'; + + @tagName('span') + export default class FooComponent extends Component { + } + +~~~~~~~~~~ +foo +~~~~~~~~~~ + => tagName: span +~~~~~~~~~~ + + import { tagName } from '@ember-decorators/component'; + + @tagName(\\"\\") + export default class FooComponent extends Component { + } + ~~~~~~~~~~ foo diff --git a/lib/__tests__/transform.js b/lib/__tests__/transform.js index f528203..f29dcd6 100644 --- a/lib/__tests__/transform.js +++ b/lib/__tests__/transform.js @@ -16,88 +16,100 @@ ${result.template} ==========`; } -test('basic', () => { +test('throws if supported component type is not found', () => { let source = ` + export default class extends Dummy { + } + `; + + expect(() => transform(source, '')).toThrowErrorMatchingInlineSnapshot( + `"Unsupported component type."` + ); +}); + +describe('classic components', function() { + test('basic', () => { + let source = ` export default Component.extend({ }); `; - let template = `foo`; + let template = `foo`; - expect(generateSnapshot(source, template)).toMatchSnapshot(); -}); + expect(generateSnapshot(source, template)).toMatchSnapshot(); + }); -test('replaces existing `tagName`', () => { - let source = ` + test('replaces existing `tagName`', () => { + let source = ` export default Component.extend({ tagName: 'span', }); `; - let template = `foo`; + let template = `foo`; - expect(generateSnapshot(source, template)).toMatchSnapshot(); -}); + expect(generateSnapshot(source, template)).toMatchSnapshot(); + }); -test('handles `elementId` correctly', () => { - let source = ` + test('handles `elementId` correctly', () => { + let source = ` export default Component.extend({ elementId: 'qux', }); `; - let template = `foo`; + let template = `foo`; - expect(generateSnapshot(source, template)).toMatchSnapshot(); -}); + expect(generateSnapshot(source, template)).toMatchSnapshot(); + }); -test('handles `attributeBindings` correctly', () => { - let source = ` + test('handles `attributeBindings` correctly', () => { + let source = ` export default Component.extend({ attributeBindings: ['foo', 'bar:baz'], }); `; - let template = `foo`; + let template = `foo`; - expect(generateSnapshot(source, template)).toMatchSnapshot(); -}); + expect(generateSnapshot(source, template)).toMatchSnapshot(); + }); -test('handles `classNames` correctly', () => { - let source = ` + test('handles `classNames` correctly', () => { + let source = ` export default Component.extend({ classNames: ['foo', 'bar:baz'], }); `; - let template = `foo`; + let template = `foo`; - expect(generateSnapshot(source, template)).toMatchSnapshot(); -}); + expect(generateSnapshot(source, template)).toMatchSnapshot(); + }); -test('handles single `classNames` item correctly', () => { - let source = ` + test('handles single `classNames` item correctly', () => { + let source = ` export default Component.extend({ classNames: ['foo'], }); `; - let template = `foo`; + let template = `foo`; - expect(generateSnapshot(source, template)).toMatchSnapshot(); -}); + expect(generateSnapshot(source, template)).toMatchSnapshot(); + }); -test('handles `classNameBindings` correctly', () => { - let source = ` + test('handles `classNameBindings` correctly', () => { + let source = ` export default Component.extend({ classNameBindings: ['a:b', 'x:y:z', 'foo::bar'], }); `; - let template = `foo`; + let template = `foo`; - expect(generateSnapshot(source, template)).toMatchSnapshot(); -}); + expect(generateSnapshot(source, template)).toMatchSnapshot(); + }); test('handles `ariaRole` correctly', () => { let source = ` @@ -127,28 +139,28 @@ test('throws if `Component.extend({ ... })` argument is not found', () => { export default Component.extend(); `; - expect(() => transform(source, '')).toThrowErrorMatchingInlineSnapshot( - `"Could not find object argument in \`export default Component.extend({ ... });\`"` - ); -}); + expect(() => transform(source, '')).toThrowErrorMatchingInlineSnapshot( + `"Could not find object argument in \`export default Component.extend({ ... });\`"` + ); + }); -test('skips tagless components', () => { - let source = ` + test('skips tagless components', () => { + let source = ` export default Component.extend({ tagName: '', }); `; - let template = 'foo'; + let template = 'foo'; - let result = transform(source, template); - expect(result.tagName).toEqual(undefined); - expect(result.source).toEqual(source); - expect(result.template).toEqual(template); -}); + let result = transform(source, template); + expect(result.tagName).toEqual(undefined); + expect(result.source).toEqual(source); + expect(result.template).toEqual(template); + }); -test('throws if component is using `this.element`', () => { - let source = ` + test('throws if component is using `this.element`', () => { + let source = ` export default Component.extend({ didInsertElement() { console.log(this.element); @@ -156,13 +168,13 @@ test('throws if component is using `this.element`', () => { }); `; - expect(() => transform(source, '')).toThrowErrorMatchingInlineSnapshot( - `"Using \`this.element\` is not supported in tagless components"` - ); -}); + expect(() => transform(source, '')).toThrowErrorMatchingInlineSnapshot( + `"Using \`this.element\` is not supported in tagless components"` + ); + }); -test('throws if component is using `this.elementId`', () => { - let source = ` + test('throws if component is using `this.elementId`', () => { + let source = ` export default Component.extend({ didInsertElement() { console.log(this.elementId); @@ -170,13 +182,13 @@ test('throws if component is using `this.elementId`', () => { }); `; - expect(() => transform(source, '')).toThrowErrorMatchingInlineSnapshot( - `"Using \`this.elementId\` is not supported in tagless components"` - ); -}); + expect(() => transform(source, '')).toThrowErrorMatchingInlineSnapshot( + `"Using \`this.elementId\` is not supported in tagless components"` + ); + }); -test('throws if component is using `keyDown()`', () => { - let source = ` + test('throws if component is using `keyDown()`', () => { + let source = ` export default Component.extend({ keyDown() { console.log('Hello World!'); @@ -184,13 +196,13 @@ test('throws if component is using `keyDown()`', () => { }); `; - expect(() => transform(source, '')).toThrowErrorMatchingInlineSnapshot( - `"Using \`keyDown()\` is not supported in tagless components"` - ); -}); + expect(() => transform(source, '')).toThrowErrorMatchingInlineSnapshot( + `"Using \`keyDown()\` is not supported in tagless components"` + ); + }); -test('throws if component is using `click()`', () => { - let source = ` + test('throws if component is using `click()`', () => { + let source = ` export default Component.extend({ click() { console.log('Hello World!'); @@ -198,10 +210,10 @@ test('throws if component is using `click()`', () => { }); `; - expect(() => transform(source, '')).toThrowErrorMatchingInlineSnapshot( - `"Using \`click()\` is not supported in tagless components"` - ); -}); + expect(() => transform(source, '')).toThrowErrorMatchingInlineSnapshot( + `"Using \`click()\` is not supported in tagless components"` + ); + }); test('throws if component is using a computed property for `ariaRole`', () => { let source = ` @@ -220,7 +232,7 @@ test('throws if component is using a computed property for `ariaRole`', () => { test('multi-line template', () => { let source = `export default Component.extend({});`; - let template = ` + let template = ` {{#if this.foo}} FOO {{else}} @@ -228,17 +240,226 @@ test('multi-line template', () => { {{/if}} `.trim(); - expect(generateSnapshot(source, template)).toMatchSnapshot(); -}); + expect(generateSnapshot(source, template)).toMatchSnapshot(); + }); -test('handles `hasComponentCSS` option correctly', () => { - let source = ` + test('handles `hasComponentCSS` option correctly', () => { + let source = ` export default Component.extend({ classNames: ['foo', 'bar:baz'], }); `; - let template = `foo`; + let template = `foo`; + + expect(generateSnapshot(source, template, { hasComponentCSS: true })).toMatchSnapshot(); + }); +}); + +describe('native components', function() { + test('basic', () => { + let source = ` + export default class FooComponent extends Component { + } + `; - expect(generateSnapshot(source, template, { hasComponentCSS: true })).toMatchSnapshot(); + let template = `foo`; + + expect(generateSnapshot(source, template)).toMatchSnapshot(); + }); + + test('replaces existing `tagName`', () => { + let source = ` + import { tagName } from '@ember-decorators/component'; + + @tagName('span') + export default class FooComponent extends Component { + } + `; + + let template = `foo`; + + expect(generateSnapshot(source, template)).toMatchSnapshot(); + }); + + test('handles `elementId` correctly', () => { + let source = ` + export default class FooComponent extends Component { + elementId = 'qux'; + } + `; + + let template = `foo`; + + expect(generateSnapshot(source, template)).toMatchSnapshot(); + }); + + test('handles `@attributeBindings` correctly', () => { + let source = ` + import { attributeBindings } from '@ember-decorators/component'; + + @attributeBindings('foo', 'bar:baz') + export default class FooComponent extends Component { + } + `; + + let template = `foo`; + + expect(generateSnapshot(source, template)).toMatchSnapshot(); + }); + + // + // test('handles `classNames` correctly', () => { + // let source = ` + // export default Component.extend({ + // classNames: ['foo', 'bar:baz'], + // }); + // `; + // + // let template = `foo`; + // + // expect(generateSnapshot(source, template)).toMatchSnapshot(); + // }); + // + // test('handles single `classNames` item correctly', () => { + // let source = ` + // export default Component.extend({ + // classNames: ['foo'], + // }); + // `; + // + // let template = `foo`; + // + // expect(generateSnapshot(source, template)).toMatchSnapshot(); + // }); + // + // test('handles `classNameBindings` correctly', () => { + // let source = ` + // export default Component.extend({ + // classNameBindings: ['a:b', 'x:y:z', 'foo::bar'], + // }); + // `; + // + // let template = `foo`; + // + // expect(generateSnapshot(source, template)).toMatchSnapshot(); + // }); + // + // test('throws if `Component.extend({ ... })` is not found', () => { + // let source = ` + // export default class extends Component { + // } + // `; + // + // expect(() => transform(source, '')).toThrowErrorMatchingInlineSnapshot( + // `"Unsupported component type. Only classic components (\`Component.extend({ ... }\`) are supported currently."` + // ); + // }); + // + // test('throws if `Component.extend({ ... })` argument is not found', () => { + // let source = ` + // export default Component.extend(); + // `; + // + // expect(() => transform(source, '')).toThrowErrorMatchingInlineSnapshot( + // `"Could not find object argument in \`export default Component.extend({ ... });\`"` + // ); + // }); + // + // test('skips tagless components', () => { + // let source = ` + // export default Component.extend({ + // tagName: '', + // }); + // `; + // + // let template = 'foo'; + // + // let result = transform(source, template); + // expect(result.tagName).toEqual(undefined); + // expect(result.source).toEqual(source); + // expect(result.template).toEqual(template); + // }); + // + // test('throws if component is using `this.element`', () => { + // let source = ` + // export default Component.extend({ + // didInsertElement() { + // console.log(this.element); + // }, + // }); + // `; + // + // expect(() => transform(source, '')).toThrowErrorMatchingInlineSnapshot( + // `"Using \`this.element\` is not supported in tagless components"` + // ); + // }); + // + // test('throws if component is using `this.elementId`', () => { + // let source = ` + // export default Component.extend({ + // didInsertElement() { + // console.log(this.elementId); + // }, + // }); + // `; + // + // expect(() => transform(source, '')).toThrowErrorMatchingInlineSnapshot( + // `"Using \`this.elementId\` is not supported in tagless components"` + // ); + // }); + // + // test('throws if component is using `keyDown()`', () => { + // let source = ` + // export default Component.extend({ + // keyDown() { + // console.log('Hello World!'); + // }, + // }); + // `; + // + // expect(() => transform(source, '')).toThrowErrorMatchingInlineSnapshot( + // `"Using \`keyDown()\` is not supported in tagless components"` + // ); + // }); + // + // test('throws if component is using `click()`', () => { + // let source = ` + // export default Component.extend({ + // click() { + // console.log('Hello World!'); + // }, + // }); + // `; + // + // expect(() => transform(source, '')).toThrowErrorMatchingInlineSnapshot( + // `"Using \`click()\` is not supported in tagless components"` + // ); + // }); + // + // test('multi-line template', () => { + // let source = `export default Component.extend({});`; + // + // let template = ` + // {{#if this.foo}} + // FOO + // {{else}} + // BAR + // {{/if}} + // `.trim(); + // + // expect(generateSnapshot(source, template)).toMatchSnapshot(); + // }); + // + // test('handles `hasComponentCSS` option correctly', () => { + // let source = ` + // export default Component.extend({ + // classNames: ['foo', 'bar:baz'], + // }); + // `; + // + // let template = `foo`; + // + // expect(generateSnapshot(source, template, { hasComponentCSS: true })).toMatchSnapshot(); + // }); }); diff --git a/lib/transform.js b/lib/transform.js index c4ff5a7..0f77fd9 100644 --- a/lib/transform.js +++ b/lib/transform.js @@ -5,6 +5,7 @@ const _debug = require('debug')('tagless-ember-components-codemod'); const SilentError = require('./silent-error'); const transformClassicComponent = require('./transform/classic'); +const transformNativeComponent = require('./transform/native'); const transformTemplate = require('./transform/template'); function transformPath(componentPath, options) { @@ -31,20 +32,35 @@ function transformPath(componentPath, options) { function checkComponentType(root) { // find `export default Component.extend({ ... });` AST node - let exportDefaultDeclarations = root.find(j.ExportDefaultDeclaration, { - declaration: { - type: 'CallExpression', - callee: { - type: 'MemberExpression', - object: { name: 'Component' }, - property: { name: 'extend' }, + if ( + root.find(j.ExportDefaultDeclaration, { + declaration: { + type: 'CallExpression', + callee: { + type: 'MemberExpression', + object: { name: 'Component' }, + property: { name: 'extend' }, + }, }, - }, - }); - - if (exportDefaultDeclarations.length === 1) { + }).length === 1 + ) { return 'classic'; } + + // find `export default class FooComponent extends Component {}` AST node + if ( + root.find(j.ExportDefaultDeclaration, { + declaration: { + type: 'ClassDeclaration', + superClass: { + type: 'Identifier', + name: 'Component', + }, + }, + }).length === 1 + ) { + return 'native'; + } } function transform(source, template, options = {}) { @@ -54,6 +70,8 @@ function transform(source, template, options = {}) { if (type === 'classic') { result = transformClassicComponent(root, options); + } else if (type === 'native') { + result = transformNativeComponent(root, options); } else { throw new SilentError( `Unsupported component type. Only classic components (\`Component.extend({ ... }\`) are supported currently.` diff --git a/lib/transform/native.js b/lib/transform/native.js new file mode 100644 index 0000000..a8feb47 --- /dev/null +++ b/lib/transform/native.js @@ -0,0 +1,207 @@ +const j = require('jscodeshift').withParser('ts'); +const _debug = require('debug')('tagless-ember-components-codemod'); +const templateRecast = require('ember-template-recast'); + +const SilentError = require('../silent-error'); +const { + addClassDecorator, + findTagName, + findElementId, + findDecorator, + findClassNames, + findClassNameBindings, + findAttributeBindings, + isMethod, + ensureImport, + removeImport, + isProperty, +} = require('../utils/native'); + +const b = templateRecast.builders; + +const EVENT_HANDLER_METHODS = [ + // Touch events + 'touchStart', + 'touchMove', + 'touchEnd', + 'touchCancel', + + // Keyboard events + 'keyDown', + 'keyUp', + 'keyPress', + + // Mouse events + 'mouseDown', + 'mouseUp', + 'contextMenu', + 'click', + 'doubleClick', + 'focusIn', + 'focusOut', + + // Form events + 'submit', + 'change', + 'focusIn', + 'focusOut', + 'input', + + // Drag and drop events + 'dragStart', + 'drag', + 'dragEnter', + 'dragLeave', + 'dragOver', + 'dragEnd', + 'drop', +]; + +module.exports = function transformNativeComponent(root, options) { + let debug = options.debug || _debug; + + let exportDefaultDeclarations = root.find(j.ExportDefaultDeclaration); + + if (exportDefaultDeclarations.length !== 1) { + throw new SilentError( + `Could not find \`export default class SomeComponent extends Component { ... }\`` + ); + } + + let exportDefaultDeclaration = exportDefaultDeclarations.get(); + let classDeclaration = exportDefaultDeclaration.get('declaration'); + // find class body + let classBody = classDeclaration.get('body', 'body'); + + // find `tagName` property if it exists + let tagName = findTagName(classDeclaration); + + // skip tagless components (silent) + if (tagName === '') { + debug('tagName: %o -> skip', tagName); + return; + } + + debug('tagName: %o', tagName); + + /* + // skip components that use `this.element` + let thisElementPaths = j(objectArg).find(j.MemberExpression, { + object: { type: 'ThisExpression' }, + property: { name: 'element' }, + }); + if (thisElementPaths.length !== 0) { + throw new SilentError(`Using \`this.element\` is not supported in tagless components`); + } + + // skip components that use `this.elementId` + let thisElementIdPaths = j(objectArg).find(j.MemberExpression, { + object: { type: 'ThisExpression' }, + property: { name: 'elementId' }, + }); + if (thisElementIdPaths.length !== 0) { + throw new SilentError(`Using \`this.elementId\` is not supported in tagless components`); + } + + // skip components that use `click()` etc. + for (let methodName of EVENT_HANDLER_METHODS) { + let handlerMethod = properties.filter(path => isMethod(path, methodName))[0]; + if (handlerMethod) { + throw new SilentError(`Using \`${methodName}()\` is not supported in tagless components`); + } + } +*/ + // analyze `elementId`, `attributeBindings`, `classNames` and `classNameBindings` + let elementId = findElementId(classBody); + debug('elementId: %o', elementId); + + let attributeBindings = findAttributeBindings(classDeclaration); + debug('attributeBindings: %o', attributeBindings); + + /* + let classNames = findClassNames(properties); + debug('classNames: %o', classNames); + + let classNameBindings = findClassNameBindings(properties); + debug('classNameBindings: %o', classNameBindings); + */ + + // set `@tagName('')` + addClassDecorator(exportDefaultDeclaration, 'tagName', [j.stringLiteral('')]); + ensureImport(root, 'tagName', '@ember-decorators/component'); + // let tagNamePath = j(classBody) + // .find(j.ClassProperty) + // // .filter(path => path.parentPath === properties) + // .filter(path => isProperty(path, 'tagName')); + // + // if (tagNamePath.length === 1) { + // j(tagNamePath.get('value')).replaceWith(j.stringLiteral('')); + // } else { + // classBody.unshift(j.classProperty(j.identifier('tagName'), j.stringLiteral(''))); + // } + + // remove `elementId`, `attributeBindings`, `classNames` and `classNameBindings` + j(classBody) + .find(j.ClassProperty) + // .filter(path => path.parentPath === properties) + .filter( + path => isProperty(path, 'elementId') + // isProperty(path, 'attributeBindings') || + // isProperty(path, 'classNames') || + // isProperty(path, 'classNameBindings') + ) + .remove(); + + let attributeBindingsDecorator = findDecorator(classDeclaration, 'attributeBindings'); + if (attributeBindingsDecorator) { + j(attributeBindingsDecorator).remove(); + removeImport(root, 'attributeBindings', '@ember-decorators/component'); + } + + let newSource = root.toSource(); + + // wrap existing template with root element + let classNodes = []; + if (options.hasComponentCSS) { + classNodes.push(b.mustache('styleNamespace')); + } + /* + for (let className of classNames) { + classNodes.push(b.text(className)); + } + classNameBindings.forEach(([truthy, falsy], property) => { + if (!truthy) { + classNodes.push(b.mustache(`unless this.${property} "${falsy}"`)); + } else { + classNodes.push(b.mustache(`if this.${property} "${truthy}"${falsy ? ` "${falsy}"` : ''}`)); + } + }); + */ + + let attrs = []; + + if (elementId) { + attrs.push(b.attr('id', b.text(elementId))); + } + + attributeBindings.forEach((value, key) => { + attrs.push(b.attr(key, b.mustache(`this.${value}`))); + }); + /* + if (classNodes.length === 1) { + attrs.push(b.attr('class', classNodes[0])); + } else if (classNodes.length !== 0) { + let parts = []; + classNodes.forEach((node, i) => { + if (i !== 0) parts.push(b.text(' ')); + parts.push(node); + }); + + attrs.push(b.attr('class', b.concat(parts))); + } + + */ + attrs.push(b.attr('...attributes', b.text(''))); + + return { newSource, attrs, tagName }; +}; diff --git a/lib/utils/native.js b/lib/utils/native.js new file mode 100644 index 0000000..fd39755 --- /dev/null +++ b/lib/utils/native.js @@ -0,0 +1,257 @@ +const j = require('jscodeshift').withParser('ts'); + +const SilentError = require('../silent-error'); + +function addClassDecorator(classDeclaration, name, args) { + let existing = findDecorator(classDeclaration, name); + + if (existing) { + existing.value.expression.arguments = args; + } else { + if (classDeclaration.value.decorators === undefined) { + classDeclaration.value.decorators = []; + } + classDeclaration.value.decorators.unshift( + j.decorator(j.callExpression(j.identifier(name), args)) + ); + } +} + +function isDecorator(path, name) { + let node = path.value; + return ( + node.type === 'Decorator' && + node.expression.type === 'CallExpression' && + node.expression.callee.type === 'Identifier' && + node.expression.callee.name === name + ); +} + +function isProperty(path, name) { + let node = path.value; + return node.type === 'ClassProperty' && node.key.type === 'Identifier' && node.key.name === name; +} + +function isMethod(path, name) { + let node = path.value; + return node.type === 'ObjectMethod' && node.key.type === 'Identifier' && node.key.name === name; +} + +function findStringProperty(properties, name, defaultValue = null) { + let propertyPath = properties.filter(path => isProperty(path, name))[0]; + if (!propertyPath) { + return defaultValue; + } + + let valuePath = propertyPath.get('value'); + if (valuePath.value.type !== 'StringLiteral') { + throw new SilentError(`Unexpected \`${name}\` value: ${j(valuePath).toSource()}`); + } + + return valuePath.value.value; +} + +function findDecorator(path, name) { + let decorators = path.get('decorators'); + if (decorators.value === undefined) { + return; + } + + let existing = decorators.filter(path => isDecorator(path, name)); + + if (existing.length > 0) { + return existing[0]; + } +} + +function findStringDecorator(path, name, defaultValue = null) { + let decorator = findDecorator(path, name); + + if (!decorator) { + return defaultValue; + } + + return decorator.value.expression.arguments[0].value; +} + +function findTagName(path) { + return findStringDecorator(path, 'tagName', 'div'); +} + +function findElementId(properties) { + return findStringProperty(properties, 'elementId'); +} + +function findStringArrayProperties(properties, name) { + let propertyPath = properties.filter(path => isProperty(path, name))[0]; + if (!propertyPath) { + return []; + } + + let arrayPath = propertyPath.get('value'); + if (arrayPath.value.type !== 'ArrayExpression') { + throw new SilentError(`Unexpected \`${name}\` value: ${j(arrayPath).toSource()}`); + } + + return arrayPath.get('elements').value.map(element => { + if (element.type !== 'StringLiteral') { + throw new SilentError(`Unexpected \`${name}\` value: ${j(arrayPath).toSource()}`); + } + + return element.value; + }); +} + +function findStringArrayDecorator(path, name) { + let decorator = findDecorator(path, name); + if (!decorator) { + return []; + } + + let args = decorator.get('expression').value.arguments; + + return args.map(element => { + if (element.type !== 'StringLiteral') { + throw new SilentError(`Unexpected \`${name}\` value: ${j(args).toSource()}`); + } + + return element.value; + }); +} + +function findAttributeBindings(classDeclaration) { + let attrBindings = new Map(); + for (let binding of findStringArrayDecorator(classDeclaration, 'attributeBindings')) { + let [value, attr] = binding.split(':'); + attrBindings.set(attr || value, value); + } + + return attrBindings; +} + +function findClassNames(properties) { + return findStringArrayProperties(properties, 'classNames'); +} + +function findClassNameBindings(properties) { + let classNameBindings = new Map(); + for (let binding of findStringArrayProperties(properties, 'classNameBindings')) { + let parts = binding.split(':'); + + if (parts.length === 1) { + throw new SilentError(`Unsupported non-boolean \`classNameBindings\` value: ${binding}`); + } else if (parts.length === 2) { + classNameBindings.set(parts[0], [parts[1], null]); + } else if (parts.length === 3) { + classNameBindings.set(parts[0], [parts[1] || null, parts[2]]); + } else { + throw new SilentError(`Unexpected \`classNameBindings\` value: ${binding}`); + } + } + + return classNameBindings; +} + +function indentLines(content) { + return content + .split('\n') + .map(it => ` ${it}`) + .join('\n'); +} + +function ensureImport(root, name, source) { + let body = root.get().value.program.body; + + let declaration = root.find(j.ImportDeclaration, { + source: { value: source }, + }); + + if (declaration.length) { + if ( + declaration.find(j.ImportSpecifier, { + imported: { + name, + }, + }).length === 0 + ) { + declaration.get('specifiers').push(j.importSpecifier(j.identifier(name))); + } + } else { + let importStatement = createImportStatement(source, null, [name]); + body.unshift(importStatement); + body[0].comments = body[1].comments; + delete body[1].comments; + } +} + +function removeImport(root, name, source) { + let declaration = root.find(j.ImportDeclaration, { + source: { value: source }, + }); + + if (declaration.length) { + declaration + .find(j.ImportSpecifier, { + imported: { + name, + }, + }) + .remove(); + + if (declaration.get().value.specifiers.length === 0) { + // console.log(root.get('program', 'body')) + declaration.remove(); + } + } +} + +// shamelessly taken from https://github.com/ember-codemods/ember-modules-codemod/blob/3afaab6b77fcb494873e2667f6e1bb14362f3845/transform.js#L607 +function createImportStatement(source, imported, local) { + let declaration, variable, idIdentifier, nameIdentifier; + + // if no variable name, return `import 'jquery'` + if (!local) { + declaration = j.importDeclaration([], j.literal(source)); + return declaration; + } + + // multiple variable names indicates a destructured import + if (Array.isArray(local)) { + let variableIds = local.map(function(v) { + return j.importSpecifier(j.identifier(v), j.identifier(v)); + }); + + declaration = j.importDeclaration(variableIds, j.literal(source)); + } else { + // else returns `import $ from 'jquery'` + nameIdentifier = j.identifier(local); //import var name + variable = j.importDefaultSpecifier(nameIdentifier); + + // if propName, use destructuring `import {pluck} from 'underscore'` + if (imported && imported !== 'default') { + idIdentifier = j.identifier(imported); + variable = j.importSpecifier(idIdentifier, nameIdentifier); // if both are same, one is dropped... + } + + declaration = j.importDeclaration([variable], j.literal(source)); + } + + return declaration; +} + +module.exports = { + addClassDecorator, + isProperty, + isMethod, + findStringProperty, + findStringArrayProperties, + findAttributeBindings, + findClassNames, + findClassNameBindings, + findElementId, + findTagName, + findDecorator, + ensureImport, + removeImport, + indentLines, +}; From 703a3fa402c684df6b4faf4a9aca96b04b3e43d6 Mon Sep 17 00:00:00 2001 From: simonihmig Date: Sun, 12 Jan 2020 20:48:32 +0100 Subject: [PATCH 02/11] Handle class names in native transform --- lib/__tests__/__snapshots__/transform.js.snap | 84 ++++++++++++++++ lib/__tests__/transform.js | 97 ++++++++----------- lib/transform/native.js | 24 ++--- lib/utils/native.js | 17 +++- 4 files changed, 146 insertions(+), 76 deletions(-) diff --git a/lib/__tests__/__snapshots__/transform.js.snap b/lib/__tests__/__snapshots__/transform.js.snap index 92b48d4..bd3a3fa 100644 --- a/lib/__tests__/__snapshots__/transform.js.snap +++ b/lib/__tests__/__snapshots__/transform.js.snap @@ -293,6 +293,62 @@ foo ==========" `; +exports[`native components handles \`classNameBindings\` correctly 1`] = ` +"========== + + import { classNameBindings } from '@ember-decorators/component'; + + @classNameBindings('a:b', 'x:y:z', 'foo::bar') + export default class FooComponent extends Component { + } + +~~~~~~~~~~ +foo +~~~~~~~~~~ + => tagName: div +~~~~~~~~~~ + + import { tagName } from '@ember-decorators/component'; + + @tagName(\\"\\") + export default class FooComponent extends Component { + } + +~~~~~~~~~~ +
+ foo +
+==========" +`; + +exports[`native components handles \`classNames\` correctly 1`] = ` +"========== + + import { classNames } from '@ember-decorators/component'; + + @classNames('foo', 'bar:baz') + export default class FooComponent extends Component { + } + +~~~~~~~~~~ +foo +~~~~~~~~~~ + => tagName: div +~~~~~~~~~~ + + import { tagName } from '@ember-decorators/component'; + + @tagName(\\"\\") + export default class FooComponent extends Component { + } + +~~~~~~~~~~ +
+ foo +
+==========" +`; + exports[`native components handles \`elementId\` correctly 1`] = ` "========== @@ -317,6 +373,34 @@ foo ==========" `; +exports[`native components handles single \`classNames\` item correctly 1`] = ` +"========== + + import { classNames } from '@ember-decorators/component'; + + @classNames('foo') + export default class FooComponent extends Component { + } + +~~~~~~~~~~ +foo +~~~~~~~~~~ + => tagName: div +~~~~~~~~~~ + + import { tagName } from '@ember-decorators/component'; + + @tagName(\\"\\") + export default class FooComponent extends Component { + } + +~~~~~~~~~~ +
+ foo +
+==========" +`; + exports[`native components replaces existing \`tagName\` 1`] = ` "========== diff --git a/lib/__tests__/transform.js b/lib/__tests__/transform.js index f29dcd6..c5e53e0 100644 --- a/lib/__tests__/transform.js +++ b/lib/__tests__/transform.js @@ -308,63 +308,48 @@ describe('native components', function() { expect(generateSnapshot(source, template)).toMatchSnapshot(); }); + test('handles `classNames` correctly', () => { + let source = ` + import { classNames } from '@ember-decorators/component'; + + @classNames('foo', 'bar:baz') + export default class FooComponent extends Component { + } + `; + + let template = `foo`; + + expect(generateSnapshot(source, template)).toMatchSnapshot(); + }); + + test('handles single `classNames` item correctly', () => { + let source = ` + import { classNames } from '@ember-decorators/component'; + + @classNames('foo') + export default class FooComponent extends Component { + } + `; + + let template = `foo`; + + expect(generateSnapshot(source, template)).toMatchSnapshot(); + }); + + test('handles `classNameBindings` correctly', () => { + let source = ` + import { classNameBindings } from '@ember-decorators/component'; + + @classNameBindings('a:b', 'x:y:z', 'foo::bar') + export default class FooComponent extends Component { + } + `; + + let template = `foo`; + + expect(generateSnapshot(source, template)).toMatchSnapshot(); + }); // - // test('handles `classNames` correctly', () => { - // let source = ` - // export default Component.extend({ - // classNames: ['foo', 'bar:baz'], - // }); - // `; - // - // let template = `foo`; - // - // expect(generateSnapshot(source, template)).toMatchSnapshot(); - // }); - // - // test('handles single `classNames` item correctly', () => { - // let source = ` - // export default Component.extend({ - // classNames: ['foo'], - // }); - // `; - // - // let template = `foo`; - // - // expect(generateSnapshot(source, template)).toMatchSnapshot(); - // }); - // - // test('handles `classNameBindings` correctly', () => { - // let source = ` - // export default Component.extend({ - // classNameBindings: ['a:b', 'x:y:z', 'foo::bar'], - // }); - // `; - // - // let template = `foo`; - // - // expect(generateSnapshot(source, template)).toMatchSnapshot(); - // }); - // - // test('throws if `Component.extend({ ... })` is not found', () => { - // let source = ` - // export default class extends Component { - // } - // `; - // - // expect(() => transform(source, '')).toThrowErrorMatchingInlineSnapshot( - // `"Unsupported component type. Only classic components (\`Component.extend({ ... }\`) are supported currently."` - // ); - // }); - // - // test('throws if `Component.extend({ ... })` argument is not found', () => { - // let source = ` - // export default Component.extend(); - // `; - // - // expect(() => transform(source, '')).toThrowErrorMatchingInlineSnapshot( - // `"Could not find object argument in \`export default Component.extend({ ... });\`"` - // ); - // }); // // test('skips tagless components', () => { // let source = ` diff --git a/lib/transform/native.js b/lib/transform/native.js index a8feb47..50a73bb 100644 --- a/lib/transform/native.js +++ b/lib/transform/native.js @@ -12,6 +12,7 @@ const { findClassNameBindings, findAttributeBindings, isMethod, + removeDecorator, ensureImport, removeImport, isProperty, @@ -118,13 +119,11 @@ module.exports = function transformNativeComponent(root, options) { let attributeBindings = findAttributeBindings(classDeclaration); debug('attributeBindings: %o', attributeBindings); - /* - let classNames = findClassNames(properties); + let classNames = findClassNames(classDeclaration); debug('classNames: %o', classNames); - let classNameBindings = findClassNameBindings(properties); + let classNameBindings = findClassNameBindings(classDeclaration); debug('classNameBindings: %o', classNameBindings); - */ // set `@tagName('')` addClassDecorator(exportDefaultDeclaration, 'tagName', [j.stringLiteral('')]); @@ -146,17 +145,12 @@ module.exports = function transformNativeComponent(root, options) { // .filter(path => path.parentPath === properties) .filter( path => isProperty(path, 'elementId') - // isProperty(path, 'attributeBindings') || - // isProperty(path, 'classNames') || - // isProperty(path, 'classNameBindings') ) .remove(); - let attributeBindingsDecorator = findDecorator(classDeclaration, 'attributeBindings'); - if (attributeBindingsDecorator) { - j(attributeBindingsDecorator).remove(); - removeImport(root, 'attributeBindings', '@ember-decorators/component'); - } + removeDecorator(root, classDeclaration, 'attributeBindings', '@ember-decorators/component'); + removeDecorator(root, classDeclaration, 'classNames', '@ember-decorators/component'); + removeDecorator(root, classDeclaration, 'classNameBindings', '@ember-decorators/component'); let newSource = root.toSource(); @@ -165,7 +159,7 @@ module.exports = function transformNativeComponent(root, options) { if (options.hasComponentCSS) { classNodes.push(b.mustache('styleNamespace')); } - /* + for (let className of classNames) { classNodes.push(b.text(className)); } @@ -176,7 +170,6 @@ module.exports = function transformNativeComponent(root, options) { classNodes.push(b.mustache(`if this.${property} "${truthy}"${falsy ? ` "${falsy}"` : ''}`)); } }); - */ let attrs = []; @@ -187,7 +180,7 @@ module.exports = function transformNativeComponent(root, options) { attributeBindings.forEach((value, key) => { attrs.push(b.attr(key, b.mustache(`this.${value}`))); }); - /* + if (classNodes.length === 1) { attrs.push(b.attr('class', classNodes[0])); } else if (classNodes.length !== 0) { @@ -200,7 +193,6 @@ module.exports = function transformNativeComponent(root, options) { attrs.push(b.attr('class', b.concat(parts))); } - */ attrs.push(b.attr('...attributes', b.text(''))); return { newSource, attrs, tagName }; diff --git a/lib/utils/native.js b/lib/utils/native.js index fd39755..21e9c12 100644 --- a/lib/utils/native.js +++ b/lib/utils/native.js @@ -129,13 +129,13 @@ function findAttributeBindings(classDeclaration) { return attrBindings; } -function findClassNames(properties) { - return findStringArrayProperties(properties, 'classNames'); +function findClassNames(classDeclaration) { + return findStringArrayDecorator(classDeclaration, 'classNames'); } -function findClassNameBindings(properties) { +function findClassNameBindings(classDeclaration) { let classNameBindings = new Map(); - for (let binding of findStringArrayProperties(properties, 'classNameBindings')) { + for (let binding of findStringArrayDecorator(classDeclaration, 'classNameBindings')) { let parts = binding.split(':'); if (parts.length === 1) { @@ -152,6 +152,14 @@ function findClassNameBindings(properties) { return classNameBindings; } +function removeDecorator(root, classDeclaration, name, source) { + let decorator = findDecorator(classDeclaration, name); + if (decorator) { + j(decorator).remove(); + removeImport(root, name, source); + } +} + function indentLines(content) { return content .split('\n') @@ -251,6 +259,7 @@ module.exports = { findElementId, findTagName, findDecorator, + removeDecorator, ensureImport, removeImport, indentLines, From 0b0226aa9c8fdfd7f0a84c96fbab57104e679f38 Mon Sep 17 00:00:00 2001 From: simonihmig Date: Sun, 12 Jan 2020 21:15:37 +0100 Subject: [PATCH 03/11] Logic to skip/throw in native transform --- lib/__tests__/transform.js | 145 +++++++++++++++++++------------------ lib/transform/native.js | 13 ++-- lib/utils/native.js | 2 +- 3 files changed, 79 insertions(+), 81 deletions(-) diff --git a/lib/__tests__/transform.js b/lib/__tests__/transform.js index c5e53e0..81bc481 100644 --- a/lib/__tests__/transform.js +++ b/lib/__tests__/transform.js @@ -349,78 +349,79 @@ describe('native components', function() { expect(generateSnapshot(source, template)).toMatchSnapshot(); }); - // - // - // test('skips tagless components', () => { - // let source = ` - // export default Component.extend({ - // tagName: '', - // }); - // `; - // - // let template = 'foo'; - // - // let result = transform(source, template); - // expect(result.tagName).toEqual(undefined); - // expect(result.source).toEqual(source); - // expect(result.template).toEqual(template); - // }); - // - // test('throws if component is using `this.element`', () => { - // let source = ` - // export default Component.extend({ - // didInsertElement() { - // console.log(this.element); - // }, - // }); - // `; - // - // expect(() => transform(source, '')).toThrowErrorMatchingInlineSnapshot( - // `"Using \`this.element\` is not supported in tagless components"` - // ); - // }); - // - // test('throws if component is using `this.elementId`', () => { - // let source = ` - // export default Component.extend({ - // didInsertElement() { - // console.log(this.elementId); - // }, - // }); - // `; - // - // expect(() => transform(source, '')).toThrowErrorMatchingInlineSnapshot( - // `"Using \`this.elementId\` is not supported in tagless components"` - // ); - // }); - // - // test('throws if component is using `keyDown()`', () => { - // let source = ` - // export default Component.extend({ - // keyDown() { - // console.log('Hello World!'); - // }, - // }); - // `; - // - // expect(() => transform(source, '')).toThrowErrorMatchingInlineSnapshot( - // `"Using \`keyDown()\` is not supported in tagless components"` - // ); - // }); - // - // test('throws if component is using `click()`', () => { - // let source = ` - // export default Component.extend({ - // click() { - // console.log('Hello World!'); - // }, - // }); - // `; - // - // expect(() => transform(source, '')).toThrowErrorMatchingInlineSnapshot( - // `"Using \`click()\` is not supported in tagless components"` - // ); - // }); + + test('skips tagless components', () => { + let source = ` + import { tagName } from '@ember-decorators/component'; + + @tagName('') + export default class FooComponent extends Component { + } + `; + + let template = 'foo'; + + let result = transform(source, template); + expect(result.tagName).toEqual(undefined); + expect(result.source).toEqual(source); + expect(result.template).toEqual(template); + }); + + test('throws if component is using `this.element`', () => { + let source = ` + export default class FooComponent extends Component { + didInsertElement() { + console.log(this.element); + } + } + `; + + expect(() => transform(source, '')).toThrowErrorMatchingInlineSnapshot( + `"Using \`this.element\` is not supported in tagless components"` + ); + }); + + test('throws if component is using `this.elementId`', () => { + let source = ` + export default class FooComponent extends Component { + didInsertElement() { + console.log(this.elementId); + } + } + `; + + expect(() => transform(source, '')).toThrowErrorMatchingInlineSnapshot( + `"Using \`this.elementId\` is not supported in tagless components"` + ); + }); + + test('throws if component is using `keyDown()`', () => { + let source = ` + export default class FooComponent extends Component { + keyDown() { + console.log('Hello World!'); + } + } + `; + + expect(() => transform(source, '')).toThrowErrorMatchingInlineSnapshot( + `"Using \`keyDown()\` is not supported in tagless components"` + ); + }); + + test('throws if component is using `click()`', () => { + let source = ` + export default class FooComponent extends Component { + click() { + console.log('Hello World!'); + } + } + `; + + expect(() => transform(source, '')).toThrowErrorMatchingInlineSnapshot( + `"Using \`click()\` is not supported in tagless components"` + ); + }); // // test('multi-line template', () => { // let source = `export default Component.extend({});`; diff --git a/lib/transform/native.js b/lib/transform/native.js index 50a73bb..32bce76 100644 --- a/lib/transform/native.js +++ b/lib/transform/native.js @@ -85,9 +85,8 @@ module.exports = function transformNativeComponent(root, options) { debug('tagName: %o', tagName); - /* // skip components that use `this.element` - let thisElementPaths = j(objectArg).find(j.MemberExpression, { + let thisElementPaths = j(classBody).find(j.MemberExpression, { object: { type: 'ThisExpression' }, property: { name: 'element' }, }); @@ -96,7 +95,7 @@ module.exports = function transformNativeComponent(root, options) { } // skip components that use `this.elementId` - let thisElementIdPaths = j(objectArg).find(j.MemberExpression, { + let thisElementIdPaths = j(classBody).find(j.MemberExpression, { object: { type: 'ThisExpression' }, property: { name: 'elementId' }, }); @@ -106,12 +105,12 @@ module.exports = function transformNativeComponent(root, options) { // skip components that use `click()` etc. for (let methodName of EVENT_HANDLER_METHODS) { - let handlerMethod = properties.filter(path => isMethod(path, methodName))[0]; + let handlerMethod = classBody.filter(path => isMethod(path, methodName))[0]; if (handlerMethod) { throw new SilentError(`Using \`${methodName}()\` is not supported in tagless components`); } } -*/ + // analyze `elementId`, `attributeBindings`, `classNames` and `classNameBindings` let elementId = findElementId(classBody); debug('elementId: %o', elementId); @@ -143,9 +142,7 @@ module.exports = function transformNativeComponent(root, options) { j(classBody) .find(j.ClassProperty) // .filter(path => path.parentPath === properties) - .filter( - path => isProperty(path, 'elementId') - ) + .filter(path => isProperty(path, 'elementId')) .remove(); removeDecorator(root, classDeclaration, 'attributeBindings', '@ember-decorators/component'); diff --git a/lib/utils/native.js b/lib/utils/native.js index 21e9c12..77ebd7e 100644 --- a/lib/utils/native.js +++ b/lib/utils/native.js @@ -34,7 +34,7 @@ function isProperty(path, name) { function isMethod(path, name) { let node = path.value; - return node.type === 'ObjectMethod' && node.key.type === 'Identifier' && node.key.name === name; + return node.type === 'ClassMethod' && node.key.type === 'Identifier' && node.key.name === name; } function findStringProperty(properties, name, defaultValue = null) { From 9dc7427c9ccb56e6978cf1a2bbba7422b3d84ad1 Mon Sep 17 00:00:00 2001 From: simonihmig Date: Sun, 12 Jan 2020 21:19:59 +0100 Subject: [PATCH 04/11] Cleanup --- lib/__tests__/find-properties.js | 2 +- lib/transform/classic.js | 2 +- lib/transform/native.js | 2 -- lib/transform/template.js | 2 +- lib/{utils.js => utils/classic.js} | 2 +- lib/utils/native.js | 26 ++------------------------ 6 files changed, 6 insertions(+), 30 deletions(-) rename lib/{utils.js => utils/classic.js} (98%) diff --git a/lib/__tests__/find-properties.js b/lib/__tests__/find-properties.js index 77d646a..af43339 100644 --- a/lib/__tests__/find-properties.js +++ b/lib/__tests__/find-properties.js @@ -6,7 +6,7 @@ const { findAttributeBindings, findClassNames, findClassNameBindings, -} = require('../utils'); +} = require('../utils/classic'); describe('findTagName()', () => { const TESTS = [ diff --git a/lib/transform/classic.js b/lib/transform/classic.js index 9285c18..aea07e0 100644 --- a/lib/transform/classic.js +++ b/lib/transform/classic.js @@ -11,7 +11,7 @@ const { findAriaRole, isMethod, isProperty, -} = require('../utils'); +} = require('../utils/classic'); const EVENT_HANDLER_METHODS = [ // Touch events diff --git a/lib/transform/native.js b/lib/transform/native.js index 32bce76..343c1be 100644 --- a/lib/transform/native.js +++ b/lib/transform/native.js @@ -7,14 +7,12 @@ const { addClassDecorator, findTagName, findElementId, - findDecorator, findClassNames, findClassNameBindings, findAttributeBindings, isMethod, removeDecorator, ensureImport, - removeImport, isProperty, } = require('../utils/native'); diff --git a/lib/transform/template.js b/lib/transform/template.js index 1eee2ce..7077e0e 100644 --- a/lib/transform/template.js +++ b/lib/transform/template.js @@ -1,4 +1,4 @@ -const { indentLines } = require('../utils'); +const { indentLines } = require('../utils/classic'); const templateRecast = require('ember-template-recast'); diff --git a/lib/utils.js b/lib/utils/classic.js similarity index 98% rename from lib/utils.js rename to lib/utils/classic.js index 7462017..cb83e73 100644 --- a/lib/utils.js +++ b/lib/utils/classic.js @@ -1,6 +1,6 @@ const j = require('jscodeshift').withParser('ts'); -const SilentError = require('./silent-error'); +const SilentError = require('../silent-error'); function isProperty(path, name) { let node = path.value; diff --git a/lib/utils/native.js b/lib/utils/native.js index 77ebd7e..a27bfe3 100644 --- a/lib/utils/native.js +++ b/lib/utils/native.js @@ -78,28 +78,8 @@ function findTagName(path) { return findStringDecorator(path, 'tagName', 'div'); } -function findElementId(properties) { - return findStringProperty(properties, 'elementId'); -} - -function findStringArrayProperties(properties, name) { - let propertyPath = properties.filter(path => isProperty(path, name))[0]; - if (!propertyPath) { - return []; - } - - let arrayPath = propertyPath.get('value'); - if (arrayPath.value.type !== 'ArrayExpression') { - throw new SilentError(`Unexpected \`${name}\` value: ${j(arrayPath).toSource()}`); - } - - return arrayPath.get('elements').value.map(element => { - if (element.type !== 'StringLiteral') { - throw new SilentError(`Unexpected \`${name}\` value: ${j(arrayPath).toSource()}`); - } - - return element.value; - }); +function findElementId(path) { + return findStringProperty(path, 'elementId'); } function findStringArrayDecorator(path, name) { @@ -251,8 +231,6 @@ module.exports = { addClassDecorator, isProperty, isMethod, - findStringProperty, - findStringArrayProperties, findAttributeBindings, findClassNames, findClassNameBindings, From 09666fdf1e6de658f755a7016a7824fd34ffccbd Mon Sep 17 00:00:00 2001 From: simonihmig Date: Tue, 14 Jan 2020 23:10:28 +0100 Subject: [PATCH 05/11] Fix rebase --- lib/transform.js | 4 +--- lib/transform/native.js | 43 +---------------------------------------- 2 files changed, 2 insertions(+), 45 deletions(-) diff --git a/lib/transform.js b/lib/transform.js index 0f77fd9..41458e4 100644 --- a/lib/transform.js +++ b/lib/transform.js @@ -73,9 +73,7 @@ function transform(source, template, options = {}) { } else if (type === 'native') { result = transformNativeComponent(root, options); } else { - throw new SilentError( - `Unsupported component type. Only classic components (\`Component.extend({ ... }\`) are supported currently.` - ); + throw new SilentError(`Unsupported component type.`); } if (result) { diff --git a/lib/transform/native.js b/lib/transform/native.js index 343c1be..0ca26e0 100644 --- a/lib/transform/native.js +++ b/lib/transform/native.js @@ -149,46 +149,5 @@ module.exports = function transformNativeComponent(root, options) { let newSource = root.toSource(); - // wrap existing template with root element - let classNodes = []; - if (options.hasComponentCSS) { - classNodes.push(b.mustache('styleNamespace')); - } - - for (let className of classNames) { - classNodes.push(b.text(className)); - } - classNameBindings.forEach(([truthy, falsy], property) => { - if (!truthy) { - classNodes.push(b.mustache(`unless this.${property} "${falsy}"`)); - } else { - classNodes.push(b.mustache(`if this.${property} "${truthy}"${falsy ? ` "${falsy}"` : ''}`)); - } - }); - - let attrs = []; - - if (elementId) { - attrs.push(b.attr('id', b.text(elementId))); - } - - attributeBindings.forEach((value, key) => { - attrs.push(b.attr(key, b.mustache(`this.${value}`))); - }); - - if (classNodes.length === 1) { - attrs.push(b.attr('class', classNodes[0])); - } else if (classNodes.length !== 0) { - let parts = []; - classNodes.forEach((node, i) => { - if (i !== 0) parts.push(b.text(' ')); - parts.push(node); - }); - - attrs.push(b.attr('class', b.concat(parts))); - } - - attrs.push(b.attr('...attributes', b.text(''))); - - return { newSource, attrs, tagName }; + return { newSource, tagName, elementId, classNames, classNameBindings, attributeBindings }; }; From cbc2d519e513dc1fc0517764acb871e5f201a750 Mon Sep 17 00:00:00 2001 From: simonihmig Date: Tue, 14 Jan 2020 23:18:12 +0100 Subject: [PATCH 06/11] Enable all tests --- lib/__tests__/__snapshots__/transform.js.snap | 60 ++++++++++++++++++- lib/__tests__/transform.js | 60 ++++++++++--------- 2 files changed, 88 insertions(+), 32 deletions(-) diff --git a/lib/__tests__/__snapshots__/transform.js.snap b/lib/__tests__/__snapshots__/transform.js.snap index bd3a3fa..3797a9b 100644 --- a/lib/__tests__/__snapshots__/transform.js.snap +++ b/lib/__tests__/__snapshots__/transform.js.snap @@ -293,7 +293,7 @@ foo ==========" `; -exports[`native components handles \`classNameBindings\` correctly 1`] = ` +exports[`native components handles \`@classNameBindings\` correctly 1`] = ` "========== import { classNameBindings } from '@ember-decorators/component'; @@ -321,7 +321,7 @@ foo ==========" `; -exports[`native components handles \`classNames\` correctly 1`] = ` +exports[`native components handles \`@classNames\` correctly 1`] = ` "========== import { classNames } from '@ember-decorators/component'; @@ -373,7 +373,35 @@ foo ==========" `; -exports[`native components handles single \`classNames\` item correctly 1`] = ` +exports[`native components handles \`hasComponentCSS\` option correctly 1`] = ` +"========== + + import { classNames } from '@ember-decorators/component'; + + @classNames('foo', 'bar:baz') + export default class extends Component { + } + +~~~~~~~~~~ +foo +~~~~~~~~~~ + => tagName: div +~~~~~~~~~~ + + import { tagName } from '@ember-decorators/component'; + + @tagName(\\"\\") + export default class extends Component { + } + +~~~~~~~~~~ +
+ foo +
+==========" +`; + +exports[`native components handles single \`@classNames\` item correctly 1`] = ` "========== import { classNames } from '@ember-decorators/component'; @@ -401,6 +429,32 @@ foo ==========" `; +exports[`native components multi-line template 1`] = ` +"========== +export default class extends Component {}; +~~~~~~~~~~ +{{#if this.foo}} + FOO +{{else}} + BAR +{{/if}} +~~~~~~~~~~ + => tagName: div +~~~~~~~~~~ +import { tagName } from \\"@ember-decorators/component\\"; +@tagName(\\"\\") +export default class extends Component {} +~~~~~~~~~~ +
+ {{#if this.foo}} + FOO + {{else}} + BAR + {{/if}} +
+==========" +`; + exports[`native components replaces existing \`tagName\` 1`] = ` "========== diff --git a/lib/__tests__/transform.js b/lib/__tests__/transform.js index 81bc481..6c381b8 100644 --- a/lib/__tests__/transform.js +++ b/lib/__tests__/transform.js @@ -308,7 +308,7 @@ describe('native components', function() { expect(generateSnapshot(source, template)).toMatchSnapshot(); }); - test('handles `classNames` correctly', () => { + test('handles `@classNames` correctly', () => { let source = ` import { classNames } from '@ember-decorators/component'; @@ -322,7 +322,7 @@ describe('native components', function() { expect(generateSnapshot(source, template)).toMatchSnapshot(); }); - test('handles single `classNames` item correctly', () => { + test('handles single `@classNames` item correctly', () => { let source = ` import { classNames } from '@ember-decorators/component'; @@ -336,7 +336,7 @@ describe('native components', function() { expect(generateSnapshot(source, template)).toMatchSnapshot(); }); - test('handles `classNameBindings` correctly', () => { + test('handles `@classNameBindings` correctly', () => { let source = ` import { classNameBindings } from '@ember-decorators/component'; @@ -422,30 +422,32 @@ describe('native components', function() { `"Using \`click()\` is not supported in tagless components"` ); }); - // - // test('multi-line template', () => { - // let source = `export default Component.extend({});`; - // - // let template = ` - // {{#if this.foo}} - // FOO - // {{else}} - // BAR - // {{/if}} - // `.trim(); - // - // expect(generateSnapshot(source, template)).toMatchSnapshot(); - // }); - // - // test('handles `hasComponentCSS` option correctly', () => { - // let source = ` - // export default Component.extend({ - // classNames: ['foo', 'bar:baz'], - // }); - // `; - // - // let template = `foo`; - // - // expect(generateSnapshot(source, template, { hasComponentCSS: true })).toMatchSnapshot(); - // }); + + test('multi-line template', () => { + let source = `export default class extends Component {};`; + + let template = ` +{{#if this.foo}} + FOO +{{else}} + BAR +{{/if}} + `.trim(); + + expect(generateSnapshot(source, template)).toMatchSnapshot(); + }); + + test('handles `hasComponentCSS` option correctly', () => { + let source = ` + import { classNames } from '@ember-decorators/component'; + + @classNames('foo', 'bar:baz') + export default class extends Component { + } + `; + + let template = `foo`; + + expect(generateSnapshot(source, template, { hasComponentCSS: true })).toMatchSnapshot(); + }); }); From be8884eaec25702f488b7d0b1cf98da3ed9beff7 Mon Sep 17 00:00:00 2001 From: simonihmig Date: Wed, 15 Jan 2020 00:12:20 +0100 Subject: [PATCH 07/11] Handle `attribute` decorator --- lib/__tests__/__snapshots__/transform.js.snap | 61 +++++++++++++++++++ lib/__tests__/transform.js | 30 +++++++++ lib/transform/native.js | 2 + lib/utils/native.js | 42 +++++++++---- 4 files changed, 124 insertions(+), 11 deletions(-) diff --git a/lib/__tests__/__snapshots__/transform.js.snap b/lib/__tests__/__snapshots__/transform.js.snap index 3797a9b..9ca3fb0 100644 --- a/lib/__tests__/__snapshots__/transform.js.snap +++ b/lib/__tests__/__snapshots__/transform.js.snap @@ -265,6 +265,67 @@ foo ==========" `; +exports[`native components handles \`@attribute\` and \`@attributeBindings\` correctly 1`] = ` +"========== + + import { attribute, attributeBindings } from '@ember-decorators/component'; + + @attributeBindings('foo') + export default class FooComponent extends Component { + @attribute('baz') bar; + } + +~~~~~~~~~~ +foo +~~~~~~~~~~ + => tagName: div +~~~~~~~~~~ + + import { tagName } from '@ember-decorators/component'; + + @tagName(\\"\\") + export default class FooComponent extends Component { + bar; + } + +~~~~~~~~~~ +
+ foo +
+==========" +`; + +exports[`native components handles \`@attribute\` correctly 1`] = ` +"========== + + import { attribute } from '@ember-decorators/component'; + + export default class FooComponent extends Component { + @attribute foo; + @attribute('baz') bar; + } + +~~~~~~~~~~ +foo +~~~~~~~~~~ + => tagName: div +~~~~~~~~~~ + + import { tagName } from '@ember-decorators/component'; + + @tagName(\\"\\") + export default class FooComponent extends Component { + foo; + bar; + } + +~~~~~~~~~~ +
+ foo +
+==========" +`; + exports[`native components handles \`@attributeBindings\` correctly 1`] = ` "========== diff --git a/lib/__tests__/transform.js b/lib/__tests__/transform.js index 6c381b8..ebda7e7 100644 --- a/lib/__tests__/transform.js +++ b/lib/__tests__/transform.js @@ -308,6 +308,36 @@ describe('native components', function() { expect(generateSnapshot(source, template)).toMatchSnapshot(); }); + test('handles `@attribute` correctly', () => { + let source = ` + import { attribute } from '@ember-decorators/component'; + + export default class FooComponent extends Component { + @attribute foo; + @attribute('baz') bar; + } + `; + + let template = `foo`; + + expect(generateSnapshot(source, template)).toMatchSnapshot(); + }); + + test('handles `@attribute` and `@attributeBindings` correctly', () => { + let source = ` + import { attribute, attributeBindings } from '@ember-decorators/component'; + + @attributeBindings('foo') + export default class FooComponent extends Component { + @attribute('baz') bar; + } + `; + + let template = `foo`; + + expect(generateSnapshot(source, template)).toMatchSnapshot(); + }); + test('handles `@classNames` correctly', () => { let source = ` import { classNames } from '@ember-decorators/component'; diff --git a/lib/transform/native.js b/lib/transform/native.js index 0ca26e0..65dfb7e 100644 --- a/lib/transform/native.js +++ b/lib/transform/native.js @@ -146,6 +146,8 @@ module.exports = function transformNativeComponent(root, options) { removeDecorator(root, classDeclaration, 'attributeBindings', '@ember-decorators/component'); removeDecorator(root, classDeclaration, 'classNames', '@ember-decorators/component'); removeDecorator(root, classDeclaration, 'classNameBindings', '@ember-decorators/component'); + j(classBody).find(j.ClassProperty).forEach(path => removeDecorator(root, path, 'attribute', '@ember-decorators/component')); + let newSource = root.toSource(); diff --git a/lib/utils/native.js b/lib/utils/native.js index a27bfe3..57caca7 100644 --- a/lib/utils/native.js +++ b/lib/utils/native.js @@ -17,13 +17,16 @@ function addClassDecorator(classDeclaration, name, args) { } } -function isDecorator(path, name) { +function isDecorator(path, name, withArgs) { let node = path.value; + let isCall = + node.expression.type === 'CallExpression' && node.expression.callee.type === 'Identifier'; + return ( node.type === 'Decorator' && - node.expression.type === 'CallExpression' && - node.expression.callee.type === 'Identifier' && - node.expression.callee.name === name + ((isCall && node.expression.callee.name === name) || + (!isCall && node.expression.name === name)) && + (withArgs === undefined || ((withArgs === true && isCall) || (withArgs === false && !isCall))) ); } @@ -51,31 +54,32 @@ function findStringProperty(properties, name, defaultValue = null) { return valuePath.value.value; } -function findDecorator(path, name) { +function findDecorator(path, name, withArgs) { let decorators = path.get('decorators'); if (decorators.value === undefined) { return; } - let existing = decorators.filter(path => isDecorator(path, name)); + let existing = decorators.filter(path => isDecorator(path, name, withArgs)); if (existing.length > 0) { return existing[0]; } } -function findStringDecorator(path, name, defaultValue = null) { - let decorator = findDecorator(path, name); +function findStringDecorator(path, name) { + let decorator = findDecorator(path, name, true); if (!decorator) { - return defaultValue; + return; } return decorator.value.expression.arguments[0].value; } function findTagName(path) { - return findStringDecorator(path, 'tagName', 'div'); + let value = findStringDecorator(path, 'tagName'); + return value !== undefined ? value : 'div'; } function findElementId(path) { @@ -83,7 +87,7 @@ function findElementId(path) { } function findStringArrayDecorator(path, name) { - let decorator = findDecorator(path, name); + let decorator = findDecorator(path, name, true); if (!decorator) { return []; } @@ -106,6 +110,22 @@ function findAttributeBindings(classDeclaration) { attrBindings.set(attr || value, value); } + j(classDeclaration) + .find(j.ClassProperty) + .forEach(path => { + let value = path.node.key.name; + let key = findStringDecorator(path, 'attribute'); + if (key === undefined) { + let decorator = findDecorator(path, 'attribute', false); + if (decorator) { + key = value; + } + } + if (key !== undefined) { + attrBindings.set(key, value); + } + }); + return attrBindings; } From a8a6b4a2563d1dd87f63f84f030aac176ec55999 Mon Sep 17 00:00:00 2001 From: simonihmig Date: Wed, 15 Jan 2020 00:33:52 +0100 Subject: [PATCH 08/11] Cleanup --- lib/transform/native.js | 8 +++----- lib/utils/native.js | 2 +- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/lib/transform/native.js b/lib/transform/native.js index 65dfb7e..c78c889 100644 --- a/lib/transform/native.js +++ b/lib/transform/native.js @@ -1,6 +1,5 @@ const j = require('jscodeshift').withParser('ts'); const _debug = require('debug')('tagless-ember-components-codemod'); -const templateRecast = require('ember-template-recast'); const SilentError = require('../silent-error'); const { @@ -16,8 +15,6 @@ const { isProperty, } = require('../utils/native'); -const b = templateRecast.builders; - const EVENT_HANDLER_METHODS = [ // Touch events 'touchStart', @@ -146,8 +143,9 @@ module.exports = function transformNativeComponent(root, options) { removeDecorator(root, classDeclaration, 'attributeBindings', '@ember-decorators/component'); removeDecorator(root, classDeclaration, 'classNames', '@ember-decorators/component'); removeDecorator(root, classDeclaration, 'classNameBindings', '@ember-decorators/component'); - j(classBody).find(j.ClassProperty).forEach(path => removeDecorator(root, path, 'attribute', '@ember-decorators/component')); - + j(classBody) + .find(j.ClassProperty) + .forEach(path => removeDecorator(root, path, 'attribute', '@ember-decorators/component')); let newSource = root.toSource(); diff --git a/lib/utils/native.js b/lib/utils/native.js index 57caca7..d456193 100644 --- a/lib/utils/native.js +++ b/lib/utils/native.js @@ -26,7 +26,7 @@ function isDecorator(path, name, withArgs) { node.type === 'Decorator' && ((isCall && node.expression.callee.name === name) || (!isCall && node.expression.name === name)) && - (withArgs === undefined || ((withArgs === true && isCall) || (withArgs === false && !isCall))) + (withArgs === undefined || (withArgs === true && isCall) || (withArgs === false && !isCall)) ); } From 60262490c2b1ba5b6a9846e8d35e2892325e28f3 Mon Sep 17 00:00:00 2001 From: simonihmig Date: Wed, 15 Jan 2020 10:30:39 +0100 Subject: [PATCH 09/11] Handle @className --- lib/__tests__/__snapshots__/transform.js.snap | 33 ++++++++++++++ lib/__tests__/transform.js | 44 +++++++++++++++++++ lib/transform/native.js | 3 ++ lib/utils/native.js | 22 +++++++++- 4 files changed, 100 insertions(+), 2 deletions(-) diff --git a/lib/__tests__/__snapshots__/transform.js.snap b/lib/__tests__/__snapshots__/transform.js.snap index 9ca3fb0..4f5eeb1 100644 --- a/lib/__tests__/__snapshots__/transform.js.snap +++ b/lib/__tests__/__snapshots__/transform.js.snap @@ -354,6 +354,39 @@ foo ==========" `; +exports[`native components handles \`@className\` correctly 1`] = ` +"========== + + import { className } from '@ember-decorators/component'; + + export default class FooComponent extends Component { + @className('b') a; + @className('y', 'z') x; + @className(undefined, 'bar') foo; + } + +~~~~~~~~~~ +foo +~~~~~~~~~~ + => tagName: div +~~~~~~~~~~ + + import { tagName } from '@ember-decorators/component'; + + @tagName(\\"\\") + export default class FooComponent extends Component { + a; + x; + foo; + } + +~~~~~~~~~~ +
+ foo +
+==========" +`; + exports[`native components handles \`@classNameBindings\` correctly 1`] = ` "========== diff --git a/lib/__tests__/transform.js b/lib/__tests__/transform.js index ebda7e7..f45535f 100644 --- a/lib/__tests__/transform.js +++ b/lib/__tests__/transform.js @@ -338,6 +338,36 @@ describe('native components', function() { expect(generateSnapshot(source, template)).toMatchSnapshot(); }); + test('handles `@className` correctly', () => { + let source = ` + import { className } from '@ember-decorators/component'; + + export default class FooComponent extends Component { + @className('b') a; + @className('y', 'z') x; + @className(undefined, 'bar') foo; + } + `; + + let template = `foo`; + + expect(generateSnapshot(source, template)).toMatchSnapshot(); + }); + + test('throws for non-boolean @className', () => { + let source = ` + import { className } from '@ember-decorators/component'; + + export default class FooComponent extends Component { + @className activeClass = 'active'; + } + `; + + expect(() => transform(source, '')).toThrowErrorMatchingInlineSnapshot( + `"Unsupported non-boolean \`@className\` for property: activeClass"` + ); + }); + test('handles `@classNames` correctly', () => { let source = ` import { classNames } from '@ember-decorators/component'; @@ -380,6 +410,20 @@ describe('native components', function() { expect(generateSnapshot(source, template)).toMatchSnapshot(); }); + test('throws for non-boolean @classNameBindings', () => { + let source = ` + import { classNameBindings } from '@ember-decorators/component'; + + @classNameBindings('a:b', 'foo') + export default class FooComponent extends Component { + } + `; + + expect(() => transform(source, '')).toThrowErrorMatchingInlineSnapshot( + `"Unsupported non-boolean \`@classNameBindings\` value: foo"` + ); + }); + test('skips tagless components', () => { let source = ` import { tagName } from '@ember-decorators/component'; diff --git a/lib/transform/native.js b/lib/transform/native.js index c78c889..18975aa 100644 --- a/lib/transform/native.js +++ b/lib/transform/native.js @@ -146,6 +146,9 @@ module.exports = function transformNativeComponent(root, options) { j(classBody) .find(j.ClassProperty) .forEach(path => removeDecorator(root, path, 'attribute', '@ember-decorators/component')); + j(classBody) + .find(j.ClassProperty) + .forEach(path => removeDecorator(root, path, 'className', '@ember-decorators/component')); let newSource = root.toSource(); diff --git a/lib/utils/native.js b/lib/utils/native.js index d456193..23dba1f 100644 --- a/lib/utils/native.js +++ b/lib/utils/native.js @@ -139,16 +139,34 @@ function findClassNameBindings(classDeclaration) { let parts = binding.split(':'); if (parts.length === 1) { - throw new SilentError(`Unsupported non-boolean \`classNameBindings\` value: ${binding}`); + throw new SilentError(`Unsupported non-boolean \`@classNameBindings\` value: ${binding}`); } else if (parts.length === 2) { classNameBindings.set(parts[0], [parts[1], null]); } else if (parts.length === 3) { classNameBindings.set(parts[0], [parts[1] || null, parts[2]]); } else { - throw new SilentError(`Unexpected \`classNameBindings\` value: ${binding}`); + throw new SilentError(`Unexpected \`@classNameBindings\` value: ${binding}`); } } + j(classDeclaration) + .find(j.ClassProperty) + .forEach(path => { + let key = path.node.key.name; + + if (findDecorator(path, 'className', false)) { + throw new SilentError(`Unsupported non-boolean \`@className\` for property: ${key}`); + } + let decorator = findDecorator(path, 'className', true); + if (!decorator) { + return; + } + + let args = decorator.get('expression').value.arguments; + let [truthy, falsy] = args.map(element => element.value); + classNameBindings.set(key, [truthy || null, falsy || null]); + }); + return classNameBindings; } From 2383f846093632a802d700ddc25bf461ea27921d Mon Sep 17 00:00:00 2001 From: simonihmig Date: Wed, 15 Jan 2020 10:38:38 +0100 Subject: [PATCH 10/11] Cleanup --- lib/__tests__/__snapshots__/transform.js.snap | 56 +++++++++---------- lib/__tests__/transform.js | 41 +++++--------- lib/transform/native.js | 10 ---- lib/transform/template.js | 2 +- lib/utils/classic.js | 8 --- lib/utils/native.js | 9 --- lib/utils/template.js | 10 ++++ 7 files changed, 54 insertions(+), 82 deletions(-) create mode 100644 lib/utils/template.js diff --git a/lib/__tests__/__snapshots__/transform.js.snap b/lib/__tests__/__snapshots__/transform.js.snap index 4f5eeb1..5f4c259 100644 --- a/lib/__tests__/__snapshots__/transform.js.snap +++ b/lib/__tests__/__snapshots__/transform.js.snap @@ -5,7 +5,7 @@ exports[`classic components basic 1`] = ` export default Component.extend({ }); - + ~~~~~~~~~~ foo ~~~~~~~~~~ @@ -15,7 +15,7 @@ foo export default Component.extend({ tagName: \\"\\" }); - + ~~~~~~~~~~
foo @@ -23,13 +23,13 @@ foo ==========" `; -exports[`handles \`ariaRole\` correctly 1`] = ` +exports[`classic components handles \`ariaRole\` correctly 1`] = ` "========== export default Component.extend({ ariaRole: 'button', }); - + ~~~~~~~~~~ foo ~~~~~~~~~~ @@ -39,7 +39,7 @@ foo export default Component.extend({ tagName: \\"\\", }); - + ~~~~~~~~~~
foo @@ -47,13 +47,13 @@ foo ==========" `; -exports[`handles \`attributeBindings\` correctly 1`] = ` +exports[`classic components handles \`attributeBindings\` correctly 1`] = ` "========== export default Component.extend({ attributeBindings: ['foo', 'bar:baz'], }); - + ~~~~~~~~~~ foo ~~~~~~~~~~ @@ -63,7 +63,7 @@ foo export default Component.extend({ tagName: \\"\\", }); - + ~~~~~~~~~~
foo @@ -77,7 +77,7 @@ exports[`classic components handles \`classNameBindings\` correctly 1`] = ` export default Component.extend({ classNameBindings: ['a:b', 'x:y:z', 'foo::bar'], }); - + ~~~~~~~~~~ foo ~~~~~~~~~~ @@ -87,7 +87,7 @@ foo export default Component.extend({ tagName: \\"\\", }); - + ~~~~~~~~~~
foo @@ -101,7 +101,7 @@ exports[`classic components handles \`classNames\` correctly 1`] = ` export default Component.extend({ classNames: ['foo', 'bar:baz'], }); - + ~~~~~~~~~~ foo ~~~~~~~~~~ @@ -111,7 +111,7 @@ foo export default Component.extend({ tagName: \\"\\", }); - + ~~~~~~~~~~
foo @@ -125,7 +125,7 @@ exports[`classic components handles \`elementId\` correctly 1`] = ` export default Component.extend({ elementId: 'qux', }); - + ~~~~~~~~~~ foo ~~~~~~~~~~ @@ -135,7 +135,7 @@ foo export default Component.extend({ tagName: \\"\\", }); - + ~~~~~~~~~~
foo @@ -149,7 +149,7 @@ exports[`classic components handles \`hasComponentCSS\` option correctly 1`] = ` export default Component.extend({ classNames: ['foo', 'bar:baz'], }); - + ~~~~~~~~~~ foo ~~~~~~~~~~ @@ -159,7 +159,7 @@ foo export default Component.extend({ tagName: \\"\\", }); - + ~~~~~~~~~~
foo @@ -173,7 +173,7 @@ exports[`classic components handles single \`classNames\` item correctly 1`] = ` export default Component.extend({ classNames: ['foo'], }); - + ~~~~~~~~~~ foo ~~~~~~~~~~ @@ -183,7 +183,7 @@ foo export default Component.extend({ tagName: \\"\\", }); - + ~~~~~~~~~~
foo @@ -223,7 +223,7 @@ exports[`classic components replaces existing \`tagName\` 1`] = ` export default Component.extend({ tagName: 'span', }); - + ~~~~~~~~~~ foo ~~~~~~~~~~ @@ -233,7 +233,7 @@ foo export default Component.extend({ tagName: \\"\\", }); - + ~~~~~~~~~~ foo @@ -246,7 +246,7 @@ exports[`native components basic 1`] = ` export default class FooComponent extends Component { } - + ~~~~~~~~~~ foo ~~~~~~~~~~ @@ -257,7 +257,7 @@ foo @tagName(\\"\\") export default class FooComponent extends Component { } - + ~~~~~~~~~~
foo @@ -334,7 +334,7 @@ exports[`native components handles \`@attributeBindings\` correctly 1`] = ` @attributeBindings('foo', 'bar:baz') export default class FooComponent extends Component { } - + ~~~~~~~~~~ foo ~~~~~~~~~~ @@ -346,7 +346,7 @@ foo @tagName(\\"\\") export default class FooComponent extends Component { } - + ~~~~~~~~~~
foo @@ -449,7 +449,7 @@ exports[`native components handles \`elementId\` correctly 1`] = ` export default class FooComponent extends Component { elementId = 'qux'; } - + ~~~~~~~~~~ foo ~~~~~~~~~~ @@ -459,7 +459,7 @@ foo import { tagName } from \\"@ember-decorators/component\\"; @tagName(\\"\\") export default class FooComponent extends Component {} - + ~~~~~~~~~~
foo @@ -557,7 +557,7 @@ exports[`native components replaces existing \`tagName\` 1`] = ` @tagName('span') export default class FooComponent extends Component { } - + ~~~~~~~~~~ foo ~~~~~~~~~~ @@ -569,7 +569,7 @@ foo @tagName(\\"\\") export default class FooComponent extends Component { } - + ~~~~~~~~~~ foo diff --git a/lib/__tests__/transform.js b/lib/__tests__/transform.js index f45535f..700f3d2 100644 --- a/lib/__tests__/transform.js +++ b/lib/__tests__/transform.js @@ -111,31 +111,20 @@ describe('classic components', function() { expect(generateSnapshot(source, template)).toMatchSnapshot(); }); -test('handles `ariaRole` correctly', () => { - let source = ` + test('handles `ariaRole` correctly', () => { + let source = ` export default Component.extend({ ariaRole: 'button', }); `; - let template = `foo`; - - expect(generateSnapshot(source, template)).toMatchSnapshot(); -}); - -test('throws if `Component.extend({ ... })` is not found', () => { - let source = ` - export default class extends Component { - } - `; + let template = `foo`; - expect(() => transform(source, '')).toThrowErrorMatchingInlineSnapshot( - `"Unsupported component type. Only classic components (\`Component.extend({ ... }\`) are supported currently."` - ); -}); + expect(generateSnapshot(source, template)).toMatchSnapshot(); + }); -test('throws if `Component.extend({ ... })` argument is not found', () => { - let source = ` + test('throws if `Component.extend({ ... })` argument is not found', () => { + let source = ` export default Component.extend(); `; @@ -215,8 +204,8 @@ test('throws if `Component.extend({ ... })` argument is not found', () => { ); }); -test('throws if component is using a computed property for `ariaRole`', () => { - let source = ` + test('throws if component is using a computed property for `ariaRole`', () => { + let source = ` export default Component.extend({ ariaRole: computed(function() { return 'button'; @@ -224,13 +213,13 @@ test('throws if component is using a computed property for `ariaRole`', () => { }); `; - expect(() => transform(source, '')).toThrowErrorMatchingInlineSnapshot( - `"Codemod does not support computed properties for \`ariaRole\`."` - ); -}); + expect(() => transform(source, '')).toThrowErrorMatchingInlineSnapshot( + `"Codemod does not support computed properties for \`ariaRole\`."` + ); + }); -test('multi-line template', () => { - let source = `export default Component.extend({});`; + test('multi-line template', () => { + let source = `export default Component.extend({});`; let template = ` {{#if this.foo}} diff --git a/lib/transform/native.js b/lib/transform/native.js index 18975aa..68fdf86 100644 --- a/lib/transform/native.js +++ b/lib/transform/native.js @@ -122,16 +122,6 @@ module.exports = function transformNativeComponent(root, options) { // set `@tagName('')` addClassDecorator(exportDefaultDeclaration, 'tagName', [j.stringLiteral('')]); ensureImport(root, 'tagName', '@ember-decorators/component'); - // let tagNamePath = j(classBody) - // .find(j.ClassProperty) - // // .filter(path => path.parentPath === properties) - // .filter(path => isProperty(path, 'tagName')); - // - // if (tagNamePath.length === 1) { - // j(tagNamePath.get('value')).replaceWith(j.stringLiteral('')); - // } else { - // classBody.unshift(j.classProperty(j.identifier('tagName'), j.stringLiteral(''))); - // } // remove `elementId`, `attributeBindings`, `classNames` and `classNameBindings` j(classBody) diff --git a/lib/transform/template.js b/lib/transform/template.js index 7077e0e..3f17794 100644 --- a/lib/transform/template.js +++ b/lib/transform/template.js @@ -1,4 +1,4 @@ -const { indentLines } = require('../utils/classic'); +const { indentLines } = require('../utils/template'); const templateRecast = require('ember-template-recast'); diff --git a/lib/utils/classic.js b/lib/utils/classic.js index cb83e73..dd55803 100644 --- a/lib/utils/classic.js +++ b/lib/utils/classic.js @@ -91,13 +91,6 @@ function findClassNameBindings(properties) { return classNameBindings; } -function indentLines(content) { - return content - .split('\n') - .map(it => ` ${it}`) - .join('\n'); -} - module.exports = { isProperty, isMethod, @@ -109,5 +102,4 @@ module.exports = { findClassNameBindings, findElementId, findTagName, - indentLines, }; diff --git a/lib/utils/native.js b/lib/utils/native.js index 23dba1f..ff3e486 100644 --- a/lib/utils/native.js +++ b/lib/utils/native.js @@ -178,13 +178,6 @@ function removeDecorator(root, classDeclaration, name, source) { } } -function indentLines(content) { - return content - .split('\n') - .map(it => ` ${it}`) - .join('\n'); -} - function ensureImport(root, name, source) { let body = root.get().value.program.body; @@ -225,7 +218,6 @@ function removeImport(root, name, source) { .remove(); if (declaration.get().value.specifiers.length === 0) { - // console.log(root.get('program', 'body')) declaration.remove(); } } @@ -278,5 +270,4 @@ module.exports = { removeDecorator, ensureImport, removeImport, - indentLines, }; diff --git a/lib/utils/template.js b/lib/utils/template.js new file mode 100644 index 0000000..3db79ef --- /dev/null +++ b/lib/utils/template.js @@ -0,0 +1,10 @@ +function indentLines(content) { + return content + .split('\n') + .map(it => ` ${it}`) + .join('\n'); +} + +module.exports = { + indentLines, +}; From d76ab6709614f31b82b4cc56d10d3f0feb75bdfe Mon Sep 17 00:00:00 2001 From: Jeldrik Hanschke Date: Tue, 21 Jan 2020 14:53:59 +0100 Subject: [PATCH 11/11] support for native classes --- lib/__tests__/__snapshots__/transform.js.snap | 24 ++++++++ lib/__tests__/transform.js | 55 +++++++++++++++++++ lib/transform/native.js | 27 ++++++++- lib/utils/native.js | 5 ++ 4 files changed, 109 insertions(+), 2 deletions(-) diff --git a/lib/__tests__/__snapshots__/transform.js.snap b/lib/__tests__/__snapshots__/transform.js.snap index 5f4c259..215db87 100644 --- a/lib/__tests__/__snapshots__/transform.js.snap +++ b/lib/__tests__/__snapshots__/transform.js.snap @@ -443,6 +443,30 @@ foo ==========" `; +exports[`native components handles \`ariaRole\` correctly 1`] = ` +"========== + + export default class FooComponent extends Component { + ariaRole = 'button'; + } + +~~~~~~~~~~ +foo +~~~~~~~~~~ + => tagName: div +~~~~~~~~~~ + + import { tagName } from \\"@ember-decorators/component\\"; + @tagName(\\"\\") + export default class FooComponent extends Component {} + +~~~~~~~~~~ +
+ foo +
+==========" +`; + exports[`native components handles \`elementId\` correctly 1`] = ` "========== diff --git a/lib/__tests__/transform.js b/lib/__tests__/transform.js index 700f3d2..ea243a3 100644 --- a/lib/__tests__/transform.js +++ b/lib/__tests__/transform.js @@ -343,6 +343,61 @@ describe('native components', function() { expect(generateSnapshot(source, template)).toMatchSnapshot(); }); + test('handles `ariaRole` correctly', () => { + let source = ` + export default class FooComponent extends Component { + ariaRole = 'button'; + } + `; + + let template = `foo`; + + expect(generateSnapshot(source, template)).toMatchSnapshot(); + }); + + test('throws if component is a non-string value for `ariaRole`', () => { + let source = ` + export default class FooComponent extends Component { + ariaRole = true; + } + `; + + expect(() => transform(source, '')).toThrowErrorMatchingInlineSnapshot( + `"Codemod only supports static strings for \`ariaRole\`."` + ); + }); + + test('throws if component is using a native getter for `ariaRole`', () => { + let source = ` + export default class FooComponent extends Component { + get ariaRole() { + return 'button'; + } + } + `; + + expect(() => transform(source, '')).toThrowErrorMatchingInlineSnapshot( + `"Codemod does not support dynamic values for \`ariaRole\` (e.g. getter or computed properties)."` + ); + }); + + test('throws if component is using a computed property for `ariaRole`', () => { + let source = ` + import { computed } from '@ember/object'; + + export default class FooComponent extends Component { + @computed() + get ariaRole() { + return 'button'; + } + } + `; + + expect(() => transform(source, '')).toThrowErrorMatchingInlineSnapshot( + `"Codemod does not support dynamic values for \`ariaRole\` (e.g. getter or computed properties)."` + ); + }); + test('throws for non-boolean @className', () => { let source = ` import { className } from '@ember-decorators/component'; diff --git a/lib/transform/native.js b/lib/transform/native.js index 68fdf86..8081820 100644 --- a/lib/transform/native.js +++ b/lib/transform/native.js @@ -9,6 +9,7 @@ const { findClassNames, findClassNameBindings, findAttributeBindings, + findAriaRole, isMethod, removeDecorator, ensureImport, @@ -119,6 +120,20 @@ module.exports = function transformNativeComponent(root, options) { let classNameBindings = findClassNameBindings(classDeclaration); debug('classNameBindings: %o', classNameBindings); + let ariaRole; + try { + ariaRole = findAriaRole(classBody); + } catch (error) { + throw new SilentError('Codemod only supports static strings for `ariaRole`.'); + } + debug('ariaRole: %o', ariaRole); + + if (!ariaRole && classBody.filter(path => isMethod(path, 'ariaRole'))[0]) { + throw new SilentError( + 'Codemod does not support dynamic values for `ariaRole` (e.g. getter or computed properties).' + ); + } + // set `@tagName('')` addClassDecorator(exportDefaultDeclaration, 'tagName', [j.stringLiteral('')]); ensureImport(root, 'tagName', '@ember-decorators/component'); @@ -127,7 +142,7 @@ module.exports = function transformNativeComponent(root, options) { j(classBody) .find(j.ClassProperty) // .filter(path => path.parentPath === properties) - .filter(path => isProperty(path, 'elementId')) + .filter(path => isProperty(path, 'elementId') || isProperty(path, 'ariaRole')) .remove(); removeDecorator(root, classDeclaration, 'attributeBindings', '@ember-decorators/component'); @@ -142,5 +157,13 @@ module.exports = function transformNativeComponent(root, options) { let newSource = root.toSource(); - return { newSource, tagName, elementId, classNames, classNameBindings, attributeBindings }; + return { + newSource, + tagName, + elementId, + classNames, + classNameBindings, + attributeBindings, + ariaRole, + }; }; diff --git a/lib/utils/native.js b/lib/utils/native.js index ff3e486..9d23278 100644 --- a/lib/utils/native.js +++ b/lib/utils/native.js @@ -86,6 +86,10 @@ function findElementId(path) { return findStringProperty(path, 'elementId'); } +function findAriaRole(path) { + return findStringProperty(path, 'ariaRole'); +} + function findStringArrayDecorator(path, name) { let decorator = findDecorator(path, name, true); if (!decorator) { @@ -261,6 +265,7 @@ module.exports = { addClassDecorator, isProperty, isMethod, + findAriaRole, findAttributeBindings, findClassNames, findClassNameBindings,