diff --git a/lib/broccoli/angular-broccoli-bundle.js b/lib/broccoli/angular-broccoli-bundle.js deleted file mode 100644 index b00b4426ebf6..000000000000 --- a/lib/broccoli/angular-broccoli-bundle.js +++ /dev/null @@ -1,26 +0,0 @@ -/* jshint node: true, esversion: 6 */ -'use strict'; - -const Plugin = require('broccoli-caching-writer'); -const Builder = require('systemjs-builder'); -const fse = require('fs-extra'); -const path = require('path'); - -class BundlePlugin extends Plugin { - constructor(inputNodes, options) { - super(inputNodes, {}); - options = options || {}; - this.options = options; - } - - build() { - var relativeRoot = path.relative(process.cwd(), this.inputPaths[0]); - var builder = new Builder(relativeRoot, `${relativeRoot}/system-config.js`); - - return builder.bundle('main', `${this.outputPath}/main.js`, { minify: true }) - .then(() => fse.copySync(`${this.inputPaths[0]}/system-config.js`, - `${this.outputPath}/system-config.js`)); - } -} - -module.exports = BundlePlugin; diff --git a/lib/broccoli/angular-broccoli-compass.js b/lib/broccoli/angular-broccoli-compass.js deleted file mode 100644 index f3ac76d37f89..000000000000 --- a/lib/broccoli/angular-broccoli-compass.js +++ /dev/null @@ -1,59 +0,0 @@ -/* jshint node: true, esversion: 6 */ -'use strict'; - -const requireOrNull = require('./require-or-null'); -const Plugin = require('broccoli-caching-writer'); -const fse = require('fs-extra'); -const path = require('path'); -const Funnel = require('broccoli-funnel'); - -let sass = requireOrNull('node-sass'); -let compass = requireOrNull('compass'); -if (!sass || !compass) { - sass = requireOrNull(`${process.env.PROJECT_ROOT}/node_modules/node-sass`); - compass = requireOrNull(`${process.env.PROJECT_ROOT}/node_modules/compass-importer`); -} - -class CompassPlugin extends Plugin { - constructor(inputNodes, options) { - super(inputNodes, {}); - - options = options || {}; - Plugin.call(this, inputNodes, { - cacheInclude: [/\.scss$/, /\.sass$/] - }); - this.options = options; - } - - build() { - this.listEntries().forEach(e => { - let fileName = path.resolve(e.basePath, e.relativePath); - this.compile(fileName, this.inputPaths[0], this.outputPath); - }); - } - - compile(fileName, inputPath, outputPath) { - let sassOptions = Object.assign(this.options, { - data: '@import "compass"; .transition { @include transition(all); }', - file: fileName, - includePaths: [inputPath].concat(this.options.inputPaths || []), - importer: compass - }); - - let result = sass.renderSync(sassOptions); - let filePath = fileName.replace(inputPath, outputPath).replace(/\.s[ac]ss$/, '.css'); - - fse.outputFileSync(filePath, result.css, 'utf8'); - } -} - -exports.makeBroccoliTree = (sourceDir, options) => { - if (sass && compass) { - let compassSrcTree = new Funnel(sourceDir, { - include: ['**/*.scss', '**/*.sass'], - allowEmpty: true - }); - - return new CompassPlugin([compassSrcTree], options); - } -}; diff --git a/lib/broccoli/angular-broccoli-less.js b/lib/broccoli/angular-broccoli-less.js deleted file mode 100644 index 1f2d928eab79..000000000000 --- a/lib/broccoli/angular-broccoli-less.js +++ /dev/null @@ -1,54 +0,0 @@ -/* jshint node: true, esversion: 6 */ -'use strict'; - -const requireOrNull = require('./require-or-null'); -const Plugin = require('broccoli-caching-writer'); -const fs = require('fs'); -const fse = require('fs-extra'); -const path = require('path'); -const Funnel = require('broccoli-funnel'); - -let less = requireOrNull('less'); -if (!less) { - less = requireOrNull(`${process.env.PROJECT_ROOT}/node_modules/less`); -} - -class LESSPlugin extends Plugin { - constructor(inputNodes, options) { - super(inputNodes, {}); - - options = options || {}; - Plugin.call(this, inputNodes, { - cacheInclude: [/\.less$/] - }); - this.options = options; - } - - build() { - return Promise.all(this.listEntries().map(e => { - let fileName = path.resolve(e.basePath, e.relativePath); - return this.compile(fileName, this.inputPaths[0], this.outputPath); - })); - } - - compile(fileName, inputPath, outputPath) { - let content = fs.readFileSync(fileName, 'utf8'); - - return less.render(content, this.options) - .then(output => { - let filePath = fileName.replace(inputPath, outputPath).replace(/\.less$/, '.css'); - fse.outputFileSync(filePath, output.css, 'utf8'); - }); - } -} - -exports.makeBroccoliTree = (sourceDir, options) => { - if (less) { - let lessSrcTree = new Funnel(sourceDir, { - include: ['**/*.less'], - allowEmpty: true - }); - - return new LESSPlugin([lessSrcTree], options); - } -}; diff --git a/lib/broccoli/angular-broccoli-sass.js b/lib/broccoli/angular-broccoli-sass.js deleted file mode 100644 index aca7d93aac20..000000000000 --- a/lib/broccoli/angular-broccoli-sass.js +++ /dev/null @@ -1,74 +0,0 @@ -/* jshint node: true, esversion: 6 */ -'use strict'; - -const requireOrNull = require('./require-or-null'); -const Plugin = require('broccoli-caching-writer'); -const fse = require('fs-extra'); -const path = require('path'); -const Funnel = require('broccoli-funnel'); - -let sass = requireOrNull('node-sass'); -if (!sass) { - sass = requireOrNull(`${process.env.PROJECT_ROOT}/node_modules/node-sass`); -} - -class SASSPlugin extends Plugin { - constructor(inputNodes, options) { - super(inputNodes, {}); - - options = options || {}; - Plugin.call(this, inputNodes, { - cacheInclude: options.cacheInclude || [/\.scss$/, /\.sass$/], - cacheExclude: options.cacheExclude || undefined - }); - this.options = options; - } - - build() { - this.listFiles().forEach(fileName => { - // We skip compiling partials (_*.scss files) - if(!/^_+.*.s[ac]ss$/.test(path.basename(fileName))) { - // Normalize is necessary for changing `\`s into `/`s on windows. - this.compile(path.normalize(fileName), - path.normalize(this.inputPaths[0]), - path.normalize(this.outputPath)); - } - }); - } - - compile(fileName, inputPath, outputPath) { - const outSourceName = fileName.replace(inputPath, outputPath); - const outFileName = outSourceName.replace(/\.s[ac]ss$/, '.css'); - - // We overwrite file, outFile and include the file path for the includePath. - // We also make sure the options don't include a data field. - const sassOptions = Object.assign(this.options, { - file: fileName, - outFile: outFileName, - includePaths: [inputPath].concat(this.options.includePaths || []) - }); - delete sassOptions.data; - - const result = sass.renderSync(sassOptions); - fse.outputFileSync(outFileName, result.css, 'utf-8'); - } -} - -exports.makeBroccoliTree = (sourceDir, options) => { - options = options || {}; - - // include sass support only if compass-importer is not installed - let compass = requireOrNull('compass-importer'); - if (!compass) { - compass = requireOrNull(`${process.env.PROJECT_ROOT}/node_modules/compass-importer`); - } - - if (sass && !compass) { - let sassSrcTree = new Funnel(sourceDir, { - include: ['**/*.sass', '**/*.scss'], - allowEmpty: true - }); - - return new SASSPlugin([sassSrcTree], options); - } -}; diff --git a/lib/broccoli/angular-broccoli-stylus.js b/lib/broccoli/angular-broccoli-stylus.js deleted file mode 100644 index f867eb19a9b5..000000000000 --- a/lib/broccoli/angular-broccoli-stylus.js +++ /dev/null @@ -1,57 +0,0 @@ -/* jshint node: true, esversion: 6 */ -'use strict'; - -const requireOrNull = require('./require-or-null'); -const Plugin = require('broccoli-caching-writer'); -const fs = require('fs'); -const fse = require('fs-extra'); -const path = require('path'); -const Funnel = require('broccoli-funnel'); - -let stylus = requireOrNull('stylus'); -if (!stylus) { - stylus = requireOrNull(`${process.env.PROJECT_ROOT}/node_modules/stylus`); -} - -class StylusPlugin extends Plugin { - constructor(inputNodes, options) { - super(inputNodes, {}); - - options = options || {}; - Plugin.call(this, inputNodes, { - cacheInclude: [/\.styl$/] - }); - this.options = options; - } - - build() { - return Promise.all(this.listEntries().map(e => { - let fileName = path.resolve(e.basePath, e.relativePath); - return this.compile(fileName, this.inputPaths[0], this.outputPath); - })); - } - - compile(fileName, inputPath, outputPath) { - let content = fs.readFileSync(fileName, 'utf8'); - - const options = Object.assign(this.options, { - filename: path.basename(fileName) - }); - - return stylus.render(content, options, function(err, css) { - let filePath = fileName.replace(inputPath, outputPath).replace(/\.styl$/, '.css'); - fse.outputFileSync(filePath, css, 'utf8'); - }); - } -} - -exports.makeBroccoliTree = (sourceDir, options) => { - if (stylus) { - let stylusSrcTree = new Funnel(sourceDir, { - include: ['**/*.styl'], - allowEmpty: true - }); - - return new StylusPlugin([stylusSrcTree], options); - } -}; diff --git a/lib/broccoli/angular2-app.js b/lib/broccoli/angular2-app.js deleted file mode 100644 index 487b9bbe4771..000000000000 --- a/lib/broccoli/angular2-app.js +++ /dev/null @@ -1,487 +0,0 @@ -'use strict'; -const path = require('path'); -const fs = require('fs'); - -const BroccoliPlugin = require('broccoli-writer'); -const BroccoliTypescript = require('./broccoli-typescript'); -const BundlePlugin = require('./angular-broccoli-bundle'); -const BroccoliFunnel = require('broccoli-funnel'); -const BroccoliMergeTrees = require('broccoli-merge-trees'); -const BroccoliSource = require('broccoli-source'); -const UnwatchedDir = BroccoliSource.UnwatchedDir; -const Project = require('ember-cli/lib/models/project'); -const HandlebarReplace = require('./broccoli-handlebars'); -const config = require('../../addon/ng2/models/config'); -const loadEnvironment = require('./environment'); -const concat = require('broccoli-concat'); -const uglify = require('broccoli-uglify-js'); - -class Angular2App extends BroccoliPlugin { - constructor(project, inputNode, options) { - super(); - this.ngConfig = config.CliConfig.fromProject(); - - if (!options) { - options = inputNode; - inputNode = null; - } - - options = options || {}; - - this._options = options; - this._sourceDir = options.sourceDir - || (this.ngConfig.defaults && this.ngConfig.defaults.sourceDir) - || 'src'; - this._options.polyfills = this._options.polyfills || [ - 'vendor/es6-shim/es6-shim.js', - 'vendor/reflect-metadata/Reflect.js', - 'vendor/systemjs/dist/system.src.js', - 'vendor/zone.js/dist/zone.js' - ]; - - this._destDir = options.destDir || ''; - - // By default, add all spec files to the tsCompiler. - this._tsCompiler = options.tsCompiler || { - additionalFiles: ['**/*.spec.ts'] - }; - - this._initProject(); - this._notifyAddonIncluded(); - this._inputNode = inputNode || this._buildInputTree(); - - this._tree = this._buildTree(); - } - - /** - * For compatibility with Ember addons - * @returns {*|{}} - */ - get options(){ - return this._options; - } - - /** - * For backward compatibility. - * @public - * @method toTree - * @return {BroccoliPlugin} A broccoli plugin. - */ - toTree() { - // eslint-disable-next-line no-console - console.warn('Angular2App is now a broccoli plugin. Calling toTree() is deprecated.'); - return this; - } - - /** - * @override - */ - read(readTree) { - return this._tree.read(readTree); - } - - /** - * @override - */ - cleanup() { - return this._tree.cleanup(); - } - - _buildInputTree() { - const inputTreeArray = [ - new BroccoliFunnel(this._sourceDir, { destDir: this._sourceDir }), - new BroccoliFunnel('typings', { destDir: 'typings' }), - this._getConfigTree() - ]; - - if (fs.existsSync('public')) { - inputTreeArray.push(new BroccoliFunnel('public', { destDir: 'public' })); - } - - if (fs.existsSync('icons')) { - inputTreeArray.push(new BroccoliFunnel('icons', { destDir: 'icons' })); - } - - return new BroccoliMergeTrees(inputTreeArray, { overwrite: true }); - } - - /** - * Create and return the app build system tree that: - * - Get the `assets` tree - * - Get the TS tree - * - Get the TS src tree - * - Get the index.html tree - * - Get the NPM modules tree - * - Apply/remove stuff based on the environment (dev|prod) - * - Return the app trees to be extended - * - * @private - * @method _buildTree - * @return {BroccoliFunnel} The app trees that can be used to extend the build. - */ - _buildTree() { - var assetTree = this._getAssetsTree(); - var tsTree = this._getTsTree(); - var indexTree = this._getIndexTree(); - var vendorNpmTree = this._getVendorNpmTree(); - - var buildTrees = [assetTree, tsTree, indexTree, vendorNpmTree]; - - // Add available and supported CSS plugins. - for (const suffix of ['sass', 'less', 'stylus', 'compass']) { - const plugin = require(`./angular-broccoli-${suffix}`); - const tree = plugin.makeBroccoliTree(this._inputNode, this._options[`${suffix}Compiler`]); - - if (!tree) { - continue; - } - - buildTrees.push(new BroccoliFunnel(tree, { - include: ['**/*'], - getDestinationPath: (n) => { - if (n.startsWith(this._sourceDir)) { - return n.substr(this._sourceDir.length); - } - return n; - } - })); - } - - var merged = new BroccoliMergeTrees(buildTrees, { overwrite: true }); - - if (this.ngConfig.apps[0].mobile) { - let AppShellPlugin = require('angular2-broccoli-prerender').AppShellPlugin; - merged = new BroccoliMergeTrees([merged, new AppShellPlugin(merged, 'index.html', 'main-app-shell')], { - overwrite: true - }); - } - - if (loadEnvironment(this.project).production) { - merged = this._getBundleTree(merged); - } - - // Public folder funnel - var publicFolder = new BroccoliFunnel(this._inputNode, { - allowEmpty: true, - srcDir: 'public', - name: 'PublicFolderFunnel' - }); - - merged = new BroccoliMergeTrees([merged, publicFolder]); - - return new BroccoliFunnel(merged, { - destDir: this._destDir, - overwrite: true - }); - } - - - /** - * @private - * @method _initProject - * @param {Object} options - */ - _initProject() { - this.project = Project.closestSync(process.cwd()); - - // project root dir env used on angular-cli side for including packages from project - process.env.PROJECT_ROOT = process.env.PROJECT_ROOT || this.project.root; - } - - /** - * @private - * @method _notifyAddonIncluded - */ - _notifyAddonIncluded() { - this.initializeAddons(); - this.project.addons = this.project.addons.filter(function (addon) { - addon.app = this; - - if (!addon.isEnabled || addon.isEnabled()) { - if (addon.included) { - addon.included(this); - } - - return addon; - } - }, this); - } - - /** - * Loads and initializes addons for this project. - * Calls initializeAddons on the Project. - * - * @private - * @method initializeAddons - */ - initializeAddons() { - this.project.initializeAddons(); - } - - /** - * Returns the content for a specific type (section) for index.html. - * - * Currently supported types: - * - 'head' - * //- 'config-module' - * //- 'app' - * //- 'head-footer' - * //- 'test-header-footer' - * //- 'body-footer' - * //- 'test-body-footer' - * - * Addons can also implement this method and could also define additional - * types (eg. 'some-addon-section'). - * - * @private - * @method _contentFor - * @param {RegExP} match Regular expression to match against - * @param {String} type Type of content - * @return {String} The content. - */ - _contentFor(match, type) { - var content = []; - - /*switch (type) { - case 'head': this._contentForHead(content, config); break; - case 'config-module': this._contentForConfigModule(content, config); break; - case 'app-boot': this._contentForAppBoot(content, config); break; - }*/ - content = this.project.addons.reduce(function (content, addon) { - var addonContent = addon.contentFor ? addon.contentFor(type) : null; - if (addonContent) { - return content.concat(addonContent); - } - - return content; - }, content); - - return content.join('\n'); - } - - /** - * Returns the tree for app/index.html. - * - * @private - * @method _getIndexTree - * @return {Tree} Tree for app/index.html. - */ - _getIndexTree() { - var files = [ - 'index.html' - ]; - var mobile; - - let indexTree = new BroccoliFunnel(this._inputNode, { - include: files.map(name => path.join(this._sourceDir, name)), - getDestinationPath: (n) => { - if (n.startsWith(this._sourceDir)) { - return n.substr(this._sourceDir.length); - } - return n; - } - }); - - if (this.ngConfig.apps[0].mobile) { - mobile = { - icons: [ - { rel: 'apple-touch-icon', href: '/icons/apple-touch-icon.png' }, - { rel: 'apple-touch-icon', sizes: '57x57', href: '/icons/apple-touch-icon-57x57.png' }, - { rel: 'apple-touch-icon', sizes: '60x60', href: '/icons/apple-touch-icon-60x60.png' }, - { rel: 'apple-touch-icon', sizes: '72x72', href: '/icons/apple-touch-icon-72x72.png' }, - { rel: 'apple-touch-icon', sizes: '76x76', href: '/icons/apple-touch-icon-76x76.png' }, - { rel: 'apple-touch-icon', sizes: '114x114', href: '/icons/apple-touch-icon-114x114.png' }, - { rel: 'apple-touch-icon', sizes: '120x120', href: '/icons/apple-touch-icon-120x120.png' }, - { rel: 'apple-touch-icon', sizes: '144x144', href: '/icons/apple-touch-icon-144x144.png' }, - { rel: 'apple-touch-icon', sizes: '152x152', href: '/icons/apple-touch-icon-152x152.png' }, - { rel: 'apple-touch-icon', sizes: '180x180', href: '/icons/apple-touch-icon-180x180.png' }, - { rel: 'apple-touch-startup-image', href: '/icons/apple-touch-icon-180x180.png' } - ] - } - } - - return new HandlebarReplace(indexTree, { - config: this.ngConfig, - environment: loadEnvironment(this.project), - scripts: { - polyfills: this._options.polyfills - }, - mobile: mobile - }, { - helpers: { - 'content-for': (name) => { - // TODO: remove content-for. - // eslint-disable-next-line no-console - console.warn('"{{content-for}}" has been deprecated and will be removed before RC.'); - return this._contentFor(null, name); - } - } - }); - } - - /** - * Returns the TS tree. - * - * @private - * @method _getTsTree - * @return {Tree} Tree for TypeScript files. - */ - _getTsTree() { - var tsConfigPath = path.join(this._sourceDir, 'tsconfig.json'); - var tsTree = new BroccoliTypescript(this._inputNode, tsConfigPath, this._tsCompiler); - - var tsTreeExcludes = ['*.d.ts', 'tsconfig.json']; - var excludeSpecFiles = '**/*.spec.*'; - - if (loadEnvironment(this.project).production) { - tsTreeExcludes.push(excludeSpecFiles); - } - - tsTree = new BroccoliFunnel(tsTree, { - srcDir: this._sourceDir, - exclude: tsTreeExcludes - }); - - return tsTree; - } - - - /** - * Returns the `vendorNpm` tree by merging the CLI dependencies plus the ones - * passed by the user. - * - * @private - * @method _getVendorNpmTree - * @return {Tree} The NPM tree. - */ - _getVendorNpmTree() { - var vendorNpmFiles = [ - ]; - - if (this.ngConfig.apps[0].mobile) { - vendorNpmFiles.push('@angular/service-worker/dist/worker.js') - } - - if (this._options.vendorNpmFiles) { - vendorNpmFiles = vendorNpmFiles.concat(this._options.vendorNpmFiles); - } - - return new BroccoliFunnel(new UnwatchedDir('node_modules'), { - include: vendorNpmFiles, - destDir: 'vendor', - name: 'vendor' - }); - } - - /** - * Returns the `assets` tree. - * - * @private - * @method _getAssetsTree - * @return {Tree} The assets tree. - */ - _getAssetsTree() { - return new BroccoliFunnel(this._inputNode, { - srcDir: this._sourceDir, - exclude: [ - '**/*.ts', - '**/*.scss', - '**/*.sass', - '**/*.less', - '**/*.styl', - '**/tsconfig.json' - ], - allowEmpty: true - }); - } - - /** - * Returns the config files tree. - * - * @private - * @method _getConfigTree - * @return {Tree} The config files tree. - */ - _getConfigTree() { - let env = process.env['EMBER_ENV'] || 'dev'; - - switch (env) { - case 'production': env = 'prod'; break; - case 'development': env = 'dev'; break; - } - - var envConfigFile = `environment.${env}.ts`; - - return new BroccoliFunnel('config', { - include: [envConfigFile], - destDir: `${this._sourceDir}/app`, - getDestinationPath: () => 'environment.ts' - }); - } - - _getBundleTree(preBundleTree){ - var vendorTree = this._getVendorNpmTree(); - var assetsTree = this._getAssetsTree(); - - var scriptTree = new BroccoliFunnel(preBundleTree, { - include: this._options.polyfills - }); - - var nonJsTree = new BroccoliFunnel(preBundleTree, { - exclude: ['**/*.js', '**/*.js.map'] - }); - var jsTree = new BroccoliFunnel(preBundleTree, { - include: ['**/*.js', '**/*.js.map'] - }); - - var bundleTree = new BundlePlugin([jsTree]); - - if (this.ngConfig.apps[0].mobile) { - bundleTree = concat(BroccoliMergeTrees([vendorTree, jsTree, scriptTree, bundleTree], { - overwrite: true - }), { - headerFiles: this._options.polyfills.concat([ - 'system-config.js', - 'main.js' - ]), - inputFiles: [ - 'system-import.js' - ], - header: ';(function() {', - footer: '}());', - sourceMapConfig: { enabled: true }, - allowNone: false, - outputFile: '/app-concat.js' - }); - - bundleTree = uglify(bundleTree, { - mangle: false - }); - - // Required here since the package isn't installed for non-mobile apps. - var ServiceWorkerPlugin = require('@angular/service-worker').ServiceWorkerPlugin; - // worker.js is needed so it can be copied to dist - var workerJsTree = new BroccoliFunnel(jsTree, { - include: ['vendor/@angular/service-worker/dist/worker.js'] - }); - /** - * ServiceWorkerPlugin will automatically pre-fetch and cache every file - * in the tree it receives, so it should only receive the app bundle, - * and non-JS static files from the app. The plugin also needs to have - * the worker.js file available so it can copy it to dist. - **/ - var swTree = new ServiceWorkerPlugin(BroccoliMergeTrees([ - bundleTree, - assetsTree, - workerJsTree - ])); - bundleTree = BroccoliMergeTrees([bundleTree, swTree], { - overwrite: true - }); - } - - return BroccoliMergeTrees([nonJsTree, scriptTree, bundleTree], { overwrite: true }); - } -} - -module.exports = Angular2App; diff --git a/lib/broccoli/broccoli-handlebars.js b/lib/broccoli/broccoli-handlebars.js deleted file mode 100644 index 269cfed8160c..000000000000 --- a/lib/broccoli/broccoli-handlebars.js +++ /dev/null @@ -1,38 +0,0 @@ -'use strict'; - -const fs = require('fs-extra'); -const path = require('path'); -const BroccoliCacheWriter = require('broccoli-caching-writer'); -const Handlebars = require('handlebars'); - - -class HandlebarReplace extends BroccoliCacheWriter { - constructor(inputTree, context, options) { - super([inputTree], options); - if (options && options.helpers) { - Object.keys(options.helpers).forEach((helperName) => { - Handlebars.registerHelper(helperName, function() { - const result = options.helpers[helperName].apply(null, arguments); - return new Handlebars.SafeString(result); - }); - }) - } - this._context = context; - } - - build() { - this.listFiles().forEach((filePath) => { - filePath = path.normalize(filePath); - const destPath = filePath.replace(this.inputPaths[0], this.outputPath); - const content = fs.readFileSync(filePath, 'utf-8'); - const template = Handlebars.compile(content); - - if (!fs.existsSync(path.dirname(destPath))) { - fs.mkdirsSync(path.dirname(destPath)); - } - fs.writeFileSync(destPath, template(this._context), 'utf-8'); - }); - } -} - -module.exports = HandlebarReplace; diff --git a/lib/broccoli/broccoli-typescript.js b/lib/broccoli/broccoli-typescript.js deleted file mode 100644 index 5d33086a063a..000000000000 --- a/lib/broccoli/broccoli-typescript.js +++ /dev/null @@ -1,472 +0,0 @@ -'use strict'; - -const Plugin = require('broccoli-caching-writer'); -const fs = require('fs'); -const fse = require('fs-extra'); -const path = require('path'); -const ts = require('typescript'); -const glob = require('glob'); - -const FS_OPTS = { - encoding: 'utf-8' -}; - - -/** - * Broccoli plugin that implements incremental Typescript compiler. - * - * It instantiates a typescript compiler instance that keeps all the state about the project and - * can re-emit only the files that actually changed. - * - * Limitations: only files that map directly to the changed source file via naming conventions are - * re-emitted. This primarily affects code that uses `const enum`s, because changing the enum value - * requires global emit, which can affect many files. - */ -class BroccoliTypeScriptCompiler extends Plugin { - constructor(inputPath, tsConfigPath, options) { - super([inputPath], {}); - - this._fileRegistry = Object.create(null); - this._rootFilePaths = []; - this._tsOpts = null; - this._tsServiceHost = null; - this._tsService = null; - - this._tsConfigPath = tsConfigPath; - this._options = options; - } - - build() { - if (!this._tsServiceHost) { - this._createServiceHost(); - } - this._doIncrementalBuild(); - } - - _doIncrementalBuild() { - var errorMessages = []; - var entries = this.listEntries(); - const inputPath = this.inputPaths[0]; - - const pathsToEmit = []; - - entries.forEach(entry => { - const tsFilePath = path.join(inputPath, entry.relativePath); - if (!tsFilePath.match(/\.ts$/) || !fs.existsSync(tsFilePath)) { - return; - } - // Remove d.ts files that aren't part of the tsconfig files. - if (tsFilePath.match(/\.d\.ts$/) && this._tsConfigFiles.indexOf(tsFilePath) == -1) { - return; - } - - if (!this._fileRegistry[tsFilePath]) { - // Not in the registry? Add it. - this._addNewFileEntry(entry); - - // We need to add the file to the rootFiles as well, as otherwise it _might_ - // not get compiled. It needs to be referenced at some point, and unless we - // add the spec files first (which we don't know the order), it won't. - // So make every new files an entry point instead. - // TODO(hansl): We need to investigate if we should emit files that are not - // referenced. This doesn't take that into account. - this._tsServiceHost.fileNames.push(tsFilePath); - - pathsToEmit.push(tsFilePath); - } else if (this._fileRegistry[tsFilePath].version >= entry.mtime) { - // Nothing to do for this file. Just link the cached outputs. - this._fileRegistry[tsFilePath].outputs.forEach(absoluteFilePath => { - const outputFilePath = absoluteFilePath.replace(this.cachePath, this.outputPath); - fse.mkdirsSync(path.dirname(outputFilePath)); - try { - fs.symlinkSync(absoluteFilePath, outputFilePath); - } catch (e) { - const contentStr = fs.readFileSync(absoluteFilePath); - fs.writeFileSync(outputFilePath, contentStr); - } - }); - } else { - this._fileRegistry[tsFilePath].version = entry.mtime; - pathsToEmit.push(tsFilePath); - } - }); - - if (pathsToEmit.length > 0) { - // Force the TS Service to recreate the program (ie. call synchronizeHostData). - this._tsServiceHost.projectVersion++; - - pathsToEmit.forEach(tsFilePath => { - var output = this._tsService.getEmitOutput(tsFilePath); - if (output.emitSkipped) { - const allDiagnostics = this._tsService.getCompilerOptionsDiagnostics() - .concat(this._tsService.getSyntacticDiagnostics(tsFilePath)) - .concat(this._tsService.getSemanticDiagnostics(tsFilePath)); - - const errors = this._collectErrors(allDiagnostics); - if (errors) { - // Rebuild it next incremental pass. - delete this._fileRegistry[tsFilePath]; - errorMessages.push(errors); - } - } else { - output.outputFiles.forEach(o => { - this._outputFile(o.name, o.text, this._fileRegistry[tsFilePath]); - }); - } - }); - } - - if (errorMessages.length) { - var error = new Error('Typescript found the following errors:\n' + errorMessages.join('\n')); - error['showStack'] = false; - throw error; - } - } - - _loadTsConfig() { - var tsConfigPath = path.join(this.inputPaths[0], this._tsConfigPath); - var tsconfig = JSON.parse(fs.readFileSync(tsConfigPath, 'utf-8')); - - tsconfig.files = (tsconfig.files || []) - .map(name => path.join(path.dirname(this._tsConfigPath), name)); - - // Add all glob files to files. In some cases we don't want to specify - let globFiles = this._options.additionalFiles; - if (globFiles) { - if (typeof globFiles == 'string') { - globFiles = [globFiles]; - } - - for (const g of globFiles) { - const files = glob(g, { sync: true, cwd: this.inputPaths[0], root: this.inputPaths[0] }); - tsconfig.files = tsconfig.files.concat(files); - } - } - - // Remove dupes in tsconfig.files. - const fileNameMap = {}; - tsconfig.files = tsconfig.files.filter(fileName => { - if (fileNameMap[fileName]) { - return false; - } - fileNameMap[fileName] = true; - return true; - }); - - // Because the tsconfig does not include the source directory, add this as the first path - // element. - tsconfig.files = tsconfig.files.map(name => path.join(this.inputPaths[0], name)); - return tsconfig; - } - - _createServiceHost() { - var tsconfig = this._loadTsConfig(); - - this._tsConfigFiles = tsconfig.files.splice(0); - - this._tsOpts = ts.convertCompilerOptionsFromJson(tsconfig['compilerOptions'], - this.inputPaths[0], this._tsConfigPath).options; - this._tsOpts.rootDir = ''; - this._tsOpts.outDir = ''; - - this._tsServiceHost = new CustomLanguageServiceHost( - this._tsOpts, this._rootFilePaths, this._fileRegistry, this.inputPaths[0], - tsconfig['compilerOptions'].paths, this._tsConfigPath); - this._tsService = ts.createLanguageService(this._tsServiceHost, ts.createDocumentRegistry()); - } - - _collectErrors(allDiagnostics) { - const errors = allDiagnostics.map(diagnostic => { - const message = ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n'); - if (diagnostic.file) { - const position = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start); - const line = position.line; - const character = position.character; - return ` ${diagnostic.file.fileName} (${line + 1}, ${character + 1}): ${message}`; - } else { - return ` Error: ${message}`; - } - }); - - if (errors.length) { - return errors.join('\n'); - } - } - - _outputFile(absoluteFilePath, fileContent, registry) { - absoluteFilePath = path.resolve(this.cachePath, absoluteFilePath); - let inputFilePath = absoluteFilePath; - // Replace the input path by the output. - absoluteFilePath = absoluteFilePath.replace(this.inputPaths[0], this.cachePath); - const outputFilePath = absoluteFilePath.replace(this.cachePath, this.outputPath); - - if (registry) { - registry.outputs.add(absoluteFilePath); - } - - fse.mkdirsSync(path.dirname(absoluteFilePath)); - const content = this.fixSourceMapSources(fileContent, inputFilePath); - fs.writeFileSync(absoluteFilePath, content, FS_OPTS); - - fse.mkdirsSync(path.dirname(outputFilePath)); - try { - fs.symlinkSync(absoluteFilePath, outputFilePath); - } catch (e) { - const contentStr = fs.readFileSync(absoluteFilePath); - fs.writeFileSync(outputFilePath, contentStr); - } - } - - _addNewFileEntry(entry, checkDuplicates /* = true */) { - if (checkDuplicates === undefined) { - checkDuplicates = true; - } - - const p = path.join(this.inputPaths[0], entry.relativePath); - if (checkDuplicates && this._fileRegistry[p]) { - throw `Trying to add a new entry to an already existing one: "${p}`; - } - - this._fileRegistry[p] = { - version: entry.mtime, - outputs: new Set() - }; - } - - /** - * There is a bug in TypeScript 1.6, where the sourceRoot and inlineSourceMap properties - * are exclusive. This means that the sources property always contains relative paths - * (e.g, ../../../../@angular/di/injector.ts). - * - * Here, we normalize the sources property and remove the ../../../ - * - * This issue is fixed in https://github.com/Microsoft/TypeScript/pull/5620. - * Once we switch to TypeScript 1.8, we can remove this method. - */ - fixSourceMapSources(content, inputFilePath) { - try { - const marker = '//# sourceMappingURL=data:application/json;base64,'; - - let index = content.indexOf(marker); - if (index == -1) { - const pathMarker = '//# sourceMappingURL='; - index = content.indexOf(pathMarker); - if (index == -1) { - return content; - } - - // We have a regular path, make it relative to the input path. - let base = content.substring(0, index + pathMarker.length); - let mapPath = content.substring(index + pathMarker.length); - if (mapPath.startsWith(this.outputPath)) { - mapPath = mapPath.replace(this.outputPath, this.inputPaths[0]); - } else if (!mapPath.startsWith(this.inputPaths[0])) { - mapPath = path.join(this.inputPaths[0], path.dirname(this._tsConfigPath), mapPath); - } - - mapPath = path.relative(path.dirname(inputFilePath), mapPath); - return '' + base + mapPath; - } - - var base = content.substring(0, index + marker.length); - var sourceMapBit = new Buffer(content.substring(index + marker.length), 'base64').toString('utf8'); - var sourceMaps = JSON.parse(sourceMapBit); - var source = sourceMaps.sources[0]; - sourceMaps.sources = [source.substring(source.lastIndexOf('../') + 3)]; - return '' + base + new Buffer(JSON.stringify(sourceMaps)).toString('base64'); - } catch (e) { - return content; - } - } -} - -class CustomLanguageServiceHost { - constructor(compilerOptions, fileNames, fileRegistry, treeInputPath, paths, tsConfigPath) { - this.compilerOptions = compilerOptions; - this.fileNames = fileNames; - this.fileRegistry = fileRegistry; - this.treeInputPath = treeInputPath; - this.currentDirectory = treeInputPath; - this.defaultLibFilePath = ts.getDefaultLibFilePath(compilerOptions).replace(/\\/g, '/'); - this.paths = paths; - this.tsConfigPath = tsConfigPath; - this.projectVersion = 0; - } - - getScriptFileNames() { - return this.fileNames; - } - - getScriptVersion(fileName) { - fileName = path.resolve(this.treeInputPath, fileName); - return this.fileRegistry[fileName] && this.fileRegistry[fileName].version.toString(); - } - - getProjectVersion() { - return this.projectVersion.toString(); - } - - /** - * Resolve a moduleName based on the path mapping defined in the tsconfig. - * @param moduleName The module name to resolve. - * @returns {string|boolean} A string that is the path of the module, if found, or a boolean - * indicating if resolution should continue with default. - * @private - */ - _resolveModulePathWithMapping(moduleName) { - // check if module name should be used as-is or it should be mapped to different value - let longestMatchedPrefixLength = 0; - let matchedPattern; - let matchedWildcard; - const paths = this.paths || {}; - - for (let pattern of Object.keys(paths)) { - if (pattern.indexOf('*') != pattern.lastIndexOf('*')) { - throw `Invalid path mapping pattern: "${pattern}"`; - } - - let indexOfWildcard = pattern.indexOf('*'); - if (indexOfWildcard !== -1) { - // check if module name starts with prefix, ends with suffix and these two don't overlap - let prefix = pattern.substr(0, indexOfWildcard); - let suffix = pattern.substr(indexOfWildcard + 1); - if (moduleName.length >= prefix.length + suffix.length && - moduleName.startsWith(prefix) && - moduleName.endsWith(suffix)) { - - // use length of matched prefix as betterness criteria - if (longestMatchedPrefixLength < prefix.length) { - longestMatchedPrefixLength = prefix.length; - matchedPattern = pattern; - matchedWildcard = moduleName.substr(prefix.length, moduleName.length - suffix.length); - } - } - } else { - // Pattern does not contain asterisk - module name should exactly match pattern to succeed. - if (pattern === moduleName) { - matchedPattern = pattern; - matchedWildcard = undefined; - break; - } - } - } - - if (!matchedPattern) { - // We fallback to the old module resolution. - return true; - } - - // some pattern was matched - module name needs to be substituted - let substitutions = this.paths[matchedPattern]; - for (let subst of substitutions) { - if (subst.indexOf('*') != subst.lastIndexOf('*')) { - throw `Invalid substitution: "${subst}" for pattern "${matchedPattern}".`; - } - if (subst == '*') { - // Trigger default module resolution. - return true; - } - // replace * in substitution with matched wildcard - let p = matchedWildcard ? subst.replace('*', matchedWildcard) : subst; - // if substituion is a relative path - combine it with baseUrl - p = path.isAbsolute(p) ? p : path.join(this.treeInputPath, path.dirname(this.tsConfigPath), p); - if (fs.existsSync(p)) { - return p; - } - } - - // This is an error; there was a match but no corresponding mapping was valid. - // Do not call the default module resolution. - return false; - } - - /** - * This method is called quite a bit to lookup 3 kinds of paths: - * 1/ files in the fileRegistry - * - these are the files in our project that we are watching for changes - * - in the future we could add caching for these files and invalidate the cache when - * the file is changed lazily during lookup - * 2/ .d.ts and library files not in the fileRegistry - * - these are not our files, they come from tsd or typescript itself - * - these files change only rarely but since we need them very rarely, it's not worth the - * cache invalidation hassle to cache them - * 3/ bogus paths that typescript compiler tries to lookup during import resolution - * - these paths are tricky to cache since files come and go and paths that was bogus in the - * past might not be bogus later - * - * In the initial experiments the impact of this caching was insignificant (single digit %) and - * not worth the potential issues with stale cache records. - */ - getScriptSnapshot(tsFilePath) { - var absoluteTsFilePath; - if (tsFilePath == this.defaultLibFilePath || path.isAbsolute(tsFilePath)) { - absoluteTsFilePath = tsFilePath; - } else if (this.compilerOptions.moduleResolution === 2 /* NodeJs */ && - tsFilePath.match(/^node_modules/)) { - absoluteTsFilePath = path.resolve(tsFilePath); - } else if (tsFilePath.match(/^rxjs/)) { - absoluteTsFilePath = path.resolve('node_modules', tsFilePath); - } else { - absoluteTsFilePath = path.join(this.treeInputPath, tsFilePath); - } - if (!fs.existsSync(absoluteTsFilePath)) { - // TypeScript seems to request lots of bogus paths during import path lookup and resolution, - // so we we just return undefined when the path is not correct. - return undefined; - } - - return ts.ScriptSnapshot.fromString(fs.readFileSync(absoluteTsFilePath, FS_OPTS)); - } - - resolveModuleNames(moduleNames, containingFile)/*: ResolvedModule[]*/ { - return moduleNames.map((moduleName) => { - let shouldResolveUsingDefaultMethod = false; - for (const ext of ['', '.ts', '.d.ts']) { - const name = `${moduleName}${ext}`; - const maybeModule = this._resolveModulePathWithMapping(name, containingFile); - if (typeof maybeModule == 'string') { - return { - resolvedFileName: maybeModule, - isExternalLibraryImport: false - }; - } else { - shouldResolveUsingDefaultMethod = shouldResolveUsingDefaultMethod || maybeModule; - } - } - - return shouldResolveUsingDefaultMethod && - ts.resolveModuleName(moduleName, containingFile, this.compilerOptions, { - fileExists(fileName) { - return fs.existsSync(fileName); - }, - readFile(fileName) { - return fs.readFileSync(fileName, 'utf-8'); - }, - directoryExists(directoryName) { - try { - const stats = fs.statSync(directoryName); - return stats && stats.isDirectory(); - } catch (e) { - return false; - } - } - }).resolvedModule; - }); - } - - getCurrentDirectory() { - return this.currentDirectory; - } - - getCompilationSettings() { - return this.compilerOptions; - } - - getDefaultLibFileName(/* options */) { - // ignore options argument, options should not change during the lifetime of the plugin - return this.defaultLibFilePath; - } -} - - -module.exports = BroccoliTypeScriptCompiler; diff --git a/lib/broccoli/environment.js b/lib/broccoli/environment.js deleted file mode 100644 index 269ce90e7fc4..000000000000 --- a/lib/broccoli/environment.js +++ /dev/null @@ -1,28 +0,0 @@ -'use strict'; -// Load the environment file defined by the EMBER_ENV environment variable. -const fs = require('fs'); -const path = require('path'); -const ts = require('typescript'); - - -// Export the content of the transpiled file. -module.exports = function loadEnvironment(project, environment) { - let env = environment || process.env['EMBER_ENV'] || 'dev'; - switch (env) { - case 'production': env = 'prod'; break; - case 'development': env = 'dev'; break; - } - - // Load the content of the environment file. - const filePath = path.join(project.root, `config/environment.${env}.ts`); - const source = fs.readFileSync(filePath, 'utf-8'); - const result = ts.transpile(source, { - target: ts.ScriptTarget.ES5, - module: ts.ModuleKind.CommonJs - }); - - const Module = module.constructor; - const m = new Module(); - m._compile(result, filePath); - return m.exports.environment; -}; diff --git a/lib/broccoli/is-production.js b/lib/broccoli/is-production.js deleted file mode 100644 index 64885fd576ae..000000000000 --- a/lib/broccoli/is-production.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = (/(^production$|^prod$)/).test(process.env.EMBER_ENV); diff --git a/lib/broccoli/require-or-null.js b/lib/broccoli/require-or-null.js deleted file mode 100644 index 9866bf69c91f..000000000000 --- a/lib/broccoli/require-or-null.js +++ /dev/null @@ -1,10 +0,0 @@ -/* jshint node: true, esversion: 6 */ -'use strict'; - -module.exports = function(name) { - try { - return require(name); - } catch (e) { - return null; - } -}; diff --git a/lib/cli/index.js b/lib/cli/index.js index bcf0fdd2c3c5..ca8598fc2ee5 100644 --- a/lib/cli/index.js +++ b/lib/cli/index.js @@ -35,34 +35,6 @@ require.extensions['.ts'] = function(m, filename) { }; -/** - * Monkey patch `ember-cli/lib/utilities/find-build-file` to find our build - * file when looking for `ember-cli-build.js`. This needs to be the first - * thing that happens before we `require()` any ember files. - * - * TODO: Remove this hack and replace it with some configuration when/if we - * move away from ember. Or when ember allows us to configure this in - * an addon. - */ -const findBuildFileRequirePath = 'ember-cli/lib/utilities/find-build-file'; -const originalFindBuildFile = require(findBuildFileRequirePath); - -const mod = require.cache[require.resolve(findBuildFileRequirePath)]; -mod.exports = function patchedFindBuildFile(name) { - if (name == 'ember-cli-build.js') { - const result = originalFindBuildFile.call(this, 'angular-cli-build.js'); - - // Fallback to ember-cli-build if angular-cli-build isn't found. - if (result) { - return result; - } - } - - return originalFindBuildFile.apply(this, arguments); -}; - - - const cli = require('ember-cli/lib/cli'); const path = require('path');