Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: expose getCombinedSourcemap in webpack, rspack and farm #428

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions src/farm/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -187,7 +188,16 @@ export function toFarmPlugin(plugin: UnpluginOptions, options?: Record<string, a
&& plugin.transformInclude(params.resolvedPath)
const farmContext = createFarmContext(context, params.resolvedPath)
const resource: TransformResult = await _transform.call(
Object.assign(unpluginContext(context), farmContext),
Object.assign({
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,
)
Expand Down
11 changes: 10 additions & 1 deletion src/rspack/loaders/transform.ts
Original file line number Diff line number Diff line change
@@ -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(
Expand Down Expand Up @@ -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,
),
Expand Down
149 changes: 149 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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<DecodedSourceMap | RawSourceMap>,
): 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<Arrayable<SourceMap | string>>, 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
}
27 changes: 20 additions & 7 deletions src/webpack/loaders/transform.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { LoaderContext } from 'webpack'
import MagicString from 'magic-string'
import { resolveQuery } from '../../utils'
import { createBuildContext, createContext } from '../context'

Expand All @@ -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,
)
Expand Down