diff --git a/packages/babel/index.js b/packages/babel/index.js index 1eb67a51..24b160fc 100644 --- a/packages/babel/index.js +++ b/packages/babel/index.js @@ -3,6 +3,7 @@ const transform = require('babel-core').transform; const readPkg = require('read-pkg-up'); const flatten = require('flatten'); +const applySourceMap = require('@taskr/sourcemaps').applySourceMap; const BABEL_REGEX = /(^babel-)(preset|plugin)-(.*)/i; @@ -36,13 +37,19 @@ module.exports = function (task) { } }); } + if (file.sourceMap && opts.sourcemaps == null) { + opts.sourceMaps = true; + } // attach file's name opts.filename = file.base; const output = transform(file.data, opts); - if (output.map) { + if (file.sourceMap && output.map) { + applySourceMap(file, output.map); + } else if (output.map) { + // Backwards compatibility const map = `${file.base}.map`; // append `sourceMappingURL` to original file @@ -59,6 +66,6 @@ module.exports = function (task) { } // update file's data - file.data = new Buffer(output.code); + file.data = Buffer.from(output.code); }); }; diff --git a/packages/babel/package.json b/packages/babel/package.json index 3537c538..a0c77d4c 100644 --- a/packages/babel/package.json +++ b/packages/babel/package.json @@ -24,6 +24,7 @@ } ], "dependencies": { + "@taskr/sourcemaps": "^1.1.0", "babel-core": "^6.3.0", "flatten": "^1.0.2", "read-pkg-up": "^2.0.0" diff --git a/packages/babel/test/index.js b/packages/babel/test/index.js index e8c8629e..b185bbc6 100644 --- a/packages/babel/test/index.js +++ b/packages/babel/test/index.js @@ -8,7 +8,7 @@ const dir = join(__dirname, 'fixtures'); const tmp = join(__dirname, '.tmp'); test('@taskr/babel', t => { - t.plan(16); + t.plan(19); const src = `${dir}/a.js`; const want = '"use strict";\n\nObject.defineProperty(exports, "__esModule"'; @@ -84,10 +84,23 @@ test('@taskr/babel', t => { const str = yield f.$.read(`${tmp}/a.js`, 'utf8'); t.true(str.includes('System.register'), 'via `preload` + `presets`; keep detailed `presets` entry'); + yield f.clear(tmp); + }, + * g(f) { + yield f.source(`${dir}/*.js`, { sourcemaps: true }) + .babel({presets: ['es2015']}) + .target(tmp, { sourcemaps: '.' }); + + const arr = yield f.$.expand(`${tmp}/*`); + const str = yield f.$.read(`${tmp}/a.js`, 'utf8'); + t.equal(arr.length, 2, 'via `@taskr/sourcemaps`; create file & external sourcemap'); + t.true(/var a/.test(str), 'via `@taskr/sourcemaps`; transpile to es5 code'); + t.true(/sourceMappingURL/.test(str), 'via `@taskr/sourcemaps`; append `sourceMappingURL` link'); + yield f.clear(tmp); } } }); - taskr.serial(['a', 'b', 'c', 'd', 'e', 'f']); + taskr.serial(['a', 'b', 'c', 'd', 'e', 'f', 'g']); }); diff --git a/packages/sourcemaps/index.js b/packages/sourcemaps/index.js new file mode 100644 index 00000000..f14fb6bf --- /dev/null +++ b/packages/sourcemaps/index.js @@ -0,0 +1,244 @@ +'use strict'; + +const assert = require('assert'); +const path = require('path'); + +const Promise = require('bluebird'); +const co = Promise.coroutine; +const SourceMapGenerator = require('source-map').SourceMapGenerator; +const SourceMapConsumer = require('source-map').SourceMapConsumer; +const convertSourceMap = require('convert-source-map'); +const detectNewline = require('detect-newline'); + +const read = require('./utils/read'); + +const urlRegex = /^(https?|webpack(-[^:]+)?):\/\//; +const unixStylePath = value => value.split(path.sep).join('/'); +const isString = value => typeof value === 'string' || value instanceof String; +const isBoolean = value => typeof value === 'boolean' || value instanceof Boolean; + +/** + * Apply a source map to a taskr file, merging it into any existing sourcemaps + * + * @param {object} file + * @param {object|string} sourceMap + */ +function applySourceMap(file, sourceMap) { + if (isString(sourceMap) || Buffer.isBuffer(sourceMap)) { + sourceMap = JSON.parse(sourceMap); + } + if (file.sourceMap && isString(file.sourceMap)) { + file.sourceMap = JSON.parse(sourceMap); + } + + assert(sourceMap.file, 'Source map is missing file'); + assert(sourceMap.mappings, 'Source map is missing mappings'); + assert(sourceMap.sources, 'Source map is missing sources'); + + sourceMap.file = unixStylePath(sourceMap.file); + sourceMap.sources = sourceMap.sources.map(unixStylePath); + + if (file.sourceMap && file.sourceMap.mappings !== '') { + const generator = SourceMapGenerator.fromSourceMap(new SourceMapConsumer(sourceMap)); + generator.applySourceMap(new SourceMapConsumer(file.sourceMap)); + file.sourceMap = JSON.parse(generator.toString()); + } else { + file.sourceMap = sourceMap; + } +} + +function loadInlineSourceMap(file, sources) { + sources.map = convertSourceMap.fromSource(sources.data); + if (!sources.map) return; + + sources.map = sources.map.toObject(); + sources.path = file.dir; + sources.data = convertSourceMap.removeComments(sources.data); +} + +const loadExternalSourceMap = co(function * (file, sources) { + const comment = convertSourceMap.mapFileCommentRegex.exec(sources.data); + if (!comment) return; + + const mapFile = path.resolve(file.dir, comment[1] || comment[2]); + const sourceMap = yield read(mapFile, 'utf8'); + sources.map = JSON.parse(sourceMap); + sources.path = path.dirname(mapFile); + sources.data = convertSourceMap.removeMapFileComments(sources.data); +}); + +const fixSourceMap = co(function * (file, sources) { + const sourceMap = sources.map; + const sourcePath = sources.path; + + const fixSource = co(function * (source, i) { + let sourceContent = sourceMap.sourcesContent[i]; + const setContent = content => sourceMap.sourcesContent[i] = content; + + // Explicitly set falsy content to null + setContent(sourceContent || null); + + if (source.match(urlRegex)) return; + + // Update source path relative to file + let fullPath = path.resolve(sourcePath, source); + sourceMap.sources[i] = unixStylePath(path.relative(file.dir, fullPath)); + + if (sourceContent) return; + + // Load source content + if (sourceMap.sourceRoot) { + if (sourceMap.sourceRoot.match(urlRegex)) return; + + fullPath = path.resolve(sourcePath, sourceMap.sourceRoot, source); + } + + if (fullPath === path.join(file.dir, file.base)) { + sourceContent = file.data; + } else { + sourceContent = yield read(fullPath, 'utf8'); + } + + setContent(sourceContent); + }); + + yield Promise.all(sourceMap.sources.map(fixSource)); + + return sourceMap; +}); + +/** + * Load inline or external source map for a taskr file + * + * @param {object} file + * @returns {Promise.} sourcemap (if found) + */ +const loadSourceMap = co(function * (file) { + const sources = { + path: '', + map: null, + data: file.data.toString() + }; + + loadInlineSourceMap(file, sources); + if (!sources.map) { + yield loadExternalSourceMap(file, sources); + } + if (!sources.map) return; + + // Fix source map and remove map comment from file + const sourceMap = yield fixSourceMap(file, sources); + file.data = Buffer.from(sources.data); + + return sourceMap; +}); + +/** + * Initialize source maps for files, loading existing if specified + * + * @param {File[]} files + * @param {boolean|object} [opts] load option or opts object + * @param {boolean} [opts.load = false] + */ +const initSourceMaps = co(function * (files, opts) { + if (opts == null) opts = { load: false }; + if (isBoolean(opts)) opts = { load: opts }; + + const initSourceMap = co(function * (file) { + let sourceMap; + if (opts.load) { + sourceMap = yield loadSourceMap(file); + } + + if (!sourceMap) { + sourceMap = { + version: 3, + names: [], + mappings: '', + sources: [unixStylePath(file.base)], + sourcesContent: [file.data.toString()] + }; + } + + sourceMap.file = unixStylePath(file.base); + file.sourceMap = sourceMap; + }); + + yield Promise.all(files.map(initSourceMap)); +}); + +/** + * Write source maps for files, adding them inline or as external files + * + * ```js + * // Add source maps inline as comment in file + * writeSourceMaps(task) + * + * // Add source maps as external files (*.map) + * writeSourceMaps(task, '.'); + * writeSourceMaps(task, { dest: '.' }); + * ``` + * + * @param {File[]} files + * @param {string|object} [opts] destination or opts object + * @param {string} [opts.dest] destination, relative to file, to write source maps + */ +const writeSourceMaps = co(function * (files, opts) { + opts = opts || {}; + if (isString(opts)) opts = { dest: opts }; + + const writeSourceMap = co(function * (file) { + const sourceMap = file.sourceMap; + if (!sourceMap) return; + + sourceMap.file = unixStylePath(file.base); + sourceMap.sourceRoot = undefined; + + // Load content + const loadContent = co(function * (source, i) { + if (sourceMap.sourcesContent[i]) return; + + const sourcePath = path.resolve(file.dir, source); + sourcemaps.sourcesContent[i] = yield read(sourcePath); + }); + + sourceMap.sourcesContent = sourceMap.sourcesContent || []; + yield Promise.all(sourceMap.sources.map(loadContent)); + + const newline = detectNewline.graceful(file.data.toString() || ''); + const isCss = path.extname(file.base) === 'css'; + const toComment = isCss + ? data => `${newline}/*# ${data} */${newline}` + : data => `${newline}//# ${data}${newline}`; + + let comment; + if (opts.dest) { + const base = `${file.base}.map`; + const sourceMapPath = path.join(file.dir, opts.dest, base); + const dir = path.dirname(sourceMapPath); + const data = JSON.stringify(sourceMap); + + files.push({ base, dir, data }); + + const sourceMapUrl = unixStylePath(path.relative(file.dir, sourceMapPath)); + comment = toComment(`sourceMappingURL=${sourceMapUrl}`); + } else { + const base64 = Buffer.from(JSON.stringify(sourceMap)).toString('base64'); + comment = toComment(`sourceMappingURL=data:application/json;charset=utf8;base64,${base64}`); + } + + file.data = Buffer.concat([file.data, Buffer.from(comment)]); + }); + + yield Promise.all(files.map(writeSourceMap)); +}); + +module.exports = exports = function(task) { + task.plugin('initSourceMaps', { every: false, files: true }, initSourceMaps); + task.plugin('writeSourceMaps', { every: false, files: true }, writeSourceMaps); +}; + +exports.applySourceMap = applySourceMap; +exports.loadSourceMap = loadSourceMap; +exports.initSourceMaps = initSourceMaps; +exports.writeSourceMaps = writeSourceMaps; diff --git a/packages/sourcemaps/package.json b/packages/sourcemaps/package.json new file mode 100644 index 00000000..f61314e9 --- /dev/null +++ b/packages/sourcemaps/package.json @@ -0,0 +1,37 @@ +{ + "name": "@taskr/sourcemaps", + "version": "1.1.0", + "description": "Source maps with taskr", + "repository": "lukeed/taskr", + "license": "MIT", + "main": "index.js", + "files": [ + "index.js" + ], + "keywords": [ + "taskr", + "source", + "map", + "sourcemap" + ], + "scripts": { + "test": "tape test/*.js | tap-spec" + }, + "author": { + "name": "Luke Edwards", + "email": "luke.edwards05@gmail.com", + "url": "https://lukeed.com" + }, + "dependencies": { + "bluebird": "^3.5.0", + "convert-source-map": "^1.5.0", + "detect-newline": "^2.1.0", + "source-map": "^0.5.6" + }, + "engines": { + "node": ">= 4.6" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/sourcemaps/readme.md b/packages/sourcemaps/readme.md new file mode 100644 index 00000000..37423f9b --- /dev/null +++ b/packages/sourcemaps/readme.md @@ -0,0 +1,129 @@ +# @taskr/sourcemaps [![npm](https://img.shields.io/npm/v/@taskr/sourcemaps.svg)](https://npmjs.org/package/@taskr/sourcemaps) + +> Source maps with [Taskr](https://github.com/lukeed/taskr). + +## Install + +`@taskr/sourcemaps` is bundled with taskr for built-in source map support. For special cases where you want to initialize or write source maps outside of `task.source` or `task.target`, +you can install `@taskr/sourcemaps` and run as a plugin. For plugins, use `applySourceMap` to add source map support to your plugin. + +``` +# For standard usage, no need to install, bundled with taskr + +# For custom cases +npm install --save-dev @taskr/sourcemaps + +# For plugins +npm install --save @taskr/sourcemaps +``` + +## Usage + +Built-in support: + +```js +// 1. Initialize / load existing source maps +// 2. Write inline source maps +yield task.source('**/*.js', { sourcemaps: true }) + .babel() + .target('build'); + +// 1. Initialize / load existing source maps +// 2. Write external source maps (next to source files) +yield task.source('**/*.scss', { sourcemaps: true }) + .sass() + .target('build', { sourcemaps: '.' }); +``` + +Custom usage: + +```js +yield task.source('**/*.ts') + .typescript() + .initSourceMaps({ load: true }) + .babel() + .writeSourceMaps({ dest: '.' }) + .gzip() + .target('build'); +``` + +## Plugins + +```js +const applySourceMap = require('@taskr/sourcemaps').applySourceMap; +const transform = require('./transform'); + +module.exports = function(task, options) { + task.plugin('transform', { every: true }, function * (file) { + // If sourceMap is enabled for file, make source maps + if (file.sourceMap) { + options.makeSourceMap = true; + } + + const result = transform(file.data, options); + file.data = result.code; + + // Apply source map, merging with existing + if (file.sourceMap) { + applySourceMap(file, result.map); + } + }); +} +``` + +## API + +### .initSourceMaps(options) + +Initialize source maps for files, loading existing if specified + +#### options.load + +Type: `boolean`
+Default: `false` + +Load existing source maps for files (inline or external). + +### .writeSourceMaps(options) + +Write source maps for files, adding them inline or as external files. + +#### options + +Type: `string|object`
+Default: `undefined` + +Pass no options to write source maps inline, string for a destination, or an options object. + +#### options.dest + +Type: `string`
+Default: `undefined` + +Destination, relative to file, to write external source maps + +### applySourceMap(file, sourceMap) + +Apply a source map to a taskr file, merging it into any existing sourcemaps + +#### file + +Type: `object` + +Apply source map to this taskr file. If the file has an existing source map, the source map will be merged into it. + +#### sourceMap + +Type: `string|object` + +Source map to apply. For `string`, `JSON.parse` will be used before applying. `file`, `mappings`, and `sources` are required fields for the source map. + +## Support + +Any issues or questions can be sent to the [Taskr monorepo](https://github.com/lukeed/taskr/issues/new). + +Please be sure to specify that you are using `@taskr/sourcemaps`. + +## License + +MIT © [Luke Edwards](https://lukeed.com) diff --git a/packages/sourcemaps/test/fixtures/applied.js.map b/packages/sourcemaps/test/fixtures/applied.js.map new file mode 100644 index 00000000..1b0f4621 --- /dev/null +++ b/packages/sourcemaps/test/fixtures/applied.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["existing.js"],"names":["module","exports","sayHi","options","name"],"mappings":";;AAAAA,OAAOC,OAAP,GAAiB,SAASC,KAAT,CAAeC,OAAf,EAAwB;AAAA,QAC7BC,IAD6B,GACpBD,OADoB,CAC7BC,IAD6B;;AAErC,WAAQ,SAAQA,IAAK,GAArB;AACH,CAHD","file":"applied.js","sourcesContent":["module.exports = function sayHi(options) {\n const { name } = options;\n return `Howdy ${name}!`;\n};\n"]} \ No newline at end of file diff --git a/packages/sourcemaps/test/fixtures/existing.js.map b/packages/sourcemaps/test/fixtures/existing.js.map new file mode 100644 index 00000000..778b5a84 --- /dev/null +++ b/packages/sourcemaps/test/fixtures/existing.js.map @@ -0,0 +1 @@ +{"version":3,"file":"existing.js","sourceRoot":"","sources":["say-hi.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,OAAO,GAAG,eAAe,OAAyB;IACvD,MAAM,EAAE,IAAI,EAAE,GAAG,OAAO,CAAC;IACzB,MAAM,CAAC,SAAS,IAAI,GAAG,CAAC;AAC1B,CAAC,CAAC","sourcesContent":["module.exports = function sayHi(options: { name: string }): string {\n const { name } = options;\n return `Howdy ${name}!`;\n};\n"]} \ No newline at end of file diff --git a/packages/sourcemaps/test/fixtures/merged.js.map b/packages/sourcemaps/test/fixtures/merged.js.map new file mode 100644 index 00000000..5912ccea --- /dev/null +++ b/packages/sourcemaps/test/fixtures/merged.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["say-hi.ts"],"names":["module","exports","sayHi","options","name"],"mappings":";;AAAAA,OAAOC,OAAP,GAAiB,SAAAC,KAAA,CAAeC,OAAf,EAAwC;AAAA,QAC/CC,IAD+C,GACtCD,OADsC,CAC/CC,IAD+C;;AAEvD,WAAO,SAASA,IAAI,GAApB;AACD,CAHD","file":"applied.js","sourcesContent":["module.exports = function sayHi(options: { name: string }): string {\n const { name } = options;\n return `Howdy ${name}!`;\n};\n"]} \ No newline at end of file diff --git a/packages/sourcemaps/test/fixtures/say-hi-external.js b/packages/sourcemaps/test/fixtures/say-hi-external.js new file mode 100644 index 00000000..10ea3d81 --- /dev/null +++ b/packages/sourcemaps/test/fixtures/say-hi-external.js @@ -0,0 +1,6 @@ +module.exports = function sayHi(options) { + const { name } = options; + return `Howdy ${name}!`; +}; + +//# sourceMappingURL=say-hi-external.js.map diff --git a/packages/sourcemaps/test/fixtures/say-hi-external.js.map b/packages/sourcemaps/test/fixtures/say-hi-external.js.map new file mode 100644 index 00000000..fe583cdf --- /dev/null +++ b/packages/sourcemaps/test/fixtures/say-hi-external.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["say-hi.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,OAAO,GAAG,eAAe,OAAyB;IACxD,MAAM,EAAE,IAAI,EAAE,GAAG,OAAO,CAAC;IACzB,MAAM,CAAC,SAAS,IAAI,GAAG,CAAC;AACzB,CAAC,CAAC","file":"say-hi-external.js","sourcesContent":["module.exports = function sayHi(options: { name: string }): string {\n\tconst { name } = options;\n\treturn `Howdy ${name}!`;\n};\n"]} \ No newline at end of file diff --git a/packages/sourcemaps/test/fixtures/say-hi-internal.js b/packages/sourcemaps/test/fixtures/say-hi-internal.js new file mode 100644 index 00000000..7bf8abf0 --- /dev/null +++ b/packages/sourcemaps/test/fixtures/say-hi-internal.js @@ -0,0 +1,6 @@ +module.exports = function sayHi(options) { + const { name } = options; + return `Howdy ${name}!`; +}; + +//# sourceMappingURL=data:application/json;charset=utf8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbInNheS1oaS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxNQUFNLENBQUMsT0FBTyxHQUFHLGVBQWUsT0FBeUI7SUFDeEQsTUFBTSxFQUFFLElBQUksRUFBRSxHQUFHLE9BQU8sQ0FBQztJQUN6QixNQUFNLENBQUMsU0FBUyxJQUFJLEdBQUcsQ0FBQztBQUN6QixDQUFDLENBQUMiLCJmaWxlIjoic2F5LWhpLWludGVybmFsLmpzIiwic291cmNlc0NvbnRlbnQiOlsibW9kdWxlLmV4cG9ydHMgPSBmdW5jdGlvbiBzYXlIaShvcHRpb25zOiB7IG5hbWU6IHN0cmluZyB9KTogc3RyaW5nIHtcblx0Y29uc3QgeyBuYW1lIH0gPSBvcHRpb25zO1xuXHRyZXR1cm4gYEhvd2R5ICR7bmFtZX0hYDtcbn07XG4iXX0= diff --git a/packages/sourcemaps/test/fixtures/say-hi-internal.js.map b/packages/sourcemaps/test/fixtures/say-hi-internal.js.map new file mode 100644 index 00000000..67ea20f4 --- /dev/null +++ b/packages/sourcemaps/test/fixtures/say-hi-internal.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["say-hi.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,OAAO,GAAG,eAAe,OAAyB;IACxD,MAAM,EAAE,IAAI,EAAE,GAAG,OAAO,CAAC;IACzB,MAAM,CAAC,SAAS,IAAI,GAAG,CAAC;AACzB,CAAC,CAAC","file":"say-hi-internal.js","sourcesContent":["module.exports = function sayHi(options: { name: string }): string {\n\tconst { name } = options;\n\treturn `Howdy ${name}!`;\n};\n"]} \ No newline at end of file diff --git a/packages/sourcemaps/test/fixtures/say-hi.js b/packages/sourcemaps/test/fixtures/say-hi.js new file mode 100644 index 00000000..df853b7a --- /dev/null +++ b/packages/sourcemaps/test/fixtures/say-hi.js @@ -0,0 +1,4 @@ +module.exports = function sayHi(options) { + const { name } = options; + return `Howdy ${name}!`; +}; diff --git a/packages/sourcemaps/test/fixtures/say-hi.ts b/packages/sourcemaps/test/fixtures/say-hi.ts new file mode 100644 index 00000000..9cdc0e3e --- /dev/null +++ b/packages/sourcemaps/test/fixtures/say-hi.ts @@ -0,0 +1,4 @@ +module.exports = function sayHi(options: { name: string }): string { + const { name } = options; + return `Howdy ${name}!`; +}; diff --git a/packages/sourcemaps/test/index.js b/packages/sourcemaps/test/index.js new file mode 100644 index 00000000..7d6e9ff3 --- /dev/null +++ b/packages/sourcemaps/test/index.js @@ -0,0 +1,140 @@ +"use strict" + +const test = require("tape"); +const join = require("path").join; +const parse = require("path").parse; +const co = require("bluebird").coroutine; +const read = require('../utils/read'); + +const applySourceMap = require('../').applySourceMap; +const loadSourceMap = require('../').loadSourceMap; +const initSourceMaps = require('../').initSourceMaps; +const writeSourceMaps = require('../').writeSourceMaps; + +const fixture = file => join(__dirname, 'fixtures', file); + +const loadFixture = co(function * (file) { + return JSON.parse(yield read(fixture(file))); +}); +const prepareFile = co(function * (file, sourceMap) { + const filePath = fixture(file); + const parsed = parse(filePath); + + const prepared = { + dir: parsed.dir, + base: parsed.base, + data: yield read(filePath) + }; + + if (sourceMap) { + sourceMap = yield loadFixture(sourceMap); + applySourceMap(prepared, sourceMap); + } + + return prepared; +}); + +test("applySourceMap", co(function * (t) { + t.plan(4); + + let file, sourceMap; + + file = {}; + sourceMap = { file: 'a.js', mappings: ['...'], sources: ['b.js'] }; + applySourceMap(file, sourceMap); + + t.deepEqual(file.sourceMap, sourceMap, 'applies sourcemap'); + + file = {}; + sourceMap = { file: 'nested\\a.js', mappings: ['...'], sources: ['nested\\b.js'] }; + applySourceMap(file, sourceMap); + + t.equal(file.sourceMap.file, 'nested/a.js', 'normalizes source map file'); + t.equal(file.sourceMap.sources[0], 'nested/b.js', 'normalizes source map sources'); + + file = { + sourceMap: yield loadFixture('existing.js.map') + }; + sourceMap = yield loadFixture('applied.js.map'); + applySourceMap(file, sourceMap); + + t.deepEqual(file.sourceMap, yield loadFixture('merged.js.map'), 'merges source map into existing'); +})); + +test("loadSourceMap", co(function * (t) { + t.plan(3); + + let file, sourceMap, expected; + + file = yield prepareFile('say-hi-internal.js'); + sourceMap = yield loadSourceMap(file); + expected = yield loadFixture('say-hi-internal.js.map'); + + t.deepLooseEqual(sourceMap, expected, 'loads internal source map'); + + file = yield prepareFile('say-hi-external.js'); + sourceMap = yield loadSourceMap(file); + expected = yield loadFixture('say-hi-external.js.map'); + + t.deepLooseEqual(sourceMap, expected, 'loads external source map'); + + file = yield prepareFile('say-hi.js'); + sourceMap = yield loadSourceMap(file); + expected = undefined; + + t.equal(sourceMap, expected, 'returns undefined for no source map'); +})); + +test("initSourceMaps", co(function * (t) { + t.plan(6); + + const files = yield Promise.all([ + prepareFile('say-hi.js'), + prepareFile('say-hi-internal.js'), + prepareFile('say-hi-external.js') + ]); + + yield initSourceMaps(files); + t.equal(files[1].sourceMap.version, 3, 'initializes empty source map with version = 3'); + t.equal(files[1].sourceMap.mappings, '', 'initializes empty source map with empty mappings'); + t.deepEqual(files[1].sourceMap.sources, ['say-hi-internal.js'], 'initializes empty source map with sources'); + t.deepEqual(files[1].sourceMap.sourcesContent, [files[1].data.toString()], 'initializes empty source map with sourcesContent'); + + yield initSourceMaps(files, { load: true }); + t.deepEqual(files[1].sourceMap, yield loadFixture('say-hi-internal.js.map'), 'loads existing internal source map'); + t.deepEqual(files[2].sourceMap, yield loadFixture('say-hi-external.js.map'), 'loads existing external source map'); +})); + +test("writeSourceMaps", co(function * (t) { + t.plan(5); + + let file, files, data, sourceMap; + + file = yield prepareFile('say-hi.js', 'say-hi-external.js.map'); + file.base = 'say-hi-internal.js'; + files = [file]; + data = yield read(fixture('say-hi-internal.js')); + + yield writeSourceMaps(files); + t.deepEqual(file.data.toString(), data.toString(), 'writes inline sourcemap by default'); + + file = yield prepareFile('say-hi.js', 'say-hi-external.js.map'); + file.base = 'say-hi-external.js'; + files = [file]; + data = yield read(fixture('say-hi-external.js')); + sourceMap = yield read(fixture('say-hi-external.js.map')); + + yield writeSourceMaps(files, '.'); + t.deepEqual(file.data.toString(), data.toString(), 'writes external sourcemap comment with string option'); + t.deepEqual(files[1].data.toString(), sourceMap.toString(), 'writes external sourcemap with string option'); + + file = yield prepareFile('say-hi.js', 'say-hi-external.js.map'); + file.base = 'say-hi-external.js'; + files = [file]; + data = yield read(fixture('say-hi-external.js')); + sourceMap = yield read(fixture('say-hi-external.js.map')); + + yield writeSourceMaps(files, { dest: '.' }); + t.deepEqual(file.data.toString(), data.toString(), 'writes external sourcemap comment with options'); + t.deepEqual(files[1].data.toString(), sourceMap.toString(), 'writes external sourcemap with options') +})); diff --git a/packages/sourcemaps/utils/read.js b/packages/sourcemaps/utils/read.js new file mode 100644 index 00000000..8accb258 --- /dev/null +++ b/packages/sourcemaps/utils/read.js @@ -0,0 +1,20 @@ +'use strict'; + +// TODO Load directly from taskr/utils +// (introduces a cylical dependency currently) + +const fs = require('fs'); +const Promise = require('bluebird'); +const stat = Promise.promisify(fs.stat); +const read = Promise.promisify(fs.readFile); + +/** + * Return a file's contents. Will not read directory! + * @param {String} file The file's path. + * @param {Object|String} opts See `fs.readFile`. + * @yield {Buffer|String} + */ +module.exports = Promise.coroutine(function * (file, opts) { + const s = yield stat(file) + return s.isFile() ? yield read(file, opts) : null +}); diff --git a/packages/taskr/lib/task.js b/packages/taskr/lib/task.js index 149b3e7d..e64e23ab 100644 --- a/packages/taskr/lib/task.js +++ b/packages/taskr/lib/task.js @@ -6,6 +6,7 @@ const wrapp = require('./wrapp'); const util = require('./utils'); const boot = require('./boot'); const $ = require('./fn'); +const sourcemaps = require('@taskr/sourcemaps'); const RGX = /[\\|\/]/g; const co = Promise.coroutine; @@ -13,6 +14,8 @@ const normalize = p.normalize; const format = p.format; const parse = p.parse; const sep = p.sep; +const initSourceMaps = sourcemaps.initSourceMaps; +const writeSourceMaps = sourcemaps.writeSourceMaps; function Task(ctx) { // construct shape @@ -44,6 +47,8 @@ Task.prototype.run = co(function * (opts, func) { Task.prototype.source = co(function * (globs, opts) { globs = $.flatten($.toArray(globs)); + opts = opts || {}; + const files = yield this.$.expand(globs, opts); if (globs.length && !files.length) { @@ -64,11 +69,19 @@ Task.prototype.source = co(function * (globs, opts) { base: obj.base }; }); + + if (opts.sourcemaps) { + yield initSourceMaps(this._.files, opts.sourcemaps); + } }); Task.prototype.target = co(function * (dirs, opts) { dirs = $.flatten($.toArray(dirs)); - opts = opts || {}; + opts = opts || { sourcemaps: true }; + + if (opts.sourcemaps !== false) { + yield writeSourceMaps(this._.files, opts.sourcemaps); + } const files = this._.files; // using `watcher`? original globs passed as `prevs` diff --git a/packages/taskr/package.json b/packages/taskr/package.json index 0f969afc..59c89165 100644 --- a/packages/taskr/package.json +++ b/packages/taskr/package.json @@ -19,6 +19,7 @@ "taskr.d.ts" ], "dependencies": { + "@taskr/sourcemaps": "^1.1.0", "bluebird": "^3.5.0", "clor": "^5.1.0", "glob": "^7.1.2", diff --git a/packages/taskr/test/fixtures/sourcemaps/say-hi-external.js b/packages/taskr/test/fixtures/sourcemaps/say-hi-external.js new file mode 100644 index 00000000..10ea3d81 --- /dev/null +++ b/packages/taskr/test/fixtures/sourcemaps/say-hi-external.js @@ -0,0 +1,6 @@ +module.exports = function sayHi(options) { + const { name } = options; + return `Howdy ${name}!`; +}; + +//# sourceMappingURL=say-hi-external.js.map diff --git a/packages/taskr/test/fixtures/sourcemaps/say-hi-external.js.map b/packages/taskr/test/fixtures/sourcemaps/say-hi-external.js.map new file mode 100644 index 00000000..fe583cdf --- /dev/null +++ b/packages/taskr/test/fixtures/sourcemaps/say-hi-external.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["say-hi.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,OAAO,GAAG,eAAe,OAAyB;IACxD,MAAM,EAAE,IAAI,EAAE,GAAG,OAAO,CAAC;IACzB,MAAM,CAAC,SAAS,IAAI,GAAG,CAAC;AACzB,CAAC,CAAC","file":"say-hi-external.js","sourcesContent":["module.exports = function sayHi(options: { name: string }): string {\n\tconst { name } = options;\n\treturn `Howdy ${name}!`;\n};\n"]} \ No newline at end of file diff --git a/packages/taskr/test/fixtures/sourcemaps/say-hi-internal.js b/packages/taskr/test/fixtures/sourcemaps/say-hi-internal.js new file mode 100644 index 00000000..7bf8abf0 --- /dev/null +++ b/packages/taskr/test/fixtures/sourcemaps/say-hi-internal.js @@ -0,0 +1,6 @@ +module.exports = function sayHi(options) { + const { name } = options; + return `Howdy ${name}!`; +}; + +//# sourceMappingURL=data:application/json;charset=utf8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbInNheS1oaS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxNQUFNLENBQUMsT0FBTyxHQUFHLGVBQWUsT0FBeUI7SUFDeEQsTUFBTSxFQUFFLElBQUksRUFBRSxHQUFHLE9BQU8sQ0FBQztJQUN6QixNQUFNLENBQUMsU0FBUyxJQUFJLEdBQUcsQ0FBQztBQUN6QixDQUFDLENBQUMiLCJmaWxlIjoic2F5LWhpLWludGVybmFsLmpzIiwic291cmNlc0NvbnRlbnQiOlsibW9kdWxlLmV4cG9ydHMgPSBmdW5jdGlvbiBzYXlIaShvcHRpb25zOiB7IG5hbWU6IHN0cmluZyB9KTogc3RyaW5nIHtcblx0Y29uc3QgeyBuYW1lIH0gPSBvcHRpb25zO1xuXHRyZXR1cm4gYEhvd2R5ICR7bmFtZX0hYDtcbn07XG4iXX0= diff --git a/packages/taskr/test/fixtures/sourcemaps/say-hi-internal.js.map b/packages/taskr/test/fixtures/sourcemaps/say-hi-internal.js.map new file mode 100644 index 00000000..67ea20f4 --- /dev/null +++ b/packages/taskr/test/fixtures/sourcemaps/say-hi-internal.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["say-hi.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,OAAO,GAAG,eAAe,OAAyB;IACxD,MAAM,EAAE,IAAI,EAAE,GAAG,OAAO,CAAC;IACzB,MAAM,CAAC,SAAS,IAAI,GAAG,CAAC;AACzB,CAAC,CAAC","file":"say-hi-internal.js","sourcesContent":["module.exports = function sayHi(options: { name: string }): string {\n\tconst { name } = options;\n\treturn `Howdy ${name}!`;\n};\n"]} \ No newline at end of file diff --git a/packages/taskr/test/fixtures/sourcemaps/say-hi.js b/packages/taskr/test/fixtures/sourcemaps/say-hi.js new file mode 100644 index 00000000..df853b7a --- /dev/null +++ b/packages/taskr/test/fixtures/sourcemaps/say-hi.js @@ -0,0 +1,4 @@ +module.exports = function sayHi(options) { + const { name } = options; + return `Howdy ${name}!`; +}; diff --git a/packages/taskr/test/fixtures/sourcemaps/say-hi.js.map b/packages/taskr/test/fixtures/sourcemaps/say-hi.js.map new file mode 100644 index 00000000..67ea20f4 --- /dev/null +++ b/packages/taskr/test/fixtures/sourcemaps/say-hi.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["say-hi.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,OAAO,GAAG,eAAe,OAAyB;IACxD,MAAM,EAAE,IAAI,EAAE,GAAG,OAAO,CAAC;IACzB,MAAM,CAAC,SAAS,IAAI,GAAG,CAAC;AACzB,CAAC,CAAC","file":"say-hi-internal.js","sourcesContent":["module.exports = function sayHi(options: { name: string }): string {\n\tconst { name } = options;\n\treturn `Howdy ${name}!`;\n};\n"]} \ No newline at end of file diff --git a/packages/taskr/test/fixtures/sourcemaps/say-hi.ts b/packages/taskr/test/fixtures/sourcemaps/say-hi.ts new file mode 100644 index 00000000..9cdc0e3e --- /dev/null +++ b/packages/taskr/test/fixtures/sourcemaps/say-hi.ts @@ -0,0 +1,4 @@ +module.exports = function sayHi(options: { name: string }): string { + const { name } = options; + return `Howdy ${name}!`; +}; diff --git a/packages/taskr/test/task.js b/packages/taskr/test/task.js index e4352b43..a02d0234 100644 --- a/packages/taskr/test/task.js +++ b/packages/taskr/test/task.js @@ -3,6 +3,7 @@ const Promise = require("bluebird") const join = require("path").join const test = require("tape") +const applySourceMap = require("@taskr/sourcemaps").applySourceMap; const del = require("./helpers").del const isMode = require("./helpers").isMode @@ -72,10 +73,11 @@ test("task.constructor (internal)", co(function * (t) { })) test("task.source", co(function * (t) { - t.plan(18) + t.plan(23) const glob1 = ["*.a", "*.b", "*.c"] const glob2 = join(fixtures, "*.*") + const glob3 = join(fixtures, "sourcemaps/*.js") const opts1 = { ignore: "foo" } const taskr = new Taskr({ @@ -105,6 +107,21 @@ test("task.source", co(function * (t) { *baz(f) { yield f.source(glob2, { ignore: join(fixtures, "taskfile.js") }) t.equal(f._.files.length, 3, "tunnels options to `utils.expand` (ignore)") + }, + *qux(f) { + yield f.source(glob3) + t.false(f._.files[0].sourceMap, "does not initialize source maps by default") + + yield f.source(glob3, { sourcemaps: true }) + const external = f._.files[0] + const inline = f._.files[1] + const none = f._.files[2] + t.deepEqual(external.sourceMap.sources, ["say-hi.ts"], "loads external source map") + t.deepEqual(inline.sourceMap.sources, ["say-hi.ts"], "loads inline source map") + t.equal(none.sourceMap.version, 3, "initializes empty source map") + + yield f.source(glob3, { sourcemaps: { load: false }}) + t.deepEqual(f._.files[0].sourceMap.sources, ["say-hi-external.js"], "passes options to source map") } } }) @@ -115,15 +132,16 @@ test("task.source", co(function * (t) { t.deepEqual(o, opts1, "warning receives the `expand` options") }) - yield taskr.parallel(["foo", "bar", "baz"]) + yield taskr.parallel(["foo", "bar", "baz", "qux"]) })) test("task.target", co(function * (t) { - t.plan(11) + t.plan(13) const glob1 = join(fixtures, "one", "two", "*.md") const glob2 = join(fixtures, "one", "*.md") const glob3 = join(fixtures, "**", "*.md") const glob4 = join(fixtures, "one", "**", "*.md") + const glob5 = join(fixtures, "sourcemaps/*.js"); const dest1 = join(fixtures, ".tmp1") const dest2 = join(fixtures, ".tmp2") @@ -131,9 +149,11 @@ test("task.target", co(function * (t) { const dest4 = join(fixtures, ".tmp4") const dest5 = join(fixtures, ".tmp5") const dest6 = join(fixtures, ".tmp6") + const dest7 = join(fixtures, ".tmp7") + const dest8 = join(fixtures, ".tmp8") // clean slate - yield del([dest1, dest2, dest3, dest4, dest5]) + yield del([dest1, dest2, dest3, dest4, dest5, dest6, dest7, dest8]) const taskr = new Taskr({ plugins: [{ @@ -142,6 +162,13 @@ test("task.target", co(function * (t) { *func(all) { this._.files = [{ dir: all[0].dir, base: "fake.foo", data: new Buffer("bar") }] } + }, { + every: false, + name: "fakeSourceMap", + *func(all) { + const sourceMap = yield this.$.read(join(fixtures, "sourcemaps/say-hi.js.map")); + applySourceMap(all[2], sourceMap); + } }], tasks: { *a(f) { @@ -188,13 +215,26 @@ test("task.target", co(function * (t) { yield f.source(src).target(dest1, {mode: 0o755}) const isExe = yield isMode(`${dest1}/bar.txt`, 755) t.true(isExe, "pass `mode` option to `utils.write`") + }, + *g(f) { + yield f.source(glob5, { sourcemaps: true }).fakeSourceMap().target(dest7) + const arr1 = yield f.$.expand(join(dest7, "*.*")) + const f1 = yield f.$.read(join(dest7, "say-hi-external.js")) + const hasInlineSourceMap = f1.toString().indexOf("sourceMappingURL=data") >= 0 + t.true(arr1.length === 3 && hasInlineSourceMap, "write inline source maps"); + + yield f.source(glob5, { sourcemaps: true }).fakeSourceMap().target(dest8, { sourcemaps: '.' }) + const arr2 = yield f.$.expand(join(dest8, "*.*")) + const f2 = yield f.$.read(join(dest8, "say-hi-internal.js")) + const hasExternalSourceMap = f2.toString().indexOf("sourceMappingURL=say-hi-internal.js.map") >= 0 + t.true(arr2.length === 6 && hasExternalSourceMap, "write external source maps") } } }) - yield taskr.parallel(["a", "b", "c", "d", "e", "f"]) + yield taskr.parallel(["a", "b", "c", "d", "e", "f", "g"]) // clean up - yield del([dest1, dest2, dest3, dest4, dest5, dest6]) + yield del([dest1, dest2, dest3, dest4, dest5, dest6, dest7, dest8]) })) test("task.run (w/function)", co(function * (t) {