From 15686b80714b2969eb6585e02073d5becf4ba22b Mon Sep 17 00:00:00 2001 From: KID-joker Date: Wed, 16 Oct 2024 20:20:57 +0800 Subject: [PATCH 1/2] feat: expose getCombinedSourcemap in webpack, rspack and farm --- src/farm/index.ts | 6 +- src/rspack/loaders/transform.ts | 4 +- src/utils.ts | 149 +++++++++++++++++++++++++++++++ src/webpack/loaders/transform.ts | 20 +++-- 4 files changed, 169 insertions(+), 10 deletions(-) diff --git a/src/farm/index.ts b/src/farm/index.ts index d0ed9c47..c6f151a2 100644 --- a/src/farm/index.ts +++ b/src/farm/index.ts @@ -17,7 +17,7 @@ import type { } from '../types' import type { WatchChangeEvents } from './utils' import path from 'path' -import { toArray } from '../utils' +import { getCombinedSourcemap, toArray } from '../utils' import { createFarmContext, unpluginContext } from './context' import { @@ -187,7 +187,9 @@ export function toFarmPlugin(plugin: UnpluginOptions, options?: Record getCombinedSourcemap(params.sourceMapChain, params.resolvedPath, params.content), + }, unpluginContext(context), farmContext), params.content, params.resolvedPath, ) diff --git a/src/rspack/loaders/transform.ts b/src/rspack/loaders/transform.ts index a142a2ae..0ccdec78 100644 --- a/src/rspack/loaders/transform.ts +++ b/src/rspack/loaders/transform.ts @@ -26,7 +26,9 @@ export default async function transform( const context = createContext(this) const res = await plugin.transform.call( Object.assign( - {}, + { + getCombinedSourcemap: () => 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..7a229eac 100644 --- a/src/webpack/loaders/transform.ts +++ b/src/webpack/loaders/transform.ts @@ -13,14 +13,20 @@ 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: () => 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, ) From 9d1cf6e4d9253fbeac8cbd1f1b8057b96d450093 Mon Sep 17 00:00:00 2001 From: KID-joker Date: Sun, 10 Nov 2024 22:24:18 +0800 Subject: [PATCH 2/2] fix: return default value if null --- src/farm/index.ts | 12 ++++++++++-- src/rspack/loaders/transform.ts | 9 ++++++++- src/webpack/loaders/transform.ts | 9 ++++++++- 3 files changed, 26 insertions(+), 4 deletions(-) diff --git a/src/farm/index.ts b/src/farm/index.ts index c6f151a2..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 MagicString from 'magic-string' import { getCombinedSourcemap, toArray } from '../utils' -import { createFarmContext, unpluginContext } from './context' +import { createFarmContext, unpluginContext } from './context' import { convertEnforceToPriority, convertWatchEventChange, @@ -188,7 +189,14 @@ export function toFarmPlugin(plugin: UnpluginOptions, options?: Record getCombinedSourcemap(params.sourceMapChain, params.resolvedPath, params.content), + getCombinedSourcemap: () => { + 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 0ccdec78..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( @@ -27,7 +28,13 @@ export default async function transform( const res = await plugin.transform.call( Object.assign( { - getCombinedSourcemap: () => map, + 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/webpack/loaders/transform.ts b/src/webpack/loaders/transform.ts index 7a229eac..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' @@ -15,7 +16,13 @@ export default async function transform(this: LoaderContext<{ unpluginName: stri const res = await plugin.transform.call( Object.assign( { - getCombinedSourcemap: () => map, + getCombinedSourcemap: () => { + if (!map) { + const magicString = new MagicString(source) + return magicString.generateMap({ hires: true, includeContent: true, source: this.resource }) + } + return map + }, }, createBuildContext({ addWatchFile: (file) => {