From 53da71a597228c21b9df6615a1315923199f8275 Mon Sep 17 00:00:00 2001 From: Alexey Lavinsky Date: Fri, 28 Aug 2020 17:23:09 +0300 Subject: [PATCH] refactor: sourcemap paths BREAKING CHANGE: source map contains absolute `sources` --- package-lock.json | 58 ++++++++++++- package.json | 2 + src/index.js | 33 ++++++-- src/utils.js | 75 ++++++++++++++++- test/fixtures/less/index.js | 3 + test/fixtures/less/style.less | 1 + test/helpers/getCodeFromBundle.js | 4 +- .../__snapshots__/sourceMap.test.js.snap | 68 ++++++++++++--- test/options/sourceMap.test.js | 83 ++++++++++++++++++- 9 files changed, 303 insertions(+), 24 deletions(-) create mode 100644 test/fixtures/less/index.js create mode 100644 test/fixtures/less/style.less diff --git a/package-lock.json b/package-lock.json index 039935bc..dc78ad69 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3806,6 +3806,12 @@ } } }, + "clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=", + "dev": true + }, "co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -8187,6 +8193,13 @@ "integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==", "dev": true }, + "image-size": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz", + "integrity": "sha1-Cd/Uq50g4p6xw+gLiZA3jfnjy5w=", + "dev": true, + "optional": true + }, "import-fresh": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.1.tgz", @@ -10670,6 +10683,34 @@ "integrity": "sha512-xf88rTeHiXk+XE2Vhi6yj8Wm3gMZrygGdKjJqN8HkV+PwF/t50/LdAKHoHpPcxFAlmQszTZ1CugrK25S7qDRLA==", "dev": true }, + "less": { + "version": "3.12.2", + "resolved": "https://registry.npmjs.org/less/-/less-3.12.2.tgz", + "integrity": "sha512-+1V2PCMFkL+OIj2/HrtrvZw0BC0sYLMICJfbQjuj/K8CEnlrFX6R5cKKgzzttsZDHyxQNL1jqMREjKN3ja/E3Q==", + "dev": true, + "requires": { + "errno": "^0.1.1", + "graceful-fs": "^4.1.2", + "image-size": "~0.5.0", + "make-dir": "^2.1.0", + "mime": "^1.4.1", + "native-request": "^1.0.5", + "source-map": "~0.6.0", + "tslib": "^1.10.0" + } + }, + "less-loader": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/less-loader/-/less-loader-6.2.0.tgz", + "integrity": "sha512-Cl5h95/Pz/PWub/tCBgT1oNMFeH1WTD33piG80jn5jr12T4XbxZcjThwNXDQ7AG649WEynuIzO4b0+2Tn9Qolg==", + "dev": true, + "requires": { + "clone": "^2.1.2", + "less": "^3.11.3", + "loader-utils": "^2.0.0", + "schema-utils": "^2.7.0" + } + }, "leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -11759,6 +11800,13 @@ } } }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "optional": true + }, "mime-db": { "version": "1.44.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz", @@ -12043,6 +12091,13 @@ "to-regex": "^3.0.1" } }, + "native-request": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/native-request/-/native-request-1.0.7.tgz", + "integrity": "sha512-9nRjinI9bmz+S7dgNtf4A70+/vPhnd+2krGpy4SUlADuOuSa24IDkNaZ+R/QT1wQ6S8jBdi6wE7fLekFZNfUpQ==", + "dev": true, + "optional": true + }, "natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -12167,8 +12222,7 @@ "normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" }, "normalize-url": { "version": "3.3.0", diff --git a/package.json b/package.json index e117fdd9..4e0da12d 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "dependencies": { "cosmiconfig": "^7.0.0", "loader-utils": "^2.0.0", + "normalize-path": "^3.0.0", "postcss": "^7.0.0", "schema-utils": "^2.7.0" }, @@ -67,6 +68,7 @@ "jest": "^26.2.2", "jest-junit": "^11.1.0", "jsdoc-to-markdown": "^6.0.1", + "less-loader": "^6.2.0", "lint-staged": "^10.2.11", "memfs": "^3.2.0", "midas": "^2.0.3", diff --git a/src/index.js b/src/index.js index 990fa935..2e23506c 100644 --- a/src/index.js +++ b/src/index.js @@ -8,7 +8,14 @@ import postcss from 'postcss'; import Warning from './Warning'; import SyntaxError from './Error'; import schema from './options.json'; -import { exec, loadConfig, getArrayPlugins } from './utils'; +import { + exec, + loadConfig, + getArrayPlugins, + getSourceMapAbsolutePath, + getSourceMapRelativePath, + normalizeSourceMap, +} from './utils'; /** * **PostCSS Loader** @@ -103,8 +110,18 @@ export default async function loader(content, sourceMap, meta = {}) { ? options.sourceMap : this.sourceMap; + const sourceMapNormalized = + sourceMap && useSourceMap ? normalizeSourceMap(sourceMap) : null; + + if (sourceMapNormalized) { + sourceMapNormalized.sources = sourceMapNormalized.sources.map((src) => + getSourceMapRelativePath(src, path.dirname(file)) + ); + } + const postcssOptions = { from: file, + to: file, map: useSourceMap ? options.sourceMap === 'inline' ? { inline: true, annotation: false } @@ -115,9 +132,8 @@ export default async function loader(content, sourceMap, meta = {}) { stringifier, }; - if (postcssOptions.map && sourceMap) { - postcssOptions.map.prev = - typeof sourceMap === 'string' ? JSON.parse(sourceMap) : sourceMap; + if (postcssOptions.map && sourceMapNormalized) { + postcssOptions.map.prev = sourceMapNormalized; } // Loader Exec (Deprecated) @@ -210,8 +226,13 @@ export default async function loader(content, sourceMap, meta = {}) { map = map ? map.toJSON() : null; if (map) { - map.file = path.resolve(map.file); - map.sources = map.sources.map((src) => path.resolve(src)); + if (typeof map.file !== 'undefined') { + delete map.file; + } + + map.sources = map.sources.map((src) => + getSourceMapAbsolutePath(src, postcssOptions.to) + ); } const ast = { diff --git a/src/utils.js b/src/utils.js index eb85578d..b120dc04 100644 --- a/src/utils.js +++ b/src/utils.js @@ -2,6 +2,8 @@ import path from 'path'; import Module from 'module'; +import normalizePath from 'normalize-path'; + import postcssPkg from 'postcss/package.json'; import { cosmiconfig } from 'cosmiconfig'; @@ -242,4 +244,75 @@ function getArrayPlugins(plugins, file, disabledPlugins, loaderContext) { return pluginsProcessing(plugins, file, disabledPlugins); } -export { exec, loadConfig, getArrayPlugins }; +// TODO Remove, when postcss 8 will be released +function normalizeSourceMap(map) { + let newMap = map; + + // Some loader emit source map as string + // Strip any JSON XSSI avoidance prefix from the string (as documented in the source maps specification), and then parse the string as JSON. + if (typeof newMap === 'string') { + newMap = JSON.parse(newMap); + } + + // Source maps should use forward slash because it is URLs (https://github.com/mozilla/source-map/issues/91) + // We should normalize path because previous loaders like `sass-loader` using backslash when generate source map + + if (newMap.file) { + delete newMap.file; + } + + const { sourceRoot } = newMap; + + if (newMap.sourceRoot) { + delete newMap.sourceRoot; + } + + if (newMap.sources) { + newMap.sources = newMap.sources.map((source) => { + return !sourceRoot + ? normalizePath(source) + : normalizePath(path.resolve(sourceRoot, source)); + }); + } + + return newMap; +} + +function getSourceMapRelativePath(file, from) { + if (file.indexOf('<') === 0) return file; + if (/^\w+:\/\//.test(file)) return file; + + const result = path.relative(from, file); + + if (path.sep === '\\') { + return result.replace(/\\/g, '/'); + } + + return result; +} + +function getSourceMapAbsolutePath(file, to) { + if (file.indexOf('<') === 0) return file; + if (/^\w+:\/\//.test(file)) return file; + + if (typeof to === 'undefined') return file; + + const dirname = path.dirname(to); + + const result = path.resolve(dirname, file); + + if (path.sep === '\\') { + return result.replace(/\\/g, '/'); + } + + return result; +} + +export { + exec, + loadConfig, + getArrayPlugins, + getSourceMapAbsolutePath, + getSourceMapRelativePath, + normalizeSourceMap, +}; diff --git a/test/fixtures/less/index.js b/test/fixtures/less/index.js new file mode 100644 index 00000000..1a1fbf6d --- /dev/null +++ b/test/fixtures/less/index.js @@ -0,0 +1,3 @@ +import style from './style.less' + +export default style diff --git a/test/fixtures/less/style.less b/test/fixtures/less/style.less new file mode 100644 index 00000000..b83cadd7 --- /dev/null +++ b/test/fixtures/less/style.less @@ -0,0 +1 @@ +a { color: coral } diff --git a/test/helpers/getCodeFromBundle.js b/test/helpers/getCodeFromBundle.js index 8d13834c..990b1eef 100644 --- a/test/helpers/getCodeFromBundle.js +++ b/test/helpers/getCodeFromBundle.js @@ -1,6 +1,6 @@ import normalizeMap from './normalizeMap'; -export default (id, stats) => { +export default (id, stats, processMap = true) => { const { modules } = stats.compilation; const module = modules.find((m) => m.id.endsWith(id)); const { _source } = module; @@ -21,5 +21,5 @@ export default (id, stats) => { const { css, map } = result; - return { css, map: normalizeMap(map) }; + return { css, map: processMap ? normalizeMap(map) : map }; }; diff --git a/test/options/__snapshots__/sourceMap.test.js.snap b/test/options/__snapshots__/sourceMap.test.js.snap index d9f7b6aa..cf3e262d 100644 --- a/test/options/__snapshots__/sourceMap.test.js.snap +++ b/test/options/__snapshots__/sourceMap.test.js.snap @@ -1,5 +1,29 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`Options Sourcemap should generated absolute paths in sourcemap: css 1`] = ` +"a { color: black } +" +`; + +exports[`Options Sourcemap should generated absolute paths in sourcemap: errors 1`] = `Array []`; + +exports[`Options Sourcemap should generated absolute paths in sourcemap: map 1`] = ` +Object { + "mappings": "AAAA,IAAI,aAAa", + "names": Array [], + "sources": Array [ + "xxx", + ], + "sourcesContent": Array [ + "a { color: black } +", + ], + "version": 3, +} +`; + +exports[`Options Sourcemap should generated absolute paths in sourcemap: warnings 1`] = `Array []`; + exports[`Options Sourcemap should work Sourcemap - {Boolean}: css 1`] = ` "a { color: black } " @@ -9,7 +33,6 @@ exports[`Options Sourcemap should work Sourcemap - {Boolean}: errors 1`] = `Arra exports[`Options Sourcemap should work Sourcemap - {Boolean}: map 1`] = ` Object { - "file": "x", "mappings": "AAAA,IAAI,aAAa", "names": Array [], "sources": Array [ @@ -28,7 +51,7 @@ exports[`Options Sourcemap should work Sourcemap - {Boolean}: warnings 1`] = `Ar exports[`Options Sourcemap should work Sourcemap - {String}: css 1`] = ` "a { color: black } -/*# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbInRlc3QvZml4dHVyZXMvY3NzL3N0eWxlLmNzcyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxJQUFJLGFBQWEiLCJmaWxlIjoidGVzdC9maXh0dXJlcy9jc3Mvc3R5bGUuY3NzIiwic291cmNlc0NvbnRlbnQiOlsiYSB7IGNvbG9yOiBibGFjayB9XG4iXX0= */" +/*# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbInN0eWxlLmNzcyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxJQUFJLGFBQWEiLCJmaWxlIjoic3R5bGUuY3NzIiwic291cmNlc0NvbnRlbnQiOlsiYSB7IGNvbG9yOiBibGFjayB9XG4iXX0= */" `; exports[`Options Sourcemap should work Sourcemap - {String}: errors 1`] = `Array []`; @@ -44,32 +67,53 @@ exports[`Options Sourcemap should work disable Sourcemap - {Boolean}: errors 1`] exports[`Options Sourcemap should work disable Sourcemap - {Boolean}: warnings 1`] = `Array []`; -exports[`Options Sourcemap should work with prev sourceMap: css 1`] = ` +exports[`Options Sourcemap should work with prev sourceMap (less-loader): css 1`] = ` "a { color: coral; -}" +} +" `; -exports[`Options Sourcemap should work with prev sourceMap: errors 1`] = `Array []`; +exports[`Options Sourcemap should work with prev sourceMap (less-loader): errors 1`] = `Array []`; -exports[`Options Sourcemap should work with prev sourceMap: map 1`] = ` +exports[`Options Sourcemap should work with prev sourceMap (less-loader): map 1`] = ` Object { - "file": "x", - "mappings": "AAAA;EAAI,YAAA;ACEJ", + "mappings": "AAAA;EAAI,YAAA;AAEJ", "names": Array [], "sources": Array [ "xxx", - "xxx", ], "sourcesContent": Array [ "a { color: coral } ", - "a { + ], + "version": 3, +} +`; + +exports[`Options Sourcemap should work with prev sourceMap (less-loader): warnings 1`] = `Array []`; + +exports[`Options Sourcemap should work with prev sourceMap (sass-loader): css 1`] = ` +"a { color: coral; -}", +}" +`; + +exports[`Options Sourcemap should work with prev sourceMap (sass-loader): errors 1`] = `Array []`; + +exports[`Options Sourcemap should work with prev sourceMap (sass-loader): map 1`] = ` +Object { + "mappings": "AAAA;EAAI,YAAA;AAEJ", + "names": Array [], + "sources": Array [ + "xxx", + ], + "sourcesContent": Array [ + "a { color: coral } +", ], "version": 3, } `; -exports[`Options Sourcemap should work with prev sourceMap: warnings 1`] = `Array []`; +exports[`Options Sourcemap should work with prev sourceMap (sass-loader): warnings 1`] = `Array []`; diff --git a/test/options/sourceMap.test.js b/test/options/sourceMap.test.js index d3574dbc..a50cc63c 100644 --- a/test/options/sourceMap.test.js +++ b/test/options/sourceMap.test.js @@ -1,3 +1,7 @@ +/** + * @jest-environment node + */ + import path from 'path'; import { @@ -53,7 +57,7 @@ describe('Options Sourcemap', () => { expect(getErrors(stats)).toMatchSnapshot('errors'); }); - it('should work with prev sourceMap', async () => { + it('should work with prev sourceMap (sass-loader)', async () => { const compiler = getCompiler( './scss/index.js', {}, @@ -94,4 +98,81 @@ describe('Options Sourcemap', () => { expect(getWarnings(stats)).toMatchSnapshot('warnings'); expect(getErrors(stats)).toMatchSnapshot('errors'); }); + + it('should work with prev sourceMap (less-loader)', async () => { + const compiler = getCompiler( + './less/index.js', + { + config: false, + }, + { + devtool: 'source-map', + module: { + rules: [ + { + test: /\.less$/i, + use: [ + { + loader: require.resolve('../helpers/testLoader'), + options: {}, + }, + { + loader: path.resolve(__dirname, '../../src'), + options: { + config: false, + }, + }, + { + loader: 'less-loader', + }, + ], + }, + ], + }, + } + ); + const stats = await compile(compiler); + + const codeFromBundle = getCodeFromBundle('style.less', stats); + + expect(codeFromBundle.css).toMatchSnapshot('css'); + expect(codeFromBundle.map).toMatchSnapshot('map'); + expect(getWarnings(stats)).toMatchSnapshot('warnings'); + expect(getErrors(stats)).toMatchSnapshot('errors'); + }); + + it('should generated absolute paths in sourcemap', async () => { + const compiler = getCompiler('./css/index.js', { + sourceMap: true, + }); + const stats = await compile(compiler); + + const codeFromBundle = getCodeFromBundle('style.css', stats); + const notNormalizecodeFromBundle = getCodeFromBundle( + 'style.css', + stats, + false + ); + + const { sources } = notNormalizecodeFromBundle.map; + const expectedFile = path.resolve( + __dirname, + '..', + 'fixtures', + 'css', + 'style.css' + ); + + const normalizePath = (src) => + path.sep === '\\' ? src.replace(/\\/g, '/') : src; + + sources.forEach((source) => + expect(source).toEqual(normalizePath(expectedFile)) + ); + + expect(codeFromBundle.css).toMatchSnapshot('css'); + expect(codeFromBundle.map).toMatchSnapshot('map'); + expect(getWarnings(stats)).toMatchSnapshot('warnings'); + expect(getErrors(stats)).toMatchSnapshot('errors'); + }); });