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 }))