Skip to content

Commit

Permalink
Merge pull request #314 from jamescdavis/add_typescript_transform_plugin
Browse files Browse the repository at this point in the history
Provide TypeScript compilation support when using ember-cli-typescript@4 or higher.
  • Loading branch information
rwjblue committed Feb 5, 2020
2 parents 317f3f4 + 4ab55d1 commit 2b697ca
Show file tree
Hide file tree
Showing 5 changed files with 249 additions and 16 deletions.
34 changes: 34 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ allow you to use latest Javascript in your Ember CLI project.
+ [Enabling Source Maps](#enabling-source-maps)
+ [Modules](#modules)
+ [Disabling Debug Tooling Support](#disabling-debug-tooling-support)
+ [Enabling TypeScript Transpilation](#enabling-typescript-transpilation)
* [Addon usage](#addon-usage)
+ [Adding Custom Plugins](#adding-custom-plugins)
+ [Additional Trees](#additional-trees)
Expand Down Expand Up @@ -122,6 +123,7 @@ interface EmberCLIBabelConfig {
disableEmberModulesAPIPolyfill?: boolean;
disableEmberDataPackagesPolyfill?: boolean;
disableDecoratorTransforms?: boolean;
enableTypeScriptTransform?: boolean;
extensions?: string[];
};
}
Expand Down Expand Up @@ -260,6 +262,38 @@ module.exports = function(defaults) {
}
```

#### Enabling TypeScript Transpilation

The transform plugin required for Babel to transpile TypeScript will
automatically be enabled when `ember-cli-typescript` >= 4.0 is installed.

You can enable the TypeScript Babel transform manually *without*
`ember-cli-typescript` by setting the `enableTypeScriptTransform` to `true`.

NOTE: Setting this option to `true` is not compatible with
`ember-cli-typescript` < 4.0 because of conflicting Babel plugin ordering
constraints and is unnecessary because `ember-cli-typescript` < 4.0 adds the
TypeScript Babel transform itself.

NOTE: Setting this option to `true` does *not* enable type-checking. For
integrated type-checking, you will need
[`ember-cli-typescript`](https://ember-cli-typescript.com).

In an app, manually enabling the TypeScript transform would look like:

```js
// ember-cli-build.js
module.exports = function(defaults) {
let app = new EmberApp(defaults, {
'ember-cli-babel': {
enableTypeScriptTransform: true
}
});

return app.toTree();
}
```

### Addon usage

#### Adding Custom Plugins
Expand Down
88 changes: 76 additions & 12 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ module.exports = {
return this._cachedDebugTree.apply(null, arguments);
},

transpileTree(inputTree, config) {
transpileTree(inputTree, _config) {
let config = _config || this._getAddonOptions();
let description = `000${++count}`.slice(-3);
let postDebugTree = this._debugTree(inputTree, `${description}:input`);

Expand All @@ -51,7 +52,15 @@ module.exports = {
output = postDebugTree;
} else {
let BabelTranspiler = require('broccoli-babel-transpiler');
output = new BabelTranspiler(postDebugTree, options);
let transpilationInput = postDebugTree;

if (this._shouldHandleTypeScript(config)) {
let Funnel = require('broccoli-funnel');
let inputWithoutDeclarations = new Funnel(transpilationInput, { exclude: ['**/*.d.ts'] });
transpilationInput = this._debugTree(inputWithoutDeclarations, `${description}:filtered-input`);
}

output = new BabelTranspiler(transpilationInput, options);
}

return this._debugTree(output, `${description}:output`);
Expand Down Expand Up @@ -252,14 +261,16 @@ module.exports = {
},

_getExtensions(config) {
let shouldHandleTypeScript = this._shouldHandleTypeScript(config);
let emberCLIBabelConfig = config['ember-cli-babel'] || {};
return emberCLIBabelConfig.extensions || ['js'];
return emberCLIBabelConfig.extensions || (shouldHandleTypeScript ? ['js', 'ts'] : ['js']);
},

_getBabelOptions(config) {
let addonProvidedConfig = this._getAddonProvidedConfig(config);
let shouldCompileModules = this._shouldCompileModules(config);
let shouldIncludeHelpers = this._shouldIncludeHelpers(config);
let shouldHandleTypeScript = this._shouldHandleTypeScript(config);
let shouldIncludeDecoratorPlugins = this._shouldIncludeDecoratorPlugins(config);

let emberCLIBabelConfig = config['ember-cli-babel'];
Expand Down Expand Up @@ -290,8 +301,12 @@ module.exports = {
let userPlugins = addonProvidedConfig.plugins;
let userPostTransformPlugins = addonProvidedConfig.postTransformPlugins;

if (shouldHandleTypeScript) {
userPlugins = this._addTypeScriptPlugin(userPlugins.slice(), addonProvidedConfig.options);
}

if (shouldIncludeDecoratorPlugins) {
userPlugins = this._addDecoratorPlugins(userPlugins.slice(), addonProvidedConfig.options);
userPlugins = this._addDecoratorPlugins(userPlugins.slice(), addonProvidedConfig.options, config);
}

options.plugins = [].concat(
Expand Down Expand Up @@ -319,13 +334,63 @@ module.exports = {
return options;
},

_shouldHandleTypeScript(config) {
let emberCLIBabelConfig = config['ember-cli-babel'] || {};
if (typeof emberCLIBabelConfig.enableTypeScriptTransform === 'boolean') {
return emberCLIBabelConfig.enableTypeScriptTransform;
}
let typeScriptAddon = this.parent.addons
&& this.parent.addons.find(a => a.name === 'ember-cli-typescript');
return typeof typeScriptAddon !== 'undefined'
&& semver.gte(typeScriptAddon.pkg.version, '4.0.0-alpha.1');
},

_buildClassFeaturePluginConstraints(constraints, config) {
// With versions of ember-cli-typescript < 4.0, class feature plugins like
// @babel/plugin-proposal-class-properties were run before the TS transform.
if (!this._shouldHandleTypeScript(config)) {
constraints.before = constraints.before || [];
constraints.before.push('@babel/plugin-transform-typescript');
}

return constraints;
},

_addTypeScriptPlugin(plugins) {
const { hasPlugin, addPlugin } = require('ember-cli-babel-plugin-helpers');

if (hasPlugin(plugins, '@babel/plugin-transform-typescript')) {
if (this.parent === this.project) {
this.project.ui.writeWarnLine(`${
this._parentName()
} has added the TypeScript transform plugin to its build, but ember-cli-babel provides this by default now when ember-cli-typescript >= 4.0 is installed! You can remove the transform, or the addon that provided it.`);
}
} else {
addPlugin(
plugins,
[
require.resolve('@babel/plugin-transform-typescript'),
{ allowDeclareFields: true },
],
{
before: [
'@babel/plugin-proposal-class-properties',
'@babel/plugin-proposal-private-methods',
'@babel/plugin-proposal-decorators',
]
}
);
}
return plugins;
},

_shouldIncludeDecoratorPlugins(config) {
let customOptions = config['ember-cli-babel'] || {};

return customOptions.disableDecoratorTransforms !== true;
},

_addDecoratorPlugins(plugins, options) {
_addDecoratorPlugins(plugins, options, config) {
const { hasPlugin, addPlugin } = require('ember-cli-babel-plugin-helpers');

if (hasPlugin(plugins, '@babel/plugin-proposal-decorators')) {
Expand All @@ -338,9 +403,9 @@ module.exports = {
addPlugin(
plugins,
[require.resolve('@babel/plugin-proposal-decorators'), { legacy: true }],
{
before: ['@babel/plugin-proposal-class-properties', '@babel/plugin-transform-typescript']
}
this._buildClassFeaturePluginConstraints({
before: ['@babel/plugin-proposal-class-properties']
}, config)
);
}

Expand All @@ -355,10 +420,9 @@ module.exports = {
addPlugin(
plugins,
[require.resolve('@babel/plugin-proposal-class-properties'), { loose: options.loose || false }],
{
after: ['@babel/plugin-proposal-decorators'],
before: ['@babel/plugin-transform-typescript']
}
this._buildClassFeaturePluginConstraints({
after: ['@babel/plugin-proposal-decorators']
}, config)
);
}

Expand Down
131 changes: 128 additions & 3 deletions node-tests/addon-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -651,6 +651,42 @@ describe('ember-cli-babel', function() {
}));
});

describe('TypeScript transpilation', function() {
beforeEach(function() {
this.addon.parent.addons.push({
name: 'ember-cli-typescript',
pkg: {
version: '4.0.0-alpha.1'
}
});
});

it('should transpile .ts files', co.wrap(function*() {
input.write({ 'foo.ts': `let foo: string = "hi";` });

subject = this.addon.transpileTree(input.path());
output = createBuilder(subject);

yield output.build();

expect(
output.read()
).to.deep.equal({
'foo.js': `define("foo", [], function () {\n "use strict";\n\n var foo = "hi";\n});`
});
}));

it('should exclude .d.ts files', co.wrap(function*() {
input.write({ 'foo.d.ts': `declare let foo: string;` });

subject = this.addon.transpileTree(input.path());
output = createBuilder(subject);

yield output.build();

expect(output.read()).to.deep.equal({});
}))
});

describe('_shouldDoNothing', function() {
it("will no-op if nothing to do", co.wrap(function* () {
Expand Down Expand Up @@ -762,9 +798,65 @@ describe('ember-cli-babel', function() {
});
});

describe('_shouldHandleTypeScript', function() {
it('should return false by default', function() {
expect(this.addon._shouldHandleTypeScript({})).to.be.false;
});
it('should return true when ember-cli-typescript >= 4.0.0-alpha.1 is installed', function() {
this.addon.parent.addons.push({
name: 'ember-cli-typescript',
pkg: {
version: '4.0.0-alpha.1',
},
});
expect(this.addon._shouldHandleTypeScript({})).to.be.true;
});
it('should return false when ember-cli-typescript < 4.0.0-alpha.1 is installed', function() {
this.addon.parent.addons.push({
name: 'ember-cli-typescript',
pkg: {
version: '3.0.0',
},
});
expect(this.addon._shouldHandleTypeScript({})).to.be.false;
});
it('should return true when the TypeScript transform is manually enabled', function() {
expect(this.addon._shouldHandleTypeScript({ 'ember-cli-babel': { enableTypeScriptTransform: true } })).to.be.true;
});
it('should return false when the TypeScript transforms is manually disabled', function() {
expect(this.addon._shouldHandleTypeScript({ 'ember-cli-babel': { enableTypeScriptTransform: false } })).to.be.false;
});
it('should return false when the TypeScript transform is manually disabled, even when ember-cli-typescript >= 4.0.0-alpha.1 is installed', function() {
this.addon.parent.addons.push({
name: 'ember-cli-typescript',
pkg: {
version: '4.0.0-alpha.1',
},
});
expect(this.addon._shouldHandleTypeScript({ 'ember-cli-babel': { enableTypeScriptTransform: false } })).to.be.false;
});
});

describe('_addTypeScriptPlugin', function() {
it('should warn and not add the TypeScript plugin if already added', function() {
this.addon.project.ui = {
writeWarnLine(message) {
expect(message).to.match(/has added the TypeScript transform plugin to its build/);
}
};

expect(
this.addon._addTypeScriptPlugin([
['@babel/plugin-transform-typescript']
],
{}
).length).to.equal(1, 'plugin was not added');
});
});

describe('_addDecoratorPlugins', function() {
it('should include babel transforms by default', function() {
expect(this.addon._addDecoratorPlugins([], {}).length).to.equal(2, 'plugins added correctly');
expect(this.addon._addDecoratorPlugins([], {}, {}).length).to.equal(2, 'plugins added correctly');
});

it('should include only fields if it detects decorators plugin', function() {
Expand All @@ -778,6 +870,7 @@ describe('ember-cli-babel', function() {
this.addon._addDecoratorPlugins([
['@babel/plugin-proposal-decorators']
],
{},
{}
).length).to.equal(2, 'plugins were not added');
});
Expand All @@ -794,20 +887,35 @@ describe('ember-cli-babel', function() {
[
['@babel/plugin-proposal-class-properties']
],
{},
{}
).length
).to.equal(2, 'plugins were not added');
});

it('should use babel options loose mode for class properties', function() {
let strictPlugins = this.addon._addDecoratorPlugins([], {});
let strictPlugins = this.addon._addDecoratorPlugins([], {}, {});

expect(strictPlugins[1][1].loose).to.equal(false, 'loose is false if no option is provided');

let loosePlugins = this.addon._addDecoratorPlugins([], { loose: true });
let loosePlugins = this.addon._addDecoratorPlugins([], { loose: true }, {});

expect(loosePlugins[1][1].loose).to.equal(true, 'loose setting added correctly');
});

it('should include class fields and decorators after typescript if handling typescript', function() {
this.addon._shouldHandleTypeScript = function() { return true; }
let plugins = this.addon._addDecoratorPlugins(['@babel/plugin-transform-typescript'], {}, {});
expect(plugins[0]).to.equal('@babel/plugin-transform-typescript', 'typescript still first');
expect(plugins.length).to.equal(3, 'class fields and decorators added');
});

it('should include class fields and decorators before typescript if not handling typescript', function() {
this.addon._shouldHandleTypeScript = function() { return false; }
let plugins = this.addon._addDecoratorPlugins(['@babel/plugin-transform-typescript'], {}, {});
expect(plugins.length).to.equal(3, 'class fields and decorators added');
expect(plugins[2]).to.equal('@babel/plugin-transform-typescript', 'typescript is now last');
});
});

describe('_shouldIncludeHelpers()', function() {
Expand Down Expand Up @@ -989,6 +1097,23 @@ describe('ember-cli-babel', function() {
});
});

describe('_getExtensions', function() {
it('defaults to js only', function() {
expect(this.addon._getExtensions({})).to.have.members(['js']);
});
it('adds ts automatically', function() {
this.addon._shouldHandleTypeScript = function() { return true; }
expect(this.addon._getExtensions({})).to.have.members(['js', 'ts']);
});
it('respects user-configured extensions', function() {
expect(this.addon._getExtensions({ 'ember-cli-babel': { extensions: ['coffee'] } })).to.have.members(['coffee']);
});
it('respects user-configured extensions even when adding TS plugin', function() {
this.addon._shouldHandleTypeScript = function() { return true; }
expect(this.addon._getExtensions({ 'ember-cli-babel': { extensions: ['coffee'] } })).to.have.members(['coffee']);
});
});

describe('buildBabelOptions', function() {
this.timeout(0);

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
"@babel/plugin-proposal-decorators": "^7.8.3",
"@babel/plugin-transform-modules-amd": "^7.8.3",
"@babel/plugin-transform-runtime": "^7.8.3",
"@babel/plugin-transform-typescript": "^7.8.3",
"@babel/polyfill": "^7.8.3",
"@babel/preset-env": "^7.8.4",
"@babel/runtime": "^7.8.4",
Expand Down

0 comments on commit 2b697ca

Please sign in to comment.