diff --git a/src/evaluator.js b/src/evaluator.js index 1bc152c..ffa674c 100644 --- a/src/evaluator.js +++ b/src/evaluator.js @@ -56,14 +56,13 @@ function resolveRequests(context, possibleRequests, resolve) { async function getDependencies( code, + filepath, loaderContext, resolve, options, parcelOptions, seen = new Set() ) { - const filepath = loaderContext.resourcePath; - seen.add(filepath); nodes.filename = filepath; @@ -145,6 +144,7 @@ async function getDependencies( for (const [importPath, resolvedPath] of await getDependencies( source, + detected, loaderContext, resolveFilename, options @@ -171,7 +171,13 @@ export default async function createEvaluator(code, options, loaderContext) { const possibleImports = ( await Promise.all( [code, optionsImports].map((content) => - getDependencies(content, loaderContext, resolveFilename, options) + getDependencies( + content, + loaderContext.resourcePath, + loaderContext, + resolveFilename, + options + ) ) ) ).reduce((acc, map) => { diff --git a/src/index.js b/src/index.js index 6c45ef3..e713aea 100644 --- a/src/index.js +++ b/src/index.js @@ -1,5 +1,3 @@ -import { promises as fs } from 'fs'; - import stylus from 'stylus'; import { getOptions } from 'loader-utils'; @@ -7,7 +5,7 @@ import validateOptions from 'schema-utils'; import schema from './options.json'; import createEvaluator from './evaluator'; -import { getStylusOptions } from './utils'; +import { getStylusOptions, readFile, normalizeSourceMap } from './utils'; import resolver from './lib/resolver'; export default async function stylusLoader(source) { @@ -27,9 +25,9 @@ export default async function stylusLoader(source) { if (useSourceMap) { stylusOptions.sourcemap = { - content: true, comment: false, sourceRoot: this.rootContext, + basePath: this.rootContext, }; } @@ -105,21 +103,22 @@ export default async function stylusLoader(source) { } } - if (styl.sourcemap) { - delete styl.sourcemap.file; - - // load source file contents into source map - if (stylusOptions.sourcemap && stylusOptions.sourcemap.content) { - try { - styl.sourcemap.sourcesContent = await Promise.all( - styl.sourcemap.sources.map((file) => fs.readFile(file, 'utf-8')) - ); - } catch (e) { - return callback(e); - } + let map = styl.sourcemap; + + if (map && useSourceMap) { + map = normalizeSourceMap(map, this.rootContext); + + try { + map.sourcesContent = await Promise.all( + map.sources.map(async (file) => + (await readFile(this.fs, file)).toString() + ) + ); + } catch (errorFs) { + return callback(errorFs); } } - return callback(null, css, styl.sourcemap); + return callback(null, css, map); }); } diff --git a/src/utils.js b/src/utils.js index 5bb1bad..a53de5c 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,3 +1,5 @@ +import path from 'path'; + import { klona } from 'klona/full'; function getStylusOptions(loaderContext, loaderOptions) { @@ -30,9 +32,9 @@ function getStylusOptions(loaderContext, loaderOptions) { return stylusOptions; } -function readFile(inputFileSystem, path) { +function readFile(inputFileSystem, filepath) { return new Promise((resolve, reject) => { - inputFileSystem.readFile(path, (err, stats) => { + inputFileSystem.readFile(filepath, (err, stats) => { if (err) { reject(err); } @@ -41,4 +43,49 @@ function readFile(inputFileSystem, path) { }); } -export { getStylusOptions, readFile }; +const IS_NATIVE_WIN32_PATH = /^[a-z]:[/\\]|^\\\\/i; +const ABSOLUTE_SCHEME = /^[A-Za-z0-9+\-.]+:/; + +function getURLType(source) { + if (source[0] === '/') { + if (source[1] === '/') { + return 'scheme-relative'; + } + + return 'path-absolute'; + } + + if (IS_NATIVE_WIN32_PATH.test(source)) { + return 'path-absolute'; + } + + return ABSOLUTE_SCHEME.test(source) ? 'absolute' : 'path-relative'; +} + +function normalizeSourceMap(map, rootContext) { + const newMap = map; + + // result.map.file is an optional property that provides the output filename. + // Since we don't know the final filename in the webpack build chain yet, it makes no sense to have it. + // eslint-disable-next-line no-param-reassign + delete newMap.file; + + // eslint-disable-next-line no-param-reassign + newMap.sourceRoot = ''; + + // eslint-disable-next-line no-param-reassign + newMap.sources = newMap.sources.map((source) => { + const sourceType = getURLType(source); + + // Do no touch `scheme-relative`, `path-absolute` and `absolute` types + if (sourceType === 'path-relative') { + return path.resolve(rootContext, path.normalize(source)); + } + + return source; + }); + + return newMap; +} + +export { getStylusOptions, readFile, normalizeSourceMap }; diff --git a/test/__snapshots__/sourceMap-options.test.js.snap b/test/__snapshots__/sourceMap-options.test.js.snap new file mode 100644 index 0000000..b89eb3f --- /dev/null +++ b/test/__snapshots__/sourceMap-options.test.js.snap @@ -0,0 +1,235 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`"sourceMap" options should generate source maps when the "devtool" value is "source-map": css 1`] = ` +".other { + font-family: serif; +} +body { + font: 12px Helvetica, Arial, sans-serif; +} +a.button { + -webkit-border-radius: 5px; + -moz-border-radius: 5px; + border-radius: 5px; +} +.some-class { + margin: 10px; +} +" +`; + +exports[`"sourceMap" options should generate source maps when the "devtool" value is "source-map": errors 1`] = `Array []`; + +exports[`"sourceMap" options should generate source maps when the "devtool" value is "source-map": source map 1`] = ` +Object { + "mappings": "AAAA;EACC,aAAY,MAAZ;;ACID;EACE,MAAmB,kCAAnB;;AAEF;EAPE,uBAAsB,IAAtB;EACA,oBAAmB,IAAnB;EACA,eAAc,IAAd;;ACAF;EACE,QAAQ,KAAR", + "names": Array [], + "sourceRoot": "", + "sources": Array [ + "test/fixtures/paths/in-paths.styl", + "test/fixtures/basic.styl", + "test/fixtures/source-map.styl", + ], + "sourcesContent": Array [ + ".other + font-family serif", + "border-radius() + -webkit-border-radius arguments + -moz-border-radius arguments + border-radius arguments + +body + font 12px Helvetica, Arial, sans-serif + +a.button + border-radius 5px", + "@import 'in-paths' +@import 'basic' + +.some-class + margin: 10px +", + ], + "version": 3, +} +`; + +exports[`"sourceMap" options should generate source maps when the "devtool" value is "source-map": warnings 1`] = `Array []`; + +exports[`"sourceMap" options should generate source maps when value is "true" and the "devtool" value is "false": css 1`] = ` +".other { + font-family: serif; +} +body { + font: 12px Helvetica, Arial, sans-serif; +} +a.button { + -webkit-border-radius: 5px; + -moz-border-radius: 5px; + border-radius: 5px; +} +.some-class { + margin: 10px; +} +" +`; + +exports[`"sourceMap" options should generate source maps when value is "true" and the "devtool" value is "false": errors 1`] = `Array []`; + +exports[`"sourceMap" options should generate source maps when value is "true" and the "devtool" value is "false": source map 1`] = ` +Object { + "mappings": "AAAA;EACC,aAAY,MAAZ;;ACID;EACE,MAAmB,kCAAnB;;AAEF;EAPE,uBAAsB,IAAtB;EACA,oBAAmB,IAAnB;EACA,eAAc,IAAd;;ACAF;EACE,QAAQ,KAAR", + "names": Array [], + "sourceRoot": "", + "sources": Array [ + "test/fixtures/paths/in-paths.styl", + "test/fixtures/basic.styl", + "test/fixtures/source-map.styl", + ], + "sourcesContent": Array [ + ".other + font-family serif", + "border-radius() + -webkit-border-radius arguments + -moz-border-radius arguments + border-radius arguments + +body + font 12px Helvetica, Arial, sans-serif + +a.button + border-radius 5px", + "@import 'in-paths' +@import 'basic' + +.some-class + margin: 10px +", + ], + "version": 3, +} +`; + +exports[`"sourceMap" options should generate source maps when value is "true" and the "devtool" value is "false": warnings 1`] = `Array []`; + +exports[`"sourceMap" options should generate source maps when value is "true": css 1`] = ` +".other { + font-family: serif; +} +body { + font: 12px Helvetica, Arial, sans-serif; +} +a.button { + -webkit-border-radius: 5px; + -moz-border-radius: 5px; + border-radius: 5px; +} +.some-class { + margin: 10px; +} +" +`; + +exports[`"sourceMap" options should generate source maps when value is "true": errors 1`] = `Array []`; + +exports[`"sourceMap" options should generate source maps when value is "true": source map 1`] = ` +Object { + "mappings": "AAAA;EACC,aAAY,MAAZ;;ACID;EACE,MAAmB,kCAAnB;;AAEF;EAPE,uBAAsB,IAAtB;EACA,oBAAmB,IAAnB;EACA,eAAc,IAAd;;ACAF;EACE,QAAQ,KAAR", + "names": Array [], + "sourceRoot": "", + "sources": Array [ + "test/fixtures/paths/in-paths.styl", + "test/fixtures/basic.styl", + "test/fixtures/source-map.styl", + ], + "sourcesContent": Array [ + ".other + font-family serif", + "border-radius() + -webkit-border-radius arguments + -moz-border-radius arguments + border-radius arguments + +body + font 12px Helvetica, Arial, sans-serif + +a.button + border-radius 5px", + "@import 'in-paths' +@import 'basic' + +.some-class + margin: 10px +", + ], + "version": 3, +} +`; + +exports[`"sourceMap" options should generate source maps when value is "true": warnings 1`] = `Array []`; + +exports[`"sourceMap" options should not generate source maps when the "devtool" value is "false": css 1`] = ` +".other { + font-family: serif; +} +body { + font: 12px Helvetica, Arial, sans-serif; +} +a.button { + -webkit-border-radius: 5px; + -moz-border-radius: 5px; + border-radius: 5px; +} +.some-class { + margin: 10px; +} +" +`; + +exports[`"sourceMap" options should not generate source maps when the "devtool" value is "false": errors 1`] = `Array []`; + +exports[`"sourceMap" options should not generate source maps when the "devtool" value is "false": warnings 1`] = `Array []`; + +exports[`"sourceMap" options should not generate source maps when value is "false" and the "devtool" value is "source-map": css 1`] = ` +".other { + font-family: serif; +} +body { + font: 12px Helvetica, Arial, sans-serif; +} +a.button { + -webkit-border-radius: 5px; + -moz-border-radius: 5px; + border-radius: 5px; +} +.some-class { + margin: 10px; +} +" +`; + +exports[`"sourceMap" options should not generate source maps when value is "false" and the "devtool" value is "source-map": errors 1`] = `Array []`; + +exports[`"sourceMap" options should not generate source maps when value is "false" and the "devtool" value is "source-map": warnings 1`] = `Array []`; + +exports[`"sourceMap" options should not generate source maps when value is "false": css 1`] = ` +".other { + font-family: serif; +} +body { + font: 12px Helvetica, Arial, sans-serif; +} +a.button { + -webkit-border-radius: 5px; + -moz-border-radius: 5px; + border-radius: 5px; +} +.some-class { + margin: 10px; +} +" +`; + +exports[`"sourceMap" options should not generate source maps when value is "false": errors 1`] = `Array []`; + +exports[`"sourceMap" options should not generate source maps when value is "false": warnings 1`] = `Array []`; diff --git a/test/fixtures/source-map.styl b/test/fixtures/source-map.styl new file mode 100644 index 0000000..3928b45 --- /dev/null +++ b/test/fixtures/source-map.styl @@ -0,0 +1,5 @@ +@import 'in-paths' +@import 'basic' + +.some-class + margin: 10px diff --git a/test/sourceMap-options.test.js b/test/sourceMap-options.test.js new file mode 100644 index 0000000..e7496c5 --- /dev/null +++ b/test/sourceMap-options.test.js @@ -0,0 +1,175 @@ +import path from 'path'; +import fs from 'fs'; + +import { + compile, + getCodeFromBundle, + getCompiler, + getErrors, + getWarnings, +} from './helpers'; + +describe('"sourceMap" options', () => { + it('should generate source maps when value is "true"', async () => { + const testId = './source-map.styl'; + const compiler = getCompiler(testId, { + sourceMap: true, + stylusOptions: { + paths: ['test/fixtures/paths'], + }, + }); + const stats = await compile(compiler); + const codeFromBundle = getCodeFromBundle(stats, compiler); + const { css, map } = codeFromBundle; + + map.sourceRoot = ''; + map.sources = map.sources.map((source) => { + expect(path.isAbsolute(source)).toBe(true); + expect(source).toBe(path.normalize(source)); + expect(fs.existsSync(path.resolve(map.sourceRoot, source))).toBe(true); + + return path + .relative(path.resolve(__dirname, '..'), source) + .replace(/\\/g, '/'); + }); + + expect(css).toMatchSnapshot('css'); + expect(map).toMatchSnapshot('source map'); + expect(getWarnings(stats)).toMatchSnapshot('warnings'); + expect(getErrors(stats)).toMatchSnapshot('errors'); + }); + + it('should generate source maps when the "devtool" value is "source-map"', async () => { + const testId = './source-map.styl'; + const compiler = getCompiler( + testId, + { + stylusOptions: { + paths: ['test/fixtures/paths'], + }, + }, + { + devtool: 'source-map', + } + ); + const stats = await compile(compiler); + const codeFromBundle = getCodeFromBundle(stats, compiler); + const { css, map } = codeFromBundle; + + map.sourceRoot = ''; + map.sources = map.sources.map((source) => { + expect(path.isAbsolute(source)).toBe(true); + expect(source).toBe(path.normalize(source)); + expect(fs.existsSync(path.resolve(map.sourceRoot, source))).toBe(true); + + return path + .relative(path.resolve(__dirname, '..'), source) + .replace(/\\/g, '/'); + }); + + expect(css).toMatchSnapshot('css'); + expect(map).toMatchSnapshot('source map'); + expect(getWarnings(stats)).toMatchSnapshot('warnings'); + expect(getErrors(stats)).toMatchSnapshot('errors'); + }); + + it('should generate source maps when value is "true" and the "devtool" value is "false"', async () => { + const testId = './source-map.styl'; + const compiler = getCompiler( + testId, + { + sourceMap: true, + stylusOptions: { + paths: ['test/fixtures/paths'], + }, + }, + { + devtool: false, + } + ); + const stats = await compile(compiler); + const codeFromBundle = getCodeFromBundle(stats, compiler); + const { css, map } = codeFromBundle; + + map.sourceRoot = ''; + map.sources = map.sources.map((source) => { + expect(path.isAbsolute(source)).toBe(true); + expect(source).toBe(path.normalize(source)); + expect(fs.existsSync(path.resolve(map.sourceRoot, source))).toBe(true); + + return path + .relative(path.resolve(__dirname, '..'), source) + .replace(/\\/g, '/'); + }); + + expect(css).toMatchSnapshot('css'); + expect(map).toMatchSnapshot('source map'); + expect(getWarnings(stats)).toMatchSnapshot('warnings'); + expect(getErrors(stats)).toMatchSnapshot('errors'); + }); + + it('should not generate source maps when value is "false"', async () => { + const testId = './source-map.styl'; + const compiler = getCompiler(testId, { + sourceMap: false, + stylusOptions: { + paths: ['test/fixtures/paths'], + }, + }); + const stats = await compile(compiler); + const codeFromBundle = getCodeFromBundle(stats, compiler); + const { css, map } = codeFromBundle; + + expect(css).toMatchSnapshot('css'); + expect(map).toBeUndefined(); + expect(getWarnings(stats)).toMatchSnapshot('warnings'); + expect(getErrors(stats)).toMatchSnapshot('errors'); + }); + + it('should not generate source maps when the "devtool" value is "false"', async () => { + const testId = './source-map.styl'; + const compiler = getCompiler( + testId, + { + stylusOptions: { + paths: ['test/fixtures/paths'], + }, + }, + { + devtool: false, + } + ); + const stats = await compile(compiler); + const codeFromBundle = getCodeFromBundle(stats, compiler); + const { css, map } = codeFromBundle; + + expect(css).toMatchSnapshot('css'); + expect(map).toBeUndefined(); + expect(getWarnings(stats)).toMatchSnapshot('warnings'); + expect(getErrors(stats)).toMatchSnapshot('errors'); + }); + + it('should not generate source maps when value is "false" and the "devtool" value is "source-map"', async () => { + const testId = './source-map.styl'; + const compiler = getCompiler( + testId, + { + sourceMap: false, + stylusOptions: { + paths: ['test/fixtures/paths'], + }, + }, + { + devtool: 'source-map', + } + ); + const stats = await compile(compiler); + const codeFromBundle = getCodeFromBundle(stats, compiler); + const { css, map } = codeFromBundle; + + expect(css).toMatchSnapshot('css'); + expect(map).toBeUndefined(); + expect(getWarnings(stats)).toMatchSnapshot('warnings'); + expect(getErrors(stats)).toMatchSnapshot('errors'); + }); +});