diff --git a/blueprints/ember-cli-typescript/files/__root__/config/environment.d.ts b/blueprints/ember-cli-typescript/files/__config_root__/config/environment.d.ts similarity index 100% rename from blueprints/ember-cli-typescript/files/__root__/config/environment.d.ts rename to blueprints/ember-cli-typescript/files/__config_root__/config/environment.d.ts diff --git a/blueprints/ember-cli-typescript/index.js b/blueprints/ember-cli-typescript/index.js index 715923845..623d986fa 100644 --- a/blueprints/ember-cli-typescript/index.js +++ b/blueprints/ember-cli-typescript/index.js @@ -32,7 +32,10 @@ module.exports = { let inRepoAddons = (this.project.pkg['ember-addon'] || {}).paths || []; let hasMirage = 'ember-cli-mirage' in (this.project.pkg.devDependencies || {}); let isAddon = this.project.isEmberCLIAddon(); - let includes = ['app', isAddon && 'addon', 'tests', 'types'].concat(inRepoAddons).filter(Boolean); + let isMU = this._detectMU(); + let includes = isMU ? ['src'] : ['app', isAddon && 'addon'].filter(Boolean); + + includes = includes.concat(['tests', 'types']).concat(inRepoAddons); // Mirage is already covered for addons because it's under `tests/` if (hasMirage && !isAddon) { @@ -51,15 +54,24 @@ module.exports = { paths[`${appName}/mirage/*`] = [`${isAddon ? 'tests/dummy/' : ''}mirage/*`]; } - if (isAddon) { - paths[`${appName}/*`] = ['tests/dummy/app/*', 'app/*']; + if (isMU) { + if (isAddon) { + paths[`${appName}/src/*`] = ['tests/dummy/src/*']; + paths[`${dasherizedName}/src/*`] = ['src/*']; + } else { + paths[`${appName}/src/*`] = ['src/*']; + } } else { - paths[`${appName}/*`] = ['app/*']; - } - - if (isAddon) { - paths[dasherizedName] = ['addon']; - paths[`${dasherizedName}/*`] = ['addon/*']; + if (isAddon) { + paths[`${appName}/*`] = ['tests/dummy/app/*', 'app/*']; + } else { + paths[`${appName}/*`] = ['app/*']; + } + + if (isAddon) { + paths[dasherizedName] = ['addon']; + paths[`${dasherizedName}/*`] = ['addon/*']; + } } for (let addon of inRepoAddons) { @@ -79,11 +91,21 @@ module.exports = { }, fileMapTokens(/*options*/) { + let isMU = this._detectMU(); + // Return custom tokens to be replaced in your files. return { __app_name__(options) { return options.inAddon ? 'dummy' : options.dasherizedModuleName; }, + + __config_root__(options) { + if (isMU) { + return options.inAddon ? 'tests/dummy' : '.'; + } else { + return options.inAddon ? 'tests/dummy/app' : 'app'; + } + } }; }, @@ -138,6 +160,10 @@ module.exports = { return files; }, + _detectMU() { + return this.project.isModuleUnification && this.project.isModuleUnification(); + }, + _installPrecompilationHooks() { let pkgPath = `${this.project.root}/package.json`; let pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')); diff --git a/index.js b/index.js index 771ee58ac..8e254c81d 100644 --- a/index.js +++ b/index.js @@ -29,7 +29,7 @@ module.exports = { shouldIncludeChildAddon(addon) { // For testing, we have dummy in-repo addons set up, but e-c-ts doesn't depend on them; // its dummy app does. Otherwise we'd have a circular dependency. - return addon.name !== 'in-repo-a' && addon.name !== 'in-repo-b'; + return !['in-repo-a', 'in-repo-b', 'in-repo-c'].includes(addon.name); }, setupPreprocessorRegistry(type, registry) { diff --git a/lib/commands/precompile.js b/lib/commands/precompile.js index b6ab0c183..0a13d266f 100644 --- a/lib/commands/precompile.js +++ b/lib/commands/precompile.js @@ -45,10 +45,12 @@ module.exports = Command.extend({ let compiled = declSource.replace(/\.d\.ts$/, '.js'); this._copyFile(output, `${outDir}/${compiled}`, compiled); - // We can only do anything meaningful with declarations for files in addon/ + // We can only do anything meaningful with declarations for files in addon/ or src/ if (this._isAddonFile(declSource)) { let declDest = declSource.replace(/^addon\//, ''); this._copyFile(output, `${outDir}/${declSource}`, declDest); + } else if (this._isSrcFile(declSource)) { + this._copyFile(output, `${outDir}/${declSource}`, declSource); } } } @@ -60,7 +62,9 @@ module.exports = Command.extend({ }, _shouldCopy(source) { - return this._isAppFile(source) || this._isAddonFile(source); + return this._isAppFile(source) + || this._isAddonFile(source) + || this._isSrcFile(source); }, _isAppFile(source) { @@ -71,6 +75,10 @@ module.exports = Command.extend({ return source.indexOf('addon') === 0; }, + _isSrcFile(source) { + return source.indexOf('src') === 0; + }, + _copyFile(output, source, dest) { let segments = dest.split(/\/|\\/); diff --git a/lib/incremental-typescript-compiler/index.js b/lib/incremental-typescript-compiler/index.js index 1ea85bcab..cd3d1bc72 100644 --- a/lib/incremental-typescript-compiler/index.js +++ b/lib/incremental-typescript-compiler/index.js @@ -33,10 +33,22 @@ module.exports = class IncrementalTypescriptCompiler { } treeForHost() { - let appTree = new TypescriptOutput(this, { - [`${this._relativeAppRoot()}/app`]: 'app', - }); + let appRoot = `${this._relativeAppRoot()}/app`; + let srcRoot = `${this._relativeAppRoot()}/src`; + + let trees = {}; + if (fs.existsSync(appRoot)) { + trees[appRoot] = 'app'; + } + if (fs.existsSync(srcRoot)) { + // MU apps currently include tests in production builds, and it's not yet clear + // how those will be filtered out in the future. We may or may not wind up needing + // to do that filtering here. + trees[srcRoot] = 'app/src'; + } + + let appTree = new TypescriptOutput(this, trees); let mirage = this._mirageDirectory(); let mirageTree = mirage && new TypescriptOutput(this, { [mirage]: 'app/mirage', @@ -63,7 +75,16 @@ module.exports = class IncrementalTypescriptCompiler { treeForAddons() { let paths = {}; for (let addon of this.addons) { - paths[`${this._relativeAddonRoot(addon)}/addon`] = addon.name; + let absoluteRoot = this._addonRoot(addon); + let relativeRoot = this._relativeAddonRoot(addon); + + if (fs.existsSync(`${absoluteRoot}/addon`)) { + paths[`${relativeRoot}/addon`] = addon.name; + } + + if (fs.existsSync(`${absoluteRoot}/src`)) { + paths[`${relativeRoot}/src`] = `${addon.name}/src`; + } } return new TypescriptOutput(this, paths); } @@ -187,7 +208,7 @@ module.exports = class IncrementalTypescriptCompiler { } } - _relativeAddonRoot(addon) { + _addonRoot(addon) { let addonRoot = addon.root; if (addonRoot.indexOf(this.project.root) !== 0) { let packagePath = resolve.sync(`${addon.pkg.name}/package.json`, { @@ -195,8 +216,11 @@ module.exports = class IncrementalTypescriptCompiler { }); addonRoot = path.dirname(packagePath); } + return addonRoot; + } - return addonRoot.replace(this.project.root, ''); + _relativeAddonRoot(addon) { + return this._addonRoot(addon).replace(this.project.root, ''); } }; diff --git a/node-tests/blueprints/ember-cli-typescript-test.js b/node-tests/blueprints/ember-cli-typescript-test.js index 3116b437e..85c8b7919 100644 --- a/node-tests/blueprints/ember-cli-typescript-test.js +++ b/node-tests/blueprints/ember-cli-typescript-test.js @@ -5,6 +5,7 @@ const path = require('path'); const helpers = require('ember-cli-blueprint-test-helpers/helpers'); const chaiHelpers = require('ember-cli-blueprint-test-helpers/chai'); const Blueprint = require('ember-cli/lib/models/blueprint'); +const Project = require('ember-cli/lib/models/project'); const ects = require('../../blueprints/ember-cli-typescript'); @@ -130,6 +131,108 @@ describe('Acceptance: ember-cli-typescript generator', function() { }); }); + describe('module unification', function() { + const originalIsMU = Project.prototype.isModuleUnification; + + beforeEach(function() { + Project.prototype.isModuleUnification = () => true; + }); + + afterEach(function() { + Project.prototype.isModuleUnification = originalIsMU; + }); + + it('basic app', function() { + const args = ['ember-cli-typescript']; + + return helpers + .emberNew() + .then(() => helpers.emberGenerate(args)) + .then(() => { + const pkg = file('package.json'); + expect(pkg).to.exist; + + const pkgJson = JSON.parse(pkg.content); + expect(pkgJson.scripts.prepublishOnly).to.be.undefined; + expect(pkgJson.scripts.postpublish).to.be.undefined; + expect(pkgJson.devDependencies).to.include.all.keys('ember-data'); + expect(pkgJson.devDependencies).to.include.all.keys('@types/ember-data'); + expect(pkgJson.devDependencies).to.include.all.keys('ember-cli-qunit'); + expect(pkgJson.devDependencies).to.include.all.keys('@types/ember-qunit', '@types/qunit'); + expect(pkgJson.devDependencies).to.not.have.any.keys('@types/ember-mocha', '@types/mocha'); + + const tsconfig = file('tsconfig.json'); + expect(tsconfig).to.exist; + + const tsconfigJson = JSON.parse(tsconfig.content); + expect(tsconfigJson.compilerOptions.paths).to.deep.equal({ + 'my-app/tests/*': ['tests/*'], + 'my-app/src/*': ['src/*'], + '*': ['types/*'], + }); + + expect(tsconfigJson.compilerOptions.inlineSourceMap).to.equal(true); + expect(tsconfigJson.compilerOptions.inlineSources).to.equal(true); + + expect(tsconfigJson.include).to.deep.equal(['src/**/*', 'tests/**/*', 'types/**/*']); + + const projectTypes = file('types/my-app/index.d.ts'); + expect(projectTypes).to.exist; + expect(projectTypes).to.include(ects.APP_DECLARATIONS); + + const environmentTypes = file('config/environment.d.ts'); + expect(environmentTypes).to.exist; + + const emberDataCatchallTypes = file('types/ember-data.d.ts'); + expect(emberDataCatchallTypes).to.exist; + }); + }); + + it('basic addon', function() { + const args = ['ember-cli-typescript']; + + return helpers + .emberNew({ target: 'addon' }) + .then(() => helpers.emberGenerate(args)) + .then(() => { + const pkg = file('package.json'); + expect(pkg).to.exist; + + const pkgJson = JSON.parse(pkg.content); + expect(pkgJson.scripts.prepublishOnly).to.equal('ember ts:precompile'); + expect(pkgJson.scripts.postpublish).to.equal('ember ts:clean'); + expect(pkgJson.devDependencies).to.not.have.any.keys('ember-data'); + expect(pkgJson.devDependencies).to.not.have.any.keys('@types/ember-data'); + expect(pkgJson.devDependencies).to.include.all.keys('ember-cli-qunit'); + expect(pkgJson.devDependencies).to.include.all.keys('@types/ember-qunit', '@types/qunit'); + expect(pkgJson.devDependencies).to.not.have.any.keys('@types/ember-mocha', '@types/mocha'); + + const tsconfig = file('tsconfig.json'); + expect(tsconfig).to.exist; + + const tsconfigJson = JSON.parse(tsconfig.content); + expect(tsconfigJson.compilerOptions.paths).to.deep.equal({ + 'dummy/tests/*': ['tests/*'], + 'dummy/src/*': ['tests/dummy/src/*'], + 'my-addon/src/*': ['src/*'], + '*': ['types/*'], + }); + + expect(tsconfigJson.include).to.deep.equal(['src/**/*', 'tests/**/*', 'types/**/*']); + + const projectTypes = file('types/dummy/index.d.ts'); + expect(projectTypes).to.exist; + expect(projectTypes).not.to.include(ects.APP_DECLARATIONS); + + const environmentTypes = file('tests/dummy/config/environment.d.ts'); + expect(environmentTypes).to.exist; + + const emberDataCatchallTypes = file('types/ember-data.d.ts'); + expect(emberDataCatchallTypes).not.to.exist; + }); + }); + }); + it('in-repo addons', function() { const args = ['ember-cli-typescript']; diff --git a/node-tests/commands/precompile-test.js b/node-tests/commands/precompile-test.js index ef56df86a..cfd0b8005 100644 --- a/node-tests/commands/precompile-test.js +++ b/node-tests/commands/precompile-test.js @@ -47,4 +47,26 @@ describe('Acceptance: ts:precompile command', function() { expect(transpiled.content.trim()).to.equal(`export const testString = 'hello';`); }); }); + + describe('module unification', function() { + it('generates .js and .d.ts files from the src tree', function() { + fs.ensureDirSync('src'); + fs.writeFileSync('src/test-file.ts', `export const testString: string = 'hello';`); + + let tsconfig = fs.readJSONSync('tsconfig.json'); + tsconfig.include.push('src'); + fs.writeJSONSync('tsconfig.json', tsconfig); + + return ember(['ts:precompile']) + .then(() => { + let declaration = file('src/test-file.d.ts'); + expect(declaration).to.exist; + expect(declaration.content.trim()).to.equal(`export declare const testString: string;`); + + let transpiled = file('src/test-file.js'); + expect(transpiled).to.exist; + expect(transpiled.content.trim()).to.equal(`export const testString = 'hello';`); + }); + }); + }); }); diff --git a/package.json b/package.json index a7c8e02b8..0018cb86b 100644 --- a/package.json +++ b/package.json @@ -120,7 +120,8 @@ ], "paths": [ "tests/dummy/lib/in-repo-a", - "tests/dummy/lib/in-repo-b" + "tests/dummy/lib/in-repo-b", + "tests/dummy/lib/in-repo-c" ] }, "prettier": { diff --git a/tests/dummy/lib/in-repo-c/index.js b/tests/dummy/lib/in-repo-c/index.js new file mode 100644 index 000000000..aa8015e86 --- /dev/null +++ b/tests/dummy/lib/in-repo-c/index.js @@ -0,0 +1,10 @@ +/* eslint-env node */ +'use strict'; + +module.exports = { + name: 'in-repo-c', + + isDevelopingAddon() { + return true; + } +}; diff --git a/tests/dummy/lib/in-repo-c/package.json b/tests/dummy/lib/in-repo-c/package.json new file mode 100644 index 000000000..c475f8aab --- /dev/null +++ b/tests/dummy/lib/in-repo-c/package.json @@ -0,0 +1,12 @@ +{ + "name": "in-repo-c", + "keywords": [ + "ember-addon" + ], + "dependencies": { + "ember-cli-babel": "*" + }, + "devDependencies": { + "ember-cli-typescript": "*" + } +} diff --git a/tests/dummy/lib/in-repo-c/src/test-file.ts b/tests/dummy/lib/in-repo-c/src/test-file.ts new file mode 100644 index 000000000..bc1d4fb02 --- /dev/null +++ b/tests/dummy/lib/in-repo-c/src/test-file.ts @@ -0,0 +1,4 @@ +// This should wind up in the addon tree +const value: string = 'in-repo-c/src/test-file'; + +export default value; diff --git a/tests/dummy/src/test-file.ts b/tests/dummy/src/test-file.ts new file mode 100644 index 000000000..de66dd8dc --- /dev/null +++ b/tests/dummy/src/test-file.ts @@ -0,0 +1,4 @@ +// This should wind up in the app's src tree +const value: string = 'dummy/src/test-file'; + +export default value; diff --git a/tests/dummy/src/ui/styles/.gitkeep b/tests/dummy/src/ui/styles/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/tests/unit/build-test.ts b/tests/unit/build-test.ts index dfcb03e57..f10a01cf9 100644 --- a/tests/unit/build-test.ts +++ b/tests/unit/build-test.ts @@ -2,8 +2,10 @@ import { module, test } from 'qunit'; import addonFileA from 'in-repo-a/test-file'; import addonFileB from 'in-repo-b/test-file'; +import addonFileC from 'in-repo-c/src/test-file'; import fileA from 'dummy/a'; import fileB from 'dummy/b'; +import muFile from 'dummy/src/test-file'; import shadowedFile from 'dummy/shadowed-file'; module('Unit | Build', function() { @@ -20,4 +22,12 @@ module('Unit | Build', function() { test('app files aren\'t shadowed by addons\' app tree files', function(assert) { assert.equal(shadowedFile, 'dummy/shadowed-file'); }); + + test('MU app files wind up in the right place', function(assert) { + assert.equal(muFile, 'dummy/src/test-file'); + }); + + test('MU addon files wind up in the right place', function(assert) { + assert.equal(addonFileC, 'in-repo-c/src/test-file'); + }); }); diff --git a/tsconfig.json b/tsconfig.json index 52c442bd3..f9e4de09e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,9 +9,11 @@ "baseUrl": ".", "paths": { "dummy/tests/*": ["tests/*"], + "dummy/src/*": ["tests/dummy/src/*"], "dummy/*": ["tests/dummy/app/*", "tests/dummy/lib/in-repo-a/app/*", "tests/dummy/lib/in-repo-b/app/*"], "in-repo-a/*": ["tests/dummy/lib/in-repo-a/addon/*"], - "in-repo-b/*": ["tests/dummy/lib/in-repo-b/addon/*"] + "in-repo-b/*": ["tests/dummy/lib/in-repo-b/addon/*"], + "in-repo-c/src/*": ["tests/dummy/lib/in-repo-c/src/*"] } }, "include": [