diff --git a/packages/configs/default/index.json b/packages/configs/default/index.json index bda82bd3089..e6d9a21e2a9 100644 --- a/packages/configs/default/index.json +++ b/packages/configs/default/index.json @@ -53,7 +53,7 @@ ], "optimizers": { "data-url:*": ["...", "@parcel/optimizer-data-url"], - "*.css": ["@parcel/optimizer-cssnano"], + "*.css": ["@parcel/optimizer-css"], "*.{html,xhtml}": ["@parcel/optimizer-htmlnano"], "*.{js,mjs,cjs}": ["@parcel/optimizer-terser"], "*.svg": ["@parcel/optimizer-svgo"], diff --git a/packages/configs/default/package.json b/packages/configs/default/package.json index 8a8f47a369f..f6f94cad732 100644 --- a/packages/configs/default/package.json +++ b/packages/configs/default/package.json @@ -21,7 +21,7 @@ "@parcel/bundler-default": "2.3.2", "@parcel/compressor-raw": "2.3.2", "@parcel/namer-default": "2.3.2", - "@parcel/optimizer-cssnano": "2.3.2", + "@parcel/optimizer-css": "2.3.2", "@parcel/optimizer-htmlnano": "2.3.2", "@parcel/optimizer-image": "2.3.2", "@parcel/optimizer-svgo": "2.3.2", diff --git a/packages/core/integration-tests/test/css-modules.js b/packages/core/integration-tests/test/css-modules.js index de7a660b36d..f591e517873 100644 --- a/packages/core/integration-tests/test/css-modules.js +++ b/packages/core/integration-tests/test/css-modules.js @@ -1,7 +1,7 @@ import assert from 'assert'; import path from 'path'; import { - bundle as originalBundle, + bundle, run, assertBundles, distDir, @@ -10,486 +10,442 @@ import { import postcss from 'postcss'; describe('css modules', () => { - for (let name of ['old', 'new']) { - describe(name, () => { - let bundle = (entries, opts = {}) => { - if (name === 'new') { - // $FlowFixMe - opts.defaultConfig = - path.dirname(require.resolve('@parcel/test-utils')) + - '/.parcelrc-css'; - } - return originalBundle(entries, opts); - }; - - it('should support transforming css modules (require)', async () => { - let b = await bundle( - path.join(__dirname, '/integration/postcss-modules-cjs/index.js'), - ); - - assertBundles(b, [ - { - name: 'index.js', - assets: ['index.js', 'foo.module.css'], - }, - { - name: 'index.css', - assets: ['index.css', 'foo.module.css'], - }, - ]); - - let output = await run(b); - assert.equal(typeof output, 'function'); - - let value = output(); - assert(/foo_[0-9a-zA-Z]/.test(value)); - - let cssClass = value.match(/(foo_[0-9a-zA-Z])/)[1]; - - let css = await outputFS.readFile( - path.join(distDir, 'index.css'), - 'utf8', - ); - assert(css.includes(`.${cssClass}`)); - }); - - it('should support transforming css modules (import default)', async () => { - let b = await bundle( - path.join( - __dirname, - '/integration/postcss-modules-import-default/index.js', - ), - {mode: 'production'}, - ); - - assertBundles(b, [ - { - name: 'index.js', - assets: ['index.js', 'style.module.css'], - }, - { - name: 'index.css', - assets: ['style.module.css'], - }, - ]); - - let output = await run(b); - assert(/b-2_[0-9a-zA-Z]/.test(output)); - - let css = await outputFS.readFile( - b.getBundles().find(b => b.type === 'css').filePath, - 'utf8', - ); - let includedRules = new Set(); - postcss.parse(css).walkRules(rule => { - includedRules.add(rule.selector); - }); - assert(includedRules.has('.page')); - assert(includedRules.has(`.${output}`)); - }); - - it('should tree shake unused css modules classes with a namespace import', async () => { - let b = await bundle( - path.join( - __dirname, - '/integration/postcss-modules-import-namespace/index.js', - ), - {mode: 'production'}, - ); - - assertBundles(b, [ - { - name: 'index.js', - assets: ['index.js', 'style.module.css'], - }, - { - name: 'index.css', - assets: ['global.css', 'style.module.css'], - }, - ]); - - let js = await outputFS.readFile( - b.getBundles().find(b => b.type === 'js').filePath, - 'utf8', - ); - assert(!js.includes('unused')); - - let output = await run(b); - assert(/b-2_[0-9a-zA-Z]/.test(output)); - - let css = await outputFS.readFile( - b.getBundles().find(b => b.type === 'css').filePath, - 'utf8', - ); - let includedRules = new Set(); - postcss.parse(css).walkRules(rule => { - includedRules.add(rule.selector); - }); - assert.deepStrictEqual( - includedRules, - new Set(['body', `.${output}`, '.page']), - ); - }); - - it('should produce correct css without symbol propagation for css modules classes with a namespace import', async () => { - let b = await bundle( + it('should support transforming css modules (require)', async () => { + let b = await bundle( + path.join(__dirname, '/integration/postcss-modules-cjs/index.js'), + ); + + assertBundles(b, [ + { + name: 'index.js', + assets: ['index.js', 'foo.module.css'], + }, + { + name: 'index.css', + assets: ['index.css', 'foo.module.css'], + }, + ]); + + let output = await run(b); + assert.equal(typeof output, 'function'); + + let value = output(); + assert(/foo_[0-9a-zA-Z]/.test(value)); + + let cssClass = value.match(/(foo_[0-9a-zA-Z])/)[1]; + + let css = await outputFS.readFile(path.join(distDir, 'index.css'), 'utf8'); + assert(css.includes(`.${cssClass}`)); + }); + + it('should support transforming css modules (import default)', async () => { + let b = await bundle( + path.join( + __dirname, + '/integration/postcss-modules-import-default/index.js', + ), + {mode: 'production'}, + ); + + assertBundles(b, [ + { + name: 'index.js', + assets: ['index.js', 'style.module.css'], + }, + { + name: 'index.css', + assets: ['style.module.css'], + }, + ]); + + let output = await run(b); + assert(/b-2_[0-9a-zA-Z]/.test(output)); + + let css = await outputFS.readFile( + b.getBundles().find(b => b.type === 'css').filePath, + 'utf8', + ); + let includedRules = new Set(); + postcss.parse(css).walkRules(rule => { + includedRules.add(rule.selector); + }); + assert(includedRules.has('.page')); + assert(includedRules.has(`.${output}`)); + }); + + it('should tree shake unused css modules classes with a namespace import', async () => { + let b = await bundle( + path.join( + __dirname, + '/integration/postcss-modules-import-namespace/index.js', + ), + {mode: 'production'}, + ); + + assertBundles(b, [ + { + name: 'index.js', + assets: ['index.js', 'style.module.css'], + }, + { + name: 'index.css', + assets: ['global.css', 'style.module.css'], + }, + ]); + + let js = await outputFS.readFile( + b.getBundles().find(b => b.type === 'js').filePath, + 'utf8', + ); + assert(!js.includes('unused')); + + let output = await run(b); + assert(/b-2_[0-9a-zA-Z]/.test(output)); + + let css = await outputFS.readFile( + b.getBundles().find(b => b.type === 'css').filePath, + 'utf8', + ); + let includedRules = new Set(); + postcss.parse(css).walkRules(rule => { + includedRules.add(rule.selector); + }); + assert.deepStrictEqual( + includedRules, + new Set(['body', `.${output}`, '.page']), + ); + }); + + it('should produce correct css without symbol propagation for css modules classes with a namespace import', async () => { + let b = await bundle( + path.join( + __dirname, + '/integration/postcss-modules-import-namespace/index.js', + ), + { + mode: 'production', + defaultTargetOptions: { + shouldScopeHoist: false, + }, + }, + ); + + assertBundles(b, [ + { + name: 'index.js', + assets: ['index.js', 'style.module.css'], + }, + { + name: 'index.css', + assets: ['global.css', 'style.module.css'], + }, + ]); + + let {output} = await run(b, null, {require: false}); + assert(/b-2_[0-9a-zA-Z]/.test(output)); + + let css = await outputFS.readFile( + b.getBundles().find(b => b.type === 'css').filePath, + 'utf8', + ); + let includedRules = new Set(); + postcss.parse(css).walkRules(rule => { + includedRules.add(rule.selector); + }); + assert(includedRules.has('body')); + assert(includedRules.has(`.${output}`)); + assert(includedRules.has('.page')); + }); + + it('should support importing css modules with a non-static namespace import', async () => { + let b = await bundle( + path.join( + __dirname, + '/integration/postcss-modules-import-namespace-whole/index.js', + ), + {mode: 'production'}, + ); + + assertBundles(b, [ + { + name: 'index.js', + assets: ['index.js', 'style.module.css'], + }, + { + name: 'index.css', + assets: ['global.css', 'style.module.css'], + }, + ]); + + let js = await outputFS.readFile( + b.getBundles().find(b => b.type === 'js').filePath, + 'utf8', + ); + assert(js.includes('unused')); + + let output = await run(b); + assert(/b-2_[0-9a-zA-Z]/.test(output['b-2'])); + assert(/unused_[0-9a-zA-Z]/.test(output['unused'])); + + let css = await outputFS.readFile( + b.getBundles().find(b => b.type === 'css').filePath, + 'utf8', + ); + let includedRules = new Set(); + postcss.parse(css).walkRules(rule => { + includedRules.add(rule.selector); + }); + assert.deepStrictEqual( + includedRules, + new Set(['body', `.${output['b-2']}`, `.${output['unused']}`, '.page']), + ); + }); + + it('should support css modules composes imports', async () => { + let b = await bundle( + path.join(__dirname, '/integration/postcss-composes/index.js'), + ); + + assertBundles(b, [ + { + name: 'index.js', + assets: [ + 'index.js', + 'composes-1.module.css', + 'composes-2.module.css', + 'mixins.module.css', + ], + }, + { + name: 'index.css', + assets: [ + 'composes-1.module.css', + 'composes-2.module.css', + 'mixins.module.css', + ], + }, + ]); + + 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 outputFS.readFile(path.join(distDir, 'index.css'), 'utf8'); + let cssClass1 = value.composes1.match(/(composes1_[0-9a-zA-Z]+)/)[1]; + assert(css.includes(`.${cssClass1}`)); + let cssClass2 = value.composes2.match(/(composes2_[0-9a-zA-Z]+)/)[1]; + assert(css.includes(`.${cssClass2}`)); + }); + + it('should not include css twice for composes imports', async () => { + let b = await bundle( + path.join(__dirname, '/integration/postcss-composes/index.js'), + ); + + await run(b); + + let css = await outputFS.readFile(path.join(distDir, 'index.css'), 'utf8'); + assert.equal( + css.indexOf('height: 100px;'), + css.lastIndexOf('height: 100px;'), + ); + }); + + it('should support composes imports for sass', async () => { + let b = await bundle( + path.join(__dirname, '/integration/postcss-composes/index2.js'), + ); + + assertBundles(b, [ + { + name: 'index2.js', + assets: ['index2.js', 'composes-3.module.css', 'mixins.module.scss'], + }, + { + name: 'index2.css', + assets: ['composes-3.module.css', 'mixins.module.scss'], + }, + ]); + + 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 outputFS.readFile(path.join(distDir, 'index2.css'), 'utf8'); + assert(css.includes('height: 200px;')); + }); + + it('should support composes imports with custom path names', async () => { + let b = await bundle( + path.join(__dirname, '/integration/postcss-composes/index3.js'), + ); + + assertBundles(b, [ + { + name: 'index3.js', + assets: ['index3.js', 'composes-4.module.css', 'mixins.module.css'], + }, + { + name: 'index3.css', + assets: ['composes-4.module.css', 'mixins.module.css'], + }, + ]); + + 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 outputFS.readFile(path.join(distDir, 'index3.css'), 'utf8'); + assert(css.includes('height: 100px;')); + }); + + it('should support deep nested composes imports', async () => { + let b = await bundle( + path.join(__dirname, '/integration/postcss-composes/index4.js'), + ); + + assertBundles(b, [ + { + name: 'index4.js', + assets: [ + 'index4.js', + 'composes-5.module.css', + 'mixins-intermediate.module.css', + 'mixins.module.css', + ], + }, + { + name: 'index4.css', + assets: [ + 'composes-5.module.css', + 'mixins-intermediate.module.css', + 'mixins.module.css', + ], + }, + ]); + + 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 outputFS.readFile(path.join(distDir, 'index4.css'), 'utf8'); + assert(css.includes('height: 100px;')); + assert(css.includes('height: 300px;')); + assert(css.indexOf('.test_') < css.indexOf('.intermediate_')); + }); + + it('should support composes imports for multiple selectors', async () => { + let b = await bundle( + path.join(__dirname, '/integration/postcss-composes/index5.js'), + ); + + assertBundles(b, [ + { + name: 'index5.js', + assets: ['index5.js', 'composes-6.module.css', 'mixins.module.css'], + }, + { + name: 'index5.css', + assets: ['composes-6.module.css', 'mixins.module.css'], + }, + ]); + + 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 throw an error when importing a missing class', async function () { + await assert.rejects( + () => + bundle( path.join( __dirname, - '/integration/postcss-modules-import-namespace/index.js', + '/integration/no-export-error-with-correct-filetype/src/App.jsx', ), { - mode: 'production', + shouldDisableCache: true, defaultTargetOptions: { - shouldScopeHoist: false, + shouldScopeHoist: true, }, }, - ); - - assertBundles(b, [ - { - name: 'index.js', - assets: ['index.js', 'style.module.css'], - }, - { - name: 'index.css', - assets: ['global.css', 'style.module.css'], - }, - ]); - - let {output} = await run(b, null, {require: false}); - assert(/b-2_[0-9a-zA-Z]/.test(output)); - - let css = await outputFS.readFile( - b.getBundles().find(b => b.type === 'css').filePath, - 'utf8', - ); - let includedRules = new Set(); - postcss.parse(css).walkRules(rule => { - includedRules.add(rule.selector); - }); - assert(includedRules.has('body')); - assert(includedRules.has(`.${output}`)); - assert(includedRules.has('.page')); - }); - - it('should support importing css modules with a non-static namespace import', async () => { - let b = await bundle( - path.join( - __dirname, - '/integration/postcss-modules-import-namespace-whole/index.js', - ), - {mode: 'production'}, - ); - - assertBundles(b, [ - { - name: 'index.js', - assets: ['index.js', 'style.module.css'], - }, - { - name: 'index.css', - assets: ['global.css', 'style.module.css'], - }, - ]); - - let js = await outputFS.readFile( - b.getBundles().find(b => b.type === 'js').filePath, - 'utf8', - ); - assert(js.includes('unused')); - - let output = await run(b); - assert(/b-2_[0-9a-zA-Z]/.test(output['b-2'])); - assert(/unused_[0-9a-zA-Z]/.test(output['unused'])); - - let css = await outputFS.readFile( - b.getBundles().find(b => b.type === 'css').filePath, - 'utf8', - ); - let includedRules = new Set(); - postcss.parse(css).walkRules(rule => { - includedRules.add(rule.selector); - }); - assert.deepStrictEqual( - includedRules, - new Set([ - 'body', - `.${output['b-2']}`, - `.${output['unused']}`, - '.page', - ]), - ); - }); - - it('should support css modules composes imports', async () => { - let b = await bundle( - path.join(__dirname, '/integration/postcss-composes/index.js'), - ); - - assertBundles(b, [ - { - name: 'index.js', - assets: [ - 'index.js', - 'composes-1.module.css', - 'composes-2.module.css', - 'mixins.module.css', - ], - }, - { - name: 'index.css', - assets: [ - 'composes-1.module.css', - 'composes-2.module.css', - 'mixins.module.css', - ], - }, - ]); - - 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 outputFS.readFile( - path.join(distDir, 'index.css'), - 'utf8', - ); - let cssClass1 = value.composes1.match(/(composes1_[0-9a-zA-Z]+)/)[1]; - assert(css.includes(`.${cssClass1}`)); - let cssClass2 = value.composes2.match(/(composes2_[0-9a-zA-Z]+)/)[1]; - assert(css.includes(`.${cssClass2}`)); - }); - - it('should not include css twice for composes imports', async () => { - let b = await bundle( - path.join(__dirname, '/integration/postcss-composes/index.js'), - ); - - await run(b); - - let css = await outputFS.readFile( - path.join(distDir, 'index.css'), - 'utf8', - ); - assert.equal( - css.indexOf('height: 100px;'), - css.lastIndexOf('height: 100px;'), - ); - }); - - it('should support composes imports for sass', async () => { - let b = await bundle( - path.join(__dirname, '/integration/postcss-composes/index2.js'), - ); - - assertBundles(b, [ - { - name: 'index2.js', - assets: [ - 'index2.js', - 'composes-3.module.css', - 'mixins.module.scss', - ], - }, - { - name: 'index2.css', - assets: ['composes-3.module.css', 'mixins.module.scss'], - }, - ]); - - 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 outputFS.readFile( - path.join(distDir, 'index2.css'), - 'utf8', - ); - assert(css.includes('height: 200px;')); - }); - - it('should support composes imports with custom path names', async () => { - let b = await bundle( - path.join(__dirname, '/integration/postcss-composes/index3.js'), - ); - - assertBundles(b, [ - { - name: 'index3.js', - assets: ['index3.js', 'composes-4.module.css', 'mixins.module.css'], - }, + ), + { + name: 'BuildError', + diagnostics: [ { - name: 'index3.css', - assets: ['composes-4.module.css', 'mixins.module.css'], - }, - ]); - - 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 outputFS.readFile( - path.join(distDir, 'index3.css'), - 'utf8', - ); - assert(css.includes('height: 100px;')); - }); - - it('should support deep nested composes imports', async () => { - let b = await bundle( - path.join(__dirname, '/integration/postcss-composes/index4.js'), - ); - - assertBundles(b, [ - { - name: 'index4.js', - assets: [ - 'index4.js', - 'composes-5.module.css', - 'mixins-intermediate.module.css', - 'mixins.module.css', - ], - }, - { - name: 'index4.css', - assets: [ - 'composes-5.module.css', - 'mixins-intermediate.module.css', - 'mixins.module.css', - ], - }, - ]); - - 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 outputFS.readFile( - path.join(distDir, 'index4.css'), - 'utf8', - ); - assert(css.includes('height: 100px;')); - assert(css.includes('height: 300px;')); - assert(css.indexOf('.test_') < css.indexOf('.intermediate_')); - }); - - it('should support composes imports for multiple selectors', async () => { - let b = await bundle( - path.join(__dirname, '/integration/postcss-composes/index5.js'), - ); - - assertBundles(b, [ - { - name: 'index5.js', - assets: ['index5.js', 'composes-6.module.css', 'mixins.module.css'], - }, - { - name: 'index5.css', - assets: ['composes-6.module.css', 'mixins.module.css'], - }, - ]); - - 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 throw an error when importing a missing class', async function () { - await assert.rejects( - () => - bundle( - path.join( - __dirname, - '/integration/no-export-error-with-correct-filetype/src/App.jsx', - ), + codeFrames: [ { - shouldDisableCache: true, - defaultTargetOptions: { - shouldScopeHoist: true, - }, - }, - ), - { - name: 'BuildError', - diagnostics: [ - { - codeFrames: [ + filePath: path.join( + __dirname, + '/integration/no-export-error-with-correct-filetype/src/App.jsx', + ), + language: 'js', + codeHighlights: [ { - filePath: path.join( - __dirname, - '/integration/no-export-error-with-correct-filetype/src/App.jsx', - ), - language: 'js', - codeHighlights: [ - { - end: { - column: 45, - line: 7, - }, - start: { - column: 28, - line: 7, - }, - }, - ], + end: { + column: 45, + line: 7, + }, + start: { + column: 28, + line: 7, + }, }, ], - message: - "integration/no-export-error-with-correct-filetype/src/app.module.css does not export 'notExisting'", - origin: '@parcel/core', }, ], + message: + "integration/no-export-error-with-correct-filetype/src/app.module.css does not export 'notExisting'", + origin: '@parcel/core', }, - ); - }); - - it('should fall back to postcss for legacy css modules', async function () { - let b = await bundle( - path.join(__dirname, '/integration/css-modules-legacy/index.js'), - ); - - assertBundles(b, [ - { - name: 'index.js', - assets: ['index.js', 'index.module.css'], - }, - { - name: 'index.css', - assets: ['index.module.css'], - }, - ]); - - let css = await outputFS.readFile( - path.join(distDir, 'index.css'), - 'utf8', - ); - assert(css.includes('color: red')); - }); - }); - } + ], + }, + ); + }); + + it('should fall back to postcss for legacy css modules', async function () { + let b = await bundle( + path.join(__dirname, '/integration/css-modules-legacy/index.js'), + ); + + assertBundles(b, [ + { + name: 'index.js', + assets: ['index.js', 'index.module.css'], + }, + { + name: 'index.css', + assets: ['index.module.css'], + }, + ]); + + let css = await outputFS.readFile(path.join(distDir, 'index.css'), 'utf8'); + assert(css.includes('color: red')); + }); }); diff --git a/packages/core/integration-tests/test/css.js b/packages/core/integration-tests/test/css.js index 45e575a0874..dcb3adab669 100644 --- a/packages/core/integration-tests/test/css.js +++ b/packages/core/integration-tests/test/css.js @@ -2,7 +2,7 @@ import assert from 'assert'; import path from 'path'; import { - bundle as originalBundle, + bundle, run, assertBundles, distDir, @@ -16,467 +16,410 @@ describe('css', () => { await removeDistDirectory(); }); - for (let name of ['old', 'new']) { - describe(name, () => { - let bundle = (entries, opts = {}) => { - if (name === 'new') { - // $FlowFixMe - opts.defaultConfig = - path.dirname(require.resolve('@parcel/test-utils')) + - '/.parcelrc-css'; - } - return originalBundle(entries, opts); - }; - - it('should produce two bundles when importing a CSS file', async () => { - let b = await bundle(path.join(__dirname, '/integration/css/index.js')); - - assertBundles(b, [ - { - name: 'index.js', - assets: ['index.js', 'local.js'], - }, - { - name: 'index.css', - assets: ['index.css', 'local.css'], - }, - ]); - - let output = await run(b); - assert.equal(typeof output, 'function'); - assert.equal(output(), 3); - }); - - it('should bundle css dependencies in the correct, postorder traversal order', async () => { - let b = await bundle( - path.join(__dirname, '/integration/css-order/a.css'), - ); - - // Given a tree of css with imports: - // A - // / \ - // B E - // / \ - // C D - // - // (A imports B (which imports C and D) and E) - // - // ...styles should be applied in the order C, D, B, E, A - - assertBundles(b, [ - { - name: 'a.css', - assets: ['a.css', 'b.css', 'c.css', 'd.css', 'e.css'], - }, - ]); - - let css = await outputFS.readFile(path.join(distDir, 'a.css'), 'utf8'); - assert.ok( - css.indexOf('.c {') < css.indexOf('.d {') && - css.indexOf('.d {') < css.indexOf('.b {') && - css.indexOf('.b {') < css.indexOf('.e {') && - css.indexOf('.e {') < css.indexOf('.a {'), - ); - }); - - it('should support loading a CSS bundle along side dynamic imports', async () => { - let b = await bundle( - path.join(__dirname, '/integration/dynamic-css/index.js'), - ); - - assertBundles(b, [ - { - name: 'index.js', - assets: [ - 'bundle-url.js', - 'cacheLoader.js', - 'css-loader.js', - 'index.js', - 'js-loader.js', - ], - }, - {name: /local\.[0-9a-f]{8}\.js/, assets: ['local.js']}, - {name: /local\.[0-9a-f]{8}\.css/, assets: ['local.css']}, - {name: 'index.css', assets: ['index.css']}, - ]); - - let output = await run(b); - assert.equal(typeof output, 'function'); - assert.equal(await output(), 3); - }); - - it('should support importing CSS from a CSS file', async function () { - let b = await bundle( - path.join(__dirname, '/integration/css-import/index.js'), - ); - - assertBundles(b, [ - { - name: 'index.js', - assets: ['index.js'], - }, - { - name: 'index.css', - assets: ['index.css', 'other.css', 'local.css'], - }, - ]); - - let output = await run(b); - assert.equal(typeof output, 'function'); - assert.equal(output(), 2); - - let css = await outputFS.readFile( - path.join(distDir, '/index.css'), - 'utf8', - ); - assert(css.includes('.local')); - assert(css.includes('.other')); - assert(/@media print {\s*.other/.test(css)); - assert(css.includes('.index')); - }); - - it('should support linking to assets with url() from CSS', async function () { - let b = await bundle( - path.join(__dirname, '/integration/css-url/index.js'), - ); - - assertBundles(b, [ - { - name: 'index.js', - assets: ['index.js'], - }, - { - name: 'index.css', - assets: ['index.css'], - }, - { - type: 'woff2', - assets: ['test.woff2'], - }, - ]); - - let output = await run(b); - assert.equal(typeof output, 'function'); - assert.equal(output(), 2); - - let css = await outputFS.readFile( - path.join(distDir, 'index.css'), - 'utf8', - ); - assert(/url\("test\.[0-9a-f]+\.woff2"\)/.test(css)); - assert(css.includes('url("http://google.com")')); - assert(css.includes('.index')); - assert(css.includes('url("data:image/gif;base64,quotes")')); - assert(css.includes('.quotes')); - assert(css.includes('url("data:image/gif;base64,no-quote")')); - assert(css.includes('.no-quote')); - - assert( - await outputFS.exists( - path.join( - distDir, - css.match(/url\("(test\.[0-9a-f]+\.woff2)"\)/)[1], - ), - ), - ); - }); - - it('should support linking to assets with url() from CSS in production', async function () { - let b = await bundle( - path.join(__dirname, '/integration/css-url/index.js'), - { - defaultTargetOptions: { - shouldOptimize: true, - }, - }, - ); + it('should produce two bundles when importing a CSS file', async () => { + let b = await bundle(path.join(__dirname, '/integration/css/index.js')); - assertBundles(b, [ - { - name: 'index.js', - assets: ['index.js'], - }, - { - name: 'index.css', - assets: ['index.css'], - }, - { - type: 'woff2', - assets: ['test.woff2'], - }, - ]); - - let output = await run(b); - assert.equal(typeof output, 'function'); - assert.equal(output(), 2); - - let css = await outputFS.readFile( - path.join(distDir, 'index.css'), - 'utf8', - ); - assert( - /url\(test\.[0-9a-f]+\.woff2\)/.test(css), - 'woff ext found in css', - ); - assert(css.includes('url(http://google.com)'), 'url() found'); - assert(css.includes('.index'), '.index found'); - assert(/url\("?data:image\/gif;base64,quotes"?\)/.test(css)); - assert(css.includes('.quotes')); - assert(/url\("?data:image\/gif;base64,no-quote"?\)/.test(css)); - assert(css.includes('.no-quote')); - - assert( - await outputFS.exists( - path.join(distDir, css.match(/url\((test\.[0-9a-f]+\.woff2)\)/)[1]), - ), - ); - }); - - it('should support linking to assets in parent folders with url() from CSS', async function () { - let b = await bundle( - [ - path.join( - __dirname, - '/integration/css-url-relative/src/a/style1.css', - ), - path.join( - __dirname, - '/integration/css-url-relative/src/b/style2.css', - ), - ], - { - defaultTargetOptions: { - shouldOptimize: true, - sourceMaps: false, - }, - }, - ); + assertBundles(b, [ + { + name: 'index.js', + assets: ['index.js', 'local.js'], + }, + { + name: 'index.css', + assets: ['index.css', 'local.css'], + }, + ]); - assertBundles(b, [ - { - type: 'css', - assets: ['style1.css'], - }, - { - type: 'css', - assets: ['style2.css'], - }, - { - type: 'png', - assets: ['foo.png'], - }, - ]); + let output = await run(b); + assert.equal(typeof output, 'function'); + assert.equal(output(), 3); + }); - let cssPath = path.join(distDir, 'a', 'style1.css'); - let css = await outputFS.readFile(cssPath, 'utf8'); + it('should bundle css dependencies in the correct, postorder traversal order', async () => { + let b = await bundle(path.join(__dirname, '/integration/css-order/a.css')); + + // Given a tree of css with imports: + // A + // / \ + // B E + // / \ + // C D + // + // (A imports B (which imports C and D) and E) + // + // ...styles should be applied in the order C, D, B, E, A + + assertBundles(b, [ + { + name: 'a.css', + assets: ['a.css', 'b.css', 'c.css', 'd.css', 'e.css'], + }, + ]); + + let css = await outputFS.readFile(path.join(distDir, 'a.css'), 'utf8'); + assert.ok( + css.indexOf('.c {') < css.indexOf('.d {') && + css.indexOf('.d {') < css.indexOf('.b {') && + css.indexOf('.b {') < css.indexOf('.e {') && + css.indexOf('.e {') < css.indexOf('.a {'), + ); + }); - assert(css.includes('background-image'), 'includes `background-image`'); - assert(/url\([^)]*\)/.test(css), 'includes url()'); + it('should support loading a CSS bundle along side dynamic imports', async () => { + let b = await bundle( + path.join(__dirname, '/integration/dynamic-css/index.js'), + ); - assert( - await outputFS.exists( - path.resolve(path.dirname(cssPath), css.match(/url\(([^)]*)\)/)[1]), - ), - 'path specified in url() exists', - ); - }); + assertBundles(b, [ + { + name: 'index.js', + assets: [ + 'bundle-url.js', + 'cacheLoader.js', + 'css-loader.js', + 'index.js', + 'js-loader.js', + ], + }, + {name: /local\.[0-9a-f]{8}\.js/, assets: ['local.js']}, + {name: /local\.[0-9a-f]{8}\.css/, assets: ['local.css']}, + {name: 'index.css', assets: ['index.css']}, + ]); + + let output = await run(b); + assert.equal(typeof output, 'function'); + assert.equal(await output(), 3); + }); - it('should ignore url() with IE behavior specifiers', async function () { - let b = await bundle( - path.join(__dirname, '/integration/css-url-behavior/index.css'), - ); + it('should support importing CSS from a CSS file', async function () { + let b = await bundle( + path.join(__dirname, '/integration/css-import/index.js'), + ); - assertBundles(b, [ - { - name: 'index.css', - assets: ['index.css'], - }, - ]); + assertBundles(b, [ + { + name: 'index.js', + assets: ['index.js'], + }, + { + name: 'index.css', + assets: ['index.css', 'other.css', 'local.css'], + }, + ]); - let css = await outputFS.readFile( - path.join(distDir, 'index.css'), - 'utf8', - ); + let output = await run(b); + assert.equal(typeof output, 'function'); + assert.equal(output(), 2); - assert(css.includes('url(#default#VML)')); - }); + let css = await outputFS.readFile(path.join(distDir, '/index.css'), 'utf8'); + assert(css.includes('.local')); + assert(css.includes('.other')); + assert(/@media print {\s*.other/.test(css)); + assert(css.includes('.index')); + }); - it('should minify CSS when minify is set', async function () { - let b = await bundle( - path.join(__dirname, '/integration/cssnano/index.js'), - { - defaultTargetOptions: { - shouldOptimize: true, - sourceMaps: false, - }, - }, - ); + it('should support linking to assets with url() from CSS', async function () { + let b = await bundle(path.join(__dirname, '/integration/css-url/index.js')); - let output = await run(b); - assert.equal(typeof output, 'function'); - assert.equal(output(), 3); + assertBundles(b, [ + { + name: 'index.js', + assets: ['index.js'], + }, + { + name: 'index.css', + assets: ['index.css'], + }, + { + type: 'woff2', + assets: ['test.woff2'], + }, + ]); + + let output = await run(b); + assert.equal(typeof output, 'function'); + assert.equal(output(), 2); + + let css = await outputFS.readFile(path.join(distDir, 'index.css'), 'utf8'); + assert(/url\("test\.[0-9a-f]+\.woff2"\)/.test(css)); + assert(css.includes('url("http://google.com")')); + assert(css.includes('.index')); + assert(css.includes('url("data:image/gif;base64,quotes")')); + assert(css.includes('.quotes')); + assert(css.includes('url("data:image/gif;base64,no-quote")')); + assert(css.includes('.no-quote')); + + assert( + await outputFS.exists( + path.join(distDir, css.match(/url\("(test\.[0-9a-f]+\.woff2)"\)/)[1]), + ), + ); + }); - let css = await outputFS.readFile( - path.join(distDir, 'index.css'), - 'utf8', - ); - assert(css.includes('.local')); - assert(css.includes('.index')); + it('should support linking to assets with url() from CSS in production', async function () { + let b = await bundle( + path.join(__dirname, '/integration/css-url/index.js'), + { + defaultTargetOptions: { + shouldOptimize: true, + }, + }, + ); - assert.equal(css.split('\n').length, 1); - }); + assertBundles(b, [ + { + name: 'index.js', + assets: ['index.js'], + }, + { + name: 'index.css', + assets: ['index.css'], + }, + { + type: 'woff2', + assets: ['test.woff2'], + }, + ]); + + let output = await run(b); + assert.equal(typeof output, 'function'); + assert.equal(output(), 2); + + let css = await outputFS.readFile(path.join(distDir, 'index.css'), 'utf8'); + assert(/url\(test\.[0-9a-f]+\.woff2\)/.test(css), 'woff ext found in css'); + assert(css.includes('url(http://google.com)'), 'url() found'); + assert(css.includes('.index'), '.index found'); + assert(/url\("?data:image\/gif;base64,quotes"?\)/.test(css)); + assert(css.includes('.quotes')); + assert(/url\("?data:image\/gif;base64,no-quote"?\)/.test(css)); + assert(css.includes('.no-quote')); + + assert( + await outputFS.exists( + path.join(distDir, css.match(/url\((test\.[0-9a-f]+\.woff2)\)/)[1]), + ), + ); + }); - it('should produce a sourcemap when sourceMaps are used', async function () { - await bundle(path.join(__dirname, '/integration/cssnano/index.js'), { - defaultTargetOptions: { - shouldOptimize: true, - }, - }); - - let css = await outputFS.readFile( - path.join(distDir, 'index.css'), - 'utf8', - ); - assert(css.includes('.local')); - assert(css.includes('.index')); - - let lines = css.trim().split('\n'); - assert.equal(lines.length, 2); - assert.equal(lines[1], '/*# sourceMappingURL=index.css.map */'); - - let map = JSON.parse( - await outputFS.readFile(path.join(distDir, 'index.css.map'), 'utf8'), - ); - assert.equal(map.file, 'index.css.map'); - assert(map.sources.includes('integration/cssnano/local.css')); - assert(map.sources.includes('integration/cssnano/index.css')); - }); - - it('should inline data-urls for text-encoded files', async () => { - await bundle(path.join(__dirname, '/integration/data-url/text.css'), { - defaultTargetOptions: { - sourceMaps: false, - }, - }); - let css = await outputFS.readFile( - path.join(distDir, 'text.css'), - 'utf8', - ); - assert.equal( - css.trim(), - `.svg-img { + it('should support linking to assets in parent folders with url() from CSS', async function () { + let b = await bundle( + [ + path.join(__dirname, '/integration/css-url-relative/src/a/style1.css'), + path.join(__dirname, '/integration/css-url-relative/src/b/style2.css'), + ], + { + defaultTargetOptions: { + shouldOptimize: true, + sourceMaps: false, + }, + }, + ); + + assertBundles(b, [ + { + type: 'css', + assets: ['style1.css'], + }, + { + type: 'css', + assets: ['style2.css'], + }, + { + type: 'png', + assets: ['foo.png'], + }, + ]); + + let cssPath = path.join(distDir, 'a', 'style1.css'); + let css = await outputFS.readFile(cssPath, 'utf8'); + + assert(css.includes('background-image'), 'includes `background-image`'); + assert(/url\([^)]*\)/.test(css), 'includes url()'); + + assert( + await outputFS.exists( + path.resolve(path.dirname(cssPath), css.match(/url\(([^)]*)\)/)[1]), + ), + 'path specified in url() exists', + ); + }); + + it('should ignore url() with IE behavior specifiers', async function () { + let b = await bundle( + path.join(__dirname, '/integration/css-url-behavior/index.css'), + ); + + assertBundles(b, [ + { + name: 'index.css', + assets: ['index.css'], + }, + ]); + + let css = await outputFS.readFile(path.join(distDir, 'index.css'), 'utf8'); + + assert(css.includes('url(#default#VML)')); + }); + + it('should minify CSS when minify is set', async function () { + let b = await bundle( + path.join(__dirname, '/integration/cssnano/index.js'), + { + defaultTargetOptions: { + shouldOptimize: true, + sourceMaps: false, + }, + }, + ); + + let output = await run(b); + assert.equal(typeof output, 'function'); + assert.equal(output(), 3); + + let css = await outputFS.readFile(path.join(distDir, 'index.css'), 'utf8'); + assert(css.includes('.local')); + assert(css.includes('.index')); + + assert.equal(css.split('\n').length, 1); + }); + + it('should produce a sourcemap when sourceMaps are used', async function () { + await bundle(path.join(__dirname, '/integration/cssnano/index.js'), { + defaultTargetOptions: { + shouldOptimize: true, + }, + }); + + let css = await outputFS.readFile(path.join(distDir, 'index.css'), 'utf8'); + assert(css.includes('.local')); + assert(css.includes('.index')); + + let lines = css.trim().split('\n'); + assert.equal(lines.length, 2); + assert.equal(lines[1], '/*# sourceMappingURL=index.css.map */'); + + let map = JSON.parse( + await outputFS.readFile(path.join(distDir, 'index.css.map'), 'utf8'), + ); + assert.equal(map.file, 'index.css.map'); + assert(map.sources.includes('integration/cssnano/local.css')); + assert(map.sources.includes('integration/cssnano/index.css')); + }); + + it('should inline data-urls for text-encoded files', async () => { + await bundle(path.join(__dirname, '/integration/data-url/text.css'), { + defaultTargetOptions: { + sourceMaps: false, + }, + }); + let css = await outputFS.readFile(path.join(distDir, 'text.css'), 'utf8'); + assert.equal( + css.trim(), + `.svg-img { background-image: url("data:image/svg+xml,%3Csvg%20width%3D%22120%22%20height%3D%22120%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%3Cfilter%20id%3D%22blur-_.%21~%2a%22%3E%0A%20%20%20%20%3CfeGaussianBlur%20stdDeviation%3D%225%22%3E%3C%2FfeGaussianBlur%3E%0A%20%20%3C%2Ffilter%3E%0A%20%20%3Ccircle%20cx%3D%2260%22%20cy%3D%2260%22%20r%3D%2250%22%20fill%3D%22green%22%20filter%3D%22url%28%27%23blur-_.%21~%2a%27%29%22%3E%3C%2Fcircle%3E%0A%3C%2Fsvg%3E%0A"); }`, - ); - }); - - it('should inline data-urls for binary files', async () => { - await bundle(path.join(__dirname, '/integration/data-url/binary.css')); - let css = await outputFS.readFile( - path.join(distDir, 'binary.css'), - 'utf8', - ); - assert( - css.startsWith(`.webp-img { + ); + }); + + it('should inline data-urls for binary files', async () => { + await bundle(path.join(__dirname, '/integration/data-url/binary.css')); + let css = await outputFS.readFile(path.join(distDir, 'binary.css'), 'utf8'); + assert( + css.startsWith(`.webp-img { background-image: url("data:image/webp;base64,UklGR`), - ); - }); - - it('should remap locations in diagnostics using the input source map', async () => { - let fixture = path.join( - __dirname, - 'integration/diagnostic-sourcemap/index.scss', - ); - let code = await inputFS.readFileSync(fixture, 'utf8'); - // $FlowFixMe - await assert.rejects( - () => - bundle(fixture, { - defaultTargetOptions: { - shouldOptimize: true, - }, - }), + ); + }); + + it('should remap locations in diagnostics using the input source map', async () => { + let fixture = path.join( + __dirname, + 'integration/diagnostic-sourcemap/index.scss', + ); + let code = await inputFS.readFileSync(fixture, 'utf8'); + // $FlowFixMe + await assert.rejects( + () => + bundle(fixture, { + defaultTargetOptions: { + shouldOptimize: true, + }, + }), + { + name: 'BuildError', + diagnostics: [ { - name: 'BuildError', - diagnostics: [ + message: "Failed to resolve 'x.png' from './index.scss'", + origin: '@parcel/core', + codeFrames: [ { - message: "Failed to resolve 'x.png' from './index.scss'", - origin: '@parcel/core', - codeFrames: [ + filePath: fixture, + code, + codeHighlights: [ { - filePath: fixture, - code, - codeHighlights: [ - { - start: { - line: 5, - column: 3, - }, - end: { - line: 5, - column: 3, - }, - }, - ], + start: { + line: 5, + column: 3, + }, + end: { + line: 5, + column: 3, + }, }, ], }, - { - message: "Cannot load file './x.png' in './'.", - origin: '@parcel/resolver-default', - hints: [], - }, ], }, - ); - }); - - it('should support importing CSS from node_modules with the npm: scheme', async () => { - let b = await bundle( - path.join(__dirname, '/integration/css-node-modules/index.css'), - ); - - assertBundles(b, [ { - name: 'index.css', - assets: ['index.css', 'foo.css'], + message: "Cannot load file './x.png' in './'.", + origin: '@parcel/resolver-default', + hints: [], }, - ]); - }); + ], + }, + ); + }); - it('should support external CSS imports', async () => { - let b = await bundle( - path.join(__dirname, '/integration/css-external/a.css'), - ); + it('should support importing CSS from node_modules with the npm: scheme', async () => { + let b = await bundle( + path.join(__dirname, '/integration/css-node-modules/index.css'), + ); - assertBundles(b, [ - { - name: 'a.css', - assets: ['a.css', 'b.css'], - }, - ]); + assertBundles(b, [ + { + name: 'index.css', + assets: ['index.css', 'foo.css'], + }, + ]); + }); + + it('should support external CSS imports', async () => { + let b = await bundle( + path.join(__dirname, '/integration/css-external/a.css'), + ); + + assertBundles(b, [ + { + name: 'a.css', + assets: ['a.css', 'b.css'], + }, + ]); - let res = await outputFS.readFile(b.getBundles()[0].filePath, 'utf8'); - assert( - new RegExp(`@import "http://example.com/external.css"; + let res = await outputFS.readFile(b.getBundles()[0].filePath, 'utf8'); + assert( + new RegExp(`@import "http://example.com/external.css"; .b { color: red; }\n? .a { color: green; }`).test(res), - ); - }); - }); - } + ); + }); it('should support css nesting with @parcel/css', async function () { - let b = await originalBundle( + let b = await bundle( path.join(__dirname, '/integration/css-nesting/a.css'), { - defaultConfig: - path.dirname(require.resolve('@parcel/test-utils')) + - '/.parcelrc-css', defaultTargetOptions: { engines: {}, }, diff --git a/packages/core/test-utils/src/.parcelrc-css b/packages/core/test-utils/src/.parcelrc-css deleted file mode 100644 index 1d8a4120038..00000000000 --- a/packages/core/test-utils/src/.parcelrc-css +++ /dev/null @@ -1,10 +0,0 @@ -{ - "extends": "@parcel/config-default", - "transformers": { - "*.css": ["@parcel/transformer-postcss", "@parcel/transformer-css-experimental"] - }, - "optimizers": { - "*.css": ["@parcel/optimizer-css"] - }, - "reporters": [] -} diff --git a/packages/optimizers/css/package.json b/packages/optimizers/css/package.json index 183963682a3..873bf247fd1 100644 --- a/packages/optimizers/css/package.json +++ b/packages/optimizers/css/package.json @@ -20,7 +20,8 @@ "parcel": "^2.3.2" }, "dependencies": { - "@parcel/css": "^1.0.3", + "@parcel/css": "^1.6.0", + "@parcel/diagnostic": "2.3.2", "@parcel/plugin": "2.3.2", "@parcel/source-map": "^2.0.0", "@parcel/utils": "2.3.2", diff --git a/packages/optimizers/css/src/CSSOptimizer.js b/packages/optimizers/css/src/CSSOptimizer.js index d6b415eb2c6..7e4507a609a 100644 --- a/packages/optimizers/css/src/CSSOptimizer.js +++ b/packages/optimizers/css/src/CSSOptimizer.js @@ -10,8 +10,54 @@ import { import {blobToBuffer} from '@parcel/utils'; import browserslist from 'browserslist'; import nullthrows from 'nullthrows'; +import path from 'path'; +import {md, generateJSONCodeHighlights} from '@parcel/diagnostic'; export default (new Optimizer({ + async loadConfig({config, logger, options}) { + const configFile = await config.getConfig( + ['.cssnanorc', 'cssnano.config.json', 'cssnano.config.js'], + { + packageKey: 'cssnano', + }, + ); + if (configFile) { + let filename = path.basename(configFile.filePath); + let codeHighlights; + let message; + if (filename === 'package.json') { + message = md` +Parcel\'s default CSS minifer changed from cssnano to @parcel/css, but a "cssnano" key was found in **package.json**. Either remove this configuration, or configure Parcel to use @parcel/optimizer-cssnano instead. + `; + let contents = await options.inputFS.readFile( + configFile.filePath, + 'utf8', + ); + codeHighlights = generateJSONCodeHighlights(contents, [ + {key: '/cssnano', type: 'key'}, + ]); + } else { + message = md`Parcel\'s default CSS minifer changed from cssnano to @parcel/css, but a __${filename}__ config file was found. Either remove this config file, or configure Parcel to use @parcel/optimizer-cssnano instead.`; + codeHighlights = [ + { + start: {line: 1, column: 1}, + end: {line: 1, column: 1}, + }, + ]; + } + + logger.warn({ + message, + documentationURL: 'https://parceljs.org/languages/css/#minification', + codeFrames: [ + { + filePath: configFile.filePath, + codeHighlights, + }, + ], + }); + } + }, async optimize({ bundle, bundleGraph, diff --git a/packages/transformers/css-experimental/package.json b/packages/transformers/css-experimental/package.json deleted file mode 100644 index 13594a55d33..00000000000 --- a/packages/transformers/css-experimental/package.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "name": "@parcel/transformer-css-experimental", - "version": "2.3.2", - "license": "MIT", - "publishConfig": { - "access": "public" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - }, - "repository": { - "type": "git", - "url": "https://github.com/parcel-bundler/parcel.git" - }, - "main": "lib/CSSTransformer.js", - "source": "src/CSSTransformer.js", - "engines": { - "node": ">= 12.0.0", - "parcel": "^2.3.2" - }, - "dependencies": { - "@parcel/css": "^1.0.3", - "@parcel/plugin": "2.3.2", - "@parcel/source-map": "^2.0.0", - "@parcel/utils": "2.3.2", - "browserslist": "^4.6.6", - "nullthrows": "^1.1.1" - } -} diff --git a/packages/transformers/css-experimental/src/CSSTransformer.js b/packages/transformers/css-experimental/src/CSSTransformer.js deleted file mode 100644 index 58f1638f331..00000000000 --- a/packages/transformers/css-experimental/src/CSSTransformer.js +++ /dev/null @@ -1,232 +0,0 @@ -// @flow strict-local - -import path from 'path'; -import SourceMap from '@parcel/source-map'; -import {Transformer} from '@parcel/plugin'; -import { - transform, - transformStyleAttribute, - browserslistToTargets, -} from '@parcel/css'; -import {remapSourceLocation} from '@parcel/utils'; -import browserslist from 'browserslist'; -import nullthrows from 'nullthrows'; - -export default (new Transformer({ - async loadConfig({config, options}) { - let conf = await config.getConfigFrom(options.projectRoot + '/index', [], { - packageKey: '@parcel/transformer-css', - }); - return conf?.contents; - }, - async transform({asset, config, options}) { - let [code, originalMap] = await Promise.all([ - asset.getBuffer(), - asset.getMap(), - ]); - - let targets = getTargets(asset.env.engines.browsers); - let res; - if (asset.meta.type === 'attr') { - res = transformStyleAttribute({ - code, - analyzeDependencies: true, - targets, - }); - } else { - res = transform({ - filename: path.relative(options.projectRoot, asset.filePath), - code, - cssModules: - config?.cssModules ?? - (asset.meta.cssModulesCompiled !== true && - /\.module\./.test(asset.filePath)), - analyzeDependencies: asset.meta.hasDependencies !== false, - sourceMap: !!asset.env.sourceMap, - drafts: config?.drafts, - pseudoClasses: config?.pseudoClasses, - targets, - }); - } - - asset.setBuffer(res.code); - - if (res.map != null) { - let vlqMap = JSON.parse(res.map.toString()); - let map = new SourceMap(options.projectRoot); - map.addVLQMap(vlqMap); - - if (originalMap) { - map.extends(originalMap); - } - - asset.setMap(map); - } - - if (res.dependencies) { - for (let dep of res.dependencies) { - let loc = dep.loc; - if (originalMap) { - loc = remapSourceLocation(loc, originalMap); - } - - if (dep.type === 'import') { - asset.addDependency({ - specifier: dep.url, - specifierType: 'url', - loc, - meta: { - // For the glob resolver to distinguish between `@import` and other URL dependencies. - isCSSImport: true, - media: dep.media, - }, - symbols: new Map([['*', {local: '*', isWeak: true, loc}]]), - }); - } else if (dep.type === 'url') { - asset.addURLDependency(dep.url, { - loc, - meta: { - placeholder: dep.placeholder, - }, - }); - } - } - } - - let assets = [asset]; - - if (res.exports != null) { - let exports = res.exports; - asset.symbols.ensure(); - asset.symbols.set('default', 'default'); - - let dependencies = new Map(); - let selfReferences = new Set(); - let locals = new Map(); - let c = 0; - let depjs = ''; - let js = ''; - - let jsDeps = []; - for (let dep of asset.getDependencies()) { - if (dep.priority === 'sync') { - // TODO: Figure out how to treeshake this - let d = `dep_$${c++}`; - depjs += `import * as ${d} from ${JSON.stringify(dep.specifier)};\n`; - depjs += `for (let key in ${d}) { if (key in module.exports) module.exports[key] += ' ' + ${d}[key]; else module.exports[key] = ${d}[key]; }\n`; - } - } - - for (let key in exports) { - locals.set(exports[key].name, key); - } - - let seen = new Set(); - let add = key => { - if (seen.has(key)) { - return; - } - seen.add(key); - - let e = exports[key]; - let s = `module.exports[${JSON.stringify(key)}] = \`${e.name}`; - - if (e.isReferenced) { - selfReferences.add(e.name); - } - - for (let ref of e.composes) { - s += ' '; - if (ref.type === 'local') { - add(nullthrows(locals.get(ref.name))); - s += - '${' + - `module.exports[${JSON.stringify( - nullthrows(locals.get(ref.name)), - )}]` + - '}'; - } else if (ref.type === 'global') { - s += ref.name; - } else if (ref.type === 'dependency') { - let d = dependencies.get(ref.specifier); - if (d == null) { - d = `dep_${c++}`; - depjs += `import * as ${d} from ${JSON.stringify( - ref.specifier, - )};\n`; - dependencies.set(ref.specifier, d); - - asset.addDependency({ - specifier: ref.specifier, - specifierType: 'url', - }); - } - s += '${' + `${d}[${JSON.stringify(ref.name)}]` + '}'; - } - } - - s += '`;\n'; - js += s; - }; - - for (let key in exports) { - asset.symbols.set(key, exports[key].name); - add(key); - } - - assets.push({ - type: 'js', - content: depjs + js, - dependencies: jsDeps, - env: asset.env, - }); - - if (selfReferences.size > 0) { - asset.addDependency({ - specifier: `./${path.basename(asset.filePath)}`, - specifierType: 'url', - symbols: new Map( - [...locals] - .filter(([local]) => selfReferences.has(local)) - .map(([local, exported]) => [ - exported, - {local, isWeak: false, loc: null}, - ]), - ), - }); - } - } - - // Normalize the asset's environment so that properties that only affect JS don't cause CSS to be duplicated. - // For example, with ESModule and CommonJS targets, only a single shared CSS bundle should be produced. - asset.setEnvironment({ - context: 'browser', - engines: { - browsers: asset.env.engines.browsers, - }, - shouldOptimize: asset.env.shouldOptimize, - shouldScopeHoist: asset.env.shouldScopeHoist, - sourceMap: asset.env.sourceMap, - }); - - return assets; - }, -}): Transformer); - -let cache = new Map(); - -function getTargets(browsers) { - if (browsers == null) { - return undefined; - } - - let cached = cache.get(browsers); - if (cached != null) { - return cached; - } - - let targets = browserslistToTargets(browserslist(browsers)); - - cache.set(browsers, targets); - return targets; -} diff --git a/packages/transformers/css/package.json b/packages/transformers/css/package.json index f5c0e1e7e37..43ac3330841 100644 --- a/packages/transformers/css/package.json +++ b/packages/transformers/css/package.json @@ -20,16 +20,11 @@ "parcel": "^2.3.2" }, "dependencies": { - "@parcel/hash": "2.3.2", + "@parcel/css": "^1.6.0", "@parcel/plugin": "2.3.2", "@parcel/source-map": "^2.0.0", "@parcel/utils": "2.3.2", - "nullthrows": "^1.1.1", - "postcss": "^8.4.5", - "postcss-value-parser": "^4.2.0", - "semver": "^5.7.1" - }, - "devDependencies": { - "postcss-modules": "^4.3.0" + "browserslist": "^4.6.6", + "nullthrows": "^1.1.1" } } diff --git a/packages/transformers/css/src/CSSTransformer.js b/packages/transformers/css/src/CSSTransformer.js index 8aff146b660..58f1638f331 100644 --- a/packages/transformers/css/src/CSSTransformer.js +++ b/packages/transformers/css/src/CSSTransformer.js @@ -1,376 +1,232 @@ -// @flow +// @flow strict-local -import type {Root} from 'postcss'; -import type {FilePath, MutableAsset, PluginOptions} from '@parcel/types'; - -import {hashString} from '@parcel/hash'; +import path from 'path'; import SourceMap from '@parcel/source-map'; import {Transformer} from '@parcel/plugin'; -import {createDependencyLocation, remapSourceLocation} from '@parcel/utils'; -import postcss from 'postcss'; +import { + transform, + transformStyleAttribute, + browserslistToTargets, +} from '@parcel/css'; +import {remapSourceLocation} from '@parcel/utils'; +import browserslist from 'browserslist'; import nullthrows from 'nullthrows'; -import valueParser from 'postcss-value-parser'; -import semver from 'semver'; -import path from 'path'; - -const URL_RE = /url\s*\(/; -const IMPORT_RE = /@import/; -const COMPOSES_RE = /composes:.+from\s*("|').*("|')\s*;?/; -const FROM_IMPORT_RE = /.+from\s*(?:"|')(.*)(?:"|')\s*;?/; -const MODULE_BY_NAME_RE = /\.module\./; - -function canHaveDependencies(filePath: FilePath, code: string) { - return !/\.css$/.test(filePath) || IMPORT_RE.test(code) || URL_RE.test(code); -} export default (new Transformer({ - canReuseAST({ast}) { - return ast.type === 'postcss' && semver.satisfies(ast.version, '^8.2.1'); + async loadConfig({config, options}) { + let conf = await config.getConfigFrom(options.projectRoot + '/index', [], { + packageKey: '@parcel/transformer-css', + }); + return conf?.contents; }, - - async parse({asset}) { - // This is set by other transformers (e.g. Stylus) to indicate that it has already processed - // all dependencies, and that the CSS transformer can skip this asset completely. This is - // required because when stylus processes e.g. url() it replaces them with a dependency id - // to be filled in later. When the CSS transformer runs, it would pick that up and try to - // resolve a dependency for the id which obviously doesn't exist. Also, it's faster to do - // it this way since the resulting CSS doesn't need to be re-parsed. - let isCSSModule = - asset.meta.cssModulesCompiled !== true && - MODULE_BY_NAME_RE.test(asset.filePath); - if (asset.meta.hasDependencies === false && !isCSSModule) { - return null; + async transform({asset, config, options}) { + let [code, originalMap] = await Promise.all([ + asset.getBuffer(), + asset.getMap(), + ]); + + let targets = getTargets(asset.env.engines.browsers); + let res; + if (asset.meta.type === 'attr') { + res = transformStyleAttribute({ + code, + analyzeDependencies: true, + targets, + }); + } else { + res = transform({ + filename: path.relative(options.projectRoot, asset.filePath), + code, + cssModules: + config?.cssModules ?? + (asset.meta.cssModulesCompiled !== true && + /\.module\./.test(asset.filePath)), + analyzeDependencies: asset.meta.hasDependencies !== false, + sourceMap: !!asset.env.sourceMap, + drafts: config?.drafts, + pseudoClasses: config?.pseudoClasses, + targets, + }); } - let code = await asset.getCode(); - if ( - code != null && - !canHaveDependencies(asset.filePath, code) && - !isCSSModule - ) { - return null; - } + asset.setBuffer(res.code); - return { - type: 'postcss', - version: '8.2.1', - program: postcss - .parse(code, { - from: asset.filePath, - }) - .toJSON(), - }; - }, + if (res.map != null) { + let vlqMap = JSON.parse(res.map.toString()); + let map = new SourceMap(options.projectRoot); + map.addVLQMap(vlqMap); - async transform({asset, resolve, options}) { - // Normalize the asset's environment so that properties that only affect JS don't cause CSS to be duplicated. - // For example, with ESModule and CommonJS targets, only a single shared CSS bundle should be produced. - let env = asset.env; - asset.setEnvironment({ - context: 'browser', - engines: { - browsers: asset.env.engines.browsers, - }, - shouldOptimize: asset.env.shouldOptimize, - sourceMap: asset.env.sourceMap, - }); - - let isCSSModule = - asset.meta.cssModulesCompiled !== true && - MODULE_BY_NAME_RE.test(asset.filePath); + if (originalMap) { + map.extends(originalMap); + } - // Check for `hasDependencies` being false here as well, as it's possible - // another transformer (such as PostCSSTransformer) has already parsed an - // ast and CSSTransformer's parse was never called. - let ast = await asset.getAST(); - if (!ast || (asset.meta.hasDependencies === false && !isCSSModule)) { - return [asset]; + asset.setMap(map); } - let program: Root = postcss.fromJSON(ast.program); - let assets = [asset]; - if (isCSSModule) { - assets = await compileCSSModules(asset, env, program, resolve, options); - } + if (res.dependencies) { + for (let dep of res.dependencies) { + let loc = dep.loc; + if (originalMap) { + loc = remapSourceLocation(loc, originalMap); + } - if (asset.meta.hasDependencies === false) { - return assets; + if (dep.type === 'import') { + asset.addDependency({ + specifier: dep.url, + specifierType: 'url', + loc, + meta: { + // For the glob resolver to distinguish between `@import` and other URL dependencies. + isCSSImport: true, + media: dep.media, + }, + symbols: new Map([['*', {local: '*', isWeak: true, loc}]]), + }); + } else if (dep.type === 'url') { + asset.addURLDependency(dep.url, { + loc, + meta: { + placeholder: dep.placeholder, + }, + }); + } + } } - let originalSourceMap = await asset.getMap(); - let createLoc = (start, specifier, lineOffset, colOffset, o) => { - let loc = createDependencyLocation( - start, - specifier, - lineOffset, - colOffset, - o, - ); - if (originalSourceMap) { - loc = remapSourceLocation(loc, originalSourceMap); - } - return loc; - }; + let assets = [asset]; - let isDirty = false; - program.walkAtRules('import', rule => { - let params = valueParser(rule.params); - let [name, ...media] = params.nodes; - let specifier; - if ( - name.type === 'function' && - name.value === 'url' && - name.nodes.length - ) { - name = name.nodes[0]; + if (res.exports != null) { + let exports = res.exports; + asset.symbols.ensure(); + asset.symbols.set('default', 'default'); + + let dependencies = new Map(); + let selfReferences = new Set(); + let locals = new Map(); + let c = 0; + let depjs = ''; + let js = ''; + + let jsDeps = []; + for (let dep of asset.getDependencies()) { + if (dep.priority === 'sync') { + // TODO: Figure out how to treeshake this + let d = `dep_$${c++}`; + depjs += `import * as ${d} from ${JSON.stringify(dep.specifier)};\n`; + depjs += `for (let key in ${d}) { if (key in module.exports) module.exports[key] += ' ' + ${d}[key]; else module.exports[key] = ${d}[key]; }\n`; + } } - specifier = name.value; - - if (!specifier) { - throw new Error('Could not find import name for ' + String(rule)); + for (let key in exports) { + locals.set(exports[key].name, key); } - // If this came from an inline