From 8deb85d95e19ab39a5143489eabfbca13cc59663 Mon Sep 17 00:00:00 2001 From: Niklas Mischkulnig Date: Tue, 5 Mar 2019 19:11:36 +0100 Subject: [PATCH] CSS/SASS/LESS sourcemaps (#2489) --- packages/core/integration-tests/test/css.js | 79 ++- packages/core/integration-tests/test/glob.js | 6 +- packages/core/integration-tests/test/html.js | 36 +- .../sourcemap-css-existing/style.css | 5 + .../sourcemap-css-existing/test/library.css | 2 + .../test/library.css.map | 1 + .../test/library.raw.scss | 13 + .../sourcemap-css-import/another-style.css | 3 + .../sourcemap-css-import/other-style.css | 5 + .../sourcemap-css-import/style.css | 8 + .../test/integration/sourcemap-css/style.css | 4 + .../integration/sourcemap-less/style.less | 5 + .../sourcemap-sass-imported/other.scss | 5 + .../sourcemap-sass-imported/style.css | 5 + .../integration/sourcemap-sass/style.scss | 5 + packages/core/integration-tests/test/less.js | 42 +- .../core/integration-tests/test/parser.js | 6 +- packages/core/integration-tests/test/pug.js | 6 +- packages/core/integration-tests/test/sass.js | 49 +- .../core/integration-tests/test/sourcemaps.js | 628 +++++++++++++++++- .../core/integration-tests/test/stylus.js | 30 +- packages/core/parcel-bundler/package.json | 2 +- packages/core/parcel-bundler/src/Asset.js | 1 + packages/core/parcel-bundler/src/Bundle.js | 30 +- packages/core/parcel-bundler/src/Bundler.js | 1 + packages/core/parcel-bundler/src/FSCache.js | 9 +- packages/core/parcel-bundler/src/Pipeline.js | 14 + packages/core/parcel-bundler/src/SourceMap.js | 1 - .../parcel-bundler/src/assets/CSSAsset.js | 83 ++- .../core/parcel-bundler/src/assets/JSAsset.js | 98 +-- .../parcel-bundler/src/assets/LESSAsset.js | 13 +- .../parcel-bundler/src/assets/SASSAsset.js | 14 +- .../src/assets/TypeScriptAsset.js | 2 +- .../parcel-bundler/src/assets/VueAsset.js | 10 +- .../src/packagers/CSSPackager.js | 47 +- .../src/packagers/SourceMapPackager.js | 14 +- .../parcel-bundler/src/transforms/postcss.js | 5 + .../parcel-bundler/src/transforms/terser.js | 2 +- .../parcel-bundler/src/utils/loadSourceMap.js | 75 +++ packages/core/test-utils/src/utils.js | 18 +- yarn.lock | 25 + 41 files changed, 1198 insertions(+), 209 deletions(-) create mode 100644 packages/core/integration-tests/test/integration/sourcemap-css-existing/style.css create mode 100644 packages/core/integration-tests/test/integration/sourcemap-css-existing/test/library.css create mode 100644 packages/core/integration-tests/test/integration/sourcemap-css-existing/test/library.css.map create mode 100644 packages/core/integration-tests/test/integration/sourcemap-css-existing/test/library.raw.scss create mode 100644 packages/core/integration-tests/test/integration/sourcemap-css-import/another-style.css create mode 100644 packages/core/integration-tests/test/integration/sourcemap-css-import/other-style.css create mode 100644 packages/core/integration-tests/test/integration/sourcemap-css-import/style.css create mode 100644 packages/core/integration-tests/test/integration/sourcemap-css/style.css create mode 100644 packages/core/integration-tests/test/integration/sourcemap-less/style.less create mode 100644 packages/core/integration-tests/test/integration/sourcemap-sass-imported/other.scss create mode 100644 packages/core/integration-tests/test/integration/sourcemap-sass-imported/style.css create mode 100644 packages/core/integration-tests/test/integration/sourcemap-sass/style.scss create mode 100644 packages/core/parcel-bundler/src/utils/loadSourceMap.js diff --git a/packages/core/integration-tests/test/css.js b/packages/core/integration-tests/test/css.js index 4203d105755..d55db7e807f 100644 --- a/packages/core/integration-tests/test/css.js +++ b/packages/core/integration-tests/test/css.js @@ -23,7 +23,11 @@ describe('css', function() { { name: 'index.css', assets: ['index.css', 'local.css'], - childBundles: [] + childBundles: [ + { + type: 'map' + } + ] } ] }); @@ -50,21 +54,30 @@ describe('css', function() { ], childBundles: [ { + type: 'css', name: 'index.css', assets: ['index.css'], - childBundles: [] + childBundles: [ + { + type: 'map' + } + ] }, { - type: 'map' + name: 'index.js.map' }, { type: 'js', - assets: ['local.js', 'local.css'], + assets: ['local.css', 'local.js'], childBundles: [ { type: 'css', assets: ['local.css'], - childBundles: [] + childBundles: [ + { + type: 'map' + } + ] }, { type: 'map' @@ -91,7 +104,11 @@ describe('css', function() { { name: 'index.css', assets: ['index.css', 'other.css', 'local.css'], - childBundles: [] + childBundles: [ + { + type: 'map' + } + ] }, { name: 'index.js.map', @@ -124,7 +141,11 @@ describe('css', function() { { name: 'index.css', assets: ['index.css'], - childBundles: [] + childBundles: [ + { + type: 'map' + } + ] }, { type: 'map' @@ -179,7 +200,11 @@ describe('css', function() { { name: 'index.css', assets: ['index.css'], - childBundles: [] + childBundles: [ + { + type: 'map' + } + ] }, { type: 'map' @@ -277,7 +302,11 @@ describe('css', function() { { name: 'index.css', assets: ['index.css'], - childBundles: [] + childBundles: [ + { + type: 'map' + } + ] }, { type: 'map' @@ -325,7 +354,11 @@ describe('css', function() { { name: 'index.css', assets: ['composes-1.css', 'composes-2.css', 'mixins.css'], - childBundles: [] + childBundles: [ + { + type: 'map' + } + ] }, { type: 'map' @@ -383,7 +416,11 @@ describe('css', function() { { name: 'index2.css', assets: ['composes-3.css', 'mixins.scss'], - childBundles: [] + childBundles: [ + { + type: 'map' + } + ] }, { type: 'map' @@ -418,7 +455,11 @@ describe('css', function() { { name: 'index3.css', assets: ['composes-4.css', 'mixins.css'], - childBundles: [] + childBundles: [ + { + type: 'map' + } + ] }, { type: 'map' @@ -458,7 +499,11 @@ describe('css', function() { { name: 'index4.css', assets: ['composes-5.css', 'mixins-intermediate.css', 'mixins.css'], - childBundles: [] + childBundles: [ + { + type: 'map' + } + ] }, { type: 'map' @@ -496,7 +541,11 @@ describe('css', function() { { name: 'index5.css', assets: ['composes-6.css', 'mixins.css'], - childBundles: [] + childBundles: [ + { + type: 'map' + } + ] }, { type: 'map' @@ -532,7 +581,7 @@ describe('css', function() { ); assert(css.includes('.local')); assert(css.includes('.index')); - assert(!css.includes('\n')); + assert.equal(css.split('\n').length, 2); // sourceMappingURL }); it('should automatically install postcss plugins with npm if needed', async function() { diff --git a/packages/core/integration-tests/test/glob.js b/packages/core/integration-tests/test/glob.js index 7a7253408c9..96b4582bc29 100644 --- a/packages/core/integration-tests/test/glob.js +++ b/packages/core/integration-tests/test/glob.js @@ -54,7 +54,11 @@ describe('glob', function() { { name: 'index.css', assets: ['index.css', 'other.css', 'local.css'], - childBundles: [] + childBundles: [ + { + type: 'map' + } + ] }, { type: 'map' diff --git a/packages/core/integration-tests/test/html.js b/packages/core/integration-tests/test/html.js index 31ce79fd64f..30921296ef8 100644 --- a/packages/core/integration-tests/test/html.js +++ b/packages/core/integration-tests/test/html.js @@ -24,7 +24,11 @@ describe('html', function() { { type: 'css', assets: ['index.css'], - childBundles: [] + childBundles: [ + { + type: 'map' + } + ] }, { type: 'html', @@ -151,7 +155,11 @@ describe('html', function() { { type: 'css', assets: ['index.css'], - childBundles: [] + childBundles: [ + { + type: 'map' + } + ] }, { type: 'map' @@ -185,7 +193,11 @@ describe('html', function() { { type: 'css', assets: ['index.css'], - childBundles: [] + childBundles: [ + { + type: 'map' + } + ] }, { type: 'map' @@ -219,9 +231,6 @@ describe('html', function() { type: 'css', assets: ['index.css'], childBundles: [ - { - type: 'map' - }, { type: 'js', assets: [ @@ -230,7 +239,14 @@ describe('html', function() { 'css-loader.js', 'hmr-runtime.js' ], - childBundles: [] + childBundles: [ + { + type: 'map' + } + ] + }, + { + type: 'map' } ] } @@ -257,7 +273,11 @@ describe('html', function() { { type: 'css', assets: ['index.css'], - childBundles: [] + childBundles: [ + { + type: 'map' + } + ] }, { type: 'map' diff --git a/packages/core/integration-tests/test/integration/sourcemap-css-existing/style.css b/packages/core/integration-tests/test/integration/sourcemap-css-existing/style.css new file mode 100644 index 00000000000..191ad2bf43d --- /dev/null +++ b/packages/core/integration-tests/test/integration/sourcemap-css-existing/style.css @@ -0,0 +1,5 @@ +@import "./test/library.css"; + +main { + display: none; +} diff --git a/packages/core/integration-tests/test/integration/sourcemap-css-existing/test/library.css b/packages/core/integration-tests/test/integration/sourcemap-css-existing/test/library.css new file mode 100644 index 00000000000..b13111e24ed --- /dev/null +++ b/packages/core/integration-tests/test/integration/sourcemap-css-existing/test/library.css @@ -0,0 +1,2 @@ +body{font:100% Helvetica,sans-serif;color:#333}body div{background-color:red;width:100px;height:100px} +/*# sourceMappingURL=library.css.map*/ \ No newline at end of file diff --git a/packages/core/integration-tests/test/integration/sourcemap-css-existing/test/library.css.map b/packages/core/integration-tests/test/integration/sourcemap-css-existing/test/library.css.map new file mode 100644 index 00000000000..579f260dfcf --- /dev/null +++ b/packages/core/integration-tests/test/integration/sourcemap-css-existing/test/library.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["library.scss"],"names":[],"mappings":"AAGA,KACE,+BACA,WAEA,SACC,qBACA,YACA","file":"library.css.map","sourceRoot":".","sourcesContent":["$font-stack: Helvetica, sans-serif;\n$primary-color: #333;\n\nbody {\n font: 100% $font-stack;\n color: $primary-color;\n\n div {\n \tbackground-color: red;\n \twidth: 100px;\n \theight: 100px;\n }\n}"]} \ No newline at end of file diff --git a/packages/core/integration-tests/test/integration/sourcemap-css-existing/test/library.raw.scss b/packages/core/integration-tests/test/integration/sourcemap-css-existing/test/library.raw.scss new file mode 100644 index 00000000000..93097df8f38 --- /dev/null +++ b/packages/core/integration-tests/test/integration/sourcemap-css-existing/test/library.raw.scss @@ -0,0 +1,13 @@ +$font-stack: Helvetica, sans-serif; +$primary-color: #333; + +body { + font: 100% $font-stack; + color: $primary-color; + + div { + background-color: red; + width: 100px; + height: 100px; + } +} \ No newline at end of file diff --git a/packages/core/integration-tests/test/integration/sourcemap-css-import/another-style.css b/packages/core/integration-tests/test/integration/sourcemap-css-import/another-style.css new file mode 100644 index 00000000000..107e9f29b84 --- /dev/null +++ b/packages/core/integration-tests/test/integration/sourcemap-css-import/another-style.css @@ -0,0 +1,3 @@ +main { + font-family: monospace; +} diff --git a/packages/core/integration-tests/test/integration/sourcemap-css-import/other-style.css b/packages/core/integration-tests/test/integration/sourcemap-css-import/other-style.css new file mode 100644 index 00000000000..6d844850414 --- /dev/null +++ b/packages/core/integration-tests/test/integration/sourcemap-css-import/other-style.css @@ -0,0 +1,5 @@ + +div { + width: 100px; + height: 100px; +} diff --git a/packages/core/integration-tests/test/integration/sourcemap-css-import/style.css b/packages/core/integration-tests/test/integration/sourcemap-css-import/style.css new file mode 100644 index 00000000000..01ef4f07c71 --- /dev/null +++ b/packages/core/integration-tests/test/integration/sourcemap-css-import/style.css @@ -0,0 +1,8 @@ + +@import "./other-style.css"; + +body { + background-color: red; +} + +@import "./another-style.css"; diff --git a/packages/core/integration-tests/test/integration/sourcemap-css/style.css b/packages/core/integration-tests/test/integration/sourcemap-css/style.css new file mode 100644 index 00000000000..0ac172751f5 --- /dev/null +++ b/packages/core/integration-tests/test/integration/sourcemap-css/style.css @@ -0,0 +1,4 @@ + +body { + background-color: red; +} diff --git a/packages/core/integration-tests/test/integration/sourcemap-less/style.less b/packages/core/integration-tests/test/integration/sourcemap-less/style.less new file mode 100644 index 00000000000..e1a4d641769 --- /dev/null +++ b/packages/core/integration-tests/test/integration/sourcemap-less/style.less @@ -0,0 +1,5 @@ +@value: 100px * 2; + +div { + width: @value; +} diff --git a/packages/core/integration-tests/test/integration/sourcemap-sass-imported/other.scss b/packages/core/integration-tests/test/integration/sourcemap-sass-imported/other.scss new file mode 100644 index 00000000000..3498c8f2370 --- /dev/null +++ b/packages/core/integration-tests/test/integration/sourcemap-sass-imported/other.scss @@ -0,0 +1,5 @@ +$variable: monospace; + +div { + font-family: $variable; +} diff --git a/packages/core/integration-tests/test/integration/sourcemap-sass-imported/style.css b/packages/core/integration-tests/test/integration/sourcemap-sass-imported/style.css new file mode 100644 index 00000000000..b486b67de91 --- /dev/null +++ b/packages/core/integration-tests/test/integration/sourcemap-sass-imported/style.css @@ -0,0 +1,5 @@ +@import "./other.scss"; + +body { + color: red; +} diff --git a/packages/core/integration-tests/test/integration/sourcemap-sass/style.scss b/packages/core/integration-tests/test/integration/sourcemap-sass/style.scss new file mode 100644 index 00000000000..8ce95c4110e --- /dev/null +++ b/packages/core/integration-tests/test/integration/sourcemap-sass/style.scss @@ -0,0 +1,5 @@ +$variable: #333; + +body { + color: $variable; +} diff --git a/packages/core/integration-tests/test/less.js b/packages/core/integration-tests/test/less.js index b8e3ca188d3..2a700a0dd33 100644 --- a/packages/core/integration-tests/test/less.js +++ b/packages/core/integration-tests/test/less.js @@ -17,7 +17,11 @@ describe('less', function() { { name: 'index.css', assets: ['index.less'], - childBundles: [] + childBundles: [ + { + type: 'map' + } + ] } ] }); @@ -48,7 +52,11 @@ describe('less', function() { { name: 'index.css', assets: ['index.less'], - childBundles: [] + childBundles: [ + { + type: 'map' + } + ] } ] }); @@ -80,7 +88,11 @@ describe('less', function() { { name: 'index.css', assets: ['index.less'], - childBundles: [] + childBundles: [ + { + type: 'map' + } + ] } ] }); @@ -112,7 +124,11 @@ describe('less', function() { { name: 'index.css', assets: ['index.less'], - childBundles: [] + childBundles: [ + { + type: 'map' + } + ] } ] }); @@ -125,7 +141,11 @@ describe('less', function() { path.join(__dirname, '/dist/index.css'), 'utf8' ); - assert.equal(css, ''); + assert( + /^\/\*# sourceMappingURL=\/\w*\.css\.map \*\/$/.test( + css.replace('\n', '') + ) + ); }); it('should support linking to assets with url() from less', async function() { @@ -143,7 +163,11 @@ describe('less', function() { { name: 'index.css', assets: ['index.less'], - childBundles: [] + childBundles: [ + { + type: 'map' + } + ] }, { type: 'woff2', @@ -191,7 +215,11 @@ describe('less', function() { { name: 'index.css', assets: ['index.less'], - childBundles: [] + childBundles: [ + { + type: 'map' + } + ] } ] }); diff --git a/packages/core/integration-tests/test/parser.js b/packages/core/integration-tests/test/parser.js index 4d9fd37390f..8ff2d4e3418 100644 --- a/packages/core/integration-tests/test/parser.js +++ b/packages/core/integration-tests/test/parser.js @@ -24,7 +24,11 @@ describe('parser', function() { { type: 'css', assets: ['index.cSs'], - childBundles: [] + childBundles: [ + { + type: 'map' + } + ] }, { type: 'html', diff --git a/packages/core/integration-tests/test/pug.js b/packages/core/integration-tests/test/pug.js index 81f0d9fda7c..2962fdecac8 100644 --- a/packages/core/integration-tests/test/pug.js +++ b/packages/core/integration-tests/test/pug.js @@ -24,7 +24,11 @@ describe('pug', function() { { type: 'css', assets: ['index.css'], - childBundles: [] + childBundles: [ + { + type: 'map' + } + ] }, { type: 'js', diff --git a/packages/core/integration-tests/test/sass.js b/packages/core/integration-tests/test/sass.js index e9ef743b47d..9640bd3a329 100644 --- a/packages/core/integration-tests/test/sass.js +++ b/packages/core/integration-tests/test/sass.js @@ -17,7 +17,11 @@ describe('sass', function() { { name: 'index.css', assets: ['index.sass'], - childBundles: [] + childBundles: [ + { + type: 'map' + } + ] } ] }); @@ -46,7 +50,11 @@ describe('sass', function() { { name: 'index.css', assets: ['index.scss'], - childBundles: [] + childBundles: [ + { + type: 'map' + } + ] } ] }); @@ -77,7 +85,11 @@ describe('sass', function() { { name: 'index.css', assets: ['index.scss'], - childBundles: [] + childBundles: [ + { + type: 'map' + } + ] } ] }); @@ -110,7 +122,11 @@ describe('sass', function() { { name: 'index.css', assets: ['index.scss'], - childBundles: [] + childBundles: [ + { + type: 'map' + } + ] } ] }); @@ -123,7 +139,11 @@ describe('sass', function() { path.join(__dirname, '/dist/index.css'), 'utf8' ); - assert.equal(css, ''); + assert( + /^\/\*# sourceMappingURL=\/\w*\.css\.map \*\/$/.test( + css.replace('\n', '') + ) + ); }); it('should support linking to assets with url() from scss', async function() { @@ -146,7 +166,11 @@ describe('sass', function() { { name: 'index.css', assets: ['index.scss'], - childBundles: [] + childBundles: [ + { + type: 'map' + } + ] }, { type: 'woff2', @@ -194,7 +218,11 @@ describe('sass', function() { { name: 'index.css', assets: ['index.scss'], - childBundles: [] + childBundles: [ + { + type: 'map' + } + ] } ] }); @@ -218,7 +246,12 @@ describe('sass', function() { await assertBundleTree(b, { name: 'index.css', - assets: ['index.sass'] + assets: ['index.sass'], + childBundles: [ + { + type: 'map' + } + ] }); let css = (await fs.readFile( diff --git a/packages/core/integration-tests/test/sourcemaps.js b/packages/core/integration-tests/test/sourcemaps.js index 4ccd418ab33..1a412cf1904 100644 --- a/packages/core/integration-tests/test/sourcemaps.js +++ b/packages/core/integration-tests/test/sourcemaps.js @@ -1,9 +1,76 @@ const assert = require('assert'); const fs = require('@parcel/fs'); const path = require('path'); +const os = require('os'); const mapValidator = require('sourcemap-validator'); +const SourceMap = + parseInt(process.versions.node, 10) < 8 + ? require('parcel-bundler/lib/SourceMap') + : require('parcel-bundler/src/SourceMap'); const {bundler, bundle, run, assertBundleTree} = require('@parcel/test-utils'); +function indexToLineCol(str, index) { + let beforeIndex = str.slice(0, index); + return { + line: beforeIndex.split('\n').length, + column: index - beforeIndex.lastIndexOf('\n') - 1 + }; +} + +function checkSourceMapping({ + map, + source, + generated, + str, + generatedStr = str, + sourcePath, + msg = '' +}) { + assert( + generated.indexOf(generatedStr) !== -1, + "'" + generatedStr + "' not in generated code" + ); + assert(source.indexOf(str) !== -1, "'" + str + "' not in source code"); + + let generatedPosition = indexToLineCol( + generated, + generated.indexOf(generatedStr) + ); + let sourcePosition = indexToLineCol(source, source.indexOf(str)); + + let index = map.findClosestGenerated( + generatedPosition.line, + generatedPosition.column + ); + + let mapping = map.mappings[index]; + assert(mapping, "no mapping for '" + str + "'" + msg); + + let generatedDiff = { + line: generatedPosition.line - mapping.generated.line, + column: generatedPosition.column - mapping.generated.column + }; + + let computedSourcePosition = { + line: mapping.original.line + generatedDiff.line, + column: mapping.original.column + generatedDiff.column + }; + + assert.deepStrictEqual( + { + line: computedSourcePosition.line, + column: computedSourcePosition.column, + source: mapping.source + }, + { + line: sourcePosition.line, + column: sourcePosition.column, + source: sourcePath + }, + "map '" + str + "'" + msg + ); +} + describe('sourcemaps', function() { it('should create a valid sourcemap as a child of a JS bundle', async function() { let b = bundler(path.join(__dirname, '/integration/sourcemap/index.js')); @@ -20,12 +87,11 @@ describe('sourcemaps', function() { ] }); - let raw = (await fs.readFile( - path.join(__dirname, '/dist/index.js') - )).toString(); - let map = (await fs.readFile( - path.join(__dirname, '/dist/index.js.map') - )).toString(); + let raw = await fs.readFile(path.join(__dirname, '/dist/index.js'), 'utf8'); + let map = await fs.readFile( + path.join(__dirname, '/dist/index.js.map'), + 'utf8' + ); mapValidator(raw, map); let mapObject = JSON.parse(map); assert( @@ -43,6 +109,7 @@ describe('sourcemaps', function() { ), 'combining sourceRoot and sources object should resolve to the original file' ); + assert.equal(mapObject.sources.length, 1); let output = await run(bu); assert.equal(typeof output, 'function'); @@ -65,12 +132,12 @@ describe('sourcemaps', function() { ] }); - let raw = (await fs.readFile( - path.join(__dirname, '/dist/index.js') - )).toString(); - let map = (await fs.readFile( - path.join(__dirname, '/dist/index.js.map') - )).toString(); + let raw = await fs.readFile(path.join(__dirname, '/dist/index.js'), 'utf8'); + let map = await fs.readFile( + path.join(__dirname, '/dist/index.js.map'), + 'utf8' + ); + assert.equal(JSON.parse(map).sources.length, 1); mapValidator(raw, map); let output = await run(b); @@ -94,12 +161,12 @@ describe('sourcemaps', function() { ] }); - let raw = (await fs.readFile( - path.join(__dirname, '/dist/index.js') - )).toString(); - let map = (await fs.readFile( - path.join(__dirname, '/dist/index.js.map') - )).toString(); + let raw = await fs.readFile(path.join(__dirname, '/dist/index.js'), 'utf8'); + let map = await fs.readFile( + path.join(__dirname, '/dist/index.js.map'), + 'utf8' + ); + assert.equal(JSON.parse(map).sources.length, 2); mapValidator(raw, map); let output = await run(b); @@ -123,12 +190,11 @@ describe('sourcemaps', function() { ] }); - let raw = (await fs.readFile( - path.join(__dirname, '/dist/index.js') - )).toString(); - let map = (await fs.readFile( - path.join(__dirname, '/dist/index.js.map') - )).toString(); + let raw = await fs.readFile(path.join(__dirname, '/dist/index.js'), 'utf8'); + let map = await fs.readFile( + path.join(__dirname, '/dist/index.js.map'), + 'utf8' + ); mapValidator(raw, map); let output = await run(b); @@ -155,12 +221,12 @@ describe('sourcemaps', function() { ] }); - let raw = (await fs.readFile( - path.join(__dirname, '/dist/index.js') - )).toString(); - let map = (await fs.readFile( - path.join(__dirname, '/dist/index.js.map') - )).toString(); + let raw = await fs.readFile(path.join(__dirname, '/dist/index.js'), 'utf8'); + let map = await fs.readFile( + path.join(__dirname, '/dist/index.js.map'), + 'utf8' + ); + assert.equal(JSON.parse(map).sources.length, 3); mapValidator(raw, map); let output = await run(b); @@ -189,9 +255,10 @@ describe('sourcemaps', function() { ] }); - let jsOutput = (await fs.readFile( - Array.from(b.childBundles)[0].name - )).toString(); + let jsOutput = await fs.readFile( + Array.from(b.childBundles)[0].name, + 'utf8' + ); let sourcemapReference = path.join( __dirname, @@ -203,7 +270,8 @@ describe('sourcemaps', function() { 'referenced sourcemap should exist' ); - let map = (await fs.readFile(path.join(sourcemapReference))).toString(); + let map = await fs.readFile(path.join(sourcemapReference), 'utf8'); + assert.equal(JSON.parse(map).sources.length, 2); mapValidator(jsOutput, map); }); @@ -344,4 +412,496 @@ describe('sourcemaps', function() { assert(jsOutput1.includes('//# sourceMappingURL=/a/index.js.map')); assert(jsOutput2.includes('//# sourceMappingURL=/b/index.js.map')); }); + + it('should create a valid sourcemap as a child of a CSS bundle', async function() { + async function test(minify) { + let b = await bundle( + path.join(__dirname, '/integration/sourcemap-css/style.css'), + {minify: true} + ); + + await assertBundleTree(b, { + name: 'style.css', + assets: ['style.css'], + childBundles: [ + { + name: 'style.css.map', + type: 'map' + } + ] + }); + + let input = await fs.readFile( + path.join(__dirname, '/integration/sourcemap-css/style.css'), + 'utf8' + ); + let raw = await fs.readFile( + path.join(__dirname, '/dist/style.css'), + 'utf8' + ); + let map = JSON.parse( + await fs.readFile(path.join(__dirname, '/dist/style.css.map'), 'utf8') + ); + + assert(raw.includes('/*# sourceMappingURL=/style.css.map */')); + assert.equal( + map.sourceRoot, + path.normalize('../integration/sourcemap-css') + ); + + let sourceMap = await new SourceMap().addMap(map); + assert.equal(Object.keys(sourceMap.sources).length, 1); + assert.equal(sourceMap.sources['style.css'], input); + + checkSourceMapping({ + map: sourceMap, + source: input, + generated: raw, + str: 'body', + sourcePath: 'style.css', + msg: ' ' + (minify ? 'with' : 'without') + ' minification' + }); + + checkSourceMapping({ + map: sourceMap, + source: input, + generated: raw, + str: 'background-color', + sourcePath: 'style.css', + msg: ' ' + (minify ? 'with' : 'without') + ' minification' + }); + } + + await test(false); + await test(true); + }); + + it('should create a valid sourcemap for a CSS bundle with imports', async function() { + async function test(minify) { + let b = await bundle( + path.join(__dirname, '/integration/sourcemap-css-import/style.css'), + {minify} + ); + + await assertBundleTree(b, { + name: 'style.css', + assets: ['style.css', 'other-style.css', 'another-style.css'], + childBundles: [ + { + name: 'style.css.map', + type: 'map' + } + ] + }); + + let style = await fs.readFile( + path.join(__dirname, '/integration/sourcemap-css-import/style.css'), + 'utf8' + ); + let otherStyle = await fs.readFile( + path.join( + __dirname, + '/integration/sourcemap-css-import/other-style.css' + ), + 'utf8' + ); + let anotherStyle = await fs.readFile( + path.join( + __dirname, + '/integration/sourcemap-css-import/another-style.css' + ), + 'utf8' + ); + let raw = await fs.readFile( + path.join(__dirname, '/dist/style.css'), + 'utf8' + ); + let map = JSON.parse( + await fs.readFile(path.join(__dirname, '/dist/style.css.map'), 'utf8') + ); + + assert(raw.includes('/*# sourceMappingURL=/style.css.map */')); + assert.equal( + map.sourceRoot, + path.normalize('../integration/sourcemap-css-import') + ); + + let sourceMap = await new SourceMap().addMap(map); + assert.equal(Object.keys(sourceMap.sources).length, 3); + assert.equal(sourceMap.sources['style.css'], style); + assert.equal(sourceMap.sources['other-style.css'], otherStyle); + assert.equal(sourceMap.sources['another-style.css'], anotherStyle); + + checkSourceMapping({ + map: sourceMap, + source: style, + generated: raw, + str: 'body', + sourcePath: 'style.css', + msg: ' ' + (minify ? 'with' : 'without') + ' minification' + }); + + checkSourceMapping({ + map: sourceMap, + source: style, + generated: raw, + str: 'background-color', + sourcePath: 'style.css', + msg: ' ' + (minify ? 'with' : 'without') + ' minification' + }); + + checkSourceMapping({ + map: sourceMap, + source: otherStyle, + generated: raw, + str: 'div', + sourcePath: 'other-style.css', + msg: ' ' + (minify ? 'with' : 'without') + ' minification' + }); + + checkSourceMapping({ + map: sourceMap, + source: otherStyle, + generated: raw, + str: 'width', + sourcePath: 'other-style.css', + msg: ' ' + (minify ? 'with' : 'without') + ' minification' + }); + + checkSourceMapping({ + map: sourceMap, + source: anotherStyle, + generated: raw, + str: 'main', + sourcePath: 'another-style.css', + msg: ' ' + (minify ? 'with' : 'without') + ' minification' + }); + + checkSourceMapping({ + map: sourceMap, + source: anotherStyle, + generated: raw, + str: 'font-family', + sourcePath: 'another-style.css', + msg: ' ' + (minify ? 'with' : 'without') + ' minification' + }); + } + + await test(false); + await test(true); + }); + + it('should create a valid sourcemap for a SASS asset', async function() { + async function test(minify) { + let b = await bundle( + path.join(__dirname, '/integration/sourcemap-sass/style.scss'), + {minify} + ); + + await assertBundleTree(b, { + name: 'style.css', + assets: ['style.scss'], + childBundles: [ + { + name: 'style.css.map', + type: 'map' + } + ] + }); + + let input = await fs.readFile( + path.join(__dirname, '/integration/sourcemap-sass/style.scss'), + 'utf8' + ); + let raw = await fs.readFile( + path.join(__dirname, '/dist/style.css'), + 'utf8' + ); + let map = JSON.parse( + await fs.readFile(path.join(__dirname, '/dist/style.css.map'), 'utf8') + ); + + assert(raw.includes('/*# sourceMappingURL=/style.css.map */')); + assert.equal( + map.sourceRoot, + path.normalize('../integration/sourcemap-sass') + ); + + let sourceMap = await new SourceMap().addMap(map); + assert.equal(Object.keys(sourceMap.sources).length, 1); + assert.equal(sourceMap.sources['style.scss'], input); + + checkSourceMapping({ + map: sourceMap, + source: input, + generated: raw, + str: 'body', + sourcePath: 'style.scss', + msg: ' ' + (minify ? 'with' : 'without') + ' minification' + }); + + checkSourceMapping({ + map: sourceMap, + source: input, + generated: raw, + str: 'color', + sourcePath: 'style.scss', + msg: ' ' + (minify ? 'with' : 'without') + ' minification' + }); + } + + await test(false); + await test(true); + }); + + it('should create a valid sourcemap when for a CSS asset importing SASS', async function() { + async function test(minify) { + let b = await bundle( + path.join(__dirname, '/integration/sourcemap-sass-imported/style.css'), + {minify} + ); + + await assertBundleTree(b, { + name: 'style.css', + assets: ['style.css', 'other.scss'], + childBundles: [ + { + name: 'style.css.map', + type: 'map' + } + ] + }); + + let style = await fs.readFile( + path.join(__dirname, '/integration/sourcemap-sass-imported/style.css'), + 'utf8' + ); + let other = await fs.readFile( + path.join(__dirname, '/integration/sourcemap-sass-imported/other.scss'), + 'utf8' + ); + let raw = await fs.readFile( + path.join(__dirname, '/dist/style.css'), + 'utf8' + ); + let map = JSON.parse( + await fs.readFile(path.join(__dirname, '/dist/style.css.map'), 'utf8') + ); + + assert(raw.includes('/*# sourceMappingURL=/style.css.map */')); + assert.equal( + map.sourceRoot, + path.normalize('../integration/sourcemap-sass-imported') + ); + + let sourceMap = await new SourceMap().addMap(map); + assert.equal(Object.keys(sourceMap.sources).length, 2); + assert.equal(sourceMap.sources['style.css'], style); + assert.equal(sourceMap.sources['other.scss'], other); + + checkSourceMapping({ + map: sourceMap, + source: style, + generated: raw, + str: 'body', + sourcePath: 'style.css', + msg: ' ' + (minify ? 'with' : 'without') + ' minification' + }); + + checkSourceMapping({ + map: sourceMap, + source: style, + generated: raw, + str: 'color', + sourcePath: 'style.css', + msg: ' ' + (minify ? 'with' : 'without') + ' minification' + }); + + checkSourceMapping({ + map: sourceMap, + source: other, + generated: raw, + str: 'div', + sourcePath: 'other.scss', + msg: ' ' + (minify ? 'with' : 'without') + ' minification' + }); + + checkSourceMapping({ + map: sourceMap, + source: other, + generated: raw, + str: 'font-family', + sourcePath: 'other.scss', + msg: ' ' + (minify ? 'with' : 'without') + ' minification' + }); + } + await test(false); + await test(true); + }); + + it('should create a valid sourcemap for a LESS asset', async function() { + async function test(minify) { + let b = await bundle( + path.join(__dirname, '/integration/sourcemap-less/style.less'), + {minify} + ); + + await assertBundleTree(b, { + name: 'style.css', + assets: ['style.less'], + childBundles: [ + { + name: 'style.css.map', + type: 'map' + } + ] + }); + + let input = await fs.readFile( + path.join(__dirname, '/integration/sourcemap-less/style.less'), + 'utf8' + ); + let raw = await fs.readFile( + path.join(__dirname, '/dist/style.css'), + 'utf8' + ); + let map = JSON.parse( + await fs.readFile(path.join(__dirname, '/dist/style.css.map'), 'utf8') + ); + + assert(raw.includes('/*# sourceMappingURL=/style.css.map */')); + assert.equal( + map.sourceRoot, + path.normalize('../integration/sourcemap-less') + ); + + let sourceMap = await new SourceMap().addMap(map); + assert.equal(Object.keys(sourceMap.sources).length, 1); + assert.equal( + sourceMap.sources['style.less'], + input.replace(new RegExp(os.EOL, 'g'), '\n') + ); + + checkSourceMapping({ + map: sourceMap, + source: input, + generated: raw, + str: 'div', + sourcePath: 'style.less', + msg: ' ' + (minify ? 'with' : 'without') + ' minification' + }); + + checkSourceMapping({ + map: sourceMap, + source: input, + generated: raw, + str: 'width', + sourcePath: 'style.less', + msg: ' ' + (minify ? 'with' : 'without') + ' minification' + }); + } + await test(false); + await test(true); + }); + + it('should load existing sourcemaps for CSS files', async function() { + async function test(minify) { + let b = await bundle( + path.join(__dirname, '/integration/sourcemap-css-existing/style.css'), + {minify} + ); + + await assertBundleTree(b, { + name: 'style.css', + assets: ['style.css', 'library.css'], + childBundles: [ + { + name: 'style.css.map', + type: 'map' + } + ] + }); + + let style = await fs.readFile( + path.join(__dirname, '/integration/sourcemap-css-existing/style.css'), + 'utf8' + ); + let library = await fs.readFile( + path.join( + __dirname, + '/integration/sourcemap-css-existing/test/library.raw.scss' + ), + 'utf8' + ); + let raw = await fs.readFile( + path.join(__dirname, '/dist/style.css'), + 'utf8' + ); + let map = JSON.parse( + await fs.readFile(path.join(__dirname, '/dist/style.css.map'), 'utf8') + ); + + assert(raw.includes('/*# sourceMappingURL=/style.css.map */')); + assert.equal( + map.sourceRoot, + path.normalize('../integration/sourcemap-css-existing') + ); + + let sourceMap = await new SourceMap().addMap(map); + assert.equal(Object.keys(sourceMap.sources).length, 2); + assert.equal(sourceMap.sources['style.css'], style); + assert.equal( + sourceMap.sources[path.normalize('test/library.scss')], + library.replace(new RegExp(os.EOL, 'g'), '\n') + ); + + checkSourceMapping({ + map: sourceMap, + source: style, + generated: raw, + str: 'main', + sourcePath: 'style.css', + msg: ' ' + (minify ? 'with' : 'without') + ' minification' + }); + + checkSourceMapping({ + map: sourceMap, + source: style, + generated: raw, + str: 'display', + sourcePath: 'style.css', + msg: ' ' + (minify ? 'with' : 'without') + ' minification' + }); + + checkSourceMapping({ + map: sourceMap, + source: library, + generated: raw, + str: 'body', + sourcePath: path.normalize('test/library.scss'), + msg: ' ' + (minify ? 'with' : 'without') + ' minification' + }); + + checkSourceMapping({ + map: sourceMap, + source: library, + generated: raw, + str: 'div', + generatedStr: 'body div', + sourcePath: path.normalize('test/library.scss'), + msg: ' ' + (minify ? 'with' : 'without') + ' minification' + }); + + checkSourceMapping({ + map: sourceMap, + source: library, + generated: raw, + str: 'background-color', + sourcePath: path.normalize('test/library.scss'), + msg: ' ' + (minify ? 'with' : 'without') + ' minification' + }); + } + await test(false); + await test(true); + }); }); diff --git a/packages/core/integration-tests/test/stylus.js b/packages/core/integration-tests/test/stylus.js index 34b6115ca0b..68f5a092c6a 100644 --- a/packages/core/integration-tests/test/stylus.js +++ b/packages/core/integration-tests/test/stylus.js @@ -17,7 +17,11 @@ describe('stylus', function() { { name: 'index.css', assets: ['index.styl'], - childBundles: [] + childBundles: [ + { + type: 'map' + } + ] } ] }); @@ -50,7 +54,11 @@ describe('stylus', function() { { name: 'index.css', assets: ['index.styl'], - childBundles: [] + childBundles: [ + { + type: 'map' + } + ] } ] }); @@ -84,7 +92,11 @@ describe('stylus', function() { { name: 'index.css', assets: ['index.styl'], - childBundles: [] + childBundles: [ + { + type: 'map' + } + ] }, { type: 'woff2', @@ -132,7 +144,11 @@ describe('stylus', function() { { name: 'index.css', assets: ['index.styl'], - childBundles: [] + childBundles: [ + { + type: 'map' + } + ] } ] }); @@ -163,7 +179,11 @@ describe('stylus', function() { { name: 'index.css', assets: ['index.styl'], - childBundles: [] + childBundles: [ + { + type: 'map' + } + ] } ] }); diff --git a/packages/core/parcel-bundler/package.json b/packages/core/parcel-bundler/package.json index 55b01ff9d9a..5fc36a96dd5 100644 --- a/packages/core/parcel-bundler/package.json +++ b/packages/core/parcel-bundler/package.json @@ -59,7 +59,7 @@ "node-forge": "^0.7.1", "node-libs-browser": "^2.0.0", "opn": "^5.1.0", - "postcss": "^7.0.5", + "postcss": "^7.0.11", "postcss-value-parser": "^3.3.1", "posthtml": "^0.11.2", "posthtml-parser": "^0.4.0", diff --git a/packages/core/parcel-bundler/src/Asset.js b/packages/core/parcel-bundler/src/Asset.js index ddb317efff2..381d6e9f817 100644 --- a/packages/core/parcel-bundler/src/Asset.js +++ b/packages/core/parcel-bundler/src/Asset.js @@ -34,6 +34,7 @@ class Asset { this.ast = null; this.generated = null; this.hash = null; + this.sourceMaps = null; this.parentDeps = new Set(); this.dependencies = new Map(); this.depAssets = new Map(); diff --git a/packages/core/parcel-bundler/src/Bundle.js b/packages/core/parcel-bundler/src/Bundle.js index 1847eb3eaec..478693c3030 100644 --- a/packages/core/parcel-bundler/src/Bundle.js +++ b/packages/core/parcel-bundler/src/Bundle.js @@ -39,6 +39,14 @@ class Bundle { addAsset(asset) { asset.bundles.add(this); this.assets.add(asset); + if ( + this.type != 'map' && + this.type == asset.type && + asset.options.sourceMaps && + asset.sourceMaps + ) { + this.getSiblingBundle('map').addAsset(asset); + } } removeAsset(asset) { @@ -46,12 +54,12 @@ class Bundle { this.assets.delete(asset); } - addOffset(asset, line) { - this.offsets.set(asset, line); + addOffset(asset, line, column = 0) { + this.offsets.set(asset, [line, column]); } getOffset(asset) { - return this.offsets.get(asset) || 0; + return this.offsets.get(asset) || [0, 0]; } getSiblingBundle(type) { @@ -114,19 +122,23 @@ class Bundle { getHashedBundleName(contentHash) { // If content hashing is enabled, generate a hash from all assets in the bundle. // Otherwise, use a hash of the filename so it remains consistent across builds. + + if (this.type == 'map') { + return this.parentBundle.getHashedBundleName(contentHash) + '.map'; + } + let basename = Path.basename(this.name); let ext = Path.extname(basename); - if (this.type === 'map') { - // Using this instead of Path.extname because the source map files have long - // extensions like '.js.map' but extname only return the last piece (.map). - ext = basename.substring(basename.indexOf('.')); - } let hash = (contentHash ? this.getHash() : Path.basename(this.name, ext) ).slice(-8); - let entryAsset = this.entryAsset || this.parentBundle.entryAsset; + let entryAsset = this; + while (!entryAsset.entryAsset && entryAsset.parentBundle) { + entryAsset = entryAsset.parentBundle; + } + entryAsset = entryAsset.entryAsset; let name = Path.basename(entryAsset.name, Path.extname(entryAsset.name)); let isMainEntry = entryAsset.options.entryFiles[0] === entryAsset.name; let isEntry = diff --git a/packages/core/parcel-bundler/src/Bundler.js b/packages/core/parcel-bundler/src/Bundler.js index 1e22a4e20d9..2806990608c 100644 --- a/packages/core/parcel-bundler/src/Bundler.js +++ b/packages/core/parcel-bundler/src/Bundler.js @@ -572,6 +572,7 @@ class Bundler extends EventEmitter { asset.buildTime = asset.endTime - asset.startTime; asset.id = processed.id; asset.generated = processed.generated; + asset.sourceMaps = processed.sourceMaps; asset.hash = processed.hash; asset.cacheData = processed.cacheData; diff --git a/packages/core/parcel-bundler/src/FSCache.js b/packages/core/parcel-bundler/src/FSCache.js index dedee45b2ea..80fbd4b8197 100644 --- a/packages/core/parcel-bundler/src/FSCache.js +++ b/packages/core/parcel-bundler/src/FSCache.js @@ -7,7 +7,14 @@ const logger = require('@parcel/logger'); const {isGlob, glob} = require('./utils/glob'); // These keys can affect the output, so if they differ, the cache should not match -const OPTION_KEYS = ['publicURL', 'minify', 'hmr', 'target', 'scopeHoist']; +const OPTION_KEYS = [ + 'publicURL', + 'minify', + 'hmr', + 'target', + 'scopeHoist', + 'sourceMaps' +]; class FSCache { constructor(options) { diff --git a/packages/core/parcel-bundler/src/Pipeline.js b/packages/core/parcel-bundler/src/Pipeline.js index e076fa90f81..0b016006fbc 100644 --- a/packages/core/parcel-bundler/src/Pipeline.js +++ b/packages/core/parcel-bundler/src/Pipeline.js @@ -34,6 +34,7 @@ class Pipeline { id: asset.id, dependencies: Array.from(asset.dependencies.values()), generated: generatedMap, + sourceMaps: asset.sourceMaps, error: error, hash: asset.hash, cacheData: asset.cacheData @@ -92,6 +93,19 @@ class Pipeline { throw asset.generateErrorMessage(err); } + let hasMap = false; + let sourceMaps = {}; + for (let rendition of generated) { + if (rendition.map && rendition.type == asset.type) { + sourceMaps[rendition.type] = rendition.map; + hasMap = true; + } + } + + if (hasMap) { + asset.sourceMaps = sourceMaps; + } + asset.generated = generated; asset.hash = await asset.generateHash(); diff --git a/packages/core/parcel-bundler/src/SourceMap.js b/packages/core/parcel-bundler/src/SourceMap.js index b9f4b5bb8c1..222dc3c3365 100644 --- a/packages/core/parcel-bundler/src/SourceMap.js +++ b/packages/core/parcel-bundler/src/SourceMap.js @@ -343,7 +343,6 @@ class SourceMap { stringify(file, sourceRoot) { let generator = new SourceMapGenerator({file, sourceRoot}); - this.eachMapping(mapping => generator.addMapping(mapping)); Object.keys(this.sources).forEach(sourceName => generator.setSourceContent(sourceName, this.sources[sourceName]) diff --git a/packages/core/parcel-bundler/src/assets/CSSAsset.js b/packages/core/parcel-bundler/src/assets/CSSAsset.js index 76cc0998ad5..967457c82f9 100644 --- a/packages/core/parcel-bundler/src/assets/CSSAsset.js +++ b/packages/core/parcel-bundler/src/assets/CSSAsset.js @@ -3,6 +3,9 @@ const postcss = require('postcss'); const valueParser = require('postcss-value-parser'); const postcssTransform = require('../transforms/postcss'); const CssSyntaxError = require('postcss/lib/css-syntax-error'); +const SourceMap = require('../SourceMap'); +const loadSourceMap = require('../utils/loadSourceMap'); +const path = require('path'); const URL_RE = /url\s*\("?(?![a-z]+:)/; const IMPORT_RE = /@import/; @@ -14,6 +17,9 @@ class CSSAsset extends Asset { constructor(name, options) { super(name, options); this.type = 'css'; + this.previousSourceMap = this.options.rendition + ? this.options.rendition.map + : null; } mightHaveDependencies() { @@ -26,7 +32,9 @@ class CSSAsset extends Asset { } parse(code) { - let root = postcss.parse(code, {from: this.name, to: this.name}); + let root = postcss.parse(code, { + from: this.name + }); return new CSSAst(code, root); } @@ -110,6 +118,12 @@ class CSSAsset extends Asset { }); } + async pretransform() { + if (this.options.sourceMaps && !this.previousSourceMap) { + this.previousSourceMap = await loadSourceMap(this); + } + } + async transform() { await postcssTransform(this); } @@ -117,14 +131,24 @@ class CSSAsset extends Asset { getCSSAst() { // Converts the ast to a CSS ast if needed, so we can apply postcss transforms. if (!(this.ast instanceof CSSAst)) { - this.ast = CSSAsset.prototype.parse.call(this, this.ast.render()); + this.ast = CSSAsset.prototype.parse.call( + this, + this.ast.render(this.name) + ); } return this.ast.root; } - generate() { - let css = this.ast ? this.ast.render() : this.contents; + async generate() { + let css; + if (this.ast) { + let result = this.ast.render(this.name); + css = result.css; + if (result.map) this.sourceMap = result.map; + } else { + css = this.contents; + } let js = ''; if (this.options.hmr) { @@ -142,11 +166,41 @@ class CSSAsset extends Asset { 'module.exports = ' + JSON.stringify(this.cssModules, null, 2) + ';'; } + if (this.options.sourceMaps) { + if (this.sourceMap) { + this.sourceMap = await new SourceMap().addMap(this.sourceMap); + } + + if (this.previousSourceMap) { + this.previousSourceMap.sources = this.previousSourceMap.sources.map(v => + path.join( + path.dirname(this.relativeName), + this.previousSourceMap.sourceRoot || '', + v + ) + ); + if (this.sourceMap) { + this.sourceMap = await new SourceMap().extendSourceMap( + this.previousSourceMap, + this.sourceMap + ); + } else { + this.sourceMap = await new SourceMap().addMap(this.previousSourceMap); + } + } else if (!this.sourceMap) { + this.sourceMap = new SourceMap().generateEmptyMap( + this.relativeName, + css + ); + } + } + return [ { type: 'css', value: css, - cssModules: this.cssModules + cssModules: this.cssModules, + map: this.sourceMap }, { type: 'js', @@ -189,13 +243,24 @@ class CSSAst { this.dirty = false; } - render() { + render(name) { if (this.dirty) { - this.css = ''; - postcss.stringify(this.root, c => (this.css += c)); + let {css, map} = this.root.toResult({ + to: name, + map: {inline: false, annotation: false, sourcesContent: true} + }); + + this.css = css; + + return { + css: this.css, + map: map ? map.toJSON() : null + }; } - return this.css; + return { + css: this.css + }; } } diff --git a/packages/core/parcel-bundler/src/assets/JSAsset.js b/packages/core/parcel-bundler/src/assets/JSAsset.js index e300a07e8ba..5d42aca05a7 100644 --- a/packages/core/parcel-bundler/src/assets/JSAsset.js +++ b/packages/core/parcel-bundler/src/assets/JSAsset.js @@ -14,9 +14,7 @@ const generate = require('@babel/generator').default; const terser = require('../transforms/terser'); const SourceMap = require('../SourceMap'); const hoist = require('../scope-hoisting/hoist'); -const path = require('path'); -const fs = require('@parcel/fs'); -const logger = require('@parcel/logger'); +const loadSourceMap = require('../utils/loadSourceMap'); const isAccessedVarChanged = require('../utils/isAccessedVarChanged'); const IMPORT_RE = /\b(?:import\b|export\b|require\s*\()/; @@ -26,8 +24,6 @@ const GLOBAL_RE = /\b(?:process|__dirname|__filename|global|Buffer|define)\b/; const FS_RE = /\breadFileSync\b/; const SW_RE = /\bnavigator\s*\.\s*serviceWorker\s*\.\s*register\s*\(/; const WORKER_RE = /\bnew\s*(?:Shared)?Worker\s*\(/; -const SOURCEMAP_RE = /\/\/\s*[@#]\s*sourceMappingURL\s*=\s*([^\s]+)/; -const DATA_URL_RE = /^data:[^;]+(?:;charset=[^;]+)?;base64,(.*)/; class JSAsset extends Asset { constructor(name, options) { @@ -39,7 +35,7 @@ class JSAsset extends Asset { this.outputCode = null; this.cacheData.env = {}; this.rendition = options.rendition; - this.sourceMap = this.rendition ? this.rendition.sourceMap : null; + this.sourceMap = this.rendition ? this.rendition.map : null; } shouldInvalidate(cacheData) { @@ -79,75 +75,9 @@ class JSAsset extends Asset { walk.ancestor(this.ast, collectDependencies, this); } - async loadSourceMap() { - // Get original sourcemap if there is any - let match = this.contents.match(SOURCEMAP_RE); - if (match) { - this.contents = this.contents.replace(SOURCEMAP_RE, ''); - - let url = match[1]; - let dataURLMatch = url.match(DATA_URL_RE); - - try { - let json, filename; - if (dataURLMatch) { - filename = this.name; - json = new Buffer(dataURLMatch[1], 'base64').toString(); - } else { - filename = path.join(path.dirname(this.name), url); - json = await fs.readFile(filename, 'utf8'); - - // Add as a dep so we watch the source map for changes. - this.addDependency(filename, {includedInParent: true}); - } - - this.sourceMap = JSON.parse(json); - - // Attempt to read missing source contents - if (!this.sourceMap.sourcesContent) { - this.sourceMap.sourcesContent = []; - } - - let missingSources = this.sourceMap.sources.slice( - this.sourceMap.sourcesContent.length - ); - if (missingSources.length) { - let contents = await Promise.all( - missingSources.map(async source => { - try { - let sourceFile = path.join( - path.dirname(filename), - this.sourceMap.sourceRoot || '', - source - ); - let result = await fs.readFile(sourceFile, 'utf8'); - this.addDependency(sourceFile, {includedInParent: true}); - return result; - } catch (err) { - logger.warn( - `Could not load source file "${source}" in source map of "${ - this.relativeName - }".` - ); - } - }) - ); - - this.sourceMap.sourcesContent = this.sourceMap.sourcesContent.concat( - contents - ); - } - } catch (e) { - logger.warn( - `Could not load existing sourcemap of "${this.relativeName}".` - ); - } - } - } - async pretransform() { - if (this.options.sourceMaps) { - await this.loadSourceMap(); + if (this.options.sourceMaps && !this.sourceMap) { + this.sourceMap = await loadSourceMap(this); } await babel(this); @@ -209,9 +139,6 @@ class JSAsset extends Asset { } async generate() { - let enableSourceMaps = - this.options.sourceMaps && - (!this.rendition || !!this.rendition.sourceMap); let code; if (this.isAstDirty) { let opts = { @@ -221,7 +148,7 @@ class JSAsset extends Asset { let generated = generate(this.ast, opts, this.contents); - if (enableSourceMaps && generated.rawMappings) { + if (this.options.sourceMaps && generated.rawMappings) { let rawMap = new SourceMap(generated.rawMappings, { [this.relativeName]: this.contents }); @@ -243,7 +170,7 @@ class JSAsset extends Asset { code = this.outputCode != null ? this.outputCode : this.contents; } - if (enableSourceMaps && !this.sourceMap) { + if (this.options.sourceMaps && !this.sourceMap) { this.sourceMap = new SourceMap().generateEmptyMap( this.relativeName, this.contents @@ -252,7 +179,7 @@ class JSAsset extends Asset { if (this.globals.size > 0) { code = Array.from(this.globals.values()).join('\n') + '\n' + code; - if (enableSourceMaps) { + if (this.options.sourceMaps) { if (!(this.sourceMap instanceof SourceMap)) { this.sourceMap = await new SourceMap().addMap(this.sourceMap); } @@ -261,10 +188,13 @@ class JSAsset extends Asset { } } - return { - js: code, - map: this.sourceMap - }; + return [ + { + type: 'js', + value: code, + map: this.sourceMap + } + ]; } generateErrorMessage(err) { diff --git a/packages/core/parcel-bundler/src/assets/LESSAsset.js b/packages/core/parcel-bundler/src/assets/LESSAsset.js index e87f67e885a..8791c289636 100644 --- a/packages/core/parcel-bundler/src/assets/LESSAsset.js +++ b/packages/core/parcel-bundler/src/assets/LESSAsset.js @@ -22,6 +22,9 @@ class LESSAsset extends Asset { {}; opts.filename = this.name; opts.plugins = (opts.plugins || []).concat(urlPlugin(this)); + if (this.options.sourceMaps) { + opts.sourceMap = {outputSourceFiles: true}; + } return render(code, opts); } @@ -33,11 +36,19 @@ class LESSAsset extends Asset { } generate() { + let map; + if (this.ast && this.ast.map) { + map = JSON.parse(this.ast.map.toString()); + map.sources = map.sources.map(v => + path.relative(this.options.rootDir, v) + ); + } return [ { type: 'css', value: this.ast ? this.ast.css : '', - hasDependencies: false + hasDependencies: false, + map } ]; } diff --git a/packages/core/parcel-bundler/src/assets/SASSAsset.js b/packages/core/parcel-bundler/src/assets/SASSAsset.js index 5e6fb424eaf..ee27366aac4 100644 --- a/packages/core/parcel-bundler/src/assets/SASSAsset.js +++ b/packages/core/parcel-bundler/src/assets/SASSAsset.js @@ -55,6 +55,14 @@ class SASSAsset extends Asset { .catch(err => done(normalizeError(err))); }); + if (this.options.sourceMaps) { + opts.sourceMap = true; + opts.file = this.name; + opts.outFile = this.name; + opts.omitSourceMapUrl = true; + opts.sourceMapContents = true; + } + try { return await render(opts); } catch (err) { @@ -77,7 +85,11 @@ class SASSAsset extends Asset { return [ { type: 'css', - value: this.ast ? this.ast.css.toString() : '' + value: this.ast ? this.ast.css.toString() : '', + map: + this.ast && this.ast.map + ? JSON.parse(this.ast.map.toString()) + : undefined } ]; } diff --git a/packages/core/parcel-bundler/src/assets/TypeScriptAsset.js b/packages/core/parcel-bundler/src/assets/TypeScriptAsset.js index 1e8e8197000..db301c92e15 100644 --- a/packages/core/parcel-bundler/src/assets/TypeScriptAsset.js +++ b/packages/core/parcel-bundler/src/assets/TypeScriptAsset.js @@ -66,7 +66,7 @@ class TypeScriptAsset extends Asset { { type: 'js', value: transpiled.outputText, - sourceMap + map: sourceMap } ]; } diff --git a/packages/core/parcel-bundler/src/assets/VueAsset.js b/packages/core/parcel-bundler/src/assets/VueAsset.js index ce9bb946794..ade65249427 100644 --- a/packages/core/parcel-bundler/src/assets/VueAsset.js +++ b/packages/core/parcel-bundler/src/assets/VueAsset.js @@ -35,7 +35,7 @@ class VueAsset extends Asset { parts.push({ type: descriptor.script.lang || 'js', value: descriptor.script.content, - sourceMap: descriptor.script.map + map: descriptor.script.map }); } @@ -117,15 +117,11 @@ class VueAsset extends Asset { if (js) { result.push({ type: 'js', - value: js + value: js, + map: this.options.sourceMaps && this.ast.script && generated[0].map }); } - let map = generated.find(r => r.type === 'map'); - if (map) { - result.push(map); - } - let css = this.compileStyle(generated, scopeId); if (css) { result.push({ diff --git a/packages/core/parcel-bundler/src/packagers/CSSPackager.js b/packages/core/parcel-bundler/src/packagers/CSSPackager.js index a188fb1a24b..03e91dd3d09 100644 --- a/packages/core/parcel-bundler/src/packagers/CSSPackager.js +++ b/packages/core/parcel-bundler/src/packagers/CSSPackager.js @@ -1,6 +1,14 @@ +const path = require('path'); const Packager = require('./Packager'); +const lineCounter = require('../utils/lineCounter'); +const urlJoin = require('../utils/urlJoin'); class CSSPackager extends Packager { + async start() { + this.lineOffset = 0; + this.columnOffset = 0; + } + async addAsset(asset) { let css = asset.generated.css || ''; @@ -22,7 +30,44 @@ class CSSPackager extends Packager { css = `@media ${media.join(', ')} {\n${css.trim()}\n}\n`; } - await this.write(css); + if (asset.options.sourceMaps) { + let lineCount = lineCounter(css); + + if (lineCount == 1) { + this.bundle.addOffset(asset, this.lineOffset, this.columnOffset); + await this.write(css); + this.columnOffset += css.length; + } else { + const lines = css.split('\n'); + if (this.columnOffset == 0) { + this.bundle.addOffset(asset, this.lineOffset, 0); + await this.write(css + '\n'); + } else { + this.columnOffset = 0; + this.bundle.addOffset(asset, this.lineOffset + 1, 0); + this.columnOffset = lines[lines.length - 1].length; + await this.write('\n' + css); + } + this.lineOffset += lineCount; + } + } else { + await this.write(css); + } + } + + async end() { + if (this.options.sourceMaps) { + // Add source map url if a map bundle exists + let mapBundle = this.bundle.siblingBundlesMap.get('map'); + if (mapBundle) { + let mapUrl = urlJoin( + this.options.publicURL, + path.basename(mapBundle.name) + ); + await this.write(`\n/*# sourceMappingURL=${mapUrl} */`); + } + } + await super.end(); } } diff --git a/packages/core/parcel-bundler/src/packagers/SourceMapPackager.js b/packages/core/parcel-bundler/src/packagers/SourceMapPackager.js index 1b570d14981..063eedea2f1 100644 --- a/packages/core/parcel-bundler/src/packagers/SourceMapPackager.js +++ b/packages/core/parcel-bundler/src/packagers/SourceMapPackager.js @@ -8,14 +8,18 @@ class SourceMapPackager extends Packager { } async addAsset(asset) { - await this.sourceMap.addMap( - asset.generated.map, - this.bundle.parentBundle.getOffset(asset) - ); + let offsets = this.bundle.parentBundle.getOffset(asset); + if (asset.sourceMaps[asset.type]) { + await this.sourceMap.addMap( + asset.sourceMaps[asset.type], + offsets[0], + offsets[1] + ); + } } async end() { - let file = path.basename(this.bundle.name); + let file = path.basename(this.bundle.parentBundle.name); await this.write( this.sourceMap.stringify( diff --git a/packages/core/parcel-bundler/src/transforms/postcss.js b/packages/core/parcel-bundler/src/transforms/postcss.js index 0e209a3578a..4718d1477ed 100644 --- a/packages/core/parcel-bundler/src/transforms/postcss.js +++ b/packages/core/parcel-bundler/src/transforms/postcss.js @@ -18,6 +18,7 @@ module.exports = async function(asset) { asset.ast.css = res.css; asset.ast.dirty = false; + asset.sourceMap = res.map ? res.map.toJSON() : null; }; async function getConfig(asset) { @@ -34,6 +35,10 @@ async function getConfig(asset) { config = config || {}; + if (asset.options.sourceMaps) { + config.map = {inline: false, annotation: false, sourcesContent: true}; + } + if (typeof config !== 'object') { throw new Error('PostCSS config should be an object.'); } diff --git a/packages/core/parcel-bundler/src/transforms/terser.js b/packages/core/parcel-bundler/src/transforms/terser.js index 75249dd2e58..64f9e38e8b3 100644 --- a/packages/core/parcel-bundler/src/transforms/terser.js +++ b/packages/core/parcel-bundler/src/transforms/terser.js @@ -5,7 +5,7 @@ module.exports = async function(asset) { await asset.parseIfNeeded(); // Convert AST into JS - let source = (await asset.generate()).js; + let source = (await asset.generate())[0].value; let customConfig = await asset.getConfig(['.uglifyrc', '.terserrc']); let options = { diff --git a/packages/core/parcel-bundler/src/utils/loadSourceMap.js b/packages/core/parcel-bundler/src/utils/loadSourceMap.js new file mode 100644 index 00000000000..d7fb5931009 --- /dev/null +++ b/packages/core/parcel-bundler/src/utils/loadSourceMap.js @@ -0,0 +1,75 @@ +const logger = require('@parcel/logger'); +const path = require('path'); +const fs = require('@parcel/fs'); + +const SOURCEMAP_RE = /(?:\/\*|\/\/)\s*[@#]\s*sourceMappingURL\s*=\s*([^\s*]+)(?:\s*\*\/)?/; +const DATA_URL_RE = /^data:[^;]+(?:;charset=[^;]+)?;base64,(.*)/; + +async function loadSourceMap(asset) { + // Get original sourcemap if there is any + let match = asset.contents.match(SOURCEMAP_RE); + let sourceMap; + if (match) { + asset.contents = asset.contents.replace(SOURCEMAP_RE, ''); + + let url = match[1]; + let dataURLMatch = url.match(DATA_URL_RE); + + try { + let json, filename; + if (dataURLMatch) { + filename = asset.name; + json = Buffer.from(dataURLMatch[1], 'base64').toString(); + } else { + filename = path.join(path.dirname(asset.name), url); + json = await fs.readFile(filename, 'utf8'); + + // Add as a dep so we watch the source map for changes. + asset.addDependency(filename, {includedInParent: true}); + } + + sourceMap = JSON.parse(json); + + // Attempt to read missing source contents + if (!sourceMap.sourcesContent) { + sourceMap.sourcesContent = []; + } + + let missingSources = sourceMap.sources.slice( + sourceMap.sourcesContent.length + ); + if (missingSources.length) { + let contents = await Promise.all( + missingSources.map(async source => { + try { + let sourceFile = path.join( + path.dirname(filename), + sourceMap.sourceRoot || '', + source + ); + let result = await fs.readFile(sourceFile, 'utf8'); + asset.addDependency(sourceFile, {includedInParent: true}); + return result; + } catch (err) { + logger.warn( + `Could not load source file "${source}" in source map of "${ + asset.relativeName + }".` + ); + } + }) + ); + + sourceMap.sourcesContent = sourceMap.sourcesContent.concat(contents); + } + } catch (e) { + logger.warn( + `Could not load existing sourcemap of "${asset.relativeName}".` + ); + sourceMap = undefined; + } + } + return sourceMap; +} + +module.exports = loadSourceMap; diff --git a/packages/core/test-utils/src/utils.js b/packages/core/test-utils/src/utils.js index 4f71ff3303c..63dc8436f53 100644 --- a/packages/core/test-utils/src/utils.js +++ b/packages/core/test-utils/src/utils.js @@ -134,13 +134,17 @@ async function assertBundleTree(bundle, tree) { let childBundles = Array.isArray(tree) ? tree : tree.childBundles; if (childBundles) { - let children = Array.from(bundle.childBundles).sort( - (a, b) => - Array.from(a.assets).sort()[0].basename < - Array.from(b.assets).sort()[0].basename - ? -1 - : 1 - ); + let children = Array.from(bundle.childBundles).sort((a, b) => { + let assetA = Array.from(a.assets).sort()[0]; + let assetB = Array.from(b.assets).sort()[0]; + if (assetA.basename < assetB.basename) { + return -1; + } else if (assetA.basename > assetB.basename) { + return 1; + } else { + return a.type < b.type ? -1 : 1; + } + }); assert.equal( bundle.childBundles.size, childBundles.length, diff --git a/yarn.lock b/yarn.lock index a848cd60b05..bbe3f5336dd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2719,6 +2719,15 @@ chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.1, chalk@^2.4.1: escape-string-regexp "^1.0.5" supports-color "^5.3.0" +chalk@^2.4.2: + version "2.4.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + character-parser@^2.1.1: version "2.2.0" resolved "https://registry.yarnpkg.com/character-parser/-/character-parser-2.2.0.tgz#c7ce28f36d4bcd9744e5ffc2c5fcde1c73261fc0" @@ -8320,6 +8329,15 @@ postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.2, postcss@^7.0.5: source-map "^0.6.1" supports-color "^5.5.0" +postcss@^7.0.11: + version "7.0.11" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.11.tgz#f63c513b78026d66263bb2ca995bf02e3d1a697d" + integrity sha512-9AXb//5UcjeOEof9T+yPw3XTa5SL207ZOIC/lHYP4mbUTEh4M0rDAQekQpVANCZdwQwKhBtFZCk3i3h3h2hdWg== + dependencies: + chalk "^2.4.2" + source-map "^0.6.1" + supports-color "^6.1.0" + posthtml-extend@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/posthtml-extend/-/posthtml-extend-0.2.1.tgz#d023ce7ce4dd6071071b50e315dfefa87da8a979" @@ -9878,6 +9896,13 @@ supports-color@^5.3.0, supports-color@^5.4.0, supports-color@^5.5.0: dependencies: has-flag "^3.0.0" +supports-color@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-6.1.0.tgz#0764abc69c63d5ac842dd4867e8d025e880df8f3" + integrity sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ== + dependencies: + has-flag "^3.0.0" + svgo@^1.0.0, svgo@^1.0.5: version "1.1.1" resolved "https://registry.yarnpkg.com/svgo/-/svgo-1.1.1.tgz#12384b03335bcecd85cfa5f4e3375fed671cb985"