@jsenv/sourcemap 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "@jsenv/sourcemap",
3
+ "version": "1.0.0",
4
+ "license": "MIT",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/jsenv/jsenv-core",
8
+ "directory": "packages/utils"
9
+ },
10
+ "publishConfig": {
11
+ "access": "public",
12
+ "registry": "https://registry.npmjs.org"
13
+ },
14
+ "type": "module",
15
+ "exports": {
16
+ ".": "./src/main.js"
17
+ },
18
+ "scripts": {
19
+ "test": "node --conditions=development ./scripts/test.mjs"
20
+ },
21
+ "dependencies": {
22
+ "@jsenv/urls": "1.2.5",
23
+ "magic-string": "0.26.2",
24
+ "source-map": "0.7.4"
25
+ }
26
+ }
@@ -0,0 +1,15 @@
1
+ import { executeTestPlan, nodeProcess } from "@jsenv/core"
2
+
3
+ await executeTestPlan({
4
+ rootDirectoryUrl: new URL("../", import.meta.url),
5
+ testPlan: {
6
+ "**/*.test.mjs": {
7
+ node: {
8
+ runtime: nodeProcess,
9
+ },
10
+ },
11
+ },
12
+ failFast: process.argv.includes("--workspace"),
13
+ completedExecutionLogMerging: process.argv.includes("--workspace"),
14
+ coverage: process.argv.includes("--coverage"),
15
+ })
@@ -0,0 +1,3 @@
1
+ // ideally we should be able to generate bundled file sourcemap
2
+ // as done in https://github.com/rollup/rollup/blob/05d294fdfca5d01e4e6b3f70e442136504a243b0/src/utils/collapseSourcemaps.ts#L203
3
+ // (would be used for CSS)
@@ -0,0 +1,18 @@
1
+ // duplicated from @jsenv/log to avoid the dependency
2
+ export const createDetailedMessage = (message, details = {}) => {
3
+ let string = `${message}`
4
+
5
+ Object.keys(details).forEach((key) => {
6
+ const value = details[key]
7
+ string += `
8
+ --- ${key} ---
9
+ ${
10
+ Array.isArray(value)
11
+ ? value.join(`
12
+ `)
13
+ : value
14
+ }`
15
+ })
16
+
17
+ return string
18
+ }
@@ -0,0 +1,141 @@
1
+ import { createDetailedMessage } from "./detailed_message.js"
2
+ import { remapStack } from "./remap_stack.js"
3
+ import { stringifyStack } from "./stringify_stack.js"
4
+
5
+ export const installErrorStackRemapping = ({
6
+ fetchFile,
7
+ resolveFile,
8
+ SourceMapConsumer,
9
+ indent = " ",
10
+ }) => {
11
+ if (typeof fetchFile !== "function") {
12
+ throw new TypeError(`fetchFile must be a function, got ${fetchFile}`)
13
+ }
14
+ if (typeof SourceMapConsumer !== "function") {
15
+ throw new TypeError(
16
+ `sourceMapConsumer must be a function, got ${SourceMapConsumer}`,
17
+ )
18
+ }
19
+ if (typeof indent !== "string") {
20
+ throw new TypeError(`indent must be a string, got ${indent}`)
21
+ }
22
+
23
+ const errorRemappingCache = new WeakMap()
24
+ const errorRemapFailureCallbackMap = new WeakMap()
25
+
26
+ let installed = false
27
+ const previousPrepareStackTrace = Error.prepareStackTrace
28
+ const install = () => {
29
+ if (installed) return
30
+ installed = true
31
+ Error.prepareStackTrace = prepareStackTrace
32
+ }
33
+
34
+ const uninstall = () => {
35
+ if (!installed) return
36
+ installed = false
37
+ Error.prepareStackTrace = previousPrepareStackTrace
38
+ }
39
+
40
+ // ensure we do not use prepareStackTrace for thoose error
41
+ // otherwise we would recursively remap error stack
42
+ // and if the reason causing the failure is still here
43
+ // it would create an infinite loop
44
+ const readErrorStack = (error) => {
45
+ uninstall()
46
+ const stack = error.stack
47
+ install()
48
+ return stack
49
+ }
50
+
51
+ const prepareStackTrace = (error, stack) => {
52
+ const onFailure = (failureData) => {
53
+ const failureCallbackArray = errorRemapFailureCallbackMap.get(error)
54
+ if (failureCallbackArray) {
55
+ failureCallbackArray.forEach((callback) => callback(failureData))
56
+ }
57
+ }
58
+
59
+ const stackRemappingPromise = remapStack({
60
+ stack,
61
+ error,
62
+ resolveFile,
63
+ fetchFile: memoizeFetch(fetchFile),
64
+ SourceMapConsumer,
65
+ readErrorStack,
66
+ indent,
67
+ onFailure,
68
+ })
69
+ errorRemappingCache.set(error, stackRemappingPromise)
70
+
71
+ return stringifyStack(stack, { error, indent })
72
+ }
73
+
74
+ const getErrorOriginalStackString = async (
75
+ error,
76
+ {
77
+ onFailure = (message) => {
78
+ console.warn(message)
79
+ },
80
+ } = {},
81
+ ) => {
82
+ if (onFailure) {
83
+ const remapFailureCallbackArray = errorRemapFailureCallbackMap.get(error)
84
+ if (remapFailureCallbackArray) {
85
+ errorRemapFailureCallbackMap.set(error, [
86
+ ...remapFailureCallbackArray,
87
+ onFailure,
88
+ ])
89
+ } else {
90
+ errorRemapFailureCallbackMap.set(error, [onFailure])
91
+ }
92
+ }
93
+
94
+ // ensure Error.prepareStackTrace gets triggered by reading error.stack now
95
+ const { stack } = error
96
+ const promise = errorRemappingCache.get(error)
97
+
98
+ if (promise) {
99
+ try {
100
+ const originalCallsites = await promise
101
+ errorRemapFailureCallbackMap.get(error)
102
+
103
+ const firstCall = originalCallsites[0]
104
+ if (firstCall) {
105
+ Object.assign(error, {
106
+ filename: firstCall.getFileName(),
107
+ lineno: firstCall.getLineNumber(),
108
+ columnno: firstCall.getColumnNumber(),
109
+ })
110
+ }
111
+ return stringifyStack(originalCallsites, { error, indent })
112
+ } catch (e) {
113
+ onFailure(
114
+ createDetailedMessage(`error while computing original stack.`, {
115
+ ["stack from error while computing"]: readErrorStack(e),
116
+ ["stack from error to remap"]: stack,
117
+ }),
118
+ )
119
+ return stack
120
+ }
121
+ }
122
+
123
+ return stack
124
+ }
125
+
126
+ install()
127
+
128
+ return { getErrorOriginalStackString, uninstall }
129
+ }
130
+
131
+ const memoizeFetch = (fetchUrl) => {
132
+ const urlCache = {}
133
+ return async (url) => {
134
+ if (url in urlCache) {
135
+ return urlCache[url]
136
+ }
137
+ const responsePromise = fetchUrl(url)
138
+ urlCache[url] = responsePromise
139
+ return responsePromise
140
+ }
141
+ }
@@ -0,0 +1,312 @@
1
+ import { startsWithWindowsDriveLetter, windowsFilePathToUrl } from "@jsenv/urls"
2
+
3
+ import { createDetailedMessage } from "./detailed_message.js"
4
+
5
+ export const remapCallSite = async (
6
+ callSite,
7
+ { urlToSourcemapConsumer, resolveFile, readErrorStack, onFailure },
8
+ ) => {
9
+ if (callSite.isNative()) {
10
+ return callSite
11
+ }
12
+
13
+ // Most call sites will return the source file from getFileName(), but code
14
+ // passed to eval() ending in "//# sourceURL=..." will return the source file
15
+ // from getScriptNameOrSourceURL() instead
16
+ const source = callSite.getFileName() || callSite.getScriptNameOrSourceURL()
17
+ if (source) {
18
+ const line = callSite.getLineNumber()
19
+ const column = callSite.getColumnNumber() - 1
20
+ const originalPosition = await remapSourcePosition({
21
+ source,
22
+ line,
23
+ column,
24
+ resolveFile,
25
+ urlToSourcemapConsumer,
26
+ readErrorStack,
27
+ onFailure,
28
+ })
29
+
30
+ const callSiteClone = cloneCallSite(callSite)
31
+
32
+ callSiteClone.getFunctionName = () =>
33
+ originalPosition.name || callSite.getFunctionName()
34
+ callSiteClone.getFileName = () => originalPosition.source
35
+ callSiteClone.getLineNumber = () => originalPosition.line
36
+ callSiteClone.getColumnNumber = () => originalPosition.column + 1
37
+ callSiteClone.getScriptNameOrSourceURL = () => originalPosition.source
38
+
39
+ return callSiteClone
40
+ }
41
+
42
+ // Code called using eval() needs special handling
43
+ if (callSite.isEval()) {
44
+ const origin = callSite.getEvalOrigin()
45
+ if (origin) {
46
+ const callSiteClone = cloneCallSite(callSite)
47
+ const originalEvalOrigin = await remapEvalOrigin(origin, {
48
+ resolveFile,
49
+ urlToSourcemapConsumer,
50
+ readErrorStack,
51
+ onFailure,
52
+ })
53
+ callSiteClone.getEvalOrigin = () => originalEvalOrigin
54
+ return callSiteClone
55
+ }
56
+ return callSite
57
+ }
58
+
59
+ // If we get here then we were unable to change the source position
60
+ return callSite
61
+ }
62
+
63
+ const cloneCallSite = (callSite) => {
64
+ const callSiteClone = {}
65
+ methods.forEach((name) => {
66
+ callSiteClone[name] = () => callSite[name]()
67
+ })
68
+ callSiteClone.toString = () => callSiteToFunctionCall(callSiteClone)
69
+
70
+ return callSiteClone
71
+ }
72
+
73
+ const methods = [
74
+ "getColumnNumber",
75
+ "getEvalOrigin",
76
+ "getFileName",
77
+ "getFunction",
78
+ "getFunctionName",
79
+ "getLineNumber",
80
+ "getMethodName",
81
+ "getPosition",
82
+ "getScriptNameOrSourceURL",
83
+ "getThis",
84
+ "getTypeName",
85
+ "isConstructor",
86
+ "isEval",
87
+ "isNative",
88
+ "isToplevel",
89
+ "toString",
90
+ ]
91
+
92
+ const callSiteToFunctionCall = (callSite) => {
93
+ const fileLocation = callSiteToFileLocation(callSite)
94
+ const isConstructor = callSite.isConstructor()
95
+ const isMethodCall = !callSite.isToplevel() && !isConstructor
96
+
97
+ if (isMethodCall) {
98
+ return `${callSiteToMethodCall(callSite)} (${fileLocation})`
99
+ }
100
+
101
+ const functionName = callSite.getFunctionName()
102
+ if (isConstructor) {
103
+ return `new ${functionName || "<anonymous>"} (${fileLocation})`
104
+ }
105
+
106
+ if (functionName) {
107
+ return `${functionName} (${fileLocation})`
108
+ }
109
+
110
+ return `${fileLocation}`
111
+ }
112
+
113
+ const callSiteToMethodCall = (callSite) => {
114
+ const functionName = callSite.getFunctionName()
115
+ const typeName = callSiteToType(callSite)
116
+
117
+ if (!functionName) {
118
+ return `${typeName}.<anonymous>`
119
+ }
120
+
121
+ const methodName = callSite.getMethodName()
122
+ const as = generateAs({ methodName, functionName })
123
+
124
+ if (typeName && !functionName.startsWith(typeName)) {
125
+ return `${typeName}.${functionName}${as}`
126
+ }
127
+
128
+ return `${functionName}${as}`
129
+ }
130
+
131
+ const generateAs = ({ methodName, functionName }) => {
132
+ if (!methodName) return ""
133
+ if (
134
+ functionName.indexOf(`.${methodName}`) ===
135
+ functionName.length - methodName.length - 1
136
+ )
137
+ return ""
138
+ return ` [as ${methodName}]`
139
+ }
140
+
141
+ const callSiteToType = (callSite) => {
142
+ const typeName = callSite.getTypeName()
143
+ // Fixes shim to be backward compatible with Node v0 to v4
144
+ if (typeName === "[object Object]") {
145
+ return "null"
146
+ }
147
+ return typeName
148
+ }
149
+
150
+ const callSiteToFileLocation = (callSite) => {
151
+ if (callSite.isNative()) return "native"
152
+
153
+ const sourceFile = callSiteToSourceFile(callSite)
154
+ const lineNumber = callSite.getLineNumber()
155
+ if (lineNumber === null) {
156
+ return sourceFile
157
+ }
158
+
159
+ const columnNumber = callSite.getColumnNumber()
160
+ if (!columnNumber) {
161
+ return `${sourceFile}:${lineNumber}`
162
+ }
163
+
164
+ return `${sourceFile}:${lineNumber}:${columnNumber}`
165
+ }
166
+
167
+ const callSiteToSourceFile = (callSite) => {
168
+ const fileName = callSite.getScriptNameOrSourceURL()
169
+
170
+ if (fileName) {
171
+ return fileName
172
+ }
173
+
174
+ // Source code does not originate from a file and is not native, but we
175
+ // can still get the source position inside the source string, e.g. in
176
+ // an eval string.
177
+ if (callSite.isEval()) {
178
+ return `${callSite.getEvalOrigin()}, <anonymous>`
179
+ }
180
+
181
+ return "<anonymous>"
182
+ }
183
+
184
+ // Parses code generated by FormatEvalOrigin(), a function inside V8:
185
+ // https://code.google.com/p/v8/source/browse/trunk/src/messages.js
186
+ const remapEvalOrigin = async (
187
+ origin,
188
+ { resolveFile, urlToSourcemapConsumer, onFailure },
189
+ ) => {
190
+ // Most eval() calls are in this format
191
+ const topLevelEvalMatch = /^eval at ([^(]+) \((.+):(\d+):(\d+)\)$/.exec(
192
+ origin,
193
+ )
194
+ if (topLevelEvalMatch) {
195
+ const source = topLevelEvalMatch[2]
196
+ const line = Number(topLevelEvalMatch[3])
197
+ const column = topLevelEvalMatch[4] - 1
198
+ const originalPosition = await remapSourcePosition({
199
+ source,
200
+ line,
201
+ column,
202
+ resolveFile,
203
+ urlToSourcemapConsumer,
204
+ onFailure,
205
+ })
206
+ return `eval at ${topLevelEvalMatch[1]} (${originalPosition.source}:${
207
+ originalPosition.line
208
+ }:${originalPosition.column + 1})`
209
+ }
210
+
211
+ // Parse nested eval() calls using recursion
212
+ const nestedEvalMatch = /^eval at ([^(]+) \((.+)\)$/.exec(origin)
213
+ if (nestedEvalMatch) {
214
+ const originalEvalOrigin = await remapEvalOrigin(nestedEvalMatch[2], {
215
+ resolveFile,
216
+ urlToSourcemapConsumer,
217
+ onFailure,
218
+ })
219
+ return `eval at ${nestedEvalMatch[1]} (${originalEvalOrigin})`
220
+ }
221
+
222
+ // Make sure we still return useful information if we didn't find anything
223
+ return origin
224
+ }
225
+
226
+ const remapSourcePosition = async ({
227
+ source,
228
+ line,
229
+ column,
230
+ resolveFile,
231
+ urlToSourcemapConsumer,
232
+ readErrorStack,
233
+ onFailure,
234
+ }) => {
235
+ const position = { source, line, column }
236
+
237
+ const url = sourceToUrl(source, { resolveFile })
238
+ if (!url) return position
239
+
240
+ const sourceMapConsumer = await urlToSourcemapConsumer(url)
241
+
242
+ if (!sourceMapConsumer) return position
243
+
244
+ try {
245
+ const originalPosition = sourceMapConsumer.originalPositionFor(position)
246
+
247
+ // Only return the original position if a matching line was found. If no
248
+ // matching line is found then we return position instead, which will cause
249
+ // the stack trace to print the path and line for the compiled file. It is
250
+ // better to give a precise location in the compiled file than a vague
251
+ // location in the original file.
252
+ const originalSource = originalPosition.source
253
+
254
+ if (originalSource === null) return position
255
+ originalPosition.source = resolveFile(originalSource, url, {
256
+ type: "file-original",
257
+ })
258
+
259
+ return originalPosition
260
+ } catch (e) {
261
+ onFailure(
262
+ createDetailedMessage(`error while remapping position.`, {
263
+ ["error stack"]: readErrorStack(e),
264
+ ["source"]: source,
265
+ ["line"]: line,
266
+ ["column"]: column,
267
+ }),
268
+ )
269
+ return position
270
+ }
271
+ }
272
+
273
+ const sourceToUrl = (source, { resolveFile }) => {
274
+ if (startsWithScheme(source)) {
275
+ return source
276
+ }
277
+
278
+ // linux filesystem path
279
+ if (source[0] === "/") {
280
+ return resolveFile(source)
281
+ }
282
+
283
+ // be careful, due to babel or something like that we might receive paths like
284
+ // C:/directory/file.js (without backslashes we would expect on windows)
285
+ // In that case we consider C: is the signe we are on windows
286
+ // And I avoid to rely on process.platform === "win32" because this file might be executed in chrome
287
+ if (startsWithWindowsDriveLetter(source)) {
288
+ return windowsFilePathToUrl(source)
289
+ }
290
+
291
+ // I don't think we will ever encounter relative file in the stack trace
292
+ // but if it ever happens we are safe :)
293
+ if (source.slice(0, 2) === "./" || source.slice(0, 3) === "../") {
294
+ return resolveFile(source)
295
+ }
296
+
297
+ // we have received a "bare specifier" for the source
298
+ // it happens for internal/process/task_queues.js for instance
299
+ // if we do return resolveFile(source) it will be converted to
300
+ // file:///C:/project-directory/internal/process/task_queues.js in node
301
+ // and
302
+ // http://domain.com/internal/process/task_queues.js
303
+ // but the file will certainly be a 404
304
+ // and if not it won't be the right file anyway
305
+ // for now we assume "bare specifier" in the stack trace
306
+ // are internal files that are pointless to try to remap
307
+ return null
308
+ }
309
+
310
+ const startsWithScheme = (string) => {
311
+ return /^[a-zA-Z]{2,}:/.test(string)
312
+ }
@@ -0,0 +1,220 @@
1
+ import { DATA_URL } from "@jsenv/urls"
2
+
3
+ import { SOURCEMAP } from "../sourcemap_comment.js"
4
+ import { createDetailedMessage } from "./detailed_message.js"
5
+ import { remapCallSite } from "./remap_call_site.js"
6
+
7
+ export const remapStack = async ({
8
+ stack,
9
+ resolveFile,
10
+ fetchFile,
11
+ SourceMapConsumer,
12
+ readErrorStack,
13
+ onFailure,
14
+ }) => {
15
+ const urlToSourcemapConsumer = memoizeByFirstArgStringValue(
16
+ async (stackTraceFileUrl) => {
17
+ if (stackTraceFileUrl.startsWith("node:")) {
18
+ return null
19
+ }
20
+
21
+ try {
22
+ let text
23
+ try {
24
+ const fileResponse = await fetchFile(stackTraceFileUrl)
25
+ const { status } = fileResponse
26
+ if (status !== 200) {
27
+ if (status === 404) {
28
+ onFailure(`stack trace file not found at ${stackTraceFileUrl}`)
29
+ } else {
30
+ onFailure(
31
+ createDetailedMessage(
32
+ `unexpected response fetching stack trace file.`,
33
+ {
34
+ ["response status"]: status,
35
+ ["response text"]: fileResponse.body,
36
+ ["stack trace file"]: stackTraceFileUrl,
37
+ },
38
+ ),
39
+ )
40
+ }
41
+ return null
42
+ }
43
+ text = await fileResponse.text()
44
+ } catch (e) {
45
+ onFailure(
46
+ createDetailedMessage(`error while fetching stack trace file.`, {
47
+ ["fetch error stack"]: readErrorStack(e),
48
+ ["stack trace file"]: stackTraceFileUrl,
49
+ }),
50
+ )
51
+
52
+ return null
53
+ }
54
+
55
+ const jsSourcemapComment = SOURCEMAP.readComment({
56
+ contentType: "text/javascript",
57
+ content: text,
58
+ })
59
+ if (!jsSourcemapComment) {
60
+ return null
61
+ }
62
+ const jsSourcemapUrl = jsSourcemapComment.specifier
63
+ let sourcemapUrl
64
+ let sourcemapString
65
+ if (jsSourcemapUrl.startsWith("data:")) {
66
+ sourcemapUrl = stackTraceFileUrl
67
+ sourcemapString = DATA_URL.parse(jsSourcemapUrl, { as: "string" })
68
+ } else {
69
+ sourcemapUrl = resolveFile(jsSourcemapUrl, stackTraceFileUrl, {
70
+ type: "source-map",
71
+ })
72
+
73
+ try {
74
+ const sourcemapResponse = await fetchFile(sourcemapUrl)
75
+ const { status } = sourcemapResponse
76
+ if (status !== 200) {
77
+ if (status === 404) {
78
+ onFailure(`sourcemap file not found at ${sourcemapUrl}`)
79
+ } else {
80
+ onFailure(
81
+ createDetailedMessage(
82
+ `unexpected response for sourcemap file.`,
83
+ {
84
+ ["response status"]: status,
85
+ ["response text"]: await sourcemapResponse.text(),
86
+ ["sourcemap url"]: sourcemapUrl,
87
+ },
88
+ ),
89
+ )
90
+ }
91
+ return null
92
+ }
93
+ sourcemapString = await sourcemapResponse.text()
94
+ } catch (e) {
95
+ onFailure(
96
+ createDetailedMessage(`error while fetching sourcemap.`, {
97
+ ["fetch error stack"]: readErrorStack(e),
98
+ ["sourcemap url"]: sourcemapUrl,
99
+ }),
100
+ )
101
+ return null
102
+ }
103
+ }
104
+
105
+ let sourceMap
106
+ try {
107
+ sourceMap = JSON.parse(sourcemapString)
108
+ } catch (e) {
109
+ onFailure(
110
+ createDetailedMessage(`error while parsing sourcemap.`, {
111
+ ["parse error stack"]: readErrorStack(e),
112
+ ["sourcemap url"]: sourcemapUrl,
113
+ }),
114
+ )
115
+ return null
116
+ }
117
+
118
+ let { sourcesContent } = sourceMap
119
+
120
+ if (!sourcesContent) {
121
+ sourcesContent = []
122
+ sourceMap.sourcesContent = sourcesContent
123
+ }
124
+
125
+ let firstSourceMapSourceFailure = null
126
+
127
+ await Promise.all(
128
+ sourceMap.sources.map(async (source, index) => {
129
+ if (index in sourcesContent) return
130
+
131
+ const sourcemapSourceUrl = resolveFile(source, sourcemapUrl, {
132
+ type: "source",
133
+ })
134
+ try {
135
+ const sourceResponse = await fetchFile(sourcemapSourceUrl)
136
+ const { status } = sourceResponse
137
+ if (status !== 200) {
138
+ if (firstSourceMapSourceFailure) return
139
+
140
+ if (status === 404) {
141
+ firstSourceMapSourceFailure = createDetailedMessage(
142
+ `sourcemap source not found.`,
143
+ {
144
+ ["sourcemap source url"]: sourcemapSourceUrl,
145
+ ["sourcemap url"]: sourcemapUrl,
146
+ },
147
+ )
148
+ return
149
+ }
150
+ firstSourceMapSourceFailure = createDetailedMessage(
151
+ `unexpected response for sourcemap source.`,
152
+ {
153
+ ["response status"]: status,
154
+ ["response text"]: await sourceResponse.text(),
155
+ ["sourcemap source url"]: sourcemapSourceUrl,
156
+ ["sourcemap url"]: sourcemapUrl,
157
+ },
158
+ )
159
+ return
160
+ }
161
+
162
+ const sourceString = await sourceResponse.text()
163
+ sourcesContent[index] = sourceString
164
+ } catch (e) {
165
+ if (firstSourceMapSourceFailure) return
166
+ firstSourceMapSourceFailure = createDetailedMessage(
167
+ `error while fetching sourcemap source.`,
168
+ {
169
+ ["fetch error stack"]: readErrorStack(e),
170
+ ["sourcemap source url"]: sourcemapSourceUrl,
171
+ ["sourcemap url"]: sourcemapUrl,
172
+ },
173
+ )
174
+ }
175
+ }),
176
+ )
177
+
178
+ if (firstSourceMapSourceFailure) {
179
+ onFailure(firstSourceMapSourceFailure)
180
+ return null
181
+ }
182
+
183
+ return new SourceMapConsumer(sourceMap)
184
+ } catch (e) {
185
+ onFailure(
186
+ createDetailedMessage(
187
+ `error while preparing a sourceMap consumer for a stack trace file.`,
188
+ {
189
+ ["error stack"]: readErrorStack(e),
190
+ ["stack trace file"]: stackTraceFileUrl,
191
+ },
192
+ ),
193
+ )
194
+ return null
195
+ }
196
+ },
197
+ )
198
+ const originalCallsites = await Promise.all(
199
+ stack.map((callSite) =>
200
+ remapCallSite(callSite, {
201
+ resolveFile,
202
+ urlToSourcemapConsumer,
203
+ readErrorStack,
204
+ onFailure,
205
+ }),
206
+ ),
207
+ )
208
+ return originalCallsites
209
+ }
210
+
211
+ const memoizeByFirstArgStringValue = (fn) => {
212
+ const stringValueCache = {}
213
+ return (firstArgValue) => {
214
+ if (firstArgValue in stringValueCache)
215
+ return stringValueCache[firstArgValue]
216
+ const value = fn(firstArgValue)
217
+ stringValueCache[firstArgValue] = value
218
+ return value
219
+ }
220
+ }
@@ -0,0 +1,9 @@
1
+ export const stringifyStack = (stack, { error, indent }) => {
2
+ const name = error.name || "Error"
3
+ const message = error.message || ""
4
+ const stackString = stack
5
+ .map((callSite) => `\n${indent}at ${callSite}`)
6
+ .join("")
7
+
8
+ return `${name}: ${message}${stackString}`
9
+ }
@@ -0,0 +1,52 @@
1
+ import MagicString from "magic-string"
2
+
3
+ export const createMagicSource = (content) => {
4
+ if (content === undefined) {
5
+ throw new Error("content missing")
6
+ }
7
+ const mutations = []
8
+ return {
9
+ prepend: (string) => {
10
+ mutations.push((magicString) => {
11
+ magicString.prepend(string)
12
+ })
13
+ },
14
+ append: (string) => {
15
+ mutations.push((magicString) => {
16
+ magicString.append(string)
17
+ })
18
+ },
19
+ replace: ({ start, end, replacement }) => {
20
+ mutations.push((magicString) => {
21
+ magicString.overwrite(start, end, replacement)
22
+ })
23
+ },
24
+ remove: ({ start, end }) => {
25
+ mutations.push((magicString) => {
26
+ magicString.remove(start, end)
27
+ })
28
+ },
29
+ toContentAndSourcemap: ({ source } = {}) => {
30
+ if (mutations.length === 0) {
31
+ return {
32
+ content,
33
+ sourcemap: null,
34
+ }
35
+ }
36
+ const magicString = new MagicString(content)
37
+ mutations.forEach((mutation) => {
38
+ mutation(magicString)
39
+ })
40
+ const code = magicString.toString()
41
+ const map = magicString.generateMap({
42
+ hires: true,
43
+ includeContent: true,
44
+ source,
45
+ })
46
+ return {
47
+ content: code,
48
+ sourcemap: map,
49
+ }
50
+ },
51
+ }
52
+ }
package/src/main.js ADDED
@@ -0,0 +1,10 @@
1
+ export { createMagicSource } from "./magic_source.js"
2
+ export { composeTwoSourcemaps } from "./sourcemap_composition_v3.js"
3
+
4
+ export { getOriginalPosition } from "./original_position.js"
5
+ export { sourcemapConverter } from "./sourcemap_converter.js"
6
+ export {
7
+ generateSourcemapFileUrl,
8
+ generateSourcemapDataUrl,
9
+ } from "./sourcemap_url_generator.js"
10
+ export { SOURCEMAP } from "./sourcemap_comment.js"
@@ -0,0 +1,19 @@
1
+ import { requireSourcemap } from "./require_sourcemap.js"
2
+
3
+ // https://github.com/mozilla/source-map#sourcemapconsumerprototypeoriginalpositionforgeneratedposition
4
+ export const getOriginalPosition = async ({
5
+ sourcemap,
6
+ line,
7
+ column,
8
+ bias,
9
+ }) => {
10
+ const { SourceMapConsumer } = requireSourcemap()
11
+
12
+ const sourceMapConsumer = await new SourceMapConsumer(sourcemap)
13
+ const originalPosition = sourceMapConsumer.originalPositionFor({
14
+ line,
15
+ column,
16
+ bias,
17
+ })
18
+ return originalPosition
19
+ }
@@ -0,0 +1,9 @@
1
+ import { createRequire } from "node:module"
2
+
3
+ const require = createRequire(import.meta.url)
4
+ // consider using https://github.com/7rulnik/source-map-js
5
+
6
+ export const requireSourcemap = () => {
7
+ const namespace = require("source-map")
8
+ return namespace
9
+ }
@@ -0,0 +1,143 @@
1
+ export const SOURCEMAP = {
2
+ enabledOnContentType: (contentType) => {
3
+ return ["text/javascript", "text/css"].includes(contentType)
4
+ },
5
+
6
+ readComment: ({ contentType, content }) => {
7
+ const read = {
8
+ "text/javascript": parseJavaScriptSourcemapComment,
9
+ "text/css": parseCssSourcemapComment,
10
+ }[contentType]
11
+ return read ? read(content) : null
12
+ },
13
+
14
+ writeComment: ({ contentType, content, specifier }) => {
15
+ const write = {
16
+ "text/javascript": setJavaScriptSourceMappingUrl,
17
+ "text/css": setCssSourceMappingUrl,
18
+ }[contentType]
19
+ return write ? write(content, specifier) : content
20
+ },
21
+ }
22
+
23
+ const parseJavaScriptSourcemapComment = (javaScriptSource) => {
24
+ let sourceMappingUrl
25
+ replaceSourceMappingUrl(
26
+ javaScriptSource,
27
+ javascriptSourceMappingUrlCommentRegexp,
28
+ (value) => {
29
+ sourceMappingUrl = value
30
+ },
31
+ )
32
+ if (!sourceMappingUrl) {
33
+ return null
34
+ }
35
+ return {
36
+ type: "sourcemap_comment",
37
+ subtype: "js",
38
+ // we assume it's on last line
39
+ line: javaScriptSource.split(/\r?\n/).length,
40
+ // ${"//#"} is to avoid static analysis to think there is a sourceMappingUrl for this file
41
+ column: `${"//#"} sourceMappingURL=`.length + 1,
42
+ specifier: sourceMappingUrl,
43
+ }
44
+ }
45
+
46
+ const setJavaScriptSourceMappingUrl = (
47
+ javaScriptSource,
48
+ sourceMappingFileUrl,
49
+ ) => {
50
+ let replaced
51
+ const sourceAfterReplace = replaceSourceMappingUrl(
52
+ javaScriptSource,
53
+ javascriptSourceMappingUrlCommentRegexp,
54
+ () => {
55
+ replaced = true
56
+ return sourceMappingFileUrl
57
+ ? writeJavaScriptSourceMappingURL(sourceMappingFileUrl)
58
+ : ""
59
+ },
60
+ )
61
+ if (replaced) {
62
+ return sourceAfterReplace
63
+ }
64
+
65
+ return sourceMappingFileUrl
66
+ ? `${javaScriptSource}
67
+ ${writeJavaScriptSourceMappingURL(sourceMappingFileUrl)}
68
+ `
69
+ : javaScriptSource
70
+ }
71
+
72
+ const parseCssSourcemapComment = (cssSource) => {
73
+ let sourceMappingUrl
74
+ replaceSourceMappingUrl(
75
+ cssSource,
76
+ cssSourceMappingUrlCommentRegExp,
77
+ (value) => {
78
+ sourceMappingUrl = value
79
+ },
80
+ )
81
+ if (!sourceMappingUrl) {
82
+ return null
83
+ }
84
+ return {
85
+ type: "sourcemap_comment",
86
+ subtype: "css",
87
+ // we assume it's on last line
88
+ line: cssSource.split(/\r?\n/).length - 1,
89
+ // ${"//*#"} is to avoid static analysis to think there is a sourceMappingUrl for this file
90
+ column: `${"//*#"} sourceMappingURL=`.length + 1,
91
+ specifier: sourceMappingUrl,
92
+ }
93
+ }
94
+
95
+ const setCssSourceMappingUrl = (cssSource, sourceMappingFileUrl) => {
96
+ let replaced
97
+ const sourceAfterReplace = replaceSourceMappingUrl(
98
+ cssSource,
99
+ cssSourceMappingUrlCommentRegExp,
100
+ () => {
101
+ replaced = true
102
+ return sourceMappingFileUrl
103
+ ? writeCssSourceMappingUrl(sourceMappingFileUrl)
104
+ : ""
105
+ },
106
+ )
107
+ if (replaced) {
108
+ return sourceAfterReplace
109
+ }
110
+ return sourceMappingFileUrl
111
+ ? `${cssSource}
112
+ ${writeCssSourceMappingUrl(sourceMappingFileUrl)}
113
+ `
114
+ : cssSource
115
+ }
116
+
117
+ const javascriptSourceMappingUrlCommentRegexp =
118
+ /\/\/ ?# ?sourceMappingURL=([^\s'"]+)/g
119
+ const cssSourceMappingUrlCommentRegExp =
120
+ /\/\*# ?sourceMappingURL=([^\s'"]+) \*\//g
121
+
122
+ // ${"//#"} is to avoid a parser thinking there is a sourceMappingUrl for this file
123
+ const writeJavaScriptSourceMappingURL = (value) =>
124
+ `${"//#"} sourceMappingURL=${value}`
125
+ const writeCssSourceMappingUrl = (value) => `/*# sourceMappingURL=${value} */`
126
+
127
+ const replaceSourceMappingUrl = (source, regexp, callback) => {
128
+ let lastSourceMappingUrl
129
+ let matchSourceMappingUrl
130
+ while ((matchSourceMappingUrl = regexp.exec(source))) {
131
+ lastSourceMappingUrl = matchSourceMappingUrl
132
+ }
133
+ if (lastSourceMappingUrl) {
134
+ const index = lastSourceMappingUrl.index
135
+ const before = source.slice(0, index)
136
+ const after = source.slice(index)
137
+ const mappedAfter = after.replace(regexp, (match, firstGroup) => {
138
+ return callback(firstGroup)
139
+ })
140
+ return `${before}${mappedAfter}`
141
+ }
142
+ return source
143
+ }
@@ -0,0 +1,87 @@
1
+ /*
2
+ * https://github.com/mozilla/source-map#sourcemapgenerator
3
+ */
4
+
5
+ import { requireSourcemap } from "./require_sourcemap.js"
6
+
7
+ const { SourceMapConsumer, SourceMapGenerator } = requireSourcemap()
8
+
9
+ export const composeTwoSourcemaps = async (firstSourcemap, secondSourcemap) => {
10
+ if (!firstSourcemap && !secondSourcemap) {
11
+ return null
12
+ }
13
+ if (!firstSourcemap) {
14
+ return secondSourcemap
15
+ }
16
+ if (!secondSourcemap) {
17
+ return firstSourcemap
18
+ }
19
+ const sourcemapGenerator = new SourceMapGenerator()
20
+ const firstSourcemapConsumer = await new SourceMapConsumer(firstSourcemap)
21
+ const secondSourcemapConsumer = await new SourceMapConsumer(secondSourcemap)
22
+ const firstMappings = readMappings(firstSourcemapConsumer)
23
+ firstMappings.forEach((mapping) => {
24
+ sourcemapGenerator.addMapping(mapping)
25
+ })
26
+ const secondMappings = readMappings(secondSourcemapConsumer)
27
+ secondMappings.forEach((mapping) => {
28
+ sourcemapGenerator.addMapping(mapping)
29
+ })
30
+ const sourcemap = sourcemapGenerator.toJSON()
31
+ const sources = []
32
+ const sourcesContent = []
33
+ const firstSourcesContent = firstSourcemap.sourcesContent
34
+ const secondSourcesContent = secondSourcemap.sourcesContent
35
+ sourcemap.sources.forEach((source) => {
36
+ sources.push(source)
37
+ if (secondSourcesContent) {
38
+ const secondSourceIndex = secondSourcemap.sources.indexOf(source)
39
+ if (secondSourceIndex > -1) {
40
+ sourcesContent.push(secondSourcesContent[secondSourceIndex])
41
+ return
42
+ }
43
+ }
44
+ if (firstSourcesContent) {
45
+ const firstSourceIndex = firstSourcemap.sources.indexOf(source)
46
+ if (firstSourceIndex > -1) {
47
+ sourcesContent.push(firstSourcesContent[firstSourceIndex])
48
+ return
49
+ }
50
+ }
51
+ sourcesContent.push(null)
52
+ })
53
+ sourcemap.sources = sources
54
+ sourcemap.sourcesContent = sourcesContent
55
+ return sourcemap
56
+ }
57
+
58
+ const readMappings = (consumer) => {
59
+ const mappings = []
60
+ consumer.eachMapping(
61
+ ({
62
+ originalColumn,
63
+ originalLine,
64
+ generatedColumn,
65
+ generatedLine,
66
+ source,
67
+ name,
68
+ }) => {
69
+ mappings.push({
70
+ original:
71
+ typeof originalColumn === "number"
72
+ ? {
73
+ column: originalColumn,
74
+ line: originalLine,
75
+ }
76
+ : undefined,
77
+ generated: {
78
+ column: generatedColumn,
79
+ line: generatedLine,
80
+ },
81
+ source: typeof originalColumn === "number" ? source : undefined,
82
+ name,
83
+ })
84
+ },
85
+ )
86
+ return mappings
87
+ }
@@ -0,0 +1,24 @@
1
+ import {
2
+ isFileSystemPath,
3
+ fileSystemPathToUrl,
4
+ urlToFileSystemPath,
5
+ } from "@jsenv/urls"
6
+
7
+ export const sourcemapConverter = {
8
+ toFileUrls: (sourcemap) => {
9
+ return {
10
+ ...sourcemap,
11
+ sources: sourcemap.sources.map((source) => {
12
+ return isFileSystemPath(source) ? fileSystemPathToUrl(source) : source
13
+ }),
14
+ }
15
+ },
16
+ toFilePaths: (sourcemap) => {
17
+ return {
18
+ ...sourcemap,
19
+ sources: sourcemap.sources.map((source) => {
20
+ return urlToFileSystemPath(source)
21
+ }),
22
+ }
23
+ },
24
+ }
@@ -0,0 +1,15 @@
1
+ export const generateSourcemapFileUrl = (url) => {
2
+ const urlObject = new URL(url)
3
+ let { origin, pathname, search, hash } = urlObject
4
+ // origin is "null" for "file://" urls with Node.js
5
+ if (origin === "null" && urlObject.href.startsWith("file:")) {
6
+ origin = "file://"
7
+ }
8
+ const sourcemapUrl = `${origin}${pathname}.map${search}${hash}`
9
+ return sourcemapUrl
10
+ }
11
+
12
+ export const generateSourcemapDataUrl = (sourcemap) => {
13
+ const asBase64 = Buffer.from(JSON.stringify(sourcemap)).toString("base64")
14
+ return `data:application/json;charset=utf-8;base64,${asBase64}`
15
+ }
@@ -0,0 +1,13 @@
1
+ import { createMagicSource } from "@jsenv/sourcemap"
2
+
3
+ const content = `import { inferContextFrom, createUrlContext } from "../url_context.js";
4
+ // toto
5
+ `
6
+ const magic = createMagicSource(content)
7
+ magic.replace({
8
+ start: 51,
9
+ end: 70,
10
+ replacement: `"/@fs/Users/d.maillard/dev/jsenv/jsenv-core/src/internal/url_context.js"`,
11
+ })
12
+ const ret = magic.toContentAndSourcemap()
13
+ console.log(ret)