diff --git a/src/farm/index.ts b/src/farm/index.ts index d0ed9c47..9186ce34 100644 --- a/src/farm/index.ts +++ b/src/farm/index.ts @@ -17,9 +17,10 @@ import type { } from '../types' import type { WatchChangeEvents } from './utils' import path from 'path' -import { toArray } from '../utils' -import { createFarmContext, unpluginContext } from './context' +import MagicString from 'magic-string' +import { getCombinedSourcemap, toArray } from '../utils' +import { createFarmContext, unpluginContext } from './context' import { convertEnforceToPriority, convertWatchEventChange, @@ -187,7 +188,16 @@ export function toFarmPlugin(plugin: UnpluginOptions, options?: Record { + const combinedMap = getCombinedSourcemap(params.sourceMapChain, params.resolvedPath, params.content) + if (!combinedMap) { + const magicString = new MagicString(params.content) + return magicString.generateMap({ hires: true, includeContent: true, source: params.resolvedPath }) + } + return combinedMap + }, + }, unpluginContext(context), farmContext), params.content, params.resolvedPath, ) diff --git a/src/rspack/loaders/transform.ts b/src/rspack/loaders/transform.ts index a142a2ae..21fd56e0 100644 --- a/src/rspack/loaders/transform.ts +++ b/src/rspack/loaders/transform.ts @@ -1,4 +1,5 @@ import type { LoaderContext } from '@rspack/core' +import MagicString from 'magic-string' import { createBuildContext, createContext } from '../context' export default async function transform( @@ -26,7 +27,15 @@ export default async function transform( const context = createContext(this) const res = await plugin.transform.call( Object.assign( - {}, + { + getCombinedSourcemap: () => { + if (!map) { + const magicString = new MagicString(source) + return magicString.generateMap({ hires: true, includeContent: true, source: id }) + } + return map + }, + }, this._compilation && createBuildContext(this._compiler, this._compilation, this), context, ), diff --git a/src/utils.ts b/src/utils.ts index 767ffc52..5a1c28cb 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,5 +1,8 @@ +import type { DecodedSourceMap, RawSourceMap } from '@ampproject/remapping' +import type { SourceMap } from 'rollup' import type { ResolvedUnpluginOptions } from './types' import { isAbsolute, normalize } from 'path' +import remapping from '@ampproject/remapping' /** * Normalizes a given path when it's absolute. Normalizing means returning a new path by converting @@ -72,3 +75,149 @@ export function resolveQuery(query: string | { unpluginName: string }) { return query.unpluginName } } + +const postfixRE = /[?#].*$/ +export function cleanUrl(url: string): string { + return url.replace(postfixRE, '') +} + +/* + The following functions are copied from vite + https://github.com/vitejs/vite/blob/0fe95d4a71930cf55acd628efef59e6eae0f77f7/packages/vite/src/node/utils.ts#L781-L868 + + MIT License + Copyright (c) 2019-present, VoidZero Inc. and Vite contributors + https://github.com/vitejs/vite/blob/main/LICENSE +*/ +const windowsDriveRE = /^[A-Z]:/ +const replaceWindowsDriveRE = /^([A-Z]):\// +const linuxAbsolutePathRE = /^\/[^/]/ +function escapeToLinuxLikePath(path: string) { + if (windowsDriveRE.test(path)) { + return path.replace(replaceWindowsDriveRE, '/windows/$1/') + } + if (linuxAbsolutePathRE.test(path)) { + return `/linux${path}` + } + return path +} + +const revertWindowsDriveRE = /^\/windows\/([A-Z])\// +function unescapeToLinuxLikePath(path: string) { + if (path.startsWith('/linux/')) { + return path.slice('/linux'.length) + } + if (path.startsWith('/windows/')) { + return path.replace(revertWindowsDriveRE, '$1:/') + } + return path +} + +const nullSourceMap: RawSourceMap = { + names: [], + sources: [], + mappings: '', + version: 3, +} +function combineSourcemaps( + filename: string, + sourcemapList: Array, +): RawSourceMap { + if ( + sourcemapList.length === 0 + || sourcemapList.every(m => m.sources.length === 0) + ) { + return { ...nullSourceMap } + } + + // hack for parse broken with normalized absolute paths on windows (C:/path/to/something). + // escape them to linux like paths + // also avoid mutation here to prevent breaking plugin's using cache to generate sourcemaps like vue (see #7442) + sourcemapList = sourcemapList.map((sourcemap) => { + const newSourcemaps = { ...sourcemap } + newSourcemaps.sources = sourcemap.sources.map(source => + source ? escapeToLinuxLikePath(source) : null, + ) + if (sourcemap.sourceRoot) { + newSourcemaps.sourceRoot = escapeToLinuxLikePath(sourcemap.sourceRoot) + } + return newSourcemaps + }) + + // We don't declare type here so we can convert/fake/map as RawSourceMap + let map // : SourceMap + let mapIndex = 1 + const useArrayInterface + = sourcemapList.slice(0, -1).find(m => m.sources.length !== 1) === undefined + if (useArrayInterface) { + map = remapping(sourcemapList, () => null) + } + else { + map = remapping(sourcemapList[0], (sourcefile) => { + const mapForSources = sourcemapList + .slice(mapIndex) + .find(s => s.sources.includes(sourcefile)) + + if (mapForSources) { + mapIndex++ + return mapForSources + } + return null + }) + } + if (!map.file) { + delete map.file + } + + // unescape the previous hack + map.sources = map.sources.map(source => + source ? unescapeToLinuxLikePath(source) : source, + ) + map.file = filename + + return map as RawSourceMap +} + +export function getCombinedSourcemap(sourcemapChain: Nullable>, filename: string, originalCode: string): SourceMap | null { + sourcemapChain = toArray(sourcemapChain) + let combinedMap = null + + for (let m of sourcemapChain) { + if (typeof m === 'string') + m = JSON.parse(m) + if (!('version' in (m as SourceMap))) { + // { mappings: '' } + if ((m as SourceMap).mappings === '') { + combinedMap = { mappings: '' } + break + } + // empty, nullified source map + combinedMap = null + break + } + if (!combinedMap) { + const sm = m as SourceMap + // sourcemap should not include `sources: [null]` (because `sources` should be string) nor + // `sources: ['']` (because `''` means the path of sourcemap) + // but MagicString generates this when `filename` option is not set. + // Rollup supports these and therefore we support this as well + if (sm.sources.length === 1 && !sm.sources[0]) { + combinedMap = { + ...sm, + sources: [filename], + sourcesContent: [originalCode], + } + } + else { + combinedMap = sm + } + } + else { + combinedMap = combineSourcemaps(cleanUrl(filename), [ + m as RawSourceMap, + combinedMap as RawSourceMap, + ]) as SourceMap + } + } + return combinedMap as SourceMap +} diff --git a/src/webpack/loaders/transform.ts b/src/webpack/loaders/transform.ts index 9f0ae5e5..4719fc59 100644 --- a/src/webpack/loaders/transform.ts +++ b/src/webpack/loaders/transform.ts @@ -1,4 +1,5 @@ import type { LoaderContext } from 'webpack' +import MagicString from 'magic-string' import { resolveQuery } from '../../utils' import { createBuildContext, createContext } from '../context' @@ -13,14 +14,26 @@ export default async function transform(this: LoaderContext<{ unpluginName: stri const context = createContext(this) const res = await plugin.transform.call( - Object.assign({}, createBuildContext({ - addWatchFile: (file) => { - this.addDependency(file) + Object.assign( + { + getCombinedSourcemap: () => { + if (!map) { + const magicString = new MagicString(source) + return magicString.generateMap({ hires: true, includeContent: true, source: this.resource }) + } + return map + }, }, - getWatchFiles: () => { - return this.getDependencies() - }, - }, this._compiler!, this._compilation, this), context), + createBuildContext({ + addWatchFile: (file) => { + this.addDependency(file) + }, + getWatchFiles: () => { + return this.getDependencies() + }, + }, this._compiler!, this._compilation, this), + context, + ), source, this.resource, )