diff --git a/src/Chunk.ts b/src/Chunk.ts index c64bd6e1fd1..04e3b0683f0 100644 --- a/src/Chunk.ts +++ b/src/Chunk.ts @@ -14,7 +14,13 @@ import ExternalModule from './ExternalModule'; import finalisers from './finalisers/index'; import Graph from './Graph'; import Module from './Module'; -import { GlobalsOption, OutputOptions, RawSourceMap, RenderedChunk, RenderedModule } from './rollup/types'; +import { + DecodedSourceMapOrMissing, + GlobalsOption, + OutputOptions, + RenderedChunk, + RenderedModule +} from './rollup/types'; import { Addons } from './utils/addons'; import { toBase64 } from './utils/base64'; import collapseSourcemaps from './utils/collapseSourcemaps'; @@ -676,7 +682,7 @@ export default class Chunk { timeEnd('render format', 3); let map: SourceMap = null as any; - const chunkSourcemapChain: RawSourceMap[] = []; + const chunkSourcemapChain: DecodedSourceMapOrMissing[] = []; return renderChunk({ chunk: this, diff --git a/src/Module.ts b/src/Module.ts index acf5541094d..bf30ec88ff2 100644 --- a/src/Module.ts +++ b/src/Module.ts @@ -32,9 +32,10 @@ import ExternalModule from './ExternalModule'; import Graph from './Graph'; import { Asset, + DecodedSourceMapOrMissing, EmittedChunk, + ExistingDecodedSourceMap, ModuleJSON, - RawSourceMap, ResolvedIdMap, RollupError, RollupWarning, @@ -199,11 +200,11 @@ export default class Module { manualChunkAlias: string = null as any; moduleSideEffects: boolean; originalCode!: string; - originalSourcemap!: RawSourceMap | null; + originalSourcemap!: ExistingDecodedSourceMap | null; reexports: { [name: string]: ReexportDescription } = Object.create(null); resolvedIds!: ResolvedIdMap; scope!: ModuleScope; - sourcemapChain!: RawSourceMap[]; + sourcemapChain!: DecodedSourceMapOrMissing[]; sources: string[] = []; transformAssets?: Asset[]; transformChunks?: EmittedChunk[]; @@ -540,7 +541,7 @@ export default class Module { this.code = code; this.originalCode = originalCode; this.originalSourcemap = originalSourcemap; - this.sourcemapChain = sourcemapChain as RawSourceMap[]; + this.sourcemapChain = sourcemapChain; if (transformAssets) { this.transformAssets = transformAssets; } diff --git a/src/rollup/types.d.ts b/src/rollup/types.d.ts index f4c84280b70..c24a7dded59 100644 --- a/src/rollup/types.d.ts +++ b/src/rollup/types.d.ts @@ -38,6 +38,21 @@ export interface RollupLogProps { url?: string; } +export type SourceMapSegment = + | [number] + | [number, number, number, number] + | [number, number, number, number, number]; + +export interface ExistingDecodedSourceMap { + file?: string; + mappings: SourceMapSegment[][]; + names: string[]; + sourceRoot?: string; + sources: string[]; + sourcesContent?: string[]; + version: number; +} + export interface ExistingRawSourceMap { file?: string; mappings: string; @@ -48,7 +63,13 @@ export interface ExistingRawSourceMap { version: number; } -export type RawSourceMap = { mappings: '' } | ExistingRawSourceMap; +export type DecodedSourceMapOrMissing = + | { + mappings?: never; + missing: true; + plugin: string; + } + | ExistingDecodedSourceMap; export interface SourceMap { file: string; @@ -61,10 +82,12 @@ export interface SourceMap { toUrl(): string; } +export type SourceMapInput = ExistingRawSourceMap | string | null | { mappings: '' }; + export interface SourceDescription { ast?: ESTree.Program; code: string; - map?: string | RawSourceMap; + map?: SourceMapInput; moduleSideEffects?: boolean | null; } @@ -79,9 +102,9 @@ export interface TransformModuleJSON { customTransformCache: boolean; moduleSideEffects: boolean | null; originalCode: string; - originalSourcemap: RawSourceMap | null; + originalSourcemap: ExistingDecodedSourceMap | null; resolvedIds?: ResolvedIdMap; - sourcemapChain: (RawSourceMap | { missing: true; plugin: string })[]; + sourcemapChain: DecodedSourceMapOrMissing[]; transformDependencies: string[] | null; } @@ -207,8 +230,8 @@ export type TransformChunkHook = ( code: string, options: OutputOptions ) => - | Promise<{ code: string; map: RawSourceMap } | null | undefined> - | { code: string; map: RawSourceMap } + | Promise<{ code: string; map?: SourceMapInput } | null | undefined> + | { code: string; map?: SourceMapInput } | null | undefined; @@ -218,8 +241,8 @@ export type RenderChunkHook = ( chunk: RenderedChunk, options: OutputOptions ) => - | Promise<{ code: string; map: RawSourceMap } | null> - | { code: string; map: RawSourceMap } + | Promise<{ code: string; map?: SourceMapInput } | null> + | { code: string; map?: SourceMapInput } | string | null; diff --git a/src/utils/collapseSourcemaps.ts b/src/utils/collapseSourcemaps.ts index dfde40ddc7c..a240190b501 100644 --- a/src/utils/collapseSourcemaps.ts +++ b/src/utils/collapseSourcemaps.ts @@ -1,7 +1,11 @@ import { DecodedSourceMap, SourceMap } from 'magic-string'; import Chunk from '../Chunk'; import Module from '../Module'; -import { ExistingRawSourceMap, RawSourceMap } from '../rollup/types'; +import { + DecodedSourceMapOrMissing, + ExistingDecodedSourceMap, + SourceMapSegment +} from '../rollup/types'; import { error } from './error'; import { basename, dirname, relative, resolve } from './path'; @@ -21,10 +25,6 @@ class Source { } } -type SourceMapSegmentVector = - | [number, number, number, number, number] - | [number, number, number, number]; - interface SourceMapSegmentObject { column: number; line: number; @@ -33,11 +33,14 @@ interface SourceMapSegmentObject { } class Link { - mappings: SourceMapSegmentVector[][]; + mappings: SourceMapSegment[][]; names: string[]; - sources: Source[]; + sources: (Source | Link)[]; - constructor(map: { mappings: SourceMapSegmentVector[][]; names: string[] }, sources: Source[]) { + constructor( + map: { mappings: SourceMapSegment[][]; names: string[] }, + sources: (Source | Link)[] + ) { this.sources = sources; this.names = map.names; this.mappings = map.mappings; @@ -51,16 +54,17 @@ class Link { const mappings = []; for (const line of this.mappings) { - const tracedLine: SourceMapSegmentVector[] = []; + const tracedLine: SourceMapSegment[] = []; for (const segment of line) { + if (segment.length == 1) continue; const source = this.sources[segment[1]]; if (!source) continue; const traced = source.traceSegment( segment[2], segment[3], - this.names[segment[4] as number] + segment.length === 5 ? this.names[segment[4]] : '' ); if (traced) { @@ -77,13 +81,11 @@ class Link { sourcesContent[sourceIndex] !== traced.source.content ) { error({ - message: `Multiple conflicting contents for sourcemap source ${ - traced.source.filename - }` + message: `Multiple conflicting contents for sourcemap source ${traced.source.filename}` }); } - const tracedSegment: SourceMapSegmentVector = [ + const tracedSegment: SourceMapSegment = [ segment[0], sourceIndex, traced.line, @@ -97,7 +99,7 @@ class Link { names.push(traced.name); } - (tracedSegment as SourceMapSegmentVector)[4] = nameIndex; + (tracedSegment as SourceMapSegment)[4] = nameIndex; } tracedLine.push(tracedSegment); @@ -110,7 +112,7 @@ class Link { return { sources, sourcesContent, names, mappings }; } - traceSegment(line: number, column: number, name: string) { + traceSegment(line: number, column: number, name: string): SourceMapSegmentObject | null { const segments = this.mappings[line]; if (!segments) return null; @@ -122,13 +124,14 @@ class Link { const m = (i + j) >> 1; const segment = segments[m]; if (segment[0] === column) { + if (segment.length == 1) return null; const source = this.sources[segment[1]]; if (!source) return null; return source.traceSegment( segment[2], segment[3], - this.names[segment[4] as number] || name + segment.length === 5 ? this.names[segment[4]] : name ); } if (segment[0] > column) { @@ -142,72 +145,69 @@ class Link { } } -// TODO TypeScript: Fix typecasts export default function collapseSourcemaps( bundle: Chunk, file: string, map: DecodedSourceMap, modules: Module[], - bundleSourcemapChain: RawSourceMap[], + bundleSourcemapChain: DecodedSourceMapOrMissing[], excludeContent: boolean ) { - function linkMap(source: Source, map: any) { - if (map.missing) { - bundle.graph.warn({ - code: 'SOURCEMAP_BROKEN', - message: `Sourcemap is likely to be incorrect: a plugin${ - map.plugin ? ` ('${map.plugin}')` : `` - } was used to transform files, but didn't generate a sourcemap for the transformation. Consult the plugin documentation for help`, - plugin: map.plugin, - url: `https://rollupjs.org/guide/en/#warning-sourcemap-is-likely-to-be-incorrect` - }); - - map = { - mappings: '', - names: [] - }; + function linkMap(source: Source | Link, map: DecodedSourceMapOrMissing) { + if (map.mappings) { + return new Link(map, [source]); } - return new Link(map, [source]); + bundle.graph.warn({ + code: 'SOURCEMAP_BROKEN', + message: `Sourcemap is likely to be incorrect: a plugin${ + map.plugin ? ` ('${map.plugin}')` : `` + } was used to transform files, but didn't generate a sourcemap for the transformation. Consult the plugin documentation for help`, + plugin: map.plugin, + url: `https://rollupjs.org/guide/en/#warning-sourcemap-is-likely-to-be-incorrect` + }); + + return new Link( + { + mappings: [], + names: [] + }, + [source] + ); } const moduleSources = modules .filter(module => !module.excludeFromSourcemap) .map(module => { - let sourcemapChain = module.sourcemapChain; - - let source: Source; - const originalSourcemap = module.originalSourcemap as ExistingRawSourceMap; + let source: Source | Link; + const originalSourcemap = module.originalSourcemap; if (!originalSourcemap) { source = new Source(module.id, module.originalCode); } else { const sources = originalSourcemap.sources; const sourcesContent = originalSourcemap.sourcesContent || []; - if (sources == null || (sources.length <= 1 && sources[0] == null)) { - source = new Source(module.id, sourcesContent[0]); - sourcemapChain = [originalSourcemap as RawSourceMap].concat(sourcemapChain); - } else { - // TODO indiscriminately treating IDs and sources as normal paths is probably bad. - const directory = dirname(module.id) || '.'; - const sourceRoot = originalSourcemap.sourceRoot || '.'; + // TODO indiscriminately treating IDs and sources as normal paths is probably bad. + const directory = dirname(module.id) || '.'; + const sourceRoot = originalSourcemap.sourceRoot || '.'; - const baseSources = sources.map( - (source, i) => new Source(resolve(directory, sourceRoot, source), sourcesContent[i]) - ); + const baseSources = sources.map( + (source, i) => new Source(resolve(directory, sourceRoot, source), sourcesContent[i]) + ); - source = new Link(originalSourcemap as any, baseSources) as any; - } + source = new Link(originalSourcemap, baseSources); } - source = sourcemapChain.reduce(linkMap as any, source); + source = module.sourcemapChain.reduce(linkMap, source); return source; }); - let source = new Link(map as any, moduleSources); + // DecodedSourceMap (from magic-string) uses a number[] instead of the more + // correct SourceMapSegment tuples. Cast it here to gain type safety. + let source = new Link(map as ExistingDecodedSourceMap, moduleSources); - source = bundleSourcemapChain.reduce(linkMap as any, source); + source = bundleSourcemapChain.reduce(linkMap, source); let { sources, sourcesContent, names, mappings } = source.traceMappings(); diff --git a/src/utils/decodedSourcemap.ts b/src/utils/decodedSourcemap.ts new file mode 100644 index 00000000000..3b4acfcf6a4 --- /dev/null +++ b/src/utils/decodedSourcemap.ts @@ -0,0 +1,29 @@ +import { decode } from 'sourcemap-codec'; +import { ExistingDecodedSourceMap, ExistingRawSourceMap, SourceMapInput } from '../rollup/types'; + +type Input = SourceMapInput | ExistingDecodedSourceMap | undefined; + +export function decodedSourcemap(map: Input): ExistingDecodedSourceMap | null { + if (!map) return null; + + if (typeof map === 'string') { + map = JSON.parse(map) as ExistingRawSourceMap; + } + if (map.mappings === '') { + return { + mappings: [], + names: [], + sources: [], + version: 3 + }; + } + + let mappings; + if (typeof map.mappings === 'string') { + mappings = decode(map.mappings); + } else { + mappings = map.mappings; + } + + return { ...(map as ExistingRawSourceMap | ExistingDecodedSourceMap), mappings }; +} diff --git a/src/utils/getOriginalLocation.ts b/src/utils/getOriginalLocation.ts index 358cc70a3ab..1da1d7aa7e5 100644 --- a/src/utils/getOriginalLocation.ts +++ b/src/utils/getOriginalLocation.ts @@ -1,24 +1,27 @@ -import { ExistingRawSourceMap, RawSourceMap } from '../rollup/types'; +import { DecodedSourceMapOrMissing, ExistingDecodedSourceMap } from '../rollup/types'; export function getOriginalLocation( - sourcemapChain: RawSourceMap[], + sourcemapChain: DecodedSourceMapOrMissing[], location: { column: number; line: number; name?: string; source?: string } ) { - const filteredSourcemapChain = sourcemapChain.filter(sourcemap => sourcemap.mappings); + // This cast is guaranteed. If it were a missing Map, it wouldn't have a mappings. + const filteredSourcemapChain = sourcemapChain.filter( + sourcemap => sourcemap.mappings + ) as ExistingDecodedSourceMap[]; while (filteredSourcemapChain.length > 0) { - const sourcemap = filteredSourcemapChain.pop() as ExistingRawSourceMap; - const line: any = sourcemap.mappings[location.line - 1]; + const sourcemap = filteredSourcemapChain.pop()!; + const line = sourcemap.mappings[location.line - 1]; let locationFound = false; if (line !== undefined) { for (const segment of line) { if (segment[0] >= location.column) { - if (segment.length < 4) break; + if (segment.length === 1) break; location = { column: segment[3], line: segment[2] + 1, - name: sourcemap.names[segment[4]], + name: segment.length === 5 ? sourcemap.names[segment[4]] : undefined, source: sourcemap.sources[segment[1]] }; locationFound = true; diff --git a/src/utils/renderChunk.ts b/src/utils/renderChunk.ts index 74c3511a3ee..d53be7c376b 100644 --- a/src/utils/renderChunk.ts +++ b/src/utils/renderChunk.ts @@ -1,7 +1,13 @@ -import { decode } from 'sourcemap-codec'; import Chunk from '../Chunk'; import Graph from '../Graph'; -import { OutputOptions, Plugin, RawSourceMap, RenderedChunk } from '../rollup/types'; +import { + DecodedSourceMapOrMissing, + OutputOptions, + Plugin, + RenderedChunk, + SourceMapInput +} from '../rollup/types'; +import { decodedSourcemap } from './decodedSourcemap'; import { error } from './error'; export default function renderChunk({ @@ -17,9 +23,13 @@ export default function renderChunk({ graph: Graph; options: OutputOptions; renderChunk: RenderedChunk; - sourcemapChain: RawSourceMap[]; + sourcemapChain: DecodedSourceMapOrMissing[]; }): Promise { - const renderChunkReducer = (code: string, result: any, plugin: Plugin): string => { + const renderChunkReducer = ( + code: string, + result: { code: string; map?: SourceMapInput }, + plugin: Plugin + ): string => { if (result == null) return code; if (typeof result === 'string') @@ -28,11 +38,11 @@ export default function renderChunk({ map: undefined }; - const map = typeof result.map === 'string' ? JSON.parse(result.map) : result.map; - if (map && typeof map.mappings === 'string') map.mappings = decode(map.mappings); - // strict null check allows 'null' maps to not be pushed to the chain, while 'undefined' gets the missing map warning - if (map !== null) sourcemapChain.push(map || { missing: true, plugin: plugin.name }); + if (result.map !== null) { + const map = decodedSourcemap(result.map); + sourcemapChain.push(map || { missing: true, plugin: plugin.name }); + } return result.code; }; diff --git a/src/utils/transform.ts b/src/utils/transform.ts index 05fc9123202..3bf570fbd6d 100644 --- a/src/utils/transform.ts +++ b/src/utils/transform.ts @@ -1,15 +1,13 @@ -import { decode } from 'sourcemap-codec'; import Graph from '../Graph'; import Module from '../Module'; import { Asset, + DecodedSourceMapOrMissing, EmitAsset, EmittedChunk, - ExistingRawSourceMap, Plugin, PluginCache, PluginContext, - RawSourceMap, RollupError, RollupWarning, TransformModuleJSON, @@ -17,6 +15,7 @@ import { TransformSourceDescription } from '../rollup/types'; import { createTransformEmitAsset } from './assetHooks'; +import { decodedSourcemap } from './decodedSourcemap'; import { augmentCodeLocation, error } from './error'; import { dirname, resolve } from './path'; import { trackPluginCache } from './pluginDriver'; @@ -27,12 +26,9 @@ export default function transform( module: Module ): Promise { const id = module.id; - const sourcemapChain: (RawSourceMap | { missing: true; plugin: string })[] = []; - - const originalSourcemap = typeof source.map === 'string' ? JSON.parse(source.map) : source.map; - if (originalSourcemap && typeof originalSourcemap.mappings === 'string') - originalSourcemap.mappings = decode(originalSourcemap.mappings); + const sourcemapChain: DecodedSourceMapOrMissing[] = []; + const originalSourcemap = source.map === null ? null : decodedSourcemap(source.map); const baseEmitAsset = graph.pluginDriver.emitAsset; const originalCode = source.code; let ast = source.ast; @@ -96,17 +92,10 @@ export default function transform( return code; } - if (result.map && typeof (result.map as ExistingRawSourceMap).mappings === 'string') { - (result.map as ExistingRawSourceMap).mappings = decode( - (result.map as ExistingRawSourceMap).mappings - ); - } - // strict null check allows 'null' maps to not be pushed to the chain, while 'undefined' gets the missing map warning if (result.map !== null) { - sourcemapChain.push( - (result.map as ExistingRawSourceMap) || { missing: true, plugin: plugin.name } - ); + const map = decodedSourcemap(result.map); + sourcemapChain.push(map || { missing: true, plugin: plugin.name }); } ast = result.ast; diff --git a/test/function/samples/handles-empty-string-sourcemap-mappings/_config.js b/test/function/samples/handles-empty-string-sourcemap-mappings/_config.js new file mode 100644 index 00000000000..764c77a324a --- /dev/null +++ b/test/function/samples/handles-empty-string-sourcemap-mappings/_config.js @@ -0,0 +1,18 @@ +module.exports = { + description: 'handles transforms that return sourcemap with empty mappings', + + options: { + plugins: [ + { + transform(code) { + return { + code, + map: { mappings: '' } + }; + } + } + ], + // ensure source maps are generated + output: { sourcemap: true } + } +}; diff --git a/test/function/samples/handles-empty-string-sourcemap-mappings/main.js b/test/function/samples/handles-empty-string-sourcemap-mappings/main.js new file mode 100644 index 00000000000..7a4e8a723a4 --- /dev/null +++ b/test/function/samples/handles-empty-string-sourcemap-mappings/main.js @@ -0,0 +1 @@ +export default 42; diff --git a/test/sourcemaps/samples/loaders/_config.js b/test/sourcemaps/samples/loaders/_config.js index 842e4f55a45..c8169ae1d10 100644 --- a/test/sourcemaps/samples/loaders/_config.js +++ b/test/sourcemaps/samples/loaders/_config.js @@ -24,9 +24,7 @@ module.exports = { source: id }); - if (/main.js$/.test(id)) { - delete out.map.sources; - } else { + if (!/main.js$/.test(id)) { const slash = out.map.sources[0].lastIndexOf('/') + 1; out.map.sources = out.map.sources.map(source => '../' + source.slice(slash)); out.map.sourceRoot = 'fake'; diff --git a/typings/declarations.d.ts b/typings/declarations.d.ts index 71f65898dd6..100ca939fa2 100644 --- a/typings/declarations.d.ts +++ b/typings/declarations.d.ts @@ -12,7 +12,6 @@ declare module 'signal-exit'; declare module 'date-time'; declare module 'locate-character'; declare module 'is-reference'; -declare module 'sourcemap-codec'; declare module 'require-relative'; declare module 'acorn-dynamic-import'; declare module 'acorn-import-meta';