diff --git a/flow-libs/postcss.js.flow b/flow-libs/postcss.js.flow index 82e12c94179..065edf416b5 100644 --- a/flow-libs/postcss.js.flow +++ b/flow-libs/postcss.js.flow @@ -24,20 +24,23 @@ declare module 'postcss' { declare interface Root extends Container {} declare class Processor { - process(css: string | Result, opts?: processOptions): Promise; + process( + css: string | Result | Root, + opts?: ProcessOptions, + ): Promise; } - declare type ProcessOptions = {| - from?: string, - to?: string, - map?: MapOptions, - parser?: parser, - stringifier?: stringifier, - syntax?: {| + declare type ProcessOptions = $Shape<{| + from: string, + to: string, + map: MapOptions, + parser: parser, + stringifier: stringifier, + syntax: {| parser: parser, stringifier: stringifier, |}, - |}; + |}>; declare type MapOptions = {| inline?: boolean, diff --git a/packages/core/integration-tests/test/css.js b/packages/core/integration-tests/test/css.js index c42859a98a4..b11ca128e15 100644 --- a/packages/core/integration-tests/test/css.js +++ b/packages/core/integration-tests/test/css.js @@ -264,6 +264,7 @@ describe('css', () => { path.join(__dirname, '/integration/cssnano/index.js'), { minify: true, + sourceMaps: false, }, ); @@ -275,19 +276,43 @@ describe('css', () => { assert(css.includes('.local')); assert(css.includes('.index')); - // TODO: Make this `2` when a `sourceMappingURL` is added 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'), { + minify: 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.equal(map.mappings, 'AAAA,OACA,WACA,CCFA,OACA,SACA'); + assert.deepEqual(map.sources, [ + 'integration/cssnano/local.css', + 'integration/cssnano/index.css', + ]); + }); + it('should inline data-urls for text-encoded files', async () => { - await bundle(path.join(__dirname, '/integration/data-url/text.css')); + await bundle(path.join(__dirname, '/integration/data-url/text.css'), { + sourceMaps: false, + }); let css = await outputFS.readFile(path.join(distDir, 'text.css'), 'utf8'); assert.equal( - css, + css.trim(), `.svg-img { background-image: url('data:image/svg+xml,%3Csvg%3E%0A%0A%3C%2Fsvg%3E%0A'); -} -`, +}`, ); }); diff --git a/packages/optimizers/cssnano/package.json b/packages/optimizers/cssnano/package.json index 2dfa58e359e..eca573bea25 100644 --- a/packages/optimizers/cssnano/package.json +++ b/packages/optimizers/cssnano/package.json @@ -16,6 +16,7 @@ }, "dependencies": { "@parcel/plugin": "^2.0.0-alpha.3.1", + "@parcel/source-map": "^2.0.0-alpha.4.6", "cssnano": "^4.1.10", "postcss": "^7.0.5" } diff --git a/packages/optimizers/cssnano/src/CSSNanoOptimizer.js b/packages/optimizers/cssnano/src/CSSNanoOptimizer.js index 5ff34b2f03a..f39cc7253f8 100644 --- a/packages/optimizers/cssnano/src/CSSNanoOptimizer.js +++ b/packages/optimizers/cssnano/src/CSSNanoOptimizer.js @@ -1,32 +1,58 @@ // @flow strict-local +import SourceMap from '@parcel/source-map'; import {Optimizer} from '@parcel/plugin'; import postcss from 'postcss'; // flowlint-next-line untyped-import:off import cssnano from 'cssnano'; export default new Optimizer({ - async optimize({bundle, contents, map}) { + async optimize({ + bundle, + contents: prevContents, + getSourceMapReference, + map: prevMap, + options, + }) { if (!bundle.env.minify) { - return {contents, map}; + return {contents: prevContents, map: prevMap}; } - if (typeof contents !== 'string') { + if (typeof prevContents !== 'string') { throw new Error( 'CSSNanoOptimizer: Only string contents are currently supported', ); } - const results = await postcss([cssnano]).process(contents, { - from: bundle.filePath, - map: {inline: false}, + const result = await postcss([cssnano]).process(prevContents, { + // Suppress postcss's warning about a missing `from` property. In this + // case, the input map contains all of the sources. + from: undefined, + map: { + annotation: false, + inline: false, + prev: prevMap ? await prevMap.stringify({}) : null, + }, }); - console.log('MAP', results.map.constructor); + let map; + if (result.map != null) { + map = new SourceMap(); + let {mappings, sources, names} = result.map.toJSON(); + map.addRawMappings(mappings, sources, names); + } + + let contents = result.css; + if (options.sourceMaps) { + let reference = await getSourceMapReference(map); + if (reference != null) { + contents += '\n' + '/*# sourceMappingURL=' + reference + ' */\n'; + } + } return { - contents: results.css, - map: results.map, + contents, + map, }; }, }); diff --git a/packages/packagers/css/package.json b/packages/packagers/css/package.json index 1c684ea6e8c..73ff00874ae 100644 --- a/packages/packagers/css/package.json +++ b/packages/packagers/css/package.json @@ -17,6 +17,7 @@ }, "dependencies": { "@parcel/plugin": "^2.0.0-alpha.3.1", + "@parcel/source-map": "^2.0.0-alpha.4.6", "@parcel/utils": "^2.0.0-alpha.3.1" } } diff --git a/packages/packagers/css/src/CSSPackager.js b/packages/packagers/css/src/CSSPackager.js index 7814d563fa2..9a8fff57913 100644 --- a/packages/packagers/css/src/CSSPackager.js +++ b/packages/packagers/css/src/CSSPackager.js @@ -1,14 +1,23 @@ // @flow +import path from 'path'; +import SourceMap from '@parcel/source-map'; import {Packager} from '@parcel/plugin'; import { PromiseQueue, + countLines, replaceInlineReferences, replaceURLReferences, } from '@parcel/utils'; export default new Packager({ - async package({bundle, bundleGraph, getInlineBundleContents}) { + async package({ + bundle, + bundleGraph, + getInlineBundleContents, + options, + getSourceMapReference, + }) { let queue = new PromiseQueue({maxConcurrent: 32}); bundle.traverseAssets({ exit: asset => { @@ -25,31 +34,68 @@ export default new Packager({ } queue.add(() => - asset.getCode().then((css: string) => { - if (media.length) { - return `@media ${media.join(', ')} {\n${css.trim()}\n}\n`; - } + Promise.all([ + asset, + asset.getCode().then((css: string) => { + if (media.length) { + return `@media ${media.join(', ')} {\n${css}\n}\n`; + } - return css; - }), + return css; + }), + options.sourceMaps && asset.getMapBuffer(), + ]), ); }, }); let outputs = await queue.run(); + let contents = ''; + let map = new SourceMap(); + let lineOffset = 0; + for (let [asset, code, mapBuffer] of outputs) { + contents += code + '\n'; + if (options.sourceMaps) { + if (mapBuffer) { + map.addBufferMappings(mapBuffer, lineOffset); + } else { + map.addEmptyMap( + path + .relative(options.projectRoot, asset.filePath) + .replace(/\\+/g, '/'), + code, + lineOffset, + ); + } + + lineOffset += countLines(code); + } + } + + if (options.sourceMaps) { + let reference = await getSourceMapReference(map); + if (reference != null) { + contents += '/*# sourceMappingURL=' + reference + ' */\n'; + } + } + + ({contents, map} = replaceURLReferences({ + bundle, + bundleGraph, + contents, + map, + })); + return replaceInlineReferences({ bundle, bundleGraph, - contents: replaceURLReferences({ - bundle, - bundleGraph, - contents: outputs.map(output => output).join('\n'), - }).contents, + contents, getInlineBundleContents, getInlineReplacement: (dep, inlineType, contents) => ({ from: dep.id, to: contents, }), + map, }); }, }); diff --git a/packages/packagers/js/src/JSPackager.js b/packages/packagers/js/src/JSPackager.js index 809bc1bd5f2..e486ea3c88e 100644 --- a/packages/packagers/js/src/JSPackager.js +++ b/packages/packagers/js/src/JSPackager.js @@ -136,7 +136,6 @@ export default new Packager({ wrapped += ']'; if (options.sourceMaps) { - let lineCount = countLines(output); if (mapBuffer) { map.addBufferMappings(mapBuffer, lineOffset); } else { @@ -149,7 +148,7 @@ export default new Packager({ ); } - lineOffset += lineCount + 1; + lineOffset += countLines(output) + 1; } i++; } diff --git a/packages/transformers/css/package.json b/packages/transformers/css/package.json index 0bcd58baaa5..cc1e58b1585 100644 --- a/packages/transformers/css/package.json +++ b/packages/transformers/css/package.json @@ -17,6 +17,7 @@ }, "dependencies": { "@parcel/plugin": "^2.0.0-alpha.3.1", + "@parcel/source-map": "^2.0.0-alpha.4.6", "@parcel/utils": "^2.0.0-alpha.3.1", "postcss": "^7.0.5", "postcss-value-parser": "^3.3.1", diff --git a/packages/transformers/css/src/CSSTransformer.js b/packages/transformers/css/src/CSSTransformer.js index b1a6982eaa2..0dd88911e1f 100644 --- a/packages/transformers/css/src/CSSTransformer.js +++ b/packages/transformers/css/src/CSSTransformer.js @@ -3,6 +3,7 @@ import type {FilePath} from '@parcel/types'; import type {Container, Node} from 'postcss'; +import SourceMap from '@parcel/source-map'; import {Transformer} from '@parcel/plugin'; import {createDependencyLocation, isURL} from '@parcel/utils'; import postcss from 'postcss'; @@ -166,7 +167,7 @@ export default new Transformer({ return [asset]; }, - generate({ast}) { + async generate({asset, ast}) { let root = ast.program; // $FlowFixMe @@ -192,13 +193,27 @@ export default new Transformer({ root.each((node, index) => convert(root, node, index)); } - let code = ''; - postcss.stringify(root, c => { - code += c; + let result = await postcss().process(root, { + from: asset.filePath, + map: { + annotation: false, + inline: false, + }, + // Pass postcss's own stringifier to it to silence its warning + // as we don't want to perform any transformations -- only generate + stringifier: postcss.stringify, }); + let map; + if (result.map != null) { + map = new SourceMap(); + let {mappings, sources, names} = result.map.toJSON(); + map.addRawMappings(mappings, sources, names); + } + return { - content: code, + content: result.css, + map, }; }, });