github.com/evanw/esbuild@v0.21.4/scripts/verify-source-map.js (about) 1 const { SourceMapConsumer } = require('source-map') 2 const { buildBinary, removeRecursiveSync } = require('./esbuild') 3 const childProcess = require('child_process') 4 const path = require('path') 5 const util = require('util') 6 const fs = require('fs').promises 7 8 const execFileAsync = util.promisify(childProcess.execFile) 9 10 const esbuildPath = buildBinary() 11 const testDir = path.join(__dirname, '.verify-source-map') 12 let tempDirCount = 0 13 14 const toSearchBundle = { 15 a0: 'a.js', 16 a1: 'a.js', 17 a2: 'a.js', 18 b0: 'b-dir/b.js', 19 b1: 'b-dir/b.js', 20 b2: 'b-dir/b.js', 21 c0: 'b-dir/c-dir/c.js', 22 c1: 'b-dir/c-dir/c.js', 23 c2: 'b-dir/c-dir/c.js', 24 } 25 26 const toSearchNoBundle = { 27 a0: 'a.js', 28 a1: 'a.js', 29 a2: 'a.js', 30 } 31 32 const toSearchNoBundleTS = { 33 a0: 'a.ts', 34 a1: 'a.ts', 35 a2: 'a.ts', 36 } 37 38 const testCaseES6 = { 39 'a.js': ` 40 import {b0} from './b-dir/b' 41 function a0() { a1("a0") } 42 function a1() { a2("a1") } 43 function a2() { b0("a2") } 44 a0() 45 `, 46 'b-dir/b.js': ` 47 import {c0} from './c-dir/c' 48 export function b0() { b1("b0") } 49 function b1() { b2("b1") } 50 function b2() { c0("b2") } 51 `, 52 'b-dir/c-dir/c.js': ` 53 export function c0() { c1("c0") } 54 function c1() { c2("c1") } 55 function c2() { throw new Error("c2") } 56 `, 57 } 58 59 const testCaseCommonJS = { 60 'a.js': ` 61 const {b0} = require('./b-dir/b') 62 function a0() { a1("a0") } 63 function a1() { a2("a1") } 64 function a2() { b0("a2") } 65 a0() 66 `, 67 'b-dir/b.js': ` 68 const {c0} = require('./c-dir/c') 69 exports.b0 = function() { b1("b0") } 70 function b1() { b2("b1") } 71 function b2() { c0("b2") } 72 `, 73 'b-dir/c-dir/c.js': ` 74 exports.c0 = function() { c1("c0") } 75 function c1() { c2("c1") } 76 function c2() { throw new Error("c2") } 77 `, 78 } 79 80 const testCaseDiscontiguous = { 81 'a.js': ` 82 import {b0} from './b-dir/b.js' 83 import {c0} from './b-dir/c-dir/c.js' 84 function a0() { a1("a0") } 85 function a1() { a2("a1") } 86 function a2() { b0("a2") } 87 a0(b0, c0) 88 `, 89 'b-dir/b.js': ` 90 exports.b0 = function() { b1("b0") } 91 function b1() { b2("b1") } 92 function b2() { c0("b2") } 93 `, 94 'b-dir/c-dir/c.js': ` 95 export function c0() { c1("c0") } 96 function c1() { c2("c1") } 97 function c2() { throw new Error("c2") } 98 `, 99 } 100 101 const testCaseTypeScriptRuntime = { 102 'a.ts': ` 103 namespace Foo { 104 export var {a, ...b} = foo() // This requires a runtime function to handle 105 console.log(a, b) 106 } 107 function a0() { a1("a0") } 108 function a1() { a2("a1") } 109 function a2() { throw new Error("a2") } 110 a0() 111 `, 112 } 113 114 const testCaseStdin = { 115 '<stdin>': `#!/usr/bin/env node 116 function a0() { a1("a0") } 117 function a1() { a2("a1") } 118 function a2() { throw new Error("a2") } 119 a0() 120 `, 121 } 122 123 const testCaseEmptyFile = { 124 'entry.js': ` 125 import './before' 126 import {fn} from './re-export' 127 import './after' 128 fn() 129 `, 130 're-export.js': ` 131 // This file will be empty in the generated code, which was causing 132 // an off-by-one error with the source index in the source map 133 export {default as fn} from './test' 134 `, 135 'test.js': ` 136 export default function() { 137 console.log("test") 138 } 139 `, 140 'before.js': ` 141 console.log("before") 142 `, 143 'after.js': ` 144 console.log("after") 145 `, 146 } 147 148 const toSearchEmptyFile = { 149 before: 'before.js', 150 test: 'test.js', 151 after: 'after.js', 152 } 153 154 const testCaseNonJavaScriptFile = { 155 'entry.js': ` 156 import './before' 157 import text from './file.txt' 158 import './after' 159 console.log(text) 160 `, 161 'file.txt': ` 162 This is some text. 163 `, 164 'before.js': ` 165 console.log("before") 166 `, 167 'after.js': ` 168 console.log("after") 169 `, 170 } 171 172 const toSearchNonJavaScriptFile = { 173 before: 'before.js', 174 after: 'after.js', 175 } 176 177 const testCaseCodeSplitting = { 178 'out.ts': ` 179 import value from './shared' 180 console.log("out", value) 181 `, 182 'other.ts': ` 183 import value from './shared' 184 console.log("other", value) 185 `, 186 'shared.ts': ` 187 export default 123 188 `, 189 } 190 191 const toSearchCodeSplitting = { 192 out: 'out.ts', 193 } 194 195 const testCaseUnicode = { 196 'entry.js': ` 197 import './a' 198 import './b' 199 `, 200 'a.js': ` 201 console.log('🍕🍕🍕', "a") 202 `, 203 'b.js': ` 204 console.log({𐀀: "b"}) 205 `, 206 } 207 208 const toSearchUnicode = { 209 a: 'a.js', 210 b: 'b.js', 211 } 212 213 const testCasePartialMappings = { 214 // The "mappings" value is "A,Q,I;A,Q,I;A,Q,I;AAMA,QAAQ,IAAI;" which contains 215 // partial mappings without original locations. This used to throw things off. 216 'entry.js': `console.log(1); 217 console.log(2); 218 console.log(3); 219 console.log("entry"); 220 //# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKIC` + 221 `Aic291cmNlcyI6IFsiZW50cnkuanMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImNvbnNvb` + 222 `GUubG9nKDEpXG5cbmNvbnNvbGUubG9nKDIpXG5cbmNvbnNvbGUubG9nKDMpXG5cbmNvbnNv` + 223 `bGUubG9nKFwiZW50cnlcIilcbiJdLAogICJtYXBwaW5ncyI6ICJBLFEsSTtBLFEsSTtBLFE` + 224 `sSTtBQU1BLFFBQVEsSUFBSTsiLAogICJuYW1lcyI6IFtdCn0= 225 `, 226 } 227 228 const testCasePartialMappingsPercentEscape = { 229 // The "mappings" value is "A,Q,I;A,Q,I;A,Q,I;AAMA,QAAQ,IAAI;" which contains 230 // partial mappings without original locations. This used to throw things off. 231 'entry.js': `console.log(1); 232 console.log(2); 233 console.log(3); 234 console.log("entry"); 235 //# sourceMappingURL=data:,%7B%22version%22%3A3%2C%22sources%22%3A%5B%22entr` + 236 `y.js%22%5D%2C%22sourcesContent%22%3A%5B%22console.log(1)%5Cn%5Cnconsole` + 237 `.log(2)%5Cn%5Cnconsole.log(3)%5Cn%5Cnconsole.log(%5C%22entry%5C%22)%5Cn` + 238 `%22%5D%2C%22mappings%22%3A%22A%2CQ%2CI%3BA%2CQ%2CI%3BA%2CQ%2CI%3BAAMA%2` + 239 `CQAAQ%2CIAAI%3B%22%2C%22names%22%3A%5B%5D%7D 240 `, 241 } 242 243 const toSearchPartialMappings = { 244 entry: 'entry.js', 245 } 246 247 const testCaseComplex = { 248 // "fuse.js" is included because it has a nested source map of some complexity. 249 // "react" is included after that because it's a big blob of code and helps 250 // make sure stuff after a nested source map works ok. 251 'entry.js': ` 252 import Fuse from 'fuse.js' 253 import * as React from 'react' 254 console.log(Fuse, React) 255 `, 256 } 257 258 const toSearchComplex = { 259 '[object Array]': '../../node_modules/fuse.js/dist/webpack:/src/helpers/is_array.js', 260 'Score average:': '../../node_modules/fuse.js/dist/webpack:/src/index.js', 261 '0123456789': '../../node_modules/object-assign/index.js', 262 'forceUpdate': '../../node_modules/react/cjs/react.production.min.js', 263 }; 264 265 const testCaseDynamicImport = { 266 'entry.js': ` 267 const then = (x) => console.log("imported", x); 268 console.log([import("./ext/a.js").then(then), import("./ext/ab.js").then(then), import("./ext/abc.js").then(then)]); 269 console.log([import("./ext/abc.js").then(then), import("./ext/ab.js").then(then), import("./ext/a.js").then(then)]); 270 `, 271 'ext/a.js': ` 272 export default 'a' 273 `, 274 'ext/ab.js': ` 275 export default 'ab' 276 `, 277 'ext/abc.js': ` 278 export default 'abc' 279 `, 280 } 281 282 const toSearchDynamicImport = { 283 './ext/a.js': 'entry.js', 284 './ext/ab.js': 'entry.js', 285 './ext/abc.js': 'entry.js', 286 }; 287 288 const toSearchBundleCSS = { 289 a0: 'a.css', 290 a1: 'a.css', 291 a2: 'a.css', 292 b0: 'b-dir/b.css', 293 b1: 'b-dir/b.css', 294 b2: 'b-dir/b.css', 295 c0: 'b-dir/c-dir/c.css', 296 c1: 'b-dir/c-dir/c.css', 297 c2: 'b-dir/c-dir/c.css', 298 } 299 300 const testCaseBundleCSS = { 301 'entry.css': ` 302 @import "a.css"; 303 `, 304 'a.css': ` 305 @import "b-dir/b.css"; 306 a:nth-child(0):after { content: "a0"; } 307 a:nth-child(1):after { content: "a1"; } 308 a:nth-child(2):after { content: "a2"; } 309 `, 310 'b-dir/b.css': ` 311 @import "c-dir/c.css"; 312 b:nth-child(0):after { content: "b0"; } 313 b:nth-child(1):after { content: "b1"; } 314 b:nth-child(2):after { content: "b2"; } 315 `, 316 'b-dir/c-dir/c.css': ` 317 c:nth-child(0):after { content: "c0"; } 318 c:nth-child(1):after { content: "c1"; } 319 c:nth-child(2):after { content: "c2"; } 320 `, 321 } 322 323 const testCaseJSXRuntime = { 324 'entry.jsx': ` 325 import { A0, A1, A2 } from './a.jsx'; 326 console.log(<A0><A1/><A2/></A0>) 327 `, 328 'a.jsx': ` 329 import {jsx} from './b-dir/b' 330 import {Fragment} from './b-dir/c-dir/c' 331 export function A0() { return <Fragment id="A0"><>a0</></Fragment> } 332 export function A1() { return <div {...jsx} data-testid="A1">a1</div> } 333 export function A2() { return <A1 id="A2"><a/><b/></A1> } 334 `, 335 'b-dir/b.js': ` 336 export const jsx = {id: 'jsx'} 337 `, 338 'b-dir/c-dir/c.jsx': ` 339 exports.Fragment = function() { return <></> } 340 `, 341 } 342 343 const toSearchJSXRuntime = { 344 A0: 'a.jsx', 345 A1: 'a.jsx', 346 A2: 'a.jsx', 347 jsx: 'b-dir/b.js', 348 } 349 350 const testCaseNames = { 351 'entry.js': ` 352 import "./nested1" 353 354 // Test regular name positions 355 var /**/foo = /**/foo || 0 356 function /**/fn(/**/bar) {} 357 class /**/cls {} 358 keep(fn, cls) // Make sure these aren't removed 359 360 // Test property mangling name positions 361 var { /**/mangle_: bar } = foo 362 var { /**/'mangle_': bar } = foo 363 foo./**/mangle_ = 1 364 foo[/**/'mangle_'] 365 foo = { /**/mangle_: 0 } 366 foo = { /**/'mangle_': 0 } 367 foo = class { /**/mangle_ = 0 } 368 foo = class { /**/'mangle_' = 0 } 369 foo = /**/'mangle_' in bar 370 `, 371 'nested1.js': ` 372 import { foo } from './nested2' 373 foo(bar) 374 `, 375 'nested2.jsx': ` 376 export let /**/foo = /**/bar => /**/bar() 377 ` 378 } 379 380 const testCaseMissingSourcesContent = { 381 'foo.js': `// foo.ts 382 var foo = { bar: "bar" }; 383 console.log({ foo }); 384 //# sourceMappingURL=maps/foo.js.map 385 `, 386 'maps/foo.js.map': `{ 387 "version": 3, 388 "sources": ["src/foo.ts"], 389 "mappings": ";AAGA,IAAM,MAAW,EAAE,KAAK,MAAM;AAC9B,QAAQ,IAAI,EAAE,IAAI,CAAC;", 390 "names": [] 391 } 392 `, 393 'maps/src/foo.ts': `interface Foo { 394 bar: string 395 } 396 const foo: Foo = { bar: 'bar' } 397 console.log({ foo }) 398 `, 399 } 400 401 const toSearchMissingSourcesContent = { 402 bar: 'src/foo.ts', 403 } 404 405 // The "null" should be filled in by the contents of "bar.ts" 406 const testCaseNullSourcesContent = { 407 'entry.js': `import './foo.js'\n`, 408 'foo.ts': `import './bar.ts'\nconsole.log("foo")`, 409 'bar.ts': `console.log("bar")\n`, 410 'foo.js': `(() => { 411 // bar.ts 412 console.log("bar"); 413 414 // foo.ts 415 console.log("foo"); 416 })(); 417 //# sourceMappingURL=foo.js.map 418 `, 419 'foo.js.map': `{ 420 "version": 3, 421 "sources": ["bar.ts", "foo.ts"], 422 "sourcesContent": [null, "import './bar.ts'\\nconsole.log(\\"foo\\")"], 423 "mappings": ";;AAAA,UAAQ,IAAI,KAAK;;;ACCjB,UAAQ,IAAI,KAAK;", 424 "names": [] 425 } 426 `, 427 } 428 429 const toSearchNullSourcesContent = { 430 bar: 'bar.ts', 431 } 432 433 async function check(kind, testCase, toSearch, { ext, flags, entryPoints, crlf, followUpFlags = [] }) { 434 let failed = 0 435 436 try { 437 const recordCheck = (success, message) => { 438 if (!success) { 439 failed++ 440 console.error(`❌ [${kind}] ${message}`) 441 } 442 } 443 444 const tempDir = path.join(testDir, `${kind}-${tempDirCount++}`) 445 await fs.mkdir(tempDir, { recursive: true }) 446 447 for (const name in testCase) { 448 if (name !== '<stdin>') { 449 const tempPath = path.join(tempDir, name) 450 let code = testCase[name] 451 await fs.mkdir(path.dirname(tempPath), { recursive: true }) 452 if (crlf) code = code.replace(/\n/g, '\r\n') 453 await fs.writeFile(tempPath, code) 454 } 455 } 456 457 const args = ['--sourcemap', '--log-level=warning'].concat(flags) 458 const isStdin = '<stdin>' in testCase 459 let stdout = '' 460 461 await new Promise((resolve, reject) => { 462 args.unshift(...entryPoints) 463 const child = childProcess.spawn(esbuildPath, args, { cwd: tempDir, stdio: ['pipe', 'pipe', 'inherit'] }) 464 if (isStdin) child.stdin.write(testCase['<stdin>']) 465 child.stdin.end() 466 child.stdout.on('data', chunk => stdout += chunk.toString()) 467 child.stdout.on('end', resolve) 468 child.on('error', reject) 469 }) 470 471 let outCode 472 let outCodeMap 473 474 if (isStdin) { 475 outCode = stdout 476 recordCheck(outCode.includes(`# sourceMappingURL=data:application/json;base64,`), `.${ext} file must contain source map`) 477 outCodeMap = Buffer.from(outCode.slice(outCode.indexOf('base64,') + 'base64,'.length).trim(), 'base64').toString() 478 } 479 480 else { 481 outCode = await fs.readFile(path.join(tempDir, `out.${ext}`), 'utf8') 482 recordCheck(outCode.includes(`# sourceMappingURL=out.${ext}.map`), `.${ext} file must link to .${ext}.map`) 483 outCodeMap = await fs.readFile(path.join(tempDir, `out.${ext}.map`), 'utf8') 484 } 485 486 // Check the mapping of various key locations back to the original source 487 const checkMap = (out, map) => { 488 for (const id in toSearch) { 489 const outIndex = out.indexOf(`"${id}"`) 490 if (outIndex < 0) throw new Error(`Failed to find "${id}" in output`) 491 const outLines = out.slice(0, outIndex).split('\n') 492 const outLine = outLines.length 493 const outLastLine = outLines[outLines.length - 1] 494 let outColumn = outLastLine.length 495 const { source, line, column } = map.originalPositionFor({ line: outLine, column: outColumn }) 496 497 const inSource = isStdin ? '<stdin>' : toSearch[id]; 498 recordCheck(source === inSource, `expected source: ${inSource}, observed source: ${source}`) 499 500 const inCode = map.sourceContentFor(source) 501 if (inCode === null) throw new Error(`Got null for source content for "${source}"`) 502 let inIndex = inCode.indexOf(`"${id}"`) 503 if (inIndex < 0) inIndex = inCode.indexOf(`'${id}'`) 504 if (inIndex < 0) throw new Error(`Failed to find "${id}" in input`) 505 const inLines = inCode.slice(0, inIndex).split('\n') 506 const inLine = inLines.length 507 const inLastLine = inLines[inLines.length - 1] 508 let inColumn = inLastLine.length 509 510 const expected = JSON.stringify({ source, line: inLine, column: inColumn }) 511 const observed = JSON.stringify({ source, line, column }) 512 recordCheck(expected === observed, `expected original position: ${expected}, observed original position: ${observed}`) 513 514 // Also check the reverse mapping 515 const positions = map.allGeneratedPositionsFor({ source, line: inLine, column: inColumn }) 516 recordCheck(positions.length > 0, `expected generated positions: 1, observed generated positions: ${positions.length}`) 517 let found = false 518 for (const { line, column } of positions) { 519 if (line === outLine && column === outColumn) { 520 found = true 521 break 522 } 523 } 524 const expectedPosition = JSON.stringify({ line: outLine, column: outColumn }) 525 const observedPositions = JSON.stringify(positions) 526 recordCheck(found, `expected generated position: ${expectedPosition}, observed generated positions: ${observedPositions}`) 527 } 528 } 529 530 const sources = JSON.parse(outCodeMap).sources 531 for (let source of sources) { 532 if (sources.filter(s => s === source).length > 1) { 533 throw new Error(`Duplicate source ${JSON.stringify(source)} found in source map`) 534 } 535 } 536 537 const outMap = await new SourceMapConsumer(outCodeMap) 538 checkMap(outCode, outMap) 539 540 // Check that every generated location has an associated original position. 541 // This only works when not bundling because bundling includes runtime code. 542 if (flags.indexOf('--bundle') < 0) { 543 // The last line doesn't have a source map entry, but that should be ok. 544 const outLines = outCode.trimRight().split('\n'); 545 546 for (let outLine = 0; outLine < outLines.length; outLine++) { 547 if (outLines[outLine].startsWith('#!') || outLines[outLine].startsWith('//')) { 548 // Ignore the hashbang line and the source map comment itself 549 continue; 550 } 551 552 for (let outColumn = 0; outColumn <= outLines[outLine].length; outColumn++) { 553 const { line, column } = outMap.originalPositionFor({ line: outLine + 1, column: outColumn }) 554 recordCheck(line !== null && column !== null, `missing location for line ${outLine} and column ${outColumn}`) 555 } 556 } 557 } 558 559 // Bundle again to test nested source map chaining 560 for (let order of [0, 1, 2]) { 561 const fileToTest = isStdin ? `stdout.${ext}` : `out.${ext}` 562 const nestedEntry = path.join(tempDir, `nested-entry.${ext}`) 563 if (isStdin) await fs.writeFile(path.join(tempDir, fileToTest), outCode) 564 await fs.writeFile(path.join(tempDir, `extra.${ext}`), `console.log('extra')`) 565 const importKeyword = ext === 'css' ? '@import' : 'import' 566 await fs.writeFile(nestedEntry, 567 order === 1 ? `${importKeyword} './${fileToTest}'; ${importKeyword} './extra.${ext}'` : 568 order === 2 ? `${importKeyword} './extra.${ext}'; ${importKeyword} './${fileToTest}'` : 569 `${importKeyword} './${fileToTest}'`) 570 await execFileAsync(esbuildPath, [ 571 nestedEntry, 572 '--bundle', 573 '--outfile=' + path.join(tempDir, `out2.${ext}`), 574 '--sourcemap', 575 ].concat(followUpFlags), { cwd: testDir }) 576 577 const out2Code = await fs.readFile(path.join(tempDir, `out2.${ext}`), 'utf8') 578 recordCheck(out2Code.includes(`# sourceMappingURL=out2.${ext}.map`), `.${ext} file must link to .${ext}.map`) 579 const out2CodeMap = await fs.readFile(path.join(tempDir, `out2.${ext}.map`), 'utf8') 580 581 const out2Map = await new SourceMapConsumer(out2CodeMap) 582 checkMap(out2Code, out2Map) 583 } 584 585 if (!failed) removeRecursiveSync(tempDir) 586 } 587 588 catch (e) { 589 console.error(`❌ [${kind}] ${e && e.message || e}`) 590 failed++ 591 } 592 593 return failed 594 } 595 596 async function checkNames(kind, testCase, { ext, flags, entryPoints, crlf }) { 597 let failed = 0 598 599 try { 600 const recordCheck = (success, message) => { 601 if (!success) { 602 failed++ 603 console.error(`❌ [${kind}] ${message}`) 604 } 605 } 606 607 const tempDir = path.join(testDir, `${kind}-${tempDirCount++}`) 608 await fs.mkdir(tempDir, { recursive: true }) 609 610 for (const name in testCase) { 611 const tempPath = path.join(tempDir, name) 612 let code = testCase[name] 613 await fs.mkdir(path.dirname(tempPath), { recursive: true }) 614 if (crlf) code = code.replace(/\n/g, '\r\n') 615 await fs.writeFile(tempPath, code) 616 } 617 618 const args = ['--sourcemap', '--log-level=warning'].concat(flags) 619 let stdout = '' 620 621 await new Promise((resolve, reject) => { 622 args.unshift(...entryPoints) 623 const child = childProcess.spawn(esbuildPath, args, { cwd: tempDir, stdio: ['pipe', 'pipe', 'inherit'] }) 624 child.stdin.end() 625 child.stdout.on('data', chunk => stdout += chunk.toString()) 626 child.stdout.on('end', resolve) 627 child.on('error', reject) 628 }) 629 630 const outCode = await fs.readFile(path.join(tempDir, `out.${ext}`), 'utf8') 631 recordCheck(outCode.includes(`# sourceMappingURL=out.${ext}.map`), `.${ext} file must link to .${ext}.map`) 632 const outCodeMap = await fs.readFile(path.join(tempDir, `out.${ext}.map`), 'utf8') 633 634 // Check the mapping of various key locations back to the original source 635 const checkMap = (out, map) => { 636 const undoQuotes = x => `'"`.includes(x[0]) ? (0, eval)(x) : x.startsWith('(') ? x.slice(1, -1) : x 637 const generatedLines = out.split(/\r\n|\r|\n/g) 638 639 for (let i = 0; i < map.sources.length; i++) { 640 const source = map.sources[i] 641 const content = map.sourcesContent[i]; 642 let index = 0 643 644 // The names for us to check are prefixed by "/**/" right before to mark them 645 const parts = content.split(/(\/\*\*\/(?:\w+|'\w+'|"\w+"))/g) 646 647 for (let j = 1; j < parts.length; j += 2) { 648 const expectedName = undoQuotes(parts[j].slice(4)) 649 index += parts[j - 1].length 650 651 const prefixLines = content.slice(0, index + 4).split(/\r\n|\r|\n/g) 652 const line = prefixLines.length 653 const column = prefixLines[prefixLines.length - 1].length 654 index += parts[j].length 655 656 // There may be multiple mappings if the expression is spread across 657 // multiple lines. Check each one to see if any pass the checks. 658 const allGenerated = map.allGeneratedPositionsFor({ source, line, column }) 659 for (let i = 0; i < allGenerated.length; i++) { 660 const canSkip = i + 1 < allGenerated.length // Don't skip the last one 661 const generated = allGenerated[i] 662 const original = map.originalPositionFor(generated) 663 if (canSkip && (original.source !== source || original.line !== line || original.column !== column)) continue 664 recordCheck(original.source === source && original.line === line && original.column === column, 665 `\n` + 666 `\n original position: ${JSON.stringify({ source, line, column })}` + 667 `\n maps to generated position: ${JSON.stringify(generated)}` + 668 `\n which maps to original position: ${JSON.stringify(original)}` + 669 `\n`) 670 671 if (original.source === source && original.line === line && original.column === column) { 672 const generatedContentAfter = generatedLines[generated.line - 1].slice(generated.column) 673 const matchAfter = /^(?:\w+|'\w+'|"\w+"|\(\w+\))/.exec(generatedContentAfter) 674 if (canSkip && matchAfter === null) continue 675 recordCheck(matchAfter !== null, `expected the identifier ${JSON.stringify(expectedName)} starting on line ${generated.line} here: ${generatedContentAfter.slice(0, 100)}`) 676 677 if (matchAfter !== null) { 678 const observedName = undoQuotes(matchAfter[0]) 679 if (canSkip && expectedName !== (original.name || observedName)) continue 680 recordCheck(expectedName === (original.name || observedName), 681 `\n` + 682 `\n generated position: ${JSON.stringify(generated)}` + 683 `\n original position: ${JSON.stringify(original)}` + 684 `\n` + 685 `\n original name: ${JSON.stringify(expectedName)}` + 686 `\n generated name: ${JSON.stringify(observedName)}` + 687 `\n mapping name: ${JSON.stringify(original.name)}` + 688 `\n`) 689 } 690 } 691 692 break 693 } 694 } 695 } 696 } 697 698 const outMap = await new SourceMapConsumer(outCodeMap) 699 checkMap(outCode, outMap) 700 701 // Bundle again to test nested source map chaining 702 for (let order of [0, 1, 2]) { 703 const fileToTest = `out.${ext}` 704 const nestedEntry = path.join(tempDir, `nested-entry.${ext}`) 705 await fs.writeFile(path.join(tempDir, `extra.${ext}`), `console.log('extra')`) 706 await fs.writeFile(nestedEntry, 707 order === 1 ? `import './${fileToTest}'; import './extra.${ext}'` : 708 order === 2 ? `import './extra.${ext}'; import './${fileToTest}'` : 709 `import './${fileToTest}'`) 710 await execFileAsync(esbuildPath, [ 711 nestedEntry, 712 '--bundle', 713 '--outfile=' + path.join(tempDir, `out2.${ext}`), 714 '--sourcemap', 715 ], { cwd: testDir }) 716 717 const out2Code = await fs.readFile(path.join(tempDir, `out2.${ext}`), 'utf8') 718 recordCheck(out2Code.includes(`# sourceMappingURL=out2.${ext}.map`), `.${ext} file must link to .${ext}.map`) 719 const out2CodeMap = await fs.readFile(path.join(tempDir, `out2.${ext}.map`), 'utf8') 720 721 const out2Map = await new SourceMapConsumer(out2CodeMap) 722 checkMap(out2Code, out2Map) 723 } 724 725 if (!failed) removeRecursiveSync(tempDir) 726 } 727 728 catch (e) { 729 console.error(`❌ [${kind}] ${e && e.message || e}`) 730 failed++ 731 } 732 733 return failed 734 } 735 736 async function main() { 737 const promises = [] 738 for (const crlf of [false, true]) { 739 for (const minify of [false, true]) { 740 const flags = minify ? ['--minify'] : [] 741 const suffix = (crlf ? '-crlf' : '') + (minify ? '-min' : '') 742 promises.push( 743 check('commonjs' + suffix, testCaseCommonJS, toSearchBundle, { 744 ext: 'js', 745 flags: flags.concat('--outfile=out.js', '--bundle'), 746 entryPoints: ['a.js'], 747 crlf, 748 }), 749 check('es6' + suffix, testCaseES6, toSearchBundle, { 750 ext: 'js', 751 flags: flags.concat('--outfile=out.js', '--bundle'), 752 entryPoints: ['a.js'], 753 crlf, 754 }), 755 check('discontiguous' + suffix, testCaseDiscontiguous, toSearchBundle, { 756 ext: 'js', 757 flags: flags.concat('--outfile=out.js', '--bundle'), 758 entryPoints: ['a.js'], 759 crlf, 760 }), 761 check('ts' + suffix, testCaseTypeScriptRuntime, toSearchNoBundleTS, { 762 ext: 'js', 763 flags: flags.concat('--outfile=out.js'), 764 entryPoints: ['a.ts'], 765 crlf, 766 }), 767 check('stdin-stdout' + suffix, testCaseStdin, toSearchNoBundle, { 768 ext: 'js', 769 flags: flags.concat('--sourcefile=<stdin>'), 770 entryPoints: [], 771 crlf, 772 }), 773 check('empty' + suffix, testCaseEmptyFile, toSearchEmptyFile, { 774 ext: 'js', 775 flags: flags.concat('--outfile=out.js', '--bundle'), 776 entryPoints: ['entry.js'], 777 crlf, 778 }), 779 check('non-js' + suffix, testCaseNonJavaScriptFile, toSearchNonJavaScriptFile, { 780 ext: 'js', 781 flags: flags.concat('--outfile=out.js', '--bundle'), 782 entryPoints: ['entry.js'], 783 crlf, 784 }), 785 check('splitting' + suffix, testCaseCodeSplitting, toSearchCodeSplitting, { 786 ext: 'js', 787 flags: flags.concat('--outdir=.', '--bundle', '--splitting', '--format=esm'), 788 entryPoints: ['out.ts', 'other.ts'], 789 crlf, 790 }), 791 check('unicode' + suffix, testCaseUnicode, toSearchUnicode, { 792 ext: 'js', 793 flags: flags.concat('--outfile=out.js', '--bundle', '--charset=utf8'), 794 entryPoints: ['entry.js'], 795 crlf, 796 }), 797 check('unicode-globalName' + suffix, testCaseUnicode, toSearchUnicode, { 798 ext: 'js', 799 flags: flags.concat('--outfile=out.js', '--bundle', '--global-name=πππ', '--charset=utf8'), 800 entryPoints: ['entry.js'], 801 crlf, 802 }), 803 check('dummy' + suffix, testCasePartialMappings, toSearchPartialMappings, { 804 ext: 'js', 805 flags: flags.concat('--outfile=out.js', '--bundle'), 806 entryPoints: ['entry.js'], 807 crlf, 808 }), 809 check('dummy' + suffix, testCasePartialMappingsPercentEscape, toSearchPartialMappings, { 810 ext: 'js', 811 flags: flags.concat('--outfile=out.js', '--bundle'), 812 entryPoints: ['entry.js'], 813 crlf, 814 }), 815 check('banner-footer' + suffix, testCaseES6, toSearchBundle, { 816 ext: 'js', 817 flags: flags.concat('--outfile=out.js', '--bundle', '--banner:js="/* LICENSE abc */"', '--footer:js="/* end of file banner */"'), 818 entryPoints: ['a.js'], 819 crlf, 820 }), 821 check('complex' + suffix, testCaseComplex, toSearchComplex, { 822 ext: 'js', 823 flags: flags.concat('--outfile=out.js', '--bundle', '--define:process.env.NODE_ENV="production"'), 824 entryPoints: ['entry.js'], 825 crlf, 826 }), 827 check('dynamic-import' + suffix, testCaseDynamicImport, toSearchDynamicImport, { 828 ext: 'js', 829 flags: flags.concat('--outfile=out.js', '--bundle', '--external:./ext/*', '--format=esm'), 830 entryPoints: ['entry.js'], 831 crlf, 832 followUpFlags: ['--external:./ext/*', '--format=esm'], 833 }), 834 check('dynamic-require' + suffix, testCaseDynamicImport, toSearchDynamicImport, { 835 ext: 'js', 836 flags: flags.concat('--outfile=out.js', '--bundle', '--external:./ext/*', '--format=cjs'), 837 entryPoints: ['entry.js'], 838 crlf, 839 followUpFlags: ['--external:./ext/*', '--format=cjs'], 840 }), 841 check('bundle-css' + suffix, testCaseBundleCSS, toSearchBundleCSS, { 842 ext: 'css', 843 flags: flags.concat('--outfile=out.css', '--bundle'), 844 entryPoints: ['entry.css'], 845 crlf, 846 }), 847 check('jsx-runtime' + suffix, testCaseJSXRuntime, toSearchJSXRuntime, { 848 ext: 'js', 849 flags: flags.concat('--outfile=out.js', '--bundle', '--jsx=automatic', '--external:react/jsx-runtime'), 850 entryPoints: ['entry.jsx'], 851 crlf, 852 }), 853 check('jsx-dev-runtime' + suffix, testCaseJSXRuntime, toSearchJSXRuntime, { 854 ext: 'js', 855 flags: flags.concat('--outfile=out.js', '--bundle', '--jsx=automatic', '--jsx-dev', '--external:react/jsx-dev-runtime'), 856 entryPoints: ['entry.jsx'], 857 crlf, 858 }), 859 860 // Checks for the "names" field 861 checkNames('names' + suffix, testCaseNames, { 862 ext: 'js', 863 flags: flags.concat('--outfile=out.js', '--bundle'), 864 entryPoints: ['entry.js'], 865 crlf, 866 }), 867 checkNames('names-mangle' + suffix, testCaseNames, { 868 ext: 'js', 869 flags: flags.concat('--outfile=out.js', '--bundle', '--mangle-props=^mangle_$'), 870 entryPoints: ['entry.js'], 871 crlf, 872 }), 873 checkNames('names-mangle-quoted' + suffix, testCaseNames, { 874 ext: 'js', 875 flags: flags.concat('--outfile=out.js', '--bundle', '--mangle-props=^mangle_$', '--mangle-quoted'), 876 entryPoints: ['entry.js'], 877 crlf, 878 }), 879 880 // Checks for loading missing "sourcesContent" in nested source maps 881 check('missing-sources-content' + suffix, testCaseMissingSourcesContent, toSearchMissingSourcesContent, { 882 ext: 'js', 883 flags: flags.concat('--outfile=out.js', '--bundle'), 884 entryPoints: ['foo.js'], 885 crlf, 886 }), 887 888 // Checks for null entries in "sourcesContent" in nested source maps 889 check('null-sources-content' + suffix, testCaseNullSourcesContent, toSearchNullSourcesContent, { 890 ext: 'js', 891 flags: flags.concat('--outfile=out.js', '--bundle'), 892 entryPoints: ['foo.js'], 893 crlf, 894 }), 895 ) 896 } 897 } 898 899 const failed = (await Promise.all(promises)).reduce((a, b) => a + b, 0) 900 if (failed > 0) { 901 console.error(`❌ verify source map failed`) 902 process.exit(1) 903 } else { 904 console.log(`✅ verify source map passed`) 905 removeRecursiveSync(testDir) 906 } 907 } 908 909 main().catch(e => setTimeout(() => { throw e }))