diff --git a/node-tests/babel-plugin-test.js b/node-tests/colocated-babel-plugin-test.js similarity index 100% rename from node-tests/babel-plugin-test.js rename to node-tests/colocated-babel-plugin-test.js diff --git a/node-tests/colocated-plugin-test.js b/node-tests/colocated-broccoli-plugin-test.js similarity index 100% rename from node-tests/colocated-plugin-test.js rename to node-tests/colocated-broccoli-plugin-test.js diff --git a/node-tests/colocated-test.js b/node-tests/colocated-test.js new file mode 100644 index 00000000..165be809 --- /dev/null +++ b/node-tests/colocated-test.js @@ -0,0 +1,418 @@ +'use strict'; + +const assert = require('assert'); +const ColocatedTemplateCompiler = require('../lib/colocated-broccoli-plugin'); +const ColocatedBabelPlugin = require.resolve('../lib/colocated-babel-plugin'); +const BroccoliPersistentFilter = require('broccoli-persistent-filter'); +const babel = require('@babel/core'); +const TypescriptTransform = require.resolve('@babel/plugin-transform-typescript'); +const { createTempDir, createBuilder: _createBuilder } = require('broccoli-test-helper'); +const { stripIndent } = require('common-tags'); + +// this is silly, we should use broccoli-babel-transpiler but it has **very** bad behaviors +// when used inside a process that does not end with `process.exit` (e.g. this test suite) +// See https://github.com/babel/broccoli-babel-transpiler/issues/169 for details. +class BabelTranspiler extends BroccoliPersistentFilter { + constructor(input, options = {}) { + options.async = true; + options.concurrency = 1; + + super(input, options); + + this.extensions = ['js', 'ts']; + this.targetExtension = 'js'; + + this.options = options; + } + + processString(string) { + let { code } = babel.transformSync(string, { plugins: this.options.plugins }); + + return code; + } +} + +describe('Colocation - Broccoli + Babel Integration', function() { + this.timeout(10000); + + let input, output; + + beforeEach(async function() { + input = await createTempDir(); + }); + + afterEach(async function() { + await input.dispose(); + + if (output) { + await output.dispose(); + } + }); + + function createBuilder(plugins = []) { + let colocatedTree = new ColocatedTemplateCompiler(input.path(), { + precompile(template) { + return JSON.stringify({ template }); + }, + }); + + let babelTree = new BabelTranspiler(colocatedTree, { + plugins: [...plugins, ColocatedBabelPlugin], + }); + + output = _createBuilder(babelTree); + + return output; + } + + it('works for template only component', async function() { + input.write({ + 'app-name-here': { + components: { + 'foo.hbs': `{{yield}}`, + }, + templates: { + 'application.hbs': `{{outlet}}`, + }, + }, + }); + + createBuilder(); + + await output.build(); + + assert.deepStrictEqual(output.read(), { + 'app-name-here': { + components: { + 'foo.js': stripIndent` + import { hbs } from 'ember-cli-htmlbars'; + + const __COLOCATED_TEMPLATE__ = hbs("{{yield}}", { + "contents": "{{yield}}", + "moduleName": "app-name-here/components/foo.hbs", + "parseOptions": { + "srcName": "app-name-here/components/foo.hbs" + } + }); + + import templateOnly from '@ember/component/template-only'; + export default Ember._setComponentTemplate(__COLOCATED_TEMPLATE__, templateOnly()); + `, + }, + templates: { + 'application.hbs': '{{outlet}}', + }, + }, + }); + }); + + it('works for component with template and class', async function() { + input.write({ + 'app-name-here': { + components: { + 'foo.hbs': `{{yield}}`, + 'foo.js': stripIndent` + import Component from '@glimmer/component'; + + export default class FooComponent extends Component {} + `, + }, + templates: { + 'application.hbs': `{{outlet}}`, + }, + }, + }); + + createBuilder(); + + await output.build(); + + assert.deepStrictEqual(output.read(), { + 'app-name-here': { + components: { + 'foo.js': stripIndent` + import { hbs } from 'ember-cli-htmlbars'; + + const __COLOCATED_TEMPLATE__ = hbs("{{yield}}", { + "contents": "{{yield}}", + "moduleName": "app-name-here/components/foo.hbs", + "parseOptions": { + "srcName": "app-name-here/components/foo.hbs" + } + }); + + import Component from '@glimmer/component'; + export default class FooComponent extends Component {} + + Ember._setComponentTemplate(__COLOCATED_TEMPLATE__, FooComponent); + `, + }, + templates: { + 'application.hbs': '{{outlet}}', + }, + }, + }); + }); + + it('works for typescript component class with template', async function() { + input.write({ + 'app-name-here': { + components: { + 'foo.hbs': `{{yield}}`, + 'foo.ts': stripIndent` + import Component from '@glimmer/component'; + + export default class FooComponent extends Component {} + `, + }, + templates: { + 'application.hbs': `{{outlet}}`, + }, + }, + }); + + createBuilder([TypescriptTransform]); + + await output.build(); + + assert.deepStrictEqual(output.read(), { + 'app-name-here': { + components: { + 'foo.js': stripIndent` + import { hbs } from 'ember-cli-htmlbars'; + + const __COLOCATED_TEMPLATE__ = hbs("{{yield}}", { + "contents": "{{yield}}", + "moduleName": "app-name-here/components/foo.hbs", + "parseOptions": { + "srcName": "app-name-here/components/foo.hbs" + } + }); + + import Component from '@glimmer/component'; + export default class FooComponent extends Component {} + + Ember._setComponentTemplate(__COLOCATED_TEMPLATE__, FooComponent); + `, + }, + templates: { + 'application.hbs': '{{outlet}}', + }, + }, + }); + }); + + it('works for scoped addon using template only component', async function() { + input.write({ + '@scope-name': { + 'addon-name-here': { + components: { + 'foo.hbs': `{{yield}}`, + }, + templates: { + 'application.hbs': `{{outlet}}`, + }, + }, + }, + }); + + createBuilder(); + + await output.build(); + + assert.deepStrictEqual(output.read(), { + '@scope-name': { + 'addon-name-here': { + components: { + 'foo.js': stripIndent` + import { hbs } from 'ember-cli-htmlbars'; + + const __COLOCATED_TEMPLATE__ = hbs("{{yield}}", { + "contents": "{{yield}}", + "moduleName": "@scope-name/addon-name-here/components/foo.hbs", + "parseOptions": { + "srcName": "@scope-name/addon-name-here/components/foo.hbs" + } + }); + + import templateOnly from '@ember/component/template-only'; + export default Ember._setComponentTemplate(__COLOCATED_TEMPLATE__, templateOnly()); + `, + }, + templates: { + 'application.hbs': '{{outlet}}', + }, + }, + }, + }); + }); + + it('works for scoped addon using component with template and class', async function() { + input.write({ + '@scope-name': { + 'addon-name-here': { + components: { + 'foo.hbs': `{{yield}}`, + 'foo.js': stripIndent` + import Component from '@glimmer/component'; + export default class FooComponent extends Component {} + `, + }, + templates: { + 'application.hbs': `{{outlet}}`, + }, + }, + }, + }); + + createBuilder(); + + await output.build(); + + assert.deepStrictEqual(output.read(), { + '@scope-name': { + 'addon-name-here': { + components: { + 'foo.js': stripIndent` + import { hbs } from 'ember-cli-htmlbars'; + + const __COLOCATED_TEMPLATE__ = hbs("{{yield}}", { + "contents": "{{yield}}", + "moduleName": "@scope-name/addon-name-here/components/foo.hbs", + "parseOptions": { + "srcName": "@scope-name/addon-name-here/components/foo.hbs" + } + }); + + import Component from '@glimmer/component'; + export default class FooComponent extends Component {} + + Ember._setComponentTemplate(__COLOCATED_TEMPLATE__, FooComponent); + `, + }, + templates: { + 'application.hbs': '{{outlet}}', + }, + }, + }, + }); + }); + + it('does nothing for "classic" location components', async function() { + input.write({ + 'app-name-here': { + components: { + 'foo.js': stripIndent` + import Component from '@glimmer/component'; + export default class FooComponent extends Component {} + `, + }, + templates: { + 'application.hbs': `{{outlet}}`, + components: { + 'foo.hbs': `{{yield}}`, + }, + }, + }, + }); + + createBuilder(); + + await output.build(); + + assert.deepStrictEqual(output.read(), input.read()); + }); + + it('does nothing for "pod" location templates', async function() { + input.write({ + 'addon-name-here': { + components: { + foo: { + 'template.hbs': `{{yield}}`, + }, + }, + }, + }); + + createBuilder(); + await output.build(); + + assert.deepStrictEqual(output.read(), input.read()); + }); + + it('it works if there are no input files', async function() { + input.write({}); + + createBuilder(); + await output.build(); + + assert.deepStrictEqual(output.read(), {}); + }); + + it('it works if input is manually using setComponentTemplate - no colocated template exists', async function() { + input.write({ + 'app-name-here': { + components: { + 'foo.js': stripIndent` + import Component from '@glimmer/component'; + import { setComponentTemplate } from '@ember/component'; + import hbs from 'ember-cli-htmlbars-inline-precompile'; + export default class FooComponent extends Component {} + setComponentTemplate(FooComponent, hbs\`sometemplate\`); + `, + }, + templates: { + 'application.hbs': `{{outlet}}`, + }, + }, + }); + + createBuilder(); + await output.build(); + + assert.deepStrictEqual(output.read(), { + 'app-name-here': { + components: { + 'foo.js': stripIndent` + import Component from '@glimmer/component'; + import { setComponentTemplate } from '@ember/component'; + import hbs from 'ember-cli-htmlbars-inline-precompile'; + export default class FooComponent extends Component {} + setComponentTemplate(FooComponent, hbs\`sometemplate\`); + `, + }, + templates: { + 'application.hbs': '{{outlet}}', + }, + }, + }); + }); + + it('emits an error when a default export is not present in a component JS file', async function() { + input.write({ + 'app-name-here': { + components: { + 'foo.hbs': `{{yield}}`, + 'foo.js': stripIndent` + export function whatever() {} + `, + }, + }, + }); + + createBuilder(); + await output.build(); + + assert.deepStrictEqual(output.read(), { + 'app-name-here': { + components: { + 'foo.js': stripIndent` + export function whatever() {}\nthrow new Error("\`app-name-here/components/foo.hbs\` does not contain a \`default export\`. Did you forget to export the component class?"); + `, + }, + }, + }); + }); + + it('does not break class decorator usage'); +}); diff --git a/package.json b/package.json index cefe2cbd..ca15f1e4 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ }, "devDependencies": { "@babel/core": "^7.6.4", + "@babel/plugin-transform-typescript": "^7.6.3", "@ember/optional-features": "^1.0.0", "babel-plugin-debug-macros": "^0.3.3", "broccoli-merge-trees": "^3.0.2", diff --git a/yarn.lock b/yarn.lock index 3a629243..e59e14f1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -634,6 +634,15 @@ dependencies: "@babel/helper-plugin-utils" "^7.0.0" +"@babel/plugin-transform-typescript@^7.6.3": + version "7.6.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.6.3.tgz#dddb50cf3b8b2ef70b22e5326e9a91f05a1db13b" + integrity sha512-aiWINBrPMSC3xTXRNM/dfmyYuPNKY/aexYqBgh0HBI5Y+WO5oRAqW/oROYeYHrF4Zw12r9rK4fMk/ZlAmqx/FQ== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.6.0" + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/plugin-syntax-typescript" "^7.2.0" + "@babel/plugin-transform-typescript@~7.4.0": version "7.4.5" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.4.5.tgz#ab3351ba35307b79981993536c93ff8be050ba28"