diff --git a/README.md b/README.md index 9c4ee52..8405d43 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,8 @@ -# rollup-plugin-sourcemaps - -[![npm](https://img.shields.io/npm/v/rollup-plugin-sourcemaps.svg)](https://www.npmjs.com/package/rollup-plugin-sourcemaps) -[![Build Status](https://img.shields.io/travis/maxdavidson/rollup-plugin-sourcemaps/master.svg)](https://travis-ci.org/maxdavidson/rollup-plugin-sourcemaps) -[![Coverage Status](https://img.shields.io/coveralls/maxdavidson/rollup-plugin-sourcemaps/master.svg)](https://coveralls.io/github/maxdavidson/rollup-plugin-sourcemaps?branch=master) - +# rollup-plugin-include-sourcemaps [Rollup](https://rollupjs.org) plugin for loading files with existing source maps. Inspired by [webpack/source-map-loader](https://github.com/webpack/source-map-loader). -Works with rollup 0.31.2 or later. - If you use [rollup-plugin-babel](https://github.com/rollup/rollup-plugin-babel), you might be able to use the [`inputSourceMap`](https://babeljs.io/docs/en/options#inputsourcemap) option instead of this plugin. @@ -21,7 +14,7 @@ you might be able to use the [`inputSourceMap`](https://babeljs.io/docs/en/optio ## Usage ```javascript -import sourcemaps from 'rollup-plugin-sourcemaps'; +import sourcemaps from 'rollup-plugin-include-sourcemaps'; export default { input: 'src/index.js', diff --git a/package.json b/package.json index fdeb885..6eda471 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { - "name": "rollup-plugin-sourcemaps", - "version": "0.6.3", + "name": "rollup-plugin-include-sourcemaps", + "version": "0.7.0", "description": "Rollup plugin for grabbing source maps from sourceMappingURLs", "author": "Max Davidson ", "license": "MIT", @@ -16,7 +16,7 @@ "sideEffects": false, "repository": { "type": "git", - "url": "https://github.com/maxdavidson/rollup-plugin-sourcemaps" + "url": "https://github.com/IIIMADDINIII/rollup-plugin-include-sourcemaps" }, "engines": { "node": ">=10.0.0" @@ -40,12 +40,15 @@ "dist" ], "dependencies": { - "@rollup/pluginutils": "^3.0.9", - "source-map-resolve": "^0.6.0" + "@rollup/pluginutils": "^5.0.2", + "decode-uri-component": "^0.2.0", + "atob": "^2.1.2" }, "devDependencies": { "@rollup/plugin-typescript": "6.0.0", - "@types/node": "^10.17.21", + "@types/node": "^18.11.9", + "@types/decode-uri-component": "^0.2.0", + "@types/atob": "^2.1.2", "@typescript-eslint/eslint-plugin": "^4.0.0", "@typescript-eslint/parser": "^4.0.0", "eslint": "^7.1.0", @@ -67,4 +70,4 @@ "optional": true } } -} +} \ No newline at end of file diff --git a/src/__tests__/index.ts b/src/__tests__/index.ts index cd62fba..2fb2cbe 100644 --- a/src/__tests__/index.ts +++ b/src/__tests__/index.ts @@ -23,6 +23,16 @@ const sourceMapPath = path.format({ ext: '.js.map', }); +function comparePath(a: string, b: string): boolean { + const first = a.split(/[\\/]/); + const second = b.split(/[\\/]/); + if (process.platform == 'win32') { + first[0] = first[0].toLowerCase(); + second[0] = second[0].toLowerCase(); + } + return first.every((v, i) => v == second[i]); +} + async function rollupBundle({ outputText, sourceMapText, @@ -31,16 +41,14 @@ async function rollupBundle({ pluginOptions?: SourcemapsPluginOptions; }) { const load = async (path: string) => { - switch (path) { - case inputPath: - return inputText; - case outputPath: - return outputText; - case sourceMapPath: - return sourceMapText!; - default: - throw new Error(`Unexpected path: ${path}`); + if (comparePath(path, inputPath)) { + return inputText; + } else if (comparePath(path, outputPath)) { + return outputText; + } else if (comparePath(path, sourceMapPath)) { + return sourceMapText!; } + throw new Error(`Unexpected path: ${path}`); }; const { generate } = await rollup({ @@ -82,7 +90,7 @@ it('ignores files with no source maps', async () => { const { map } = await rollupBundle({ outputText, sourceMapText }); expect(map).toBeDefined(); - expect(map!.sources).toStrictEqual([outputPath]); + expect(map!.sources.map(path.normalize)).toStrictEqual([outputPath]); expect(map!.sourcesContent).toStrictEqual([outputText]); }); @@ -123,7 +131,7 @@ describe('detects files with source maps', () => { const { map } = await rollupBundle({ outputText, sourceMapText }); expect(map).toBeDefined(); - expect(map!.sources).toStrictEqual([inputPath]); + expect(map!.sources.map(path.normalize)).toStrictEqual([inputPath]); expect(map!.sourcesContent).toStrictEqual([inputText]); }, ); @@ -150,7 +158,7 @@ describe('ignores filtered files', () => { }); expect(map).toBeDefined(); - expect(map!.sources).toStrictEqual([outputPath]); + expect(map!.sources.map(path.normalize)).toStrictEqual([outputPath]); expect(map!.sourcesContent).toStrictEqual([outputText]); }); @@ -169,12 +177,12 @@ describe('ignores filtered files', () => { outputText, sourceMapText, pluginOptions: { - exclude: [path.relative(process.cwd(), outputPath)], + exclude: [path.relative(process.cwd(), outputPath).split('\\').join('/')], }, }); expect(map).toBeDefined(); - expect(map!.sources).toStrictEqual([outputPath]); + expect(map!.sources.map(path.normalize)).toStrictEqual([outputPath]); expect(map!.sourcesContent).toStrictEqual([outputText]); }); }); @@ -194,14 +202,14 @@ it('delegates failing file reads to the next plugin', async () => { outputText, sourceMapText, pluginOptions: { - readFile(_path, cb) { + readFile(_path: string, cb: (error: Error | null, data: Buffer | string) => void) { cb(new Error('Failed!'), ''); }, }, }); expect(map).toBeDefined(); - expect(map!.sources).toStrictEqual([outputPath]); + expect(map!.sources.map(path.normalize)).toStrictEqual([outputPath]); expect(map!.sourcesContent).toStrictEqual([outputText]); }); @@ -234,6 +242,6 @@ it('handles failing source maps reads', async () => { }); expect(map).toBeDefined(); - expect(map!.sources).toStrictEqual([outputPath]); + expect(map!.sources.map(path.normalize)).toStrictEqual([outputPath]); expect(map!.sourcesContent).toStrictEqual([outputText]); }); diff --git a/src/env.d.ts b/src/env.d.ts deleted file mode 100644 index 8001819..0000000 --- a/src/env.d.ts +++ /dev/null @@ -1,64 +0,0 @@ -declare module 'source-map-resolve' { - import fs from 'fs'; - import { ExistingRawSourceMap } from 'rollup'; - - export interface ResolvedSourceMap { - /** The source map for code, as an object */ - map: ExistingRawSourceMap; - /** The url to the source map. If the source map came from a data uri, this property is null, since then there is no url to it. */ - url: string | null; - /** - * The url that the sources of the source map are relative to. - * Since the sources are relative to the source map, and the url to the source map is provided as the url property, this property might seem superfluos. - * However, remember that the url property can be null if the source map came from a data uri. - * If so, the sources are relative to the file containing the data uri—codeUrl. - * This property will be identical to the url property or codeUrl, whichever is appropriate. - * This way you can conveniently resolve the sources without having to think about where the source map came from. - */ - sourcesRelativeTo: string; - /** The url of the sourceMappingURL comment in code */ - sourceMappingURL: string; - } - - /** - * - * @param code A string of code that may or may not contain a sourceMappingURL comment. Such a comment is used to resolve the source map. - * @param codeUrl The url to the file containing code. If the sourceMappingURL is relative, it is resolved against codeUrl. - * @param read A function that reads url and responds using callback(error, content). - * @param callback A function that is invoked with either an error or null and the result. - */ - export function resolveSourceMap( - code: string, - codeUrl: string, - read: (path: string, callback: (error: Error | null, data: Buffer | string) => void) => void, - callback: ( - error: Error | null, - /** If code contains no sourceMappingURL, the result is null. */ - result: ResolvedSourceMap | null, - ) => void, - ): void; - - export interface ResolvedSources { - /** The same as map.sources, except all the sources are fully resolved. */ - sourcesResolved: string[]; - /** - * An array with the contents of all sources in map.sources, in the same order as map.sources. - * If getting the contents of a source fails, an error object is put into the array instead. - * */ - sourcesContent: (string | Error)[]; - } - - /** - * - * @param code A string of code that may or may not contain a sourceMappingURL comment. Such a comment is used to resolve the source map. - * @param codeUrl The url to the file containing code. If the sourceMappingURL is relative, it is resolved against codeUrl. - * @param read A function that reads url and responds using callback(error, content). - * @param callback A function that is invoked with either an error or null and the result. - */ - export function resolveSources( - map: ExistingRawSourceMap, - mapUrl: string, - read: (path: string, callback: (error: Error | null, data: Buffer | string) => void) => void, - callback: (error: Error | null, result: ResolvedSources) => void, - ): void; -} diff --git a/src/index.ts b/src/index.ts index 7eb805f..0538845 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,14 +1,97 @@ import fs from 'fs'; import { promisify } from 'util'; import { Plugin, ExistingRawSourceMap } from 'rollup'; -import pluginUtils, { CreateFilter } from '@rollup/pluginutils'; -import sourceMapResolve from 'source-map-resolve'; +import { CreateFilter, createFilter } from '@rollup/pluginutils'; +import atob from 'atob'; +import * as urlLib from 'url'; +import decodeUriComponentLib from 'decode-uri-component'; -const { createFilter } = pluginUtils; -const { resolveSourceMap, resolveSources } = sourceMapResolve; +interface ResolvedSourceMap { + map: ExistingRawSourceMap; + url: string | null; + sourcesRelativeTo: string; + sourceMappingURL: string; +} + +function resolveUrl(...args: string[]): string { + return args.reduce((resolved, nextUrl) => urlLib.resolve(resolved, nextUrl)); +} + +function customDecodeUriComponent(string: string): string { + return decodeUriComponentLib(string.replace(/\+/g, '%2B')); +} + +function parseMapToJSON(string: string): ExistingRawSourceMap { + return JSON.parse(string.replace(/^\)\]\}'/, '')); +} + +const sourceMappingURLRegex = RegExp( + '(?:/\\*(?:\\s*\\r?\\n(?://)?)?(?:[#@] sourceMappingURL=([^\\s\'"]*))\\s*\\*/|//(?:[#@] sourceMappingURL=([^\\s\'"]*)))\\s*', +); -const promisifiedResolveSourceMap = promisify(resolveSourceMap); -const promisifiedResolveSources = promisify(resolveSources); +function getSourceMappingUrl(code: string): string | null { + const match = sourceMappingURLRegex.exec(code); + return match ? match[1] || match[2] || '' : null; +} + +async function resolveSourceMap( + code: string, + codeUrl: string, + read: (path: string) => Promise, +): Promise { + const sourceMappingURL = getSourceMappingUrl(code); + if (!sourceMappingURL) { + return null; + } + const dataUri = /^data:([^,;]*)(;[^,;]*)*(?:,(.*))?$/.exec(sourceMappingURL); + if (dataUri) { + const mimeType = dataUri[1] || 'text/plain'; + if (!/^(?:application|text)\/json$/.test(mimeType)) { + throw new Error('Unuseful data uri mime type: ' + mimeType); + } + const map = parseMapToJSON( + (dataUri[2] === ';base64' ? atob : decodeURIComponent)(dataUri[3] || ''), + ); + return { sourceMappingURL, url: null, sourcesRelativeTo: codeUrl, map }; + } + const url = resolveUrl(codeUrl, sourceMappingURL); + const map = parseMapToJSON(String(await read(customDecodeUriComponent(url)))); + return { sourceMappingURL, url, sourcesRelativeTo: url, map }; +} + +interface ResolvedSources { + sourcesResolved: string[]; + sourcesContent: (string | Error)[]; +} + +async function resolveSources( + map: ExistingRawSourceMap, + mapUrl: string, + read: (path: string) => Promise, +): Promise { + const sourcesResolved: string[] = []; + const sourcesContent: (string | Error)[] = []; + for (let index = 0, len = map.sources.length; index < len; index++) { + const sourceRoot = map.sourceRoot; + const sourceContent = (map.sourcesContent || [])[index]; + const resolvePaths = [mapUrl, map.sources[index]]; + if (sourceRoot !== undefined && sourceRoot !== '') { + resolvePaths.splice(1, 0, sourceRoot.replace(/\/?$/, '/')); + } + sourcesResolved[index] = resolveUrl(...resolvePaths); + if (typeof sourceContent === 'string') { + sourcesContent[index] = sourceContent; + continue; + } + try { + const source = await read(customDecodeUriComponent(sourcesResolved[index])); + sourcesContent[index] = String(source); + } catch (error) { + sourcesContent[index] = error; + } + } + return { sourcesResolved, sourcesContent }; +} export interface SourcemapsPluginOptions { include?: Parameters[0]; @@ -42,7 +125,7 @@ export default function sourcemaps({ let map: ExistingRawSourceMap; try { - const result = await promisifiedResolveSourceMap(code, id, readFile); + const result = await resolveSourceMap(code, id, promisifiedReadFile); // The code contained no sourceMappingURL if (result === null) { @@ -58,7 +141,7 @@ export default function sourcemaps({ // Resolve sources if they're not included if (map.sourcesContent === undefined) { try { - const { sourcesContent } = await promisifiedResolveSources(map, id, readFile); + const { sourcesContent } = await resolveSources(map, id, promisifiedReadFile); if (sourcesContent.every(item => typeof item === 'string')) { map.sourcesContent = sourcesContent as string[]; } diff --git a/tsconfig.json b/tsconfig.json index bf7dbce..0dbfb0a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,8 +9,10 @@ "sourceMap": true, "inlineSources": true, "strict": true, - "esModuleInterop": true + "esModuleInterop": true, + "lib": ["ESNext"], + "allowSyntheticDefaultImports": true, }, "include": ["src"], "exclude": ["node_modules"] -} +} \ No newline at end of file