diff --git a/blueprints/component/files/__root__/__path__/__name__.ts b/blueprints/component/files/__root__/__path__/__name__.ts index c9a1b45a..b0d98cd9 100644 --- a/blueprints/component/files/__root__/__path__/__name__.ts +++ b/blueprints/component/files/__root__/__path__/__name__.ts @@ -1,7 +1,5 @@ -import Component from '@ember/component'; +<%= importComponent %> <%= importTemplate %> -export default class <%= classifiedModuleName %> extends Component.extend({ - // anything which *must* be merged to prototype here -}) {<%= contents %> - // normal class body definition here -}; +interface <%= classifiedModuleName %>ComponentArgs {} + +export default class <%= classifiedModuleName %>Component extends Component<<%= classifiedModuleName %>ComponentArgs> {} diff --git a/blueprints/component/index.js b/blueprints/component/index.js index 7d52375f..bb9b8b7b 100644 --- a/blueprints/component/index.js +++ b/blueprints/component/index.js @@ -1,12 +1,23 @@ 'use strict'; +const chalk = require('chalk'); const path = require('path'); +const SilentError = require('silent-error'); const stringUtil = require('ember-cli-string-utils'); const pathUtil = require('ember-cli-path-utils'); const getPathOption = require('ember-cli-get-component-path-option'); const normalizeEntityName = require('ember-cli-normalize-entity-name'); -const EOL = require('os').EOL; +const {EOL} = require('os'); +const {has} = require('@ember/edition-utils'); +const OCTANE = has('octane'); + +// TODO: this should be reading from the @ember/canary-features module +// need to refactor broccoli/features.js to be able to work more similarly +// to https://github.com/emberjs/data/pull/6231 +const EMBER_GLIMMER_SET_COMPONENT_TEMPLATE = true; + +// intentionally avoiding use-edition-detector module.exports = { description: 'Generates a component.', @@ -15,53 +26,209 @@ module.exports = { name: 'path', type: String, default: 'components', - aliases: [{ 'no-path': '' }], + aliases: [{'no-path': ''}] + }, + { + name: 'component-class', + type: ['@ember/component', '@glimmer/component', '@ember/component/template-only', ''], + default: OCTANE ? '--no-component-class' : '@ember/component', + aliases: [ + {cc: '@ember/component'}, + {gc: '@glimmer/component'}, + {tc: '@ember/component/template-only'}, + {nc: ''}, + {'no-component-class': ''}, + {'with-component-class': OCTANE ? '@glimmer/component' : '@ember/component'} + ] }, + { + name: 'component-structure', + type: OCTANE ? ['flat', 'nested', 'classic'] : ['classic'], + default: OCTANE ? 'flat' : 'classic', + aliases: OCTANE ? [{fs: 'flat'}, {ns: 'nested'}, {cs: 'classic'}] : [{cs: 'classic'}] + } ], - filesPath: function() { - let filesDirectory = 'files'; - let dependencies = this.project.dependencies(); + init() { + this._super && this._super.init.apply(this, arguments); + let isOctane = has('octane'); + + this.availableOptions.forEach(option => { + if (option.name === 'component-class') { + if (isOctane) { + option.default = '--no-component-class'; + } else { + option.default = '@ember/component'; + } + } else if (option.name === 'component-structure') { + if (isOctane) { + option.type = ['flat', 'nested', 'classic']; + option.default = 'flat'; + option.aliases = [{fs: 'flat'}, {ns: 'nested'}, {cs: 'classic'}]; + } else { + option.type = ['classic']; + option.default = 'classic'; + option.aliases = [{cs: 'classic'}]; + } + } + }); + + this.skippedJsFiles = new Set(); + this.savedLocals = {}; + + this.EMBER_GLIMMER_SET_COMPONENT_TEMPLATE = EMBER_GLIMMER_SET_COMPONENT_TEMPLATE || isOctane; + }, - if ('@glimmer/component' in dependencies) { - filesDirectory = 'glimmer-files'; + install(options) { + // Normalize the `componentClass` option. This is usually handled for us, + // but we wanted to show '--no-component-class' as the default so that is + // what's passed to us literally if the user didn't override it. + if (options.componentClass === '--no-component-class') { + options.componentClass = ''; } - return path.join(this.path, filesDirectory); + if (!this.EMBER_GLIMMER_SET_COMPONENT_TEMPLATE) { + if (options.componentClass !== '@ember/component') { + throw new SilentError( + 'Usage of --component-class argument to `ember generate component` is only available on canary' + ); + } + + if (options.componentStructure !== 'classic') { + throw new SilentError( + 'Usage of --component-structure argument to `ember generate component` is only available on canary' + ); + } + } + + return this._super.install.apply(this, arguments); }, - fileMapTokens: function() { - return { - __path__: function(options) { - if (options.pod) { + uninstall(options) { + // Force the `componentClass` option to be non-empty. It doesn't really + // matter what it is set to. All we want is to delete the optional JS + // file if the user had created one (when using this generator, created + // manually, added later with component-class generator...). + options.componentClass = '@ember/component'; + + return this._super.uninstall.apply(this, arguments); + }, + + beforeInstall(options, locals) { + this.savedLocals = locals; + }, + + afterInstall(options) { + this._super.afterInstall.apply(this, arguments); + + this.skippedJsFiles.forEach(file => { + let mapped = this.mapFile(file, this.savedLocals); + this.ui.writeLine(` ${chalk.yellow('skip')} ${mapped}`); + }); + + if (this.skippedJsFiles.size > 0) { + let command = `ember generate component-class ${options.entity.name}`; + this.ui.writeLine(` ${chalk.cyan('tip')} to add a class, run \`${command}\``); + } + }, + + fileMapTokens(options) { + let commandOptions = this.options; + + if (commandOptions.pod) { + return { + __path__() { return path.join(options.podPath, options.locals.path, options.dasherizedModuleName); - } else { + }, + __templatepath__() { + return path.join(options.podPath, options.locals.path, options.dasherizedModuleName); + }, + __templatename__() { + return 'template'; + } + }; + } else if ( + !this.EMBER_GLIMMER_SET_COMPONENT_TEMPLATE || + commandOptions.componentStructure === 'classic' + ) { + return { + __path__() { return 'components'; + }, + __templatepath__() { + return 'templates/components'; + }, + __templatename__() { + return options.dasherizedModuleName; } - }, - __templatepath__: function(options) { - if (options.pod) { - return path.join(options.podPath, options.locals.path, options.dasherizedModuleName); + }; + } else if ( + this.EMBER_GLIMMER_SET_COMPONENT_TEMPLATE && + commandOptions.componentStructure === 'flat' + ) { + return { + __path__() { + return 'components'; + }, + __templatepath__() { + return 'components'; + }, + __templatename__() { + return options.dasherizedModuleName; } - return 'templates/components'; - }, - __templatename__: function(options) { - if (options.pod) { - return 'template'; + }; + } else if ( + this.EMBER_GLIMMER_SET_COMPONENT_TEMPLATE && + commandOptions.componentStructure === 'nested' + ) { + return { + __path__() { + return `components/${options.dasherizedModuleName}`; + }, + __name__() { + return 'index'; + }, + __templatepath__() { + return `components/${options.dasherizedModuleName}`; + }, + __templatename__() { + return `index`; } - return options.dasherizedModuleName; - }, - }; + }; + } }, - normalizeEntityName: function(entityName) { - return normalizeEntityName(entityName); + files() { + let files = this._super.files.apply(this, arguments); + + if (this.EMBER_GLIMMER_SET_COMPONENT_TEMPLATE && this.options.componentClass === '') { + files = files.filter(file => { + if (file.endsWith('.js')) { + this.skippedJsFiles.add(file); + return false; + } else { + return true; + } + }); + } + + return files; + }, + + normalizeEntityName(entityName) { + return normalizeEntityName( + entityName.replace(/\.js$/, '') //Prevent generation of ".js.js" files + ); }, - locals: function(options) { + locals(options) { + let sanitizedModuleName = options.entity.name.replace(/\//g, '-'); + let classifiedModuleName = stringUtil.classify(sanitizedModuleName); + let templatePath = ''; + let importComponent = ''; let importTemplate = ''; - let contents = ''; + let defaultExport = ''; // if we're in an addon, build import statement if (options.project.isEmberCLIAddon() || (options.inRepoAddon && !options.inDummy)) { @@ -73,14 +240,38 @@ module.exports = { 'templates/components/' + stringUtil.dasherize(options.entity.name); } - importTemplate = '// @ts-ignore: Ignore import of compiled template' + EOL + 'import layout from \'' + templatePath + '\';' + EOL; - contents = EOL + ' layout = layout;'; + } + + let componentClass = this.EMBER_GLIMMER_SET_COMPONENT_TEMPLATE + ? options.componentClass + : '@ember/component'; + + switch (componentClass) { + case '@ember/component': + importComponent = `import Component from '@ember/component';`; + if (templatePath) { + importTemplate = `import layout from '${templatePath}';${EOL}`; + defaultExport = `Component.extend({${EOL} layout${EOL}});`; + } else { + defaultExport = `Component.extend({${EOL}});`; + } + break; + case '@glimmer/component': + importComponent = `import Component from '@glimmer/component';`; + defaultExport = `class ${classifiedModuleName}Component extends Component {${EOL}}`; + break; + case '@ember/component/template-only': + importComponent = `import templateOnly from '@ember/component/template-only';`; + defaultExport = `templateOnly();`; + break; } return { - importTemplate: importTemplate, - contents: contents, + importTemplate, + importComponent, + defaultExport, path: getPathOption(options), + componentClass: options.componentClass }; - }, + } };