From e9cebaa356b0718e706de7879f6ed2c99069566a Mon Sep 17 00:00:00 2001 From: jkuri Date: Sat, 2 Apr 2016 06:06:46 +0200 Subject: [PATCH 1/7] feat(css): broccoli sass plugin --- lib/broccoli/angular-broccoli-sass.js | 72 +++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 lib/broccoli/angular-broccoli-sass.js diff --git a/lib/broccoli/angular-broccoli-sass.js b/lib/broccoli/angular-broccoli-sass.js new file mode 100644 index 000000000000..61e5c0fd1f51 --- /dev/null +++ b/lib/broccoli/angular-broccoli-sass.js @@ -0,0 +1,72 @@ +/* jshint node: true, esversion: 6 */ +'use strict'; + +try { + let sass; + + if (process.platform === 'win32') { + require.resolve(`${process.env.PWD}/node_modules/node-sass`); + sass = require(`${process.env.PWD}/node_modules/node-sass`); + } else { + process.env.NODE_PATH += `:${process.env.PWD}/node_modules`; + require('module').Module._initPaths(); + require.resolve('node-sass'); + sass = require('node-sass'); + } + + const Plugin = require('broccoli-caching-writer'); + const fse = require('fs-extra'); + const path = require('path'); + const Funnel = require('broccoli-funnel'); + + class SASSPlugin extends Plugin { + constructor(inputNodes, options) { + super(inputNodes, {}); + + options = options || {}; + Plugin.call(this, inputNodes, { + cacheInclude: [/(.*?).scss$/, /(.*?).sass$/] + }); + this.options = options; + this.fileRegistry = []; + } + + build() { + let entries = this.listEntries(); + let rootFileNames = entries.map(e => { + return path.resolve(e.basePath, e.relativePath); + }); + + rootFileNames.forEach(fileName => { + this.compile(fileName, this.inputPaths[0], this.outputPath); + }); + } + + compile(fileName, inputPath, outputPath) { + let sassOptions = { + file: path.join(fileName), + includePaths: this.inputPaths + }; + + let result = sass.renderSync(sassOptions); + let filePath = fileName.replace(inputPath, outputPath) + .replace(/\.scss$/, '.css') + .replace(/\.sass$/, '.css'); + + fse.outputFileSync(filePath, result.css, 'utf8'); + } + } + + exports.makeBroccoliTree = (sourceDir) => { + let sassSrcTree = new Funnel(sourceDir, { + include: ['**/*.sass', '**/*.scss'], + allowEmpty: true + }); + + return new SASSPlugin([sassSrcTree]); + }; +} catch (e) { + exports.makeBroccoliTree = () => { + return null; + }; +} From 5f7336b9d595adc27ee757afce8a5d24ca5c0b8b Mon Sep 17 00:00:00 2001 From: jkuri Date: Sat, 2 Apr 2016 06:06:56 +0200 Subject: [PATCH 2/7] feat(css): broccoli less plugin --- lib/broccoli/angular-broccoli-less.js | 69 +++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 lib/broccoli/angular-broccoli-less.js diff --git a/lib/broccoli/angular-broccoli-less.js b/lib/broccoli/angular-broccoli-less.js new file mode 100644 index 000000000000..b2df55fadcbf --- /dev/null +++ b/lib/broccoli/angular-broccoli-less.js @@ -0,0 +1,69 @@ +/* jshint node: true, esversion: 6 */ +'use strict'; + +try { + let less; + + if (process.platform === 'win32') { + require.resolve(`${process.env.PWD}/node_modules/less`); + less = require(`${process.env.PWD}/node_modules/less`); + } else { + process.env.NODE_PATH += `:${process.env.PWD}/node_modules`; + require('module').Module._initPaths(); + require.resolve('less'); + less = require('less'); + } + + const Plugin = require('broccoli-caching-writer'); + const fs = require('fs'); + const fse = require('fs-extra'); + const path = require('path'); + const Funnel = require('broccoli-funnel'); + + class LESSPlugin extends Plugin { + constructor(inputNodes, options) { + super(inputNodes, {}); + + options = options || {}; + Plugin.call(this, inputNodes, { + cacheInclude: [/(.*?).less$/] + }); + this.options = options; + this.fileRegistry = []; + } + + build() { + let entries = this.listEntries(); + let rootFileNames = entries.map(e => { + return path.resolve(e.basePath, e.relativePath); + }); + + return Promise.all(rootFileNames.map(fileName => { + return this.compile(fileName, this.inputPaths[0], this.outputPath); + })); + } + + compile(fileName, inputPath, outputPath) { + let content = fs.readFileSync(fileName, 'utf8'); + + return less.render(content) + .then(output => { + let filePath = fileName.replace(inputPath, outputPath).replace(/\.less$/, '.css'); + fse.outputFileSync(filePath, output.css, 'utf8'); + }); + } + } + + exports.makeBroccoliTree = (sourceDir) => { + let lessSrcTree = new Funnel(sourceDir, { + include: ['**/*.less'], + allowEmpty: true + }); + + return new LESSPlugin([lessSrcTree]); + }; +} catch (e) { + exports.makeBroccoliTree = () => { + return null; + }; +} From 6a93b3ea8710c3d09cc9aab362cab77c5247e7ca Mon Sep 17 00:00:00 2001 From: jkuri Date: Sat, 2 Apr 2016 06:07:13 +0200 Subject: [PATCH 3/7] feat(css): broccoli stylus plugin --- lib/broccoli/angular-broccoli-stylus.js | 68 +++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 lib/broccoli/angular-broccoli-stylus.js diff --git a/lib/broccoli/angular-broccoli-stylus.js b/lib/broccoli/angular-broccoli-stylus.js new file mode 100644 index 000000000000..b4f4c5d0fb3d --- /dev/null +++ b/lib/broccoli/angular-broccoli-stylus.js @@ -0,0 +1,68 @@ +/* jshint node: true, esversion: 6 */ +'use strict'; + +try { + let stylus; + + if (process.platform === 'win32') { + require.resolve(`${process.env.PWD}/node_modules/stylus`); + stylus = require(`${process.env.PWD}/node_modules/stylus`); + } else { + process.env.NODE_PATH += `:${process.env.PWD}/node_modules`; + require('module').Module._initPaths(); + require.resolve('stylus'); + stylus = require('stylus'); + } + + const Plugin = require('broccoli-caching-writer'); + const fs = require('fs'); + const fse = require('fs-extra'); + const path = require('path'); + const Funnel = require('broccoli-funnel'); + + class StylusPlugin extends Plugin { + constructor(inputNodes, options) { + super(inputNodes, {}); + + options = options || {}; + Plugin.call(this, inputNodes, { + cacheInclude: [/(.*?).styl$/] + }); + this.options = options; + this.fileRegistry = []; + } + + build() { + let entries = this.listEntries(); + let rootFileNames = entries.map(e => { + return path.resolve(e.basePath, e.relativePath); + }); + + return Promise.all(rootFileNames.map(fileName => { + return this.compile(fileName, this.inputPaths[0], this.outputPath); + })); + } + + compile(fileName, inputPath, outputPath) { + let content = fs.readFileSync(fileName, 'utf8'); + + return stylus.render(content, { filename: path.basename(fileName) }, function(err, css) { + let filePath = fileName.replace(inputPath, outputPath).replace(/\.styl$/, '.css'); + fse.outputFileSync(filePath, css, 'utf8'); + }); + } + } + + exports.makeBroccoliTree = (sourceDir) => { + let stylusSrcTree = new Funnel(sourceDir, { + include: ['**/*.styl'], + allowEmpty: true + }); + + return new StylusPlugin([stylusSrcTree]); + }; +} catch (e) { + exports.makeBroccoliTree = () => { + return null; + }; +} From f5397c531be24cfcb431acc0a7547f3155295969 Mon Sep 17 00:00:00 2001 From: jkuri Date: Sat, 2 Apr 2016 06:07:27 +0200 Subject: [PATCH 4/7] feat(css): broccoli compass plugin --- lib/broccoli/angular-broccoli-compass.js | 79 ++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 lib/broccoli/angular-broccoli-compass.js diff --git a/lib/broccoli/angular-broccoli-compass.js b/lib/broccoli/angular-broccoli-compass.js new file mode 100644 index 000000000000..297b3983bebc --- /dev/null +++ b/lib/broccoli/angular-broccoli-compass.js @@ -0,0 +1,79 @@ +/* jshint node: true, esversion: 6 */ +'use strict'; + +try { + let sass; + let compass; + + if (process.platform === 'win32') { + require.resolve(`${process.env.PWD}/node_modules/node-sass`); + require.resolve(`${process.env.PWD}/node_modules/compass-importer`); + sass = require(`${process.env.PWD}/node_modules/node-sass`); + compass = require(`${process.env.PWD}/node_modules/compass-importer`); + } else { + process.env.NODE_PATH += `:${process.env.PWD}/node_modules`; + require('module').Module._initPaths(); + require.resolve('node-sass'); + require.resolve('compass-importer'); + sass = require('node-sass'); + compass = require('compass-importer'); + } + + const Plugin = require('broccoli-caching-writer'); + const fse = require('fs-extra'); + const path = require('path'); + const Funnel = require('broccoli-funnel'); + + class CompassPlugin extends Plugin { + constructor(inputNodes, options) { + super(inputNodes, {}); + + options = options || {}; + Plugin.call(this, inputNodes, { + cacheInclude: [/(.*?).scss$/, /(.*?).sass$/] + }); + this.options = options; + this.fileRegistry = []; + } + + build() { + let entries = this.listEntries(); + let rootFileNames = entries.map(e => { + return path.resolve(e.basePath, e.relativePath); + }); + + rootFileNames.forEach(fileName => { + this.compile(fileName, this.inputPaths[0], this.outputPath); + }); + } + + compile(fileName, inputPath, outputPath) { + let sassOptions = { + file: path.join(fileName), + includePaths: this.inputPaths, + data: '@import "compass"; .transition { @include transition(all); }', + importer: compass + }; + + let result = sass.renderSync(sassOptions); + let filePath = fileName.replace(inputPath, outputPath) + .replace(/\.scss$/, '.css') + .replace(/\.sass$/, '.css'); + + fse.outputFileSync(filePath, result.css, 'utf8'); + } + } + + exports.makeBroccoliTree = (sourceDir) => { + let compassSrcTree = new Funnel(sourceDir, { + include: ['**/*.scss', '**/*.sass'], + allowEmpty: true + }); + + return new CompassPlugin([compassSrcTree]); + }; +} catch (e) { + exports.makeBroccoliTree = () => { + return null; + }; +} From 12a6741c09f1688a1c16b1dcc3d1abfc65f03cfa Mon Sep 17 00:00:00 2001 From: jkuri Date: Sat, 2 Apr 2016 06:12:35 +0200 Subject: [PATCH 5/7] test(e2e): add tests for css preprocessors --- tests/e2e/e2e_workflow.spec.js | 111 +++++++++++++++++++++++++++++++++ 1 file changed, 111 insertions(+) diff --git a/tests/e2e/e2e_workflow.spec.js b/tests/e2e/e2e_workflow.spec.js index 72fefc581ea8..0662510c1fd0 100644 --- a/tests/e2e/e2e_workflow.spec.js +++ b/tests/e2e/e2e_workflow.spec.js @@ -208,6 +208,117 @@ describe('Basic end-to-end Workflow', function () { }); }); + it('Installs sass support successfully via `ng install sass`', function() { + this.timeout(420000); + + sh.exec('npm install node-sass', { silent: true }); + return ng(['generate', 'component', 'test-component']) + .then(() => { + let componentPath = path.join(process.cwd(), 'src', 'app', 'test-component'); + let cssFile = path.join(componentPath, 'test-component.css'); + let scssFile = path.join(componentPath, 'test-component.scss'); + + expect(existsSync(componentPath)).to.be.equal(true); + sh.mv(cssFile, scssFile); + expect(existsSync(scssFile)).to.be.equal(true); + expect(existsSync(cssFile)).to.be.equal(false); + let scssExample = '.outer {\n .inner { background: #fff; }\n }'; + fs.writeFileSync(scssFile, scssExample, 'utf8'); + + sh.exec('ng build --silent'); + let destCss = path.join(process.cwd(), 'dist', 'app', 'test-component', 'test-component.css'); + expect(existsSync(destCss)).to.be.equal(true); + let contents = fs.readFileSync(destCss, 'utf8'); + expect(contents).to.include('.outer .inner'); + }); + }); + + it('Uninstalls sass support successfully via `ng uninstall sass`', function(done) { + this.timeout(420000); + + sh.exec('npm uninstall node-sass', { silent: true }); + let sassPath = path.join(process.cwd(), 'node_modules', 'node-sass'); + expect(existsSync(sassPath)).to.be.equal(false); + return ng(['destroy', 'component', 'test-component']) + .then(() => { + sh.rm('-rf', path.join(process.cwd(), 'src', 'app', 'test-component')); + done(); + }); + }); + + it('Installs less support successfully via `ng install less`', function() { + this.timeout(420000); + + sh.exec('npm install less', { silent: true }); + return ng(['generate', 'component', 'test-component']) + .then(() => { + let componentPath = path.join(process.cwd(), 'src', 'app', 'test-component'); + let cssFile = path.join(componentPath, 'test-component.css'); + let lessFile = path.join(componentPath, 'test-component.less'); + + expect(existsSync(componentPath)).to.be.equal(true); + sh.mv(cssFile, lessFile); + expect(existsSync(lessFile)).to.be.equal(true); + expect(existsSync(cssFile)).to.be.equal(false); + let lessExample = '.outer {\n .inner { background: #fff; }\n }'; + fs.writeFileSync(lessFile, lessExample, 'utf8'); + + sh.exec('ng build --silent'); + let destCss = path.join(process.cwd(), 'dist', 'app', 'test-component', 'test-component.css'); + expect(existsSync(destCss)).to.be.equal(true); + let contents = fs.readFileSync(destCss, 'utf8'); + expect(contents).to.include('.outer .inner'); + }); + }); + + it('Uninstalls less support successfully via `ng uninstall less`', function() { + this.timeout(420000); + + sh.exec('npm uninstall less', { silent: true }); + let lessPath = path.join(process.cwd(), 'node_modules', 'less'); + expect(existsSync(lessPath)).to.be.equal(false); + return ng(['destroy', 'component', 'test-component']) + .then(() => { + sh.rm('-rf', path.join(process.cwd(), 'src', 'app', 'test-component')); + }); + }); + + it('Installs stylus support successfully via `ng install stylus`', function() { + this.timeout(420000); + + sh.exec('npm install stylus', { silent: true }); + return ng(['generate', 'component', 'test-component']) + .then(() => { + let componentPath = path.join(process.cwd(), 'src', 'app', 'test-component'); + let cssFile = path.join(componentPath, 'test-component.css'); + let stylusFile = path.join(componentPath, 'test-component.styl'); + + sh.mv(cssFile, stylusFile); + expect(existsSync(stylusFile)).to.be.equal(true); + expect(existsSync(cssFile)).to.be.equal(false); + let stylusExample = '.outer {\n .inner { background: #fff; }\n }'; + fs.writeFileSync(stylusFile, stylusExample, 'utf8'); + + sh.exec('ng build --silent'); + let destCss = path.join(process.cwd(), 'dist', 'app', 'test-component', 'test-component.css'); + expect(existsSync(destCss)).to.be.equal(true); + let contents = fs.readFileSync(destCss, 'utf8'); + expect(contents).to.include('.outer .inner'); + }); + }); + + it('Uninstalls stylus support successfully via `ng uninstall stylus`', function() { + this.timeout(420000); + + sh.exec('npm uninstall stylus', { silent: true }); + let stylusPath = path.join(process.cwd(), 'node_modules', 'stylus'); + expect(existsSync(stylusPath)).to.be.equal(false); + return ng(['destroy', 'component', 'test-component']) + .then(() => { + sh.rm('-rf', path.join(process.cwd(), 'src', 'app', 'test-component')); + }); + }); + it('Turn on `noImplicitAny` in tsconfig.json and rebuild', function (done) { this.timeout(420000); From d2f2404f9671439c1944a5df6b1f531924179531 Mon Sep 17 00:00:00 2001 From: jkuri Date: Sat, 2 Apr 2016 06:13:08 +0200 Subject: [PATCH 6/7] chore: add css preprocessor trees --- lib/broccoli/angular2-app.js | 67 +++++++++++++++++++++++++++++++++++- 1 file changed, 66 insertions(+), 1 deletion(-) diff --git a/lib/broccoli/angular2-app.js b/lib/broccoli/angular2-app.js index 18022b229724..c451590e3742 100644 --- a/lib/broccoli/angular2-app.js +++ b/lib/broccoli/angular2-app.js @@ -46,11 +46,76 @@ Angular2App.prototype.toTree = function () { buildTrees.push(tsSrcTree); } + var tsconfig = JSON.parse(fs.readFileSync('src/tsconfig.json', 'utf-8')); + // Add all spec files to files. We need this because spec files are their own entry + // point. + fs.readdirSync(sourceDir).forEach(function addPathRecursive(name) { + const filePath = path.join(sourceDir, name); + if (filePath.match(/\.spec\.[jt]s$/)) { + tsconfig.files.push(name); + } else if (fs.statSync(filePath).isDirectory()) { + // Recursively call this function with the full sub-path. + fs.readdirSync(filePath).forEach(function (n) { + addPathRecursive(path.join(name, n)); + }); + } + }); + + // Because the tsconfig does not include the source directory, add this as the first path + // element. + tsconfig.files = tsconfig.files.map(name => path.join(sourceDir, name)); + + var srcAndTypingsTree = mergeTrees([sourceTree, typingsTree]); + var tsTree = new compileWithTypescript(srcAndTypingsTree, tsconfig); + + tsTree = new Funnel(tsTree, { + srcDir: 'src', + exclude: ['*.d.ts', 'tsconfig.json'] + }); + + var jsTree = new Funnel(sourceDir, { + include: ['**/*.js'], + allowEmpty: true + }); + + var assetTree = new Funnel(sourceDir, { + include: ['**/*.*'], + exclude: [ + '**/*.ts', + '**/*.js', + '**/*.scss', + '**/*.sass', + '**/*.less', + '**/*.styl' + ], + allowEmpty: true + }); + + var vendorNpmTree = new Funnel('node_modules', { + include: vendorNpmFiles, + destDir: 'vendor' + }); + + var allTrees = [ + assetTree, + tsTree, + jsTree, + this.index(), + vendorNpmTree + ]; + if (fs.existsSync('public')) { buildTrees.push(excludeDotfilesTree); } - var merged = mergeTrees(buildTrees, { overwrite: true }); + allTrees = allTrees.concat( + require('./angular-broccoli-sass').makeBroccoliTree(sourceDir), + require('./angular-broccoli-less').makeBroccoliTree(sourceDir), + require('./angular-broccoli-stylus').makeBroccoliTree(sourceDir), + require('./angular-broccoli-compass').makeBroccoliTree(sourceDir) + ).filter(x => !!x); + + var merged = mergeTrees(allTrees, { overwrite: true }); return mergeTrees([merged, new SwManifest([merged])]); }; From bf1f7628873c600aa6260a45d8b0f421298fbfcc Mon Sep 17 00:00:00 2001 From: jkuri Date: Sat, 2 Apr 2016 06:20:04 +0200 Subject: [PATCH 7/7] docs: css preprocessors support --- README.md | 13 +++ lib/broccoli/angular-broccoli-compass.js | 104 +++++++++-------------- lib/broccoli/angular-broccoli-less.js | 89 ++++++++----------- lib/broccoli/angular-broccoli-sass.js | 97 ++++++++++----------- lib/broccoli/angular-broccoli-stylus.js | 89 ++++++++----------- lib/broccoli/angular2-app.js | 74 +++------------- lib/broccoli/require-or-null.js | 10 +++ tests/e2e/e2e_workflow.spec.js | 69 +++++++-------- 8 files changed, 226 insertions(+), 319 deletions(-) create mode 100644 lib/broccoli/require-or-null.js diff --git a/README.md b/README.md index 8b8711cc1a01..749147bfad21 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,7 @@ The generated project has dependencies that require **Node 4 or greater**. * [Deploying the App via GitHub Pages](#deploying-the-app-via-github-pages) * [Support for offline applications](#support-for-offline-applications) * [Commands autocompletion](#commands-autocompletion) +* [CSS preprocessor integration](#css-preprocessor-integration) * [Known Issues](#known-issues) ## Installation @@ -249,6 +250,18 @@ ng completion >> ~/.bash_profile source ~/.bash_profile ``` + +### CSS Preprocessor integration + +We support all major CSS preprocessors: +- sass (node-sass) +- less (less) +- compass (compass-importer + node-sass) +- stylus (stylus) + +To use one just install for example `npm install node-sass` and rename `.css` files in your project to `.scss` or `.sass`. They will be compiled automatically. + + ## Known issues This project is currently a prototype so there are many known issues. Just to mention a few: diff --git a/lib/broccoli/angular-broccoli-compass.js b/lib/broccoli/angular-broccoli-compass.js index 297b3983bebc..5035cf099693 100644 --- a/lib/broccoli/angular-broccoli-compass.js +++ b/lib/broccoli/angular-broccoli-compass.js @@ -1,79 +1,59 @@ /* jshint node: true, esversion: 6 */ 'use strict'; -try { - let sass; - let compass; - - if (process.platform === 'win32') { - require.resolve(`${process.env.PWD}/node_modules/node-sass`); - require.resolve(`${process.env.PWD}/node_modules/compass-importer`); - sass = require(`${process.env.PWD}/node_modules/node-sass`); - compass = require(`${process.env.PWD}/node_modules/compass-importer`); - } else { - process.env.NODE_PATH += `:${process.env.PWD}/node_modules`; - require('module').Module._initPaths(); - require.resolve('node-sass'); - require.resolve('compass-importer'); - sass = require('node-sass'); - compass = require('compass-importer'); - } - - const Plugin = require('broccoli-caching-writer'); - const fse = require('fs-extra'); - const path = require('path'); - const Funnel = require('broccoli-funnel'); - - class CompassPlugin extends Plugin { - constructor(inputNodes, options) { - super(inputNodes, {}); +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`); +} - options = options || {}; - Plugin.call(this, inputNodes, { - cacheInclude: [/(.*?).scss$/, /(.*?).sass$/] - }); - this.options = options; - this.fileRegistry = []; - } +class CompassPlugin extends Plugin { + constructor(inputNodes, options) { + super(inputNodes, {}); - build() { - let entries = this.listEntries(); - let rootFileNames = entries.map(e => { - return path.resolve(e.basePath, e.relativePath); - }); + options = options || {}; + Plugin.call(this, inputNodes, { + cacheInclude: [/\.scss$/, /\.sass$/] + }); + this.options = options; + } - rootFileNames.forEach(fileName => { - this.compile(fileName, this.inputPaths[0], this.outputPath); - }); - } + 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 = { - file: path.join(fileName), - includePaths: this.inputPaths, - data: '@import "compass"; .transition { @include transition(all); }', - importer: compass - }; + compile(fileName, inputPath, outputPath) { + let sassOptions = { + file: path.normalize(fileName), + includePaths: this.inputPaths, + data: '@import "compass"; .transition { @include transition(all); }', + importer: compass + }; - let result = sass.renderSync(sassOptions); - let filePath = fileName.replace(inputPath, outputPath) - .replace(/\.scss$/, '.css') - .replace(/\.sass$/, '.css'); + let result = sass.renderSync(sassOptions); + let filePath = fileName.replace(inputPath, outputPath).replace(/\.s[ac]ss$/, '.css'); - fse.outputFileSync(filePath, result.css, 'utf8'); - } + fse.outputFileSync(filePath, result.css, 'utf8'); } +} - exports.makeBroccoliTree = (sourceDir) => { +exports.makeBroccoliTree = (sourceDir) => { + if (sass && compass) { let compassSrcTree = new Funnel(sourceDir, { include: ['**/*.scss', '**/*.sass'], allowEmpty: true }); return new CompassPlugin([compassSrcTree]); - }; -} catch (e) { - exports.makeBroccoliTree = () => { - return null; - }; -} + } +}; diff --git a/lib/broccoli/angular-broccoli-less.js b/lib/broccoli/angular-broccoli-less.js index b2df55fadcbf..077b7358c91b 100644 --- a/lib/broccoli/angular-broccoli-less.js +++ b/lib/broccoli/angular-broccoli-less.js @@ -1,69 +1,54 @@ /* jshint node: true, esversion: 6 */ 'use strict'; -try { - let less; +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`); +} - if (process.platform === 'win32') { - require.resolve(`${process.env.PWD}/node_modules/less`); - less = require(`${process.env.PWD}/node_modules/less`); - } else { - process.env.NODE_PATH += `:${process.env.PWD}/node_modules`; - require('module').Module._initPaths(); - require.resolve('less'); - less = require('less'); - } +class LESSPlugin extends Plugin { + constructor(inputNodes, options) { + super(inputNodes, {}); - const Plugin = require('broccoli-caching-writer'); - const fs = require('fs'); - const fse = require('fs-extra'); - const path = require('path'); - const Funnel = require('broccoli-funnel'); + options = options || {}; + Plugin.call(this, inputNodes, { + cacheInclude: [/\.less$/] + }); + this.options = options; + } - class LESSPlugin extends Plugin { - constructor(inputNodes, options) { - super(inputNodes, {}); + 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); + })); + } - options = options || {}; - Plugin.call(this, inputNodes, { - cacheInclude: [/(.*?).less$/] - }); - this.options = options; - this.fileRegistry = []; - } + compile(fileName, inputPath, outputPath) { + let content = fs.readFileSync(fileName, 'utf8'); - build() { - let entries = this.listEntries(); - let rootFileNames = entries.map(e => { - return path.resolve(e.basePath, e.relativePath); + return less.render(content) + .then(output => { + let filePath = fileName.replace(inputPath, outputPath).replace(/\.less$/, '.css'); + fse.outputFileSync(filePath, output.css, 'utf8'); }); - - return Promise.all(rootFileNames.map(fileName => { - return this.compile(fileName, this.inputPaths[0], this.outputPath); - })); - } - - compile(fileName, inputPath, outputPath) { - let content = fs.readFileSync(fileName, 'utf8'); - - return less.render(content) - .then(output => { - let filePath = fileName.replace(inputPath, outputPath).replace(/\.less$/, '.css'); - fse.outputFileSync(filePath, output.css, 'utf8'); - }); - } } +} - exports.makeBroccoliTree = (sourceDir) => { +exports.makeBroccoliTree = (sourceDir) => { + if (less) { let lessSrcTree = new Funnel(sourceDir, { include: ['**/*.less'], allowEmpty: true }); return new LESSPlugin([lessSrcTree]); - }; -} catch (e) { - exports.makeBroccoliTree = () => { - return null; - }; -} + } +}; diff --git a/lib/broccoli/angular-broccoli-sass.js b/lib/broccoli/angular-broccoli-sass.js index 61e5c0fd1f51..8fc3a196d61a 100644 --- a/lib/broccoli/angular-broccoli-sass.js +++ b/lib/broccoli/angular-broccoli-sass.js @@ -1,72 +1,61 @@ /* jshint node: true, esversion: 6 */ 'use strict'; -try { - let sass; - - if (process.platform === 'win32') { - require.resolve(`${process.env.PWD}/node_modules/node-sass`); - sass = require(`${process.env.PWD}/node_modules/node-sass`); - } else { - process.env.NODE_PATH += `:${process.env.PWD}/node_modules`; - require('module').Module._initPaths(); - require.resolve('node-sass'); - sass = require('node-sass'); - } - - const Plugin = require('broccoli-caching-writer'); - const fse = require('fs-extra'); - const path = require('path'); - const Funnel = require('broccoli-funnel'); +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, {}); +class SASSPlugin extends Plugin { + constructor(inputNodes, options) { + super(inputNodes, {}); - options = options || {}; - Plugin.call(this, inputNodes, { - cacheInclude: [/(.*?).scss$/, /(.*?).sass$/] - }); - this.options = options; - this.fileRegistry = []; - } + options = options || {}; + Plugin.call(this, inputNodes, { + cacheInclude: [/\.scss$/, /\.sass$/] + }); + this.options = options; + } - build() { - let entries = this.listEntries(); - let rootFileNames = entries.map(e => { - return path.resolve(e.basePath, e.relativePath); - }); + build() { + this.listEntries().forEach(e => { + let fileName = path.resolve(e.basePath, e.relativePath); + this.compile(fileName, this.inputPaths[0], this.outputPath); + }); + } - rootFileNames.forEach(fileName => { - this.compile(fileName, this.inputPaths[0], this.outputPath); - }); - } + compile(fileName, inputPath, outputPath) { + let sassOptions = { + file: path.normalize(fileName), + includePaths: this.inputPaths + }; - compile(fileName, inputPath, outputPath) { - let sassOptions = { - file: path.join(fileName), - includePaths: this.inputPaths - }; + let result = sass.renderSync(sassOptions); + let filePath = fileName.replace(inputPath, outputPath).replace(/\.s[ac]ss$/, '.css'); - let result = sass.renderSync(sassOptions); - let filePath = fileName.replace(inputPath, outputPath) - .replace(/\.scss$/, '.css') - .replace(/\.sass$/, '.css'); + fse.outputFileSync(filePath, result.css, 'utf8'); + } +} - fse.outputFileSync(filePath, result.css, 'utf8'); - } +exports.makeBroccoliTree = (sourceDir) => { + // 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`); } - exports.makeBroccoliTree = (sourceDir) => { + if (sass && !compass) { let sassSrcTree = new Funnel(sourceDir, { include: ['**/*.sass', '**/*.scss'], allowEmpty: true }); return new SASSPlugin([sassSrcTree]); - }; -} catch (e) { - exports.makeBroccoliTree = () => { - return null; - }; -} + } +}; diff --git a/lib/broccoli/angular-broccoli-stylus.js b/lib/broccoli/angular-broccoli-stylus.js index b4f4c5d0fb3d..d7ee8e99bc99 100644 --- a/lib/broccoli/angular-broccoli-stylus.js +++ b/lib/broccoli/angular-broccoli-stylus.js @@ -1,68 +1,53 @@ /* jshint node: true, esversion: 6 */ 'use strict'; -try { - let stylus; - - if (process.platform === 'win32') { - require.resolve(`${process.env.PWD}/node_modules/stylus`); - stylus = require(`${process.env.PWD}/node_modules/stylus`); - } else { - process.env.NODE_PATH += `:${process.env.PWD}/node_modules`; - require('module').Module._initPaths(); - require.resolve('stylus'); - stylus = require('stylus'); - } - - const Plugin = require('broccoli-caching-writer'); - const fs = require('fs'); - const fse = require('fs-extra'); - const path = require('path'); - const Funnel = require('broccoli-funnel'); - - class StylusPlugin extends Plugin { - constructor(inputNodes, options) { - super(inputNodes, {}); +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`); +} - options = options || {}; - Plugin.call(this, inputNodes, { - cacheInclude: [/(.*?).styl$/] - }); - this.options = options; - this.fileRegistry = []; - } +class StylusPlugin extends Plugin { + constructor(inputNodes, options) { + super(inputNodes, {}); - build() { - let entries = this.listEntries(); - let rootFileNames = entries.map(e => { - return path.resolve(e.basePath, e.relativePath); - }); + options = options || {}; + Plugin.call(this, inputNodes, { + cacheInclude: [/\.styl$/] + }); + this.options = options; + } - return Promise.all(rootFileNames.map(fileName => { - return this.compile(fileName, this.inputPaths[0], this.outputPath); - })); - } + 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'); + compile(fileName, inputPath, outputPath) { + let content = fs.readFileSync(fileName, 'utf8'); - return stylus.render(content, { filename: path.basename(fileName) }, function(err, css) { - let filePath = fileName.replace(inputPath, outputPath).replace(/\.styl$/, '.css'); - fse.outputFileSync(filePath, css, 'utf8'); - }); - } + return stylus.render(content, { filename: path.basename(fileName) }, function(err, css) { + let filePath = fileName.replace(inputPath, outputPath).replace(/\.styl$/, '.css'); + fse.outputFileSync(filePath, css, 'utf8'); + }); } +} - exports.makeBroccoliTree = (sourceDir) => { +exports.makeBroccoliTree = (sourceDir) => { + if (stylus) { let stylusSrcTree = new Funnel(sourceDir, { include: ['**/*.styl'], allowEmpty: true }); return new StylusPlugin([stylusSrcTree]); - }; -} catch (e) { - exports.makeBroccoliTree = () => { - return null; - }; -} + } +}; diff --git a/lib/broccoli/angular2-app.js b/lib/broccoli/angular2-app.js index c451590e3742..e0a0c36f0fc5 100644 --- a/lib/broccoli/angular2-app.js +++ b/lib/broccoli/angular2-app.js @@ -46,76 +46,18 @@ Angular2App.prototype.toTree = function () { buildTrees.push(tsSrcTree); } - var tsconfig = JSON.parse(fs.readFileSync('src/tsconfig.json', 'utf-8')); - // Add all spec files to files. We need this because spec files are their own entry - // point. - fs.readdirSync(sourceDir).forEach(function addPathRecursive(name) { - const filePath = path.join(sourceDir, name); - if (filePath.match(/\.spec\.[jt]s$/)) { - tsconfig.files.push(name); - } else if (fs.statSync(filePath).isDirectory()) { - // Recursively call this function with the full sub-path. - fs.readdirSync(filePath).forEach(function (n) { - addPathRecursive(path.join(name, n)); - }); - } - }); - - // Because the tsconfig does not include the source directory, add this as the first path - // element. - tsconfig.files = tsconfig.files.map(name => path.join(sourceDir, name)); - - var srcAndTypingsTree = mergeTrees([sourceTree, typingsTree]); - var tsTree = new compileWithTypescript(srcAndTypingsTree, tsconfig); - - tsTree = new Funnel(tsTree, { - srcDir: 'src', - exclude: ['*.d.ts', 'tsconfig.json'] - }); - - var jsTree = new Funnel(sourceDir, { - include: ['**/*.js'], - allowEmpty: true - }); - - var assetTree = new Funnel(sourceDir, { - include: ['**/*.*'], - exclude: [ - '**/*.ts', - '**/*.js', - '**/*.scss', - '**/*.sass', - '**/*.less', - '**/*.styl' - ], - allowEmpty: true - }); - - var vendorNpmTree = new Funnel('node_modules', { - include: vendorNpmFiles, - destDir: 'vendor' - }); - - var allTrees = [ - assetTree, - tsTree, - jsTree, - this.index(), - vendorNpmTree - ]; - if (fs.existsSync('public')) { buildTrees.push(excludeDotfilesTree); } - allTrees = allTrees.concat( + buildTrees = buildTrees.concat( require('./angular-broccoli-sass').makeBroccoliTree(sourceDir), require('./angular-broccoli-less').makeBroccoliTree(sourceDir), require('./angular-broccoli-stylus').makeBroccoliTree(sourceDir), require('./angular-broccoli-compass').makeBroccoliTree(sourceDir) ).filter(x => !!x); - var merged = mergeTrees(allTrees, { overwrite: true }); + var merged = mergeTrees(buildTrees, { overwrite: true }); return mergeTrees([merged, new SwManifest([merged])]); }; @@ -128,6 +70,9 @@ Angular2App.prototype.toTree = function () { */ Angular2App.prototype._initProject = function () { 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; }; /** @@ -369,7 +314,14 @@ Angular2App.prototype._getVendorNpmTree = function () { Angular2App.prototype._getAssetsTree = function () { return new Funnel(sourceDir, { include: ['**/*.*'], - exclude: ['**/*.ts', '**/*.js'], + exclude: [ + '**/*.ts', + '**/*.js', + '**/*.scss', + '**/*.sass', + '**/*.less', + '**/*.styl' + ], allowEmpty: true }); }; diff --git a/lib/broccoli/require-or-null.js b/lib/broccoli/require-or-null.js new file mode 100644 index 000000000000..9866bf69c91f --- /dev/null +++ b/lib/broccoli/require-or-null.js @@ -0,0 +1,10 @@ +/* jshint node: true, esversion: 6 */ +'use strict'; + +module.exports = function(name) { + try { + return require(name); + } catch (e) { + return null; + } +}; diff --git a/tests/e2e/e2e_workflow.spec.js b/tests/e2e/e2e_workflow.spec.js index 0662510c1fd0..5eb434ee8d56 100644 --- a/tests/e2e/e2e_workflow.spec.js +++ b/tests/e2e/e2e_workflow.spec.js @@ -43,7 +43,7 @@ describe('Basic end-to-end Workflow', function () { }); it('Can create new project using `ng new test-project`', function () { - this.timeout(420000); + this.timeout(4200000); return ng(['new', 'test-project', '--silent']).then(function () { expect(existsSync(path.join(root, 'test-project'))); @@ -208,13 +208,13 @@ describe('Basic end-to-end Workflow', function () { }); }); - it('Installs sass support successfully via `ng install sass`', function() { + it('Installs sass support successfully', function() { this.timeout(420000); sh.exec('npm install node-sass', { silent: true }); return ng(['generate', 'component', 'test-component']) .then(() => { - let componentPath = path.join(process.cwd(), 'src', 'app', 'test-component'); + let componentPath = path.join(process.cwd(), 'src', 'client', 'app', 'test-component'); let cssFile = path.join(componentPath, 'test-component.css'); let scssFile = path.join(componentPath, 'test-component.scss'); @@ -230,29 +230,26 @@ describe('Basic end-to-end Workflow', function () { expect(existsSync(destCss)).to.be.equal(true); let contents = fs.readFileSync(destCss, 'utf8'); expect(contents).to.include('.outer .inner'); - }); - }); - it('Uninstalls sass support successfully via `ng uninstall sass`', function(done) { - this.timeout(420000); + sh.rm('-f', destCss); + process.chdir('src'); + sh.exec('ng build --silent'); + expect(existsSync(destCss)).to.be.equal(true); + contents = fs.readFileSync(destCss, 'utf8'); + expect(contents).to.include('.outer .inner'); - sh.exec('npm uninstall node-sass', { silent: true }); - let sassPath = path.join(process.cwd(), 'node_modules', 'node-sass'); - expect(existsSync(sassPath)).to.be.equal(false); - return ng(['destroy', 'component', 'test-component']) - .then(() => { - sh.rm('-rf', path.join(process.cwd(), 'src', 'app', 'test-component')); - done(); + process.chdir('..'); + sh.exec('npm uninstall node-sass', { silent: true }); }); }); - it('Installs less support successfully via `ng install less`', function() { + it('Installs less support successfully', function() { this.timeout(420000); sh.exec('npm install less', { silent: true }); return ng(['generate', 'component', 'test-component']) .then(() => { - let componentPath = path.join(process.cwd(), 'src', 'app', 'test-component'); + let componentPath = path.join(process.cwd(), 'src', 'client', 'app', 'test-component'); let cssFile = path.join(componentPath, 'test-component.css'); let lessFile = path.join(componentPath, 'test-component.less'); @@ -268,28 +265,26 @@ describe('Basic end-to-end Workflow', function () { expect(existsSync(destCss)).to.be.equal(true); let contents = fs.readFileSync(destCss, 'utf8'); expect(contents).to.include('.outer .inner'); - }); - }); - it('Uninstalls less support successfully via `ng uninstall less`', function() { - this.timeout(420000); + sh.rm('-f', destCss); + process.chdir('src'); + sh.exec('ng build --silent'); + expect(existsSync(destCss)).to.be.equal(true); + contents = fs.readFileSync(destCss, 'utf8'); + expect(contents).to.include('.outer .inner'); - sh.exec('npm uninstall less', { silent: true }); - let lessPath = path.join(process.cwd(), 'node_modules', 'less'); - expect(existsSync(lessPath)).to.be.equal(false); - return ng(['destroy', 'component', 'test-component']) - .then(() => { - sh.rm('-rf', path.join(process.cwd(), 'src', 'app', 'test-component')); + process.chdir('..'); + sh.exec('npm uninstall less', { silent: true }); }); }); - it('Installs stylus support successfully via `ng install stylus`', function() { + it('Installs stylus support successfully', function() { this.timeout(420000); sh.exec('npm install stylus', { silent: true }); return ng(['generate', 'component', 'test-component']) .then(() => { - let componentPath = path.join(process.cwd(), 'src', 'app', 'test-component'); + let componentPath = path.join(process.cwd(), 'src', 'client', 'app', 'test-component'); let cssFile = path.join(componentPath, 'test-component.css'); let stylusFile = path.join(componentPath, 'test-component.styl'); @@ -304,18 +299,16 @@ describe('Basic end-to-end Workflow', function () { expect(existsSync(destCss)).to.be.equal(true); let contents = fs.readFileSync(destCss, 'utf8'); expect(contents).to.include('.outer .inner'); - }); - }); - it('Uninstalls stylus support successfully via `ng uninstall stylus`', function() { - this.timeout(420000); + sh.rm('-f', destCss); + process.chdir('src'); + sh.exec('ng build --silent'); + expect(existsSync(destCss)).to.be.equal(true); + contents = fs.readFileSync(destCss, 'utf8'); + expect(contents).to.include('.outer .inner'); - sh.exec('npm uninstall stylus', { silent: true }); - let stylusPath = path.join(process.cwd(), 'node_modules', 'stylus'); - expect(existsSync(stylusPath)).to.be.equal(false); - return ng(['destroy', 'component', 'test-component']) - .then(() => { - sh.rm('-rf', path.join(process.cwd(), 'src', 'app', 'test-component')); + process.chdir('..'); + sh.exec('npm uninstall stylus', { silent: true }); }); });