diff --git a/packages/core/integration-tests/test/css.js b/packages/core/integration-tests/test/css.js index 422b8b9a01f..4203d105755 100644 --- a/packages/core/integration-tests/test/css.js +++ b/packages/core/integration-tests/test/css.js @@ -289,9 +289,9 @@ describe('css', function() { assert.equal(typeof output, 'function'); let value = output(); - assert(/_index_[0-9a-z]+_1/.test(value)); + assert(/_index_[0-9a-z]/.test(value)); - let cssClass = value.match(/(_index_[0-9a-z]+_1)/)[1]; + let cssClass = value.match(/(_index_[0-9a-z]+)/)[1]; let css = await fs.readFile( path.join(__dirname, '/dist/index.css'), @@ -313,6 +313,207 @@ describe('css', function() { assert.equal(run1(), run2()); }); + it('should support postcss composes imports', async function() { + let b = await bundle( + path.join(__dirname, '/integration/postcss-composes/index.js') + ); + + await assertBundleTree(b, { + name: 'index.js', + assets: ['index.js', 'composes-1.css', 'composes-2.css', 'mixins.css'], + childBundles: [ + { + name: 'index.css', + assets: ['composes-1.css', 'composes-2.css', 'mixins.css'], + childBundles: [] + }, + { + type: 'map' + } + ] + }); + + let output = await run(b); + assert.equal(typeof output, 'function'); + + let value = output(); + const composes1Classes = value.composes1.split(' '); + const composes2Classes = value.composes2.split(' '); + assert(composes1Classes[0].startsWith('_composes1_')); + assert(composes1Classes[1].startsWith('_test_')); + assert(composes2Classes[0].startsWith('_composes2_')); + assert(composes2Classes[1].startsWith('_test_')); + + let css = await fs.readFile( + path.join(__dirname, '/dist/index.css'), + 'utf8' + ); + let cssClass1 = value.composes1.match(/(_composes1_[0-9a-z]+)/)[1]; + assert(css.includes(`.${cssClass1}`)); + let cssClass2 = value.composes2.match(/(_composes2_[0-9a-z]+)/)[1]; + assert(css.includes(`.${cssClass2}`)); + }); + + it('should not include css twice for postcss composes imports', async function() { + let b = await bundle( + path.join(__dirname, '/integration/postcss-composes/index.js') + ); + + await run(b); + + let css = await fs.readFile( + path.join(__dirname, '/dist/index.css'), + 'utf8' + ); + assert.equal( + css.indexOf('height: 100px;'), + css.lastIndexOf('height: 100px;') + ); + }); + + it('should support postcss composes imports for sass', async function() { + let b = await bundle( + path.join(__dirname, '/integration/postcss-composes/index2.js') + ); + + await assertBundleTree(b, { + name: 'index2.js', + assets: ['index2.js', 'composes-3.css', 'mixins.scss'], + childBundles: [ + { + name: 'index2.css', + assets: ['composes-3.css', 'mixins.scss'], + childBundles: [] + }, + { + type: 'map' + } + ] + }); + + let output = await run(b); + assert.equal(typeof output, 'function'); + + let value = output(); + const composes3Classes = value.composes3.split(' '); + assert(composes3Classes[0].startsWith('_composes3_')); + assert(composes3Classes[1].startsWith('_test_')); + + let css = await fs.readFile( + path.join(__dirname, '/dist/index2.css'), + 'utf8' + ); + assert(css.includes('height: 200px;')); + }); + + it('should support postcss composes imports with custom path names', async function() { + let b = await bundle( + path.join(__dirname, '/integration/postcss-composes/index3.js') + ); + + await assertBundleTree(b, { + name: 'index3.js', + assets: ['index3.js', 'composes-4.css', 'mixins.css'], + childBundles: [ + { + name: 'index3.css', + assets: ['composes-4.css', 'mixins.css'], + childBundles: [] + }, + { + type: 'map' + } + ] + }); + + let output = await run(b); + assert.equal(typeof output, 'function'); + + let value = output(); + const composes4Classes = value.composes4.split(' '); + assert(composes4Classes[0].startsWith('_composes4_')); + assert(composes4Classes[1].startsWith('_test_')); + + let css = await fs.readFile( + path.join(__dirname, '/dist/index3.css'), + 'utf8' + ); + assert(css.includes('height: 100px;')); + }); + + it('should support deep nested postcss composes imports', async function() { + let b = await bundle( + path.join(__dirname, '/integration/postcss-composes/index4.js') + ); + + await assertBundleTree(b, { + name: 'index4.js', + assets: [ + 'index4.js', + 'composes-5.css', + 'mixins-intermediate.css', + 'mixins.css' + ], + childBundles: [ + { + name: 'index4.css', + assets: ['composes-5.css', 'mixins-intermediate.css', 'mixins.css'], + childBundles: [] + }, + { + type: 'map' + } + ] + }); + + let output = await run(b); + assert.equal(typeof output, 'function'); + + let value = output(); + const composes5Classes = value.composes5.split(' '); + assert(composes5Classes[0].startsWith('_composes5_')); + assert(composes5Classes[1].startsWith('_intermediate_')); + assert(composes5Classes[2].startsWith('_test_')); + + let css = await fs.readFile( + path.join(__dirname, '/dist/index4.css'), + 'utf8' + ); + assert(css.includes('height: 100px;')); + assert(css.includes('height: 300px;')); + assert(css.indexOf('._test_') < css.indexOf('._intermediate_')); + }); + + it('should support postcss composes imports for multiple selectors', async function() { + let b = await bundle( + path.join(__dirname, '/integration/postcss-composes/index5.js') + ); + + await assertBundleTree(b, { + name: 'index5.js', + assets: ['index5.js', 'composes-6.css', 'mixins.css'], + childBundles: [ + { + name: 'index5.css', + assets: ['composes-6.css', 'mixins.css'], + childBundles: [] + }, + { + type: 'map' + } + ] + }); + + let output = await run(b); + assert.equal(typeof output, 'function'); + + let value = output(); + const composes6Classes = value.composes6.split(' '); + assert(composes6Classes[0].startsWith('_composes6_')); + assert(composes6Classes[1].startsWith('_test_')); + assert(composes6Classes[2].startsWith('_test-2_')); + }); + it('should minify CSS in production mode', async function() { let b = await bundle( path.join(__dirname, '/integration/cssnano/index.js'), diff --git a/packages/core/integration-tests/test/integration/postcss-composes/.postcssrc b/packages/core/integration-tests/test/integration/postcss-composes/.postcssrc new file mode 100644 index 00000000000..050061e1080 --- /dev/null +++ b/packages/core/integration-tests/test/integration/postcss-composes/.postcssrc @@ -0,0 +1,3 @@ +{ + "modules": true +} diff --git a/packages/core/integration-tests/test/integration/postcss-composes/composes-1.css b/packages/core/integration-tests/test/integration/postcss-composes/composes-1.css new file mode 100644 index 00000000000..547498770fc --- /dev/null +++ b/packages/core/integration-tests/test/integration/postcss-composes/composes-1.css @@ -0,0 +1,4 @@ +.composes1 { + composes: test from './mixins.css'; + border: 3px solid orange; +} diff --git a/packages/core/integration-tests/test/integration/postcss-composes/composes-2.css b/packages/core/integration-tests/test/integration/postcss-composes/composes-2.css new file mode 100644 index 00000000000..98163adf124 --- /dev/null +++ b/packages/core/integration-tests/test/integration/postcss-composes/composes-2.css @@ -0,0 +1,4 @@ +.composes2 { + composes: test from './mixins.css'; + border: 3px solid red; +} diff --git a/packages/core/integration-tests/test/integration/postcss-composes/composes-3.css b/packages/core/integration-tests/test/integration/postcss-composes/composes-3.css new file mode 100644 index 00000000000..862fe412920 --- /dev/null +++ b/packages/core/integration-tests/test/integration/postcss-composes/composes-3.css @@ -0,0 +1,4 @@ +.composes3 { + composes: test from './mixins.scss'; + border: 3px solid brown; +} diff --git a/packages/core/integration-tests/test/integration/postcss-composes/composes-4.css b/packages/core/integration-tests/test/integration/postcss-composes/composes-4.css new file mode 100644 index 00000000000..b0f3992251d --- /dev/null +++ b/packages/core/integration-tests/test/integration/postcss-composes/composes-4.css @@ -0,0 +1,4 @@ +.composes4 { + composes: test from '~mixins.css'; + border: 3px solid black; +} diff --git a/packages/core/integration-tests/test/integration/postcss-composes/composes-5.css b/packages/core/integration-tests/test/integration/postcss-composes/composes-5.css new file mode 100644 index 00000000000..4522e2bfdff --- /dev/null +++ b/packages/core/integration-tests/test/integration/postcss-composes/composes-5.css @@ -0,0 +1,4 @@ +.composes5 { + composes: intermediate from './mixins-intermediate.css'; + border: 3px solid yellow; +} diff --git a/packages/core/integration-tests/test/integration/postcss-composes/composes-6.css b/packages/core/integration-tests/test/integration/postcss-composes/composes-6.css new file mode 100644 index 00000000000..780d4fe80b0 --- /dev/null +++ b/packages/core/integration-tests/test/integration/postcss-composes/composes-6.css @@ -0,0 +1,4 @@ +.composes6 { + composes: test test-2 from './mixins.css'; + border: 3px solid orangered; +} diff --git a/packages/core/integration-tests/test/integration/postcss-composes/index.js b/packages/core/integration-tests/test/integration/postcss-composes/index.js new file mode 100644 index 00000000000..19a9be12e73 --- /dev/null +++ b/packages/core/integration-tests/test/integration/postcss-composes/index.js @@ -0,0 +1,6 @@ +var map1 = require('./composes-1.css'); +var map2 = require('./composes-2.css'); + +module.exports = function () { + return Object.assign({}, map1, map2); +}; diff --git a/packages/core/integration-tests/test/integration/postcss-composes/index2.js b/packages/core/integration-tests/test/integration/postcss-composes/index2.js new file mode 100644 index 00000000000..b72aba8c409 --- /dev/null +++ b/packages/core/integration-tests/test/integration/postcss-composes/index2.js @@ -0,0 +1,5 @@ +var map3 = require('./composes-3.css'); + +module.exports = function () { + return map3; +}; diff --git a/packages/core/integration-tests/test/integration/postcss-composes/index3.js b/packages/core/integration-tests/test/integration/postcss-composes/index3.js new file mode 100644 index 00000000000..e1eb241f911 --- /dev/null +++ b/packages/core/integration-tests/test/integration/postcss-composes/index3.js @@ -0,0 +1,5 @@ +var map4 = require('./composes-4.css'); + +module.exports = function () { + return map4; +}; diff --git a/packages/core/integration-tests/test/integration/postcss-composes/index4.js b/packages/core/integration-tests/test/integration/postcss-composes/index4.js new file mode 100644 index 00000000000..315561cbd3b --- /dev/null +++ b/packages/core/integration-tests/test/integration/postcss-composes/index4.js @@ -0,0 +1,5 @@ +var map5 = require('./composes-5.css'); + +module.exports = function () { + return map5; +}; diff --git a/packages/core/integration-tests/test/integration/postcss-composes/index5.js b/packages/core/integration-tests/test/integration/postcss-composes/index5.js new file mode 100644 index 00000000000..31932b14b88 --- /dev/null +++ b/packages/core/integration-tests/test/integration/postcss-composes/index5.js @@ -0,0 +1,5 @@ +var map6 = require('./composes-6.css'); + +module.exports = function () { + return map6; +}; diff --git a/packages/core/integration-tests/test/integration/postcss-composes/mixins-intermediate.css b/packages/core/integration-tests/test/integration/postcss-composes/mixins-intermediate.css new file mode 100644 index 00000000000..ebbbcafc863 --- /dev/null +++ b/packages/core/integration-tests/test/integration/postcss-composes/mixins-intermediate.css @@ -0,0 +1,4 @@ +.intermediate { + composes: test from './mixins.css'; + height: 300px; +} diff --git a/packages/core/integration-tests/test/integration/postcss-composes/mixins.css b/packages/core/integration-tests/test/integration/postcss-composes/mixins.css new file mode 100644 index 00000000000..2de4a887869 --- /dev/null +++ b/packages/core/integration-tests/test/integration/postcss-composes/mixins.css @@ -0,0 +1,8 @@ +.test { + height: 100px; + width: 100px; +} + +.test-2 { + background: red; +} diff --git a/packages/core/integration-tests/test/integration/postcss-composes/mixins.scss b/packages/core/integration-tests/test/integration/postcss-composes/mixins.scss new file mode 100644 index 00000000000..6a801aa790a --- /dev/null +++ b/packages/core/integration-tests/test/integration/postcss-composes/mixins.scss @@ -0,0 +1,6 @@ +$test: 200px; + +.test { + height: $test; + width: $test; +} diff --git a/packages/core/integration-tests/test/less.js b/packages/core/integration-tests/test/less.js index 11bc5ca177f..b8e3ca188d3 100644 --- a/packages/core/integration-tests/test/less.js +++ b/packages/core/integration-tests/test/less.js @@ -198,12 +198,12 @@ describe('less', function() { let output = await run(b); assert.equal(typeof output, 'function'); - assert.equal(output(), '_index_ku5n8_1'); + assert(output().startsWith('_index_')); let css = await fs.readFile( path.join(__dirname, '/dist/index.css'), 'utf8' ); - assert(css.includes('._index_ku5n8_1')); + assert(css.includes('._index_')); }); }); diff --git a/packages/core/integration-tests/test/stylus.js b/packages/core/integration-tests/test/stylus.js index f2dd3c087bc..34b6115ca0b 100644 --- a/packages/core/integration-tests/test/stylus.js +++ b/packages/core/integration-tests/test/stylus.js @@ -139,13 +139,13 @@ describe('stylus', function() { let output = await run(b); assert.equal(typeof output, 'function'); - assert.equal(output(), '_index_g9mqo_1'); + assert(output().startsWith('_index_')); let css = await fs.readFile( path.join(__dirname, '/dist/index.css'), 'utf8' ); - assert(css.includes('._index_g9mqo_1')); + assert(css.includes('._index_')); }); it('should support requiring stylus files with glob dependencies', async function() { diff --git a/packages/core/parcel-bundler/package.json b/packages/core/parcel-bundler/package.json index 5fa76b6ed63..55b01ff9d9a 100644 --- a/packages/core/parcel-bundler/package.json +++ b/packages/core/parcel-bundler/package.json @@ -41,6 +41,7 @@ "command-exists": "^1.2.6", "commander": "^2.11.0", "cross-spawn": "^6.0.4", + "css-modules-loader-core": "^1.1.0", "cssnano": "^4.0.0", "deasync": "^0.1.14", "dotenv": "^5.0.0", diff --git a/packages/core/parcel-bundler/src/Asset.js b/packages/core/parcel-bundler/src/Asset.js index 92b443a021b..adc2591ef3b 100644 --- a/packages/core/parcel-bundler/src/Asset.js +++ b/packages/core/parcel-bundler/src/Asset.js @@ -84,16 +84,7 @@ class Asset { this.dependencies.set(name, Object.assign({name}, opts)); } - addURLDependency(url, from = this.name, opts) { - if (!url || isURL(url)) { - return url; - } - - if (typeof from === 'object') { - opts = from; - from = this.name; - } - + resolveDependency(url, from = this.name) { const parsed = URL.parse(url); let depName; let resolved; @@ -110,8 +101,24 @@ class Asset { depName = './' + path.relative(path.dirname(this.name), resolved); } + return {depName, resolved}; + } + + addURLDependency(url, from = this.name, opts) { + if (!url || isURL(url)) { + return url; + } + + if (typeof from === 'object') { + opts = from; + from = this.name; + } + + const {depName, resolved} = this.resolveDependency(url, from); + this.addDependency(depName, Object.assign({dynamic: true, resolved}, opts)); + const parsed = URL.parse(url); parsed.pathname = this.options.parser .getAsset(resolved, this.options) .generateBundleName(); diff --git a/packages/core/parcel-bundler/src/assets/CSSAsset.js b/packages/core/parcel-bundler/src/assets/CSSAsset.js index 8c3b7cde3fb..76cc0998ad5 100644 --- a/packages/core/parcel-bundler/src/assets/CSSAsset.js +++ b/packages/core/parcel-bundler/src/assets/CSSAsset.js @@ -6,6 +6,8 @@ const CssSyntaxError = require('postcss/lib/css-syntax-error'); const URL_RE = /url\s*\("?(?![a-z]+:)/; const IMPORT_RE = /@import/; +const COMPOSES_RE = /composes:.+from\s*("|').*("|')\s*;?/; +const FROM_IMPORT_RE = /.+from\s*(?:"|')(.*)(?:"|')\s*;?/; const PROTOCOL_RE = /^[a-z]+:/; class CSSAsset extends Asset { @@ -18,6 +20,7 @@ class CSSAsset extends Asset { return ( !/\.css$/.test(this.name) || IMPORT_RE.test(this.contents) || + COMPOSES_RE.test(this.contents) || URL_RE.test(this.contents) ); } @@ -90,6 +93,20 @@ class CSSAsset extends Asset { this.ast.dirty = true; } } + + if (decl.prop === 'composes' && FROM_IMPORT_RE.test(decl.value)) { + let parsed = valueParser(decl.value); + + parsed.walk(node => { + if (node.type === 'string') { + const [, importPath] = FROM_IMPORT_RE.exec(decl.value); + this.addURLDependency(importPath, { + dynamic: false, + loc: decl.source.start + }); + } + }); + } }); } diff --git a/packages/core/parcel-bundler/src/transforms/postcss.js b/packages/core/parcel-bundler/src/transforms/postcss.js index a2626640d8d..0e209a3578a 100644 --- a/packages/core/parcel-bundler/src/transforms/postcss.js +++ b/packages/core/parcel-bundler/src/transforms/postcss.js @@ -1,7 +1,11 @@ const localRequire = require('../utils/localRequire'); const loadPlugins = require('../utils/loadPlugins'); +const md5 = require('../utils/md5'); const postcss = require('postcss'); +const FileSystemLoader = require('css-modules-loader-core/lib/file-system-loader'); const semver = require('semver'); +const path = require('path'); +const fs = require('@parcel/fs'); module.exports = async function(asset) { let config = await getConfig(asset); @@ -35,7 +39,10 @@ async function getConfig(asset) { } let postcssModulesConfig = { - getJSON: (filename, json) => (asset.cssModules = json) + getJSON: (filename, json) => (asset.cssModules = json), + Loader: createLoader(asset), + generateScopedName: (name, filename) => + `_${name}_${md5(filename).substr(0, 5)}` }; if (config.plugins && config.plugins['postcss-modules']) { @@ -72,3 +79,31 @@ async function getConfig(asset) { config.to = asset.name; return config; } + +const createLoader = asset => + class ParcelFileSystemLoader extends FileSystemLoader { + async fetch(composesPath, relativeTo) { + let importPath = composesPath.replace(/^["']|["']$/g, ''); + const {resolved} = asset.resolveDependency(importPath, relativeTo); + let rootRelativePath = path.resolve(path.dirname(relativeTo), resolved); + const root = path.resolve('/'); + // fixes an issue on windows which is part of the css-modules-loader-core + // see https://github.com/css-modules/css-modules-loader-core/issues/230 + if (rootRelativePath.startsWith(root)) { + rootRelativePath = rootRelativePath.substr(root.length); + } + + const source = await fs.readFile(resolved, 'utf-8'); + const {exportTokens} = await this.core.load( + source, + rootRelativePath, + undefined, + this.fetch.bind(this) + ); + return exportTokens; + } + + get finalSource() { + return ''; + } + };