github.com/evanw/esbuild@v0.21.4/scripts/plugin-tests.js (about)

     1  const { installForTests, removeRecursiveSync, writeFileAtomic } = require('./esbuild')
     2  const assert = require('assert')
     3  const path = require('path')
     4  const util = require('util')
     5  const http = require('http')
     6  const url = require('url')
     7  const fs = require('fs')
     8  
     9  const readFileAsync = util.promisify(fs.readFile)
    10  const writeFileAsync = util.promisify(fs.writeFile)
    11  const mkdirAsync = util.promisify(fs.mkdir)
    12  
    13  const repoDir = path.dirname(__dirname)
    14  const rootTestDir = path.join(repoDir, 'scripts', '.plugin-tests')
    15  
    16  function fetch(host, port, path) {
    17    return new Promise((resolve, reject) => {
    18      http.get({ host, port, path }, res => {
    19        const chunks = []
    20        res.on('data', chunk => chunks.push(chunk))
    21        res.on('end', () => {
    22          const content = Buffer.concat(chunks)
    23          if (res.statusCode < 200 || res.statusCode > 299) {
    24            const error = new Error(`${res.statusCode} when fetching ${path}: ${content}`)
    25            error.statusCode = res.statusCode
    26            reject(error)
    27          } else {
    28            content.headers = res.headers
    29            resolve(content)
    30          }
    31        })
    32      }).on('error', reject)
    33    })
    34  }
    35  
    36  function fetchUntilSuccessOrTimeout(host, port, path) {
    37    const seconds = 5
    38    let timeout
    39    let stop = false
    40    const cancel = () => clearTimeout(timeout)
    41    const promise = Promise.race([
    42      new Promise((_, reject) => {
    43        timeout = setTimeout(() => {
    44          stop = true
    45          reject(new Error(`Waited more than ${seconds} seconds while trying to fetch "http://${host}:${port}${path}"`))
    46        }, seconds * 1000)
    47      }),
    48      (async () => {
    49        while (!stop) {
    50          try {
    51            return await fetch(host, port, path)
    52          } catch {
    53          }
    54        }
    55      })(),
    56    ])
    57    promise.then(cancel, cancel)
    58    return promise
    59  }
    60  
    61  let pluginTests = {
    62    async noPluginsWithBuildSync({ esbuild }) {
    63      try {
    64        esbuild.buildSync({
    65          entryPoints: [], logLevel: 'silent', plugins: [{
    66            name: 'name',
    67            setup() { },
    68          }],
    69        })
    70        throw new Error('Expected an error to be thrown')
    71      } catch (e) {
    72        assert.strictEqual(e.message.split('\n')[0], 'Build failed with 1 error:')
    73        assert.notStrictEqual(e.errors, void 0)
    74        assert.strictEqual(e.errors.length, 1)
    75        assert.strictEqual(e.errors[0].text, 'Cannot use plugins in synchronous API calls')
    76        assert.deepStrictEqual(e.warnings, [])
    77      }
    78    },
    79  
    80    async emptyArray({ esbuild, testDir }) {
    81      const input = path.join(testDir, 'in.js')
    82      const output = path.join(testDir, 'out.js')
    83      await writeFileAsync(input, `export default 123`)
    84      await esbuild.build({
    85        entryPoints: [input],
    86        bundle: true,
    87        outfile: output,
    88        format: 'cjs',
    89        plugins: [],
    90      })
    91      const result = require(output)
    92      assert.strictEqual(result.default, 123)
    93    },
    94  
    95    async emptyArrayWithBuildSync({ esbuild, testDir }) {
    96      const input = path.join(testDir, 'in.js')
    97      const output = path.join(testDir, 'out.js')
    98      await writeFileAsync(input, `export default 123`)
    99      esbuild.buildSync({
   100        entryPoints: [input],
   101        bundle: true,
   102        outfile: output,
   103        format: 'cjs',
   104        plugins: [],
   105      })
   106      const result = require(output)
   107      assert.strictEqual(result.default, 123)
   108    },
   109  
   110    async invalidRegExp({ esbuild }) {
   111      for (const filter of [/x(?=y)/, /x(?!y)/, /x(?<=y)/, /x(?<!y)/, /(x)\1/]) {
   112        // onResolve
   113        try {
   114          await esbuild.build({
   115            entryPoints: ['invalid.js'],
   116            write: false,
   117            plugins: [{
   118              name: 'name',
   119              setup(build) {
   120                build.onResolve({ filter }, () => { })
   121              },
   122            }],
   123          })
   124          throw new Error(`Expected filter ${filter} to fail`)
   125        } catch (e) {
   126          assert.strictEqual(e.message, `[name] "onResolve" filter is not a valid Go regular expression: ${JSON.stringify(filter.source)}`)
   127        }
   128  
   129        // onLoad
   130        try {
   131          await esbuild.build({
   132            entryPoints: ['invalid.js'],
   133            write: false,
   134            plugins: [{
   135              name: 'name',
   136              setup(build) {
   137                build.onLoad({ filter }, () => { })
   138              },
   139            }],
   140          })
   141          throw new Error(`Expected filter ${filter} to fail`)
   142        } catch (e) {
   143          assert.strictEqual(e.message, `[name] "onLoad" filter is not a valid Go regular expression: ${JSON.stringify(filter.source)}`)
   144        }
   145      }
   146    },
   147  
   148    async pluginMissingName({ esbuild }) {
   149      try {
   150        await esbuild.build({
   151          entryPoints: [],
   152          logLevel: 'silent',
   153          plugins: [{
   154            setup(build) {
   155            },
   156          }],
   157        })
   158      } catch (e) {
   159        assert.strictEqual(e.message.split('\n')[0], 'Build failed with 1 error:')
   160        assert.notStrictEqual(e.errors, void 0)
   161        assert.strictEqual(e.errors.length, 1)
   162        assert.strictEqual(e.errors[0].text, 'Plugin at index 0 is missing a name')
   163        assert.deepStrictEqual(e.warnings, [])
   164      }
   165    },
   166  
   167    async pluginMissingSetup({ esbuild }) {
   168      try {
   169        await esbuild.build({
   170          entryPoints: [],
   171          logLevel: 'silent',
   172          plugins: [{
   173            name: 'x',
   174          }],
   175        })
   176      } catch (e) {
   177        assert.strictEqual(e.message.split('\n')[0], 'Build failed with 1 error:')
   178        assert.notStrictEqual(e.errors, void 0)
   179        assert.strictEqual(e.errors.length, 1)
   180        assert.strictEqual(e.errors[0].pluginName, 'x')
   181        assert.strictEqual(e.errors[0].text, 'Plugin is missing a setup function')
   182        assert.deepStrictEqual(e.warnings, [])
   183      }
   184    },
   185  
   186    async badPluginProperty({ esbuild }) {
   187      try {
   188        await esbuild.build({
   189          entryPoints: [],
   190          logLevel: 'silent',
   191          plugins: [{
   192            name: 'x',
   193            someRandomProperty: void 0,
   194            setup(build) {
   195            },
   196          }],
   197        })
   198      } catch (e) {
   199        assert.strictEqual(e.message.split('\n')[0], 'Build failed with 1 error:')
   200        assert.notStrictEqual(e.errors, void 0)
   201        assert.strictEqual(e.errors.length, 1)
   202        assert.strictEqual(e.errors[0].text, 'Invalid option on plugin "x": "someRandomProperty"')
   203        assert.deepStrictEqual(e.warnings, [])
   204      }
   205    },
   206  
   207    async badPluginOnResolveProperty({ esbuild }) {
   208      try {
   209        await esbuild.build({
   210          entryPoints: ['entry'],
   211          logLevel: 'silent',
   212          plugins: [{
   213            name: 'x',
   214            setup(build) {
   215              build.onResolve({ whatIsThis: void 0 }, () => {
   216              })
   217            },
   218          }],
   219        })
   220      } catch (e) {
   221        assert.strictEqual(e.message.split('\n')[0], 'Build failed with 1 error:')
   222        assert.notStrictEqual(e.errors, void 0)
   223        assert.strictEqual(e.errors.length, 1)
   224        assert.strictEqual(e.errors[0].text, 'Invalid option in onResolve() call for plugin "x": "whatIsThis"')
   225        assert.deepStrictEqual(e.warnings, [])
   226      }
   227  
   228      try {
   229        await esbuild.build({
   230          entryPoints: ['entry'],
   231          logLevel: 'silent',
   232          write: false,
   233          plugins: [{
   234            name: 'x',
   235            setup(build) {
   236              build.onResolve({ filter: /.*/ }, () => {
   237                return '/'
   238              })
   239            },
   240          }],
   241        })
   242      } catch (e) {
   243        assert(e.message.endsWith('ERROR: [plugin: x] Expected onResolve() callback in plugin "x" to return an object'), e.message)
   244      }
   245  
   246      try {
   247        await esbuild.build({
   248          entryPoints: ['entry'],
   249          logLevel: 'silent',
   250          write: false,
   251          plugins: [{
   252            name: 'x',
   253            setup(build) {
   254              build.onResolve({ filter: /.*/ }, () => {
   255                return { thisIsWrong: void 0 }
   256              })
   257            },
   258          }],
   259        })
   260      } catch (e) {
   261        assert(e.message.endsWith('ERROR: [plugin: x] Invalid option from onResolve() callback in plugin "x": "thisIsWrong"'), e.message)
   262      }
   263    },
   264  
   265    async badPluginOnLoadProperty({ esbuild }) {
   266      try {
   267        await esbuild.build({
   268          entryPoints: ['entry'],
   269          logLevel: 'silent',
   270          plugins: [{
   271            name: 'x',
   272            setup(build) {
   273              build.onLoad({ whatIsThis: void 0 }, () => {
   274              })
   275            },
   276          }],
   277        })
   278      } catch (e) {
   279        assert.strictEqual(e.message.split('\n')[0], 'Build failed with 1 error:')
   280        assert.notStrictEqual(e.errors, void 0)
   281        assert.strictEqual(e.errors.length, 1)
   282        assert.strictEqual(e.errors[0].text, 'Invalid option in onLoad() call for plugin "x": "whatIsThis"')
   283        assert.deepStrictEqual(e.warnings, [])
   284      }
   285  
   286      try {
   287        await esbuild.build({
   288          entryPoints: ['entry'],
   289          logLevel: 'silent',
   290          write: false,
   291          plugins: [{
   292            name: 'x',
   293            setup(build) {
   294              build.onResolve({ filter: /.*/ }, () => {
   295                return { path: 'y', namespace: 'z' }
   296              })
   297              build.onLoad({ filter: /.*/ }, () => {
   298                return ""
   299              })
   300            },
   301          }],
   302        })
   303      } catch (e) {
   304        assert(e.message.endsWith(`ERROR: [plugin: x] Expected onLoad() callback in plugin "x" to return an object`), e.message)
   305      }
   306  
   307      try {
   308        await esbuild.build({
   309          entryPoints: ['entry'],
   310          logLevel: 'silent',
   311          write: false,
   312          plugins: [{
   313            name: 'x',
   314            setup(build) {
   315              build.onResolve({ filter: /.*/ }, () => {
   316                return { path: 'y', namespace: 'z' }
   317              })
   318              build.onLoad({ filter: /.*/ }, () => {
   319                return { thisIsWrong: void 0 }
   320              })
   321            },
   322          }],
   323        })
   324      } catch (e) {
   325        assert(e.message.endsWith('ERROR: [plugin: x] Invalid option from onLoad() callback in plugin "x": "thisIsWrong"'), e.message)
   326      }
   327    },
   328  
   329    async modifyInitialOptions({ esbuild, testDir }) {
   330      const input = path.join(testDir, 'in.what')
   331      const output = path.join(testDir, 'out.js')
   332      await writeFileAsync(input, `export default 123`)
   333      await esbuild.build({
   334        entryPoints: [input],
   335        bundle: true,
   336        outfile: output,
   337        format: 'cjs',
   338        plugins: [{
   339          name: 'what',
   340          setup(build) {
   341            build.initialOptions.loader = { '.what': 'js' }
   342          },
   343        }],
   344      })
   345      const result = require(output)
   346      assert.strictEqual(result.default, 123)
   347    },
   348  
   349    async modifyInitialOptionsAsync({ esbuild, testDir }) {
   350      const input = path.join(testDir, 'in.what')
   351      const output = path.join(testDir, 'out.js')
   352      await writeFileAsync(input, `export default 123`)
   353      await esbuild.build({
   354        entryPoints: [input],
   355        bundle: true,
   356        outfile: output,
   357        format: 'cjs',
   358        plugins: [{
   359          name: 'what',
   360          async setup(build) {
   361            await new Promise(r => setTimeout(r, 100))
   362            build.initialOptions.loader = { '.what': 'js' }
   363          },
   364        }],
   365      })
   366      const result = require(output)
   367      assert.strictEqual(result.default, 123)
   368    },
   369  
   370    async basicLoader({ esbuild, testDir }) {
   371      const input = path.join(testDir, 'in.js')
   372      const custom = path.join(testDir, 'example.custom')
   373      const output = path.join(testDir, 'out.js')
   374      await writeFileAsync(input, `
   375        import x from './example.custom'
   376        export default x
   377      `)
   378      await writeFileAsync(custom, ``)
   379      await esbuild.build({
   380        entryPoints: [input],
   381        bundle: true,
   382        outfile: output,
   383        format: 'cjs',
   384        plugins: [{
   385          name: 'name',
   386          setup(build) {
   387            build.onLoad({ filter: /\.custom$/ }, args => {
   388              assert.strictEqual(args.path, custom)
   389              return { contents: 'this is custom', loader: 'text' }
   390            })
   391          },
   392        }],
   393      })
   394      const result = require(output)
   395      assert.strictEqual(result.default, 'this is custom')
   396    },
   397  
   398    async basicResolver({ esbuild, testDir }) {
   399      const input = path.join(testDir, 'in.js')
   400      const custom = path.join(testDir, 'example.txt')
   401      const output = path.join(testDir, 'out.js')
   402      await writeFileAsync(input, `
   403        import x from 'test'
   404        export default x
   405      `)
   406      await writeFileAsync(custom, `example text`)
   407      await esbuild.build({
   408        entryPoints: [input],
   409        bundle: true,
   410        outfile: output,
   411        format: 'cjs',
   412        plugins: [{
   413          name: 'name',
   414          setup(build) {
   415            build.onResolve({ filter: /^test$/ }, args => {
   416              assert.strictEqual(args.path, 'test')
   417              return { path: custom }
   418            })
   419          },
   420        }],
   421      })
   422      const result = require(output)
   423      assert.strictEqual(result.default, 'example text')
   424    },
   425  
   426    async fibonacciResolverMemoized({ esbuild, testDir }) {
   427      const input = path.join(testDir, 'in.js')
   428      const output = path.join(testDir, 'out.js')
   429      await writeFileAsync(input, `
   430        import x from 'fib(10)'
   431        export default x
   432      `)
   433      await esbuild.build({
   434        entryPoints: [input],
   435        bundle: true,
   436        outfile: output,
   437        format: 'cjs',
   438        plugins: [{
   439          name: 'name',
   440          setup(build) {
   441            build.onResolve({ filter: /^fib\((\d+)\)$/ }, args => {
   442              return { path: args.path, namespace: 'fib' }
   443            })
   444            build.onLoad({ filter: /^fib\((\d+)\)$/, namespace: 'fib' }, args => {
   445              let match = /^fib\((\d+)\)$/.exec(args.path), n = +match[1]
   446              let contents = n < 2 ? `export default ${n}` : `
   447                import n1 from 'fib(${n - 1})'
   448                import n2 from 'fib(${n - 2})'
   449                export default n1 + n2`
   450              return { contents }
   451            })
   452          },
   453        }],
   454      })
   455      const result = require(output)
   456      assert.strictEqual(result.default, 55)
   457    },
   458  
   459    async fibonacciResolverNotMemoized({ esbuild, testDir }) {
   460      const input = path.join(testDir, 'in.js')
   461      const output = path.join(testDir, 'out.js')
   462      await writeFileAsync(input, `
   463        import x from 'fib(10)'
   464        export default x
   465      `)
   466      await esbuild.build({
   467        entryPoints: [input],
   468        bundle: true,
   469        outfile: output,
   470        format: 'cjs',
   471        plugins: [{
   472          name: 'name',
   473          setup(build) {
   474            build.onResolve({ filter: /^fib\((\d+)\)/ }, args => {
   475              return { path: args.path, namespace: 'fib' }
   476            })
   477            build.onLoad({ filter: /^fib\((\d+)\)/, namespace: 'fib' }, args => {
   478              let match = /^fib\((\d+)\)/.exec(args.path), n = +match[1]
   479              let contents = n < 2 ? `export default ${n}` : `
   480                import n1 from 'fib(${n - 1}) ${args.path}'
   481                import n2 from 'fib(${n - 2}) ${args.path}'
   482                export default n1 + n2`
   483              return { contents }
   484            })
   485          },
   486        }],
   487      })
   488      const result = require(output)
   489      assert.strictEqual(result.default, 55)
   490    },
   491  
   492    async resolversCalledInSequence({ esbuild, testDir }) {
   493      const input = path.join(testDir, 'in.js')
   494      const nested = path.join(testDir, 'nested.js')
   495      const output = path.join(testDir, 'out.js')
   496      await writeFileAsync(input, `
   497        import x from 'test'
   498        export default x
   499      `)
   500      await writeFileAsync(nested, `
   501        export default 123
   502      `)
   503      let trace = []
   504      await esbuild.build({
   505        entryPoints: [input],
   506        bundle: true,
   507        outfile: output,
   508        format: 'cjs',
   509        plugins: [
   510          {
   511            name: 'plugin1',
   512            setup(build) {
   513              build.onResolve({ filter: /^.*$/ }, () => { trace.push('called first') })
   514            },
   515          },
   516          {
   517            name: 'plugin2',
   518            setup(build) {
   519              build.onResolve({ filter: /^ignore me$/ }, () => { trace.push('not called') })
   520            },
   521          },
   522          {
   523            name: 'plugin3',
   524            setup(build) {
   525              build.onResolve({ filter: /^.*$/ }, () => {
   526                trace.push('called second')
   527                return { path: nested }
   528              })
   529            },
   530          },
   531          {
   532            name: 'plugin4',
   533            setup(build) {
   534              build.onResolve({ filter: /^.*$/ }, () => { trace.push('not called') })
   535            },
   536          }
   537        ],
   538      })
   539      const result = require(output)
   540      assert.strictEqual(result.default, 123)
   541      assert.deepStrictEqual(trace, [
   542        'called first',
   543        'called second',
   544      ])
   545    },
   546  
   547    async loadersCalledInSequence({ esbuild, testDir }) {
   548      const input = path.join(testDir, 'in.js')
   549      const nested = path.join(testDir, 'nested.js')
   550      const output = path.join(testDir, 'out.js')
   551      await writeFileAsync(input, `
   552        import x from './nested.js'
   553        export default x
   554      `)
   555      await writeFileAsync(nested, `
   556        export default 123
   557      `)
   558      let trace = []
   559      await esbuild.build({
   560        entryPoints: [input],
   561        bundle: true,
   562        outfile: output,
   563        format: 'cjs',
   564        plugins: [
   565          {
   566            name: 'plugin1',
   567            setup(build) {
   568              build.onLoad({ filter: /^.*$/ }, () => { trace.push('called first') })
   569            },
   570          },
   571          {
   572            name: 'plugin2',
   573            setup(build) {
   574              build.onLoad({ filter: /^.*$/, namespace: 'ignore-me' }, () => { trace.push('not called') })
   575            },
   576          },
   577          {
   578            name: 'plugin3',
   579            setup(build) {
   580              build.onLoad({ filter: /^.*$/, namespace: 'file' }, () => {
   581                trace.push('called second')
   582                return { contents: 'export default "abc"' }
   583              })
   584            },
   585          },
   586          {
   587            name: 'plugin4',
   588            setup(build) {
   589              build.onLoad({ filter: /^.*$/, namespace: 'file' }, () => { trace.push('not called') })
   590            },
   591          },
   592        ],
   593      })
   594      const result = require(output)
   595      assert.strictEqual(result.default, 'abc')
   596      assert.deepStrictEqual(trace, [
   597        'called first',
   598        'called second',
   599      ])
   600    },
   601  
   602    async httpRelative({ esbuild, testDir }) {
   603      const input = path.join(testDir, 'in.js')
   604      const output = path.join(testDir, 'out.js')
   605      await writeFileAsync(input, `
   606        import x from 'http://example.com/assets/js/example.js'
   607        export default x
   608      `)
   609      await esbuild.build({
   610        entryPoints: [input],
   611        bundle: true,
   612        outfile: output,
   613        format: 'cjs',
   614        plugins: [{
   615          name: 'name',
   616          setup(build) {
   617            build.onResolve({ filter: /^http:\/\// }, args => {
   618              return { path: args.path, namespace: 'http' }
   619            })
   620            build.onResolve({ filter: /.*/, namespace: 'http' }, args => {
   621              return { path: new URL(args.path, args.importer).toString(), namespace: 'http' }
   622            })
   623            build.onLoad({ filter: /^http:\/\//, namespace: 'http' }, args => {
   624              switch (args.path) {
   625                case 'http://example.com/assets/js/example.js':
   626                  return { contents: `import y from './data/base.js'; export default y` }
   627                case 'http://example.com/assets/js/data/base.js':
   628                  return { contents: `export default 123` }
   629              }
   630            })
   631          },
   632        }],
   633      })
   634      const result = require(output)
   635      assert.strictEqual(result.default, 123)
   636    },
   637  
   638    async rewriteExternalWithNamespace({ esbuild, testDir }) {
   639      const input = path.join(testDir, 'in.js')
   640      const output = path.join(testDir, 'out.js')
   641      await writeFileAsync(input, `
   642        import {exists} from 'extern'
   643        export default exists
   644      `)
   645      await esbuild.build({
   646        entryPoints: [input],
   647        bundle: true,
   648        outfile: output,
   649        format: 'cjs',
   650        plugins: [{
   651          name: 'name',
   652          setup(build) {
   653            build.onResolve({ filter: /^extern$/ }, () => {
   654              return { path: 'fs', external: true, namespace: 'for-testing' }
   655            })
   656          },
   657        }],
   658      })
   659      const result = require(output)
   660      assert.strictEqual(result.default, fs.exists)
   661    },
   662  
   663    async rewriteExternalWithoutNamespace({ esbuild, testDir }) {
   664      const input = path.join(testDir, 'in.js')
   665      const output = path.join(testDir, 'out.js')
   666      await writeFileAsync(input, `
   667        import {exists} from 'extern'
   668        export default exists
   669      `)
   670      await esbuild.build({
   671        entryPoints: [input],
   672        bundle: true,
   673        outfile: output,
   674        format: 'cjs',
   675        plugins: [{
   676          name: 'name',
   677          setup(build) {
   678            build.onResolve({ filter: /^extern$/ }, () => {
   679              return { path: 'fs', external: true }
   680            })
   681          },
   682        }],
   683      })
   684      const result = require(output)
   685      assert.strictEqual(result.default, fs.exists)
   686    },
   687  
   688    async rewriteExternalWithFileNamespace({ esbuild, testDir }) {
   689      const input = path.join(testDir, 'in.js')
   690      const outdir = path.join(testDir, 'out')
   691      const outdir2 = path.join(testDir, 'out2')
   692      const target = path.join(outdir2, 'target.js')
   693      await writeFileAsync(input, `
   694        import {exists} from 'extern'
   695        export default exists
   696      `)
   697      await mkdirAsync(outdir2, { recursive: true })
   698      await writeFileAsync(target, `
   699        module.exports = require('fs')
   700      `)
   701      await esbuild.build({
   702        entryPoints: [input],
   703        bundle: true,
   704        outdir,
   705        format: 'cjs',
   706        plugins: [{
   707          name: 'name',
   708          setup(build) {
   709            build.onResolve({ filter: /^extern$/ }, () => {
   710              return { path: path.join(outdir, 'target'), external: true, namespace: 'file' }
   711            })
   712          },
   713        }],
   714      })
   715  
   716      // Move the file to show that the output has a relative path
   717      await fs.promises.rename(path.join(outdir, 'in.js'), path.join(outdir2, 'in.js'))
   718  
   719      const result = require(path.join(outdir2, 'in.js'))
   720      assert.strictEqual(result.default, fs.exists)
   721    },
   722  
   723    async resolveDirInFileModule({ esbuild, testDir }) {
   724      const input = path.join(testDir, 'in.js')
   725      const output = path.join(testDir, 'out.js')
   726      const example = path.join(testDir, 'example.custom')
   727      const resolveDir = path.join(testDir, 'target')
   728      const loadme = path.join(resolveDir, 'loadme.js')
   729      await mkdirAsync(resolveDir)
   730      await writeFileAsync(input, `
   731        import value from './example.custom'
   732        export default value
   733      `)
   734      await writeFileAsync(example, `
   735        export {default} from './loadme'
   736      `)
   737      await writeFileAsync(loadme, `
   738        export default 123
   739      `)
   740      await esbuild.build({
   741        entryPoints: [input],
   742        bundle: true,
   743        outfile: output,
   744        format: 'cjs',
   745        plugins: [{
   746          name: 'name',
   747          setup(build) {
   748            build.onLoad({ filter: /\.custom$/ }, async (args) => {
   749              return { contents: await readFileAsync(args.path), resolveDir }
   750            })
   751          },
   752        }],
   753      })
   754      const result = require(output)
   755      assert.strictEqual(result.default, 123)
   756    },
   757  
   758    async resolveWithSideEffectsFalse({ esbuild, testDir }) {
   759      const input = path.join(testDir, 'in.js')
   760  
   761      await writeFileAsync(input, `
   762        import './re-export-unused'
   763        import {a, b, c} from './re-export-used'
   764        import './import-unused'
   765        use([a, b, c])
   766      `)
   767      await writeFileAsync(path.join(testDir, 're-export-unused.js'), `
   768        export {default as a} from 'plugin:unused-false'
   769        export {default as b} from 'plugin:unused-true'
   770        export {default as c} from 'plugin:unused-none'
   771      `)
   772      await writeFileAsync(path.join(testDir, 're-export-used.js'), `
   773        export {default as a} from 'plugin:used-false'
   774        export {default as b} from 'plugin:used-true'
   775        export {default as c} from 'plugin:used-none'
   776      `)
   777      await writeFileAsync(path.join(testDir, 'import-unused.js'), `
   778        import 'plugin:ignored-false'
   779        import 'plugin:ignored-true'
   780        import 'plugin:ignored-none'
   781      `)
   782  
   783      const result = await esbuild.build({
   784        entryPoints: [input],
   785        bundle: true,
   786        write: false,
   787        format: 'cjs',
   788        logLevel: 'error',
   789        plugins: [{
   790          name: 'name',
   791          setup(build) {
   792            build.onResolve({ filter: /^plugin:/ }, args => {
   793              return {
   794                path: args.path,
   795                namespace: 'ns',
   796                sideEffects:
   797                  args.path.endsWith('-true') ? true :
   798                    args.path.endsWith('-false') ? false :
   799                      undefined,
   800              };
   801            });
   802            build.onLoad({ filter: /^plugin:/ }, args => {
   803              return { contents: `export default use(${JSON.stringify(args.path)})` };
   804            });
   805          },
   806        }],
   807      })
   808  
   809      // Validate that the unused "sideEffects: false" files were omitted
   810      const used = [];
   811      new Function('use', result.outputFiles[0].text)(x => used.push(x));
   812      assert.deepStrictEqual(used, [
   813        'plugin:unused-true',
   814        'plugin:unused-none',
   815  
   816        'plugin:used-false',
   817        'plugin:used-true',
   818        'plugin:used-none',
   819  
   820        'plugin:ignored-true',
   821        'plugin:ignored-none',
   822  
   823        [3, 4, 5],
   824      ])
   825  
   826      // Check that the warning for "sideEffect: false" imports mentions the plugin
   827      assert.strictEqual(result.warnings.length, 1)
   828      assert.strictEqual(result.warnings[0].text,
   829        'Ignoring this import because "ns:plugin:ignored-false" was marked as having no side effects by plugin "name"')
   830    },
   831  
   832    async noResolveDirInFileModule({ esbuild, testDir }) {
   833      const input = path.join(testDir, 'in.js')
   834      const output = path.join(testDir, 'out.js')
   835      const example = path.join(testDir, 'example.custom')
   836      const resolveDir = path.join(testDir, 'target')
   837      const loadme = path.join(resolveDir, 'loadme.js')
   838      await mkdirAsync(resolveDir)
   839      await writeFileAsync(input, `
   840        import value from './example.custom'
   841        export default value
   842      `)
   843      await writeFileAsync(example, `
   844        export {default} from './target/loadme'
   845      `)
   846      await writeFileAsync(loadme, `
   847        export default 123
   848      `)
   849      await esbuild.build({
   850        entryPoints: [input],
   851        bundle: true,
   852        outfile: output,
   853        format: 'cjs',
   854        plugins: [{
   855          name: 'name',
   856          setup(build) {
   857            build.onLoad({ filter: /\.custom$/ }, async (args) => {
   858              return { contents: await readFileAsync(args.path) }
   859            })
   860          },
   861        }],
   862      })
   863      const result = require(output)
   864      assert.strictEqual(result.default, 123)
   865    },
   866  
   867    async resolveDirInVirtualModule({ esbuild, testDir }) {
   868      const input = path.join(testDir, 'in.js')
   869      const output = path.join(testDir, 'out.js')
   870      const resolveDir = path.join(testDir, 'target')
   871      const loadme = path.join(resolveDir, 'loadme.js')
   872      await mkdirAsync(resolveDir)
   873      await writeFileAsync(input, `
   874        import value from 'virtual'
   875        export default value
   876      `)
   877      await writeFileAsync(loadme, `
   878        export default 123
   879      `)
   880      await esbuild.build({
   881        entryPoints: [input],
   882        bundle: true,
   883        outfile: output,
   884        format: 'cjs',
   885        plugins: [{
   886          name: 'name',
   887          setup(build) {
   888            let contents = `export {default} from './loadme'`
   889            build.onResolve({ filter: /^virtual$/ }, () => ({ path: 'virtual', namespace: 'for-testing' }))
   890            build.onLoad({ filter: /.*/, namespace: 'for-testing' }, () => ({ contents, resolveDir }))
   891          },
   892        }],
   893      })
   894      const result = require(output)
   895      assert.strictEqual(result.default, 123)
   896    },
   897  
   898    async noResolveDirInVirtualModule({ esbuild, testDir }) {
   899      const input = path.join(testDir, 'in.js')
   900      const output = path.join(testDir, 'out.js')
   901      const resolveDir = path.join(testDir, 'target')
   902      const loadme = path.join(resolveDir, 'loadme.js')
   903      await mkdirAsync(resolveDir)
   904      await writeFileAsync(input, `
   905        import value from 'virtual'
   906        export default value
   907      `)
   908      await writeFileAsync(loadme, `
   909        export default 123
   910      `)
   911      let error
   912      try {
   913        await esbuild.build({
   914          entryPoints: [input],
   915          bundle: true,
   916          outfile: output,
   917          format: 'cjs',
   918          logLevel: 'silent', plugins: [{
   919            name: 'name',
   920            setup(build) {
   921              let contents = `export {default} from './loadme'`
   922              build.onResolve({ filter: /^virtual$/ }, () => ({ path: 'virtual', namespace: 'for-testing' }))
   923              build.onLoad({ filter: /.*/, namespace: 'for-testing' }, () => ({ contents }))
   924            },
   925          }],
   926        })
   927      } catch (e) {
   928        error = e
   929      }
   930      assert.notStrictEqual(error, void 0)
   931      if (!Array.isArray(error.errors)) throw error
   932      assert.strictEqual(error.errors.length, 1)
   933      assert.strictEqual(error.errors[0].text, `Could not resolve "./loadme"`)
   934      assert.strictEqual(error.errors[0].notes[0].text,
   935        `The plugin "name" didn't set a resolve directory for the file "for-testing:virtual", ` +
   936        `so esbuild did not search for "./loadme" on the file system.`)
   937    },
   938  
   939    async webAssembly({ esbuild, testDir }) {
   940      const input = path.join(testDir, 'in.js')
   941      const wasm = path.join(testDir, 'test.wasm')
   942      const output = path.join(testDir, 'out.js')
   943      await writeFileAsync(input, `
   944        import load from './test.wasm'
   945        export default async (x, y) => (await load()).add(x, y)
   946      `)
   947      await writeFileAsync(wasm, Buffer.of(
   948        // #[wasm_bindgen]
   949        // pub fn add(x: i32, y: i32) -> i32 { x + y }
   950        0x00, 0x61, 0x73, 0x6D, 0x01, 0x00, 0x00, 0x00, 0x01, 0x07, 0x01, 0x60,
   951        0x02, 0x7F, 0x7F, 0x01, 0x7F, 0x03, 0x02, 0x01, 0x00, 0x05, 0x03, 0x01,
   952        0x00, 0x11, 0x07, 0x10, 0x02, 0x06, 0x6D, 0x65, 0x6D, 0x6F, 0x72, 0x79,
   953        0x02, 0x00, 0x03, 0x61, 0x64, 0x64, 0x00, 0x00, 0x0A, 0x09, 0x01, 0x07,
   954        0x00, 0x20, 0x00, 0x20, 0x01, 0x6A, 0x0B,
   955      ))
   956      await esbuild.build({
   957        entryPoints: [input],
   958        bundle: true,
   959        outfile: output,
   960        format: 'cjs',
   961        plugins: [{
   962          name: 'name',
   963          setup(build) {
   964            build.onResolve({ filter: /\.wasm$/ }, args => ({
   965              path: path.isAbsolute(args.path) ? args.path : path.join(args.resolveDir, args.path),
   966              namespace: args.namespace === 'wasm-stub' ? 'wasm-binary' : 'wasm-stub',
   967            }))
   968            build.onLoad({ filter: /.*/, namespace: 'wasm-binary' }, async (args) =>
   969              ({ contents: await readFileAsync(args.path), loader: 'binary' }))
   970            build.onLoad({ filter: /.*/, namespace: 'wasm-stub' }, async (args) => ({
   971              contents: `import wasm from ${JSON.stringify(args.path)}
   972                export default async (imports) =>
   973                  (await WebAssembly.instantiate(wasm, imports)).instance.exports` }))
   974          },
   975        }],
   976      })
   977      const result = require(output)
   978      assert.strictEqual(await result.default(103, 20), 123)
   979    },
   980  
   981    async virtualEntryPoints({ esbuild, testDir }) {
   982      const result = await esbuild.build({
   983        entryPoints: ['1', '2', 'a<>:"|?b', 'a/b/c.d.e'],
   984        bundle: true,
   985        write: false,
   986        outdir: testDir,
   987        format: 'esm',
   988        plugins: [{
   989          name: 'name',
   990          setup(build) {
   991            build.onResolve({ filter: /.*/ }, args => {
   992              return { path: `input ${args.path}`, namespace: 'virtual-ns' }
   993            })
   994            build.onLoad({ filter: /.*/, namespace: 'virtual-ns' }, args => {
   995              return { contents: `console.log(${JSON.stringify(args.path)})` }
   996            })
   997          },
   998        }],
   999      })
  1000      assert.strictEqual(result.outputFiles.length, 4)
  1001      assert.strictEqual(result.outputFiles[0].path, path.join(testDir, '1.js'))
  1002      assert.strictEqual(result.outputFiles[1].path, path.join(testDir, '2.js'))
  1003      assert.strictEqual(result.outputFiles[2].path, path.join(testDir, 'a_b.js'))
  1004      assert.strictEqual(result.outputFiles[3].path, path.join(testDir, 'a/b/c.d.js'))
  1005      assert.strictEqual(result.outputFiles[0].text, `// virtual-ns:input 1\nconsole.log("input 1");\n`)
  1006      assert.strictEqual(result.outputFiles[1].text, `// virtual-ns:input 2\nconsole.log("input 2");\n`)
  1007      assert.strictEqual(result.outputFiles[2].text, `// virtual-ns:input a<>:"|?b\nconsole.log('input a<>:"|?b');\n`)
  1008      assert.strictEqual(result.outputFiles[3].text, `// virtual-ns:input a/b/c.d.e\nconsole.log("input a/b/c.d.e");\n`)
  1009    },
  1010  
  1011    async entryPointFileNamespace({ esbuild, testDir }) {
  1012      const input = path.join(testDir, 'in.js')
  1013      let worked = false
  1014      await writeFileAsync(input, 'stuff')
  1015      await esbuild.build({
  1016        entryPoints: [input],
  1017        write: false,
  1018        plugins: [{
  1019          name: 'name',
  1020          setup(build) {
  1021            build.onResolve({ filter: /.*/, namespace: 'file' }, () => {
  1022              worked = true
  1023            })
  1024          },
  1025        }],
  1026      })
  1027      assert(worked)
  1028    },
  1029  
  1030    async stdinImporter({ esbuild, testDir }) {
  1031      const output = path.join(testDir, 'out.js')
  1032      await esbuild.build({
  1033        stdin: {
  1034          contents: `import x from "plugin"; export default x`,
  1035          sourcefile: 'stdin-sourcefile',
  1036        },
  1037        bundle: true,
  1038        outfile: output,
  1039        format: 'cjs',
  1040        plugins: [{
  1041          name: 'name',
  1042          setup(build) {
  1043            build.onResolve({ filter: /^plugin$/ }, args => {
  1044              assert.strictEqual(args.namespace, '')
  1045              assert.strictEqual(args.importer, 'stdin-sourcefile')
  1046              assert.strictEqual(args.resolveDir, '')
  1047              assert.strictEqual(args.path, 'plugin')
  1048              return { path: args.path, namespace: 'worked' }
  1049            })
  1050            build.onLoad({ filter: /.*/, namespace: 'worked' }, () => {
  1051              return { contents: `export default 123` }
  1052            })
  1053          },
  1054        }],
  1055      })
  1056      const result = require(output)
  1057      assert.strictEqual(result.default, 123)
  1058    },
  1059  
  1060    async stdinImporterResolveDir({ esbuild, testDir }) {
  1061      const output = path.join(testDir, 'out.js')
  1062      await esbuild.build({
  1063        stdin: {
  1064          contents: `import x from "plugin"; export default x`,
  1065          sourcefile: 'stdin-sourcefile',
  1066          resolveDir: testDir,
  1067        },
  1068        bundle: true,
  1069        outfile: output,
  1070        format: 'cjs',
  1071        plugins: [{
  1072          name: 'name',
  1073          setup(build) {
  1074            build.onResolve({ filter: /^plugin$/ }, args => {
  1075              assert.strictEqual(args.namespace, 'file')
  1076              assert.strictEqual(args.importer, path.join(testDir, 'stdin-sourcefile'))
  1077              assert.strictEqual(args.resolveDir, testDir)
  1078              assert.strictEqual(args.path, 'plugin')
  1079              return { path: args.path, namespace: 'worked' }
  1080            })
  1081            build.onLoad({ filter: /.*/, namespace: 'worked' }, () => {
  1082              return { contents: `export default 123` }
  1083            })
  1084          },
  1085        }],
  1086      })
  1087      const result = require(output)
  1088      assert.strictEqual(result.default, 123)
  1089    },
  1090  
  1091    async stdinAbsoluteImporterResolveDir({ esbuild, testDir }) {
  1092      const output = path.join(testDir, 'out.js')
  1093      await esbuild.build({
  1094        stdin: {
  1095          contents: `import x from "plugin"; export default x`,
  1096          sourcefile: path.join(testDir, 'stdin-sourcefile'),
  1097          resolveDir: testDir,
  1098        },
  1099        bundle: true,
  1100        outfile: output,
  1101        format: 'cjs',
  1102        plugins: [{
  1103          name: 'name',
  1104          setup(build) {
  1105            build.onResolve({ filter: /^plugin$/ }, args => {
  1106              assert.strictEqual(args.namespace, 'file')
  1107              assert.strictEqual(args.importer, path.join(testDir, 'stdin-sourcefile'))
  1108              assert.strictEqual(args.resolveDir, testDir)
  1109              assert.strictEqual(args.path, 'plugin')
  1110              return { path: args.path, namespace: 'worked' }
  1111            })
  1112            build.onLoad({ filter: /.*/, namespace: 'worked' }, () => {
  1113              return { contents: `export default 123` }
  1114            })
  1115          },
  1116        }],
  1117      })
  1118      const result = require(output)
  1119      assert.strictEqual(result.default, 123)
  1120    },
  1121  
  1122    async stdinRelative({ esbuild, testDir }) {
  1123      const output = path.join(testDir, 'out.js')
  1124      await esbuild.build({
  1125        stdin: {
  1126          contents: `import x from "./stdinRelative.js"; export default x`,
  1127        },
  1128        bundle: true,
  1129        outfile: output,
  1130        format: 'cjs',
  1131        plugins: [{
  1132          name: 'name',
  1133          setup(build) {
  1134            build.onResolve({ filter: /.*/ }, args => {
  1135              assert.strictEqual(args.namespace, '')
  1136              assert.strictEqual(args.importer, '<stdin>')
  1137              assert.strictEqual(args.resolveDir, '')
  1138              assert.strictEqual(args.path, './stdinRelative.js')
  1139              return { path: args.path, namespace: 'worked' }
  1140            })
  1141            build.onLoad({ filter: /.*/, namespace: 'worked' }, () => {
  1142              return { contents: `export default 123` }
  1143            })
  1144          },
  1145        }],
  1146      })
  1147      const result = require(output)
  1148      assert.strictEqual(result.default, 123)
  1149    },
  1150  
  1151    async stdinRelativeResolveDir({ esbuild, testDir }) {
  1152      const output = path.join(testDir, 'out', 'out.js')
  1153      await esbuild.build({
  1154        stdin: {
  1155          contents: `import x from "./stdinRelative.js"; export default x`,
  1156          resolveDir: testDir,
  1157        },
  1158        bundle: true,
  1159        outfile: output,
  1160        format: 'cjs',
  1161        plugins: [{
  1162          name: 'name',
  1163          setup(build) {
  1164            build.onResolve({ filter: /.*/ }, args => {
  1165              assert.strictEqual(args.namespace, '')
  1166              assert.strictEqual(args.importer, '<stdin>')
  1167              assert.strictEqual(args.resolveDir, testDir)
  1168              assert.strictEqual(args.path, './stdinRelative.js')
  1169              return { path: args.path, namespace: 'worked' }
  1170            })
  1171            build.onLoad({ filter: /.*/, namespace: 'worked' }, () => {
  1172              return { contents: `export default 123` }
  1173            })
  1174          },
  1175        }],
  1176      })
  1177      const result = require(output)
  1178      assert.strictEqual(result.default, 123)
  1179    },
  1180  
  1181    async externalRequire({ esbuild, testDir }) {
  1182      const externalPlugin = external => ({
  1183        name: 'external',
  1184        setup(build) {
  1185          let escape = text => `^${text.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&')}$`
  1186          let filter = new RegExp(external.map(escape).join('|'))
  1187          build.onResolve({ filter: /.*/, namespace: 'external' }, args => ({
  1188            path: args.path, external: true
  1189          }))
  1190          build.onResolve({ filter }, args => ({
  1191            path: args.path, namespace: 'external'
  1192          }))
  1193          build.onLoad({ filter: /.*/, namespace: 'external' }, args => ({
  1194            contents: `import * as all from ${JSON.stringify(args.path)}; module.exports = all`
  1195          }))
  1196        },
  1197      })
  1198      const outfile = path.join(testDir, 'out', 'output.mjs')
  1199      await esbuild.build({
  1200        stdin: {
  1201          contents: `const fs = require('fs')
  1202            const url = require('url')
  1203            const path = require('path')
  1204            export default fs.readdirSync(path.dirname(url.fileURLToPath(import.meta.url)))
  1205          `,
  1206        },
  1207        bundle: true,
  1208        outfile,
  1209        format: 'esm',
  1210        plugins: [
  1211          externalPlugin(['fs', 'url', 'path'])
  1212        ],
  1213      })
  1214      const result = await import(url.pathToFileURL(outfile))
  1215      assert.deepStrictEqual(result.default, [path.basename(outfile)])
  1216    },
  1217  
  1218    async newlineInPath({ esbuild }) {
  1219      // Using a path with a newline shouldn't cause a syntax error when the path is printed in a comment
  1220      for (let nl of ['\r', '\n', '\r\n', '\u2028', '\u2029']) {
  1221        let problem = `a b${nl}c d`
  1222        const plugin = {
  1223          name: 'test',
  1224          setup(build) {
  1225            build.onResolve({ filter: /.*/ }, args => ({
  1226              path: args.path, namespace: 'test',
  1227            }))
  1228            build.onLoad({ filter: /.*/, namespace: 'test' }, args => ({
  1229              contents: `return ${JSON.stringify(args.path)}`
  1230            }))
  1231          },
  1232        }
  1233        let result = await esbuild.build({
  1234          entryPoints: [problem],
  1235          bundle: true,
  1236          write: false,
  1237          format: 'cjs',
  1238          plugins: [plugin],
  1239        })
  1240        let value = new Function(result.outputFiles[0].text)()
  1241        assert.deepStrictEqual(value, problem)
  1242      }
  1243    },
  1244  
  1245    async newlineInNamespace({ esbuild }) {
  1246      // Using a namespace with a newline shouldn't cause a syntax error when the namespace is printed in a comment
  1247      for (let nl of ['\r', '\n', '\r\n', '\u2028', '\u2029']) {
  1248        let problem = `a b${nl}c d`
  1249        const plugin = {
  1250          name: 'test',
  1251          setup(build) {
  1252            build.onResolve({ filter: /.*/ }, args => ({
  1253              path: args.path, namespace: problem,
  1254            }))
  1255            build.onLoad({ filter: /.*/, namespace: problem }, args => ({
  1256              contents: `return ${JSON.stringify(args.namespace)}`
  1257            }))
  1258          },
  1259        }
  1260        let result = await esbuild.build({
  1261          entryPoints: ['entry'],
  1262          bundle: true,
  1263          write: false,
  1264          format: 'cjs',
  1265          plugins: [plugin],
  1266        })
  1267        let value = new Function(result.outputFiles[0].text)()
  1268        assert.deepStrictEqual(value, problem)
  1269      }
  1270    },
  1271  
  1272    async transformUndefinedDetailForError({ esbuild }) {
  1273      try {
  1274        await esbuild.transform('x y')
  1275        throw new Error('Expected an error to be thrown')
  1276      } catch (e) {
  1277        assert.deepStrictEqual(e.warnings, [])
  1278        assert.deepStrictEqual(e.errors, [{
  1279          id: '',
  1280          pluginName: '',
  1281          text: 'Expected ";" but found "y"',
  1282          location: {
  1283            file: '<stdin>',
  1284            namespace: '',
  1285            line: 1,
  1286            column: 2,
  1287            length: 1,
  1288            lineText: 'x y',
  1289            suggestion: ';',
  1290          },
  1291          notes: [],
  1292          detail: void 0,
  1293        }])
  1294      }
  1295    },
  1296  
  1297    async transformUndefinedDetailForWarning({ esbuild }) {
  1298      const result = await esbuild.transform('typeof x == "null"')
  1299      assert.deepStrictEqual(result.warnings, [{
  1300        id: 'impossible-typeof',
  1301        pluginName: '',
  1302        text: 'The "typeof" operator will never evaluate to "null"',
  1303        location: {
  1304          file: '<stdin>',
  1305          namespace: '',
  1306          line: 1,
  1307          column: 12,
  1308          length: 6,
  1309          lineText: 'typeof x == "null"',
  1310          suggestion: '',
  1311        },
  1312        notes: [
  1313          {
  1314            location: null,
  1315            text: 'The expression "typeof x" actually evaluates to "object" in JavaScript, not "null". You need to use "x === null" to test for null.'
  1316          }
  1317        ],
  1318        detail: void 0,
  1319      }])
  1320    },
  1321  
  1322    async buildUndefinedDetailForError({ esbuild }) {
  1323      try {
  1324        await esbuild.build({
  1325          stdin: { contents: 'x y' },
  1326          write: false,
  1327          logLevel: 'silent',
  1328        })
  1329        throw new Error('Expected an error to be thrown')
  1330      } catch (e) {
  1331        assert.deepStrictEqual(e.warnings, [])
  1332        assert.deepStrictEqual(e.errors, [{
  1333          id: '',
  1334          pluginName: '',
  1335          text: 'Expected ";" but found "y"',
  1336          location: {
  1337            file: '<stdin>',
  1338            namespace: '',
  1339            line: 1,
  1340            column: 2,
  1341            length: 1,
  1342            lineText: 'x y',
  1343            suggestion: ';',
  1344          },
  1345          notes: [],
  1346          detail: void 0,
  1347        }])
  1348      }
  1349    },
  1350  
  1351    async buildUndefinedDetailForWarning({ esbuild }) {
  1352      const result = await esbuild.build({
  1353        stdin: { contents: 'typeof x == "null"' },
  1354        write: false,
  1355        logLevel: 'silent',
  1356      })
  1357      assert.deepStrictEqual(result.warnings, [{
  1358        id: 'impossible-typeof',
  1359        pluginName: '',
  1360        text: 'The "typeof" operator will never evaluate to "null"',
  1361        location: {
  1362          file: '<stdin>',
  1363          namespace: '',
  1364          line: 1,
  1365          column: 12,
  1366          length: 6,
  1367          lineText: 'typeof x == "null"',
  1368          suggestion: '',
  1369        },
  1370        notes: [
  1371          {
  1372            location: null,
  1373            text: 'The expression "typeof x" actually evaluates to "object" in JavaScript, not "null". You need to use "x === null" to test for null.'
  1374          }
  1375        ],
  1376        detail: void 0,
  1377      }])
  1378    },
  1379  
  1380    async specificDetailForOnResolvePluginThrowError({ esbuild }) {
  1381      const theError = new Error('theError');
  1382      try {
  1383        await esbuild.build({
  1384          entryPoints: ['entry'],
  1385          write: false,
  1386          logLevel: 'silent',
  1387          plugins: [{
  1388            name: 'the-plugin',
  1389            setup(build) {
  1390              build.onResolve({ filter: /.*/ }, () => {
  1391                throw theError;
  1392              })
  1393            },
  1394          }],
  1395        })
  1396        throw new Error('Expected an error to be thrown')
  1397      } catch (e) {
  1398        assert.strictEqual(e.warnings.length, 0)
  1399        assert.strictEqual(e.errors.length, 1)
  1400        assert.strictEqual(e.errors[0].pluginName, 'the-plugin')
  1401        assert.strictEqual(e.errors[0].text, 'theError')
  1402        assert.strictEqual(e.errors[0].detail, theError)
  1403      }
  1404    },
  1405  
  1406    async specificDetailForOnLoadPluginThrowError({ esbuild }) {
  1407      const theError = new Error('theError');
  1408      try {
  1409        await esbuild.build({
  1410          entryPoints: ['entry'],
  1411          write: false,
  1412          logLevel: 'silent',
  1413          plugins: [{
  1414            name: 'the-plugin',
  1415            setup(build) {
  1416              build.onResolve({ filter: /.*/ }, () => ({ path: 'abc', namespace: 'xyz' }))
  1417              build.onLoad({ filter: /.*/ }, () => {
  1418                throw theError;
  1419              })
  1420            },
  1421          }],
  1422        })
  1423        throw new Error('Expected an error to be thrown')
  1424      } catch (e) {
  1425        assert.strictEqual(e.warnings.length, 0)
  1426        assert.strictEqual(e.errors.length, 1)
  1427        assert.strictEqual(e.errors[0].pluginName, 'the-plugin')
  1428        assert.strictEqual(e.errors[0].text, 'theError')
  1429        assert.strictEqual(e.errors[0].detail, theError)
  1430      }
  1431    },
  1432  
  1433    async specificDetailForOnResolvePluginReturnError({ esbuild }) {
  1434      const theError = new Error('theError');
  1435      try {
  1436        await esbuild.build({
  1437          entryPoints: ['entry'],
  1438          write: false,
  1439          logLevel: 'silent',
  1440          plugins: [{
  1441            name: 'the-plugin',
  1442            setup(build) {
  1443              build.onResolve({ filter: /.*/ }, () => {
  1444                return {
  1445                  errors: [{
  1446                    text: 'some error',
  1447                    location: {
  1448                      file: 'file1',
  1449                      namespace: 'ns1',
  1450                      line: 1,
  1451                      column: 2,
  1452                      length: 3,
  1453                      lineText: 'some text',
  1454                      suggestion: '',
  1455                    },
  1456                    notes: [{
  1457                      text: 'some note',
  1458                      location: {
  1459                        file: 'file2',
  1460                        namespace: 'ns2',
  1461                        line: 4,
  1462                        column: 5,
  1463                        length: 6,
  1464                        lineText: 'more text',
  1465                        suggestion: '',
  1466                      },
  1467                    }],
  1468                    detail: theError,
  1469                  }],
  1470                };
  1471              })
  1472            },
  1473          }],
  1474        })
  1475        throw new Error('Expected an error to be thrown')
  1476      } catch (e) {
  1477        assert.strictEqual(e.warnings.length, 0)
  1478        assert.strictEqual(e.errors.length, 1)
  1479        assert.deepStrictEqual(e.errors[0], {
  1480          id: '',
  1481          pluginName: 'the-plugin',
  1482          text: 'some error',
  1483          location: {
  1484            file: 'ns1:file1',
  1485            namespace: 'ns1',
  1486            line: 1,
  1487            column: 2,
  1488            length: 3,
  1489            lineText: 'some text',
  1490            suggestion: '',
  1491          },
  1492          notes: [{
  1493            text: 'some note',
  1494            location: {
  1495              file: 'ns2:file2',
  1496              namespace: 'ns2',
  1497              line: 4,
  1498              column: 5,
  1499              length: 6,
  1500              lineText: 'more text',
  1501              suggestion: '',
  1502            },
  1503          }],
  1504          detail: theError,
  1505        })
  1506      }
  1507    },
  1508  
  1509    async specificDetailForOnResolvePluginReturnWarning({ esbuild }) {
  1510      const theError = new Error('theError');
  1511      const result = await esbuild.build({
  1512        entryPoints: ['entry'],
  1513        write: false,
  1514        logLevel: 'silent',
  1515        plugins: [{
  1516          name: 'the-plugin',
  1517          setup(build) {
  1518            build.onResolve({ filter: /.*/ }, () => {
  1519              return {
  1520                path: 'abc', namespace: 'xyz', warnings: [{
  1521                  pluginName: 'other-plugin',
  1522                  text: 'some warning',
  1523                  location: {
  1524                    file: 'file1',
  1525                    namespace: 'ns1',
  1526                    line: 1,
  1527                    column: 2,
  1528                    length: 3,
  1529                    lineText: 'some text',
  1530                    suggestion: '',
  1531                  },
  1532                  notes: [{
  1533                    text: 'some note',
  1534                    location: {
  1535                      file: 'file2',
  1536                      namespace: 'ns2',
  1537                      line: 4,
  1538                      column: 5,
  1539                      length: 6,
  1540                      lineText: 'more text',
  1541                      suggestion: '',
  1542                    },
  1543                  }],
  1544                  detail: theError,
  1545                }]
  1546              };
  1547            })
  1548            build.onLoad({ filter: /.*/ }, () => ({ contents: '' }))
  1549          },
  1550        }],
  1551      })
  1552      assert.strictEqual(result.warnings.length, 1)
  1553      assert.deepStrictEqual(result.warnings[0], {
  1554        id: '',
  1555        pluginName: 'other-plugin',
  1556        text: 'some warning',
  1557        location: {
  1558          file: 'ns1:file1',
  1559          namespace: 'ns1',
  1560          line: 1,
  1561          column: 2,
  1562          length: 3,
  1563          lineText: 'some text',
  1564          suggestion: '',
  1565        },
  1566        notes: [{
  1567          text: 'some note',
  1568          location: {
  1569            file: 'ns2:file2',
  1570            namespace: 'ns2',
  1571            line: 4,
  1572            column: 5,
  1573            length: 6,
  1574            lineText: 'more text',
  1575            suggestion: '',
  1576          },
  1577        }],
  1578        detail: theError,
  1579      })
  1580    },
  1581  
  1582    async specificDetailForOnLoadPluginReturnError({ esbuild }) {
  1583      const theError = new Error('theError');
  1584      try {
  1585        await esbuild.build({
  1586          entryPoints: ['entry'],
  1587          write: false,
  1588          logLevel: 'silent',
  1589          plugins: [{
  1590            name: 'the-plugin',
  1591            setup(build) {
  1592              build.onResolve({ filter: /.*/ }, () => ({ path: 'abc', namespace: 'xyz' }))
  1593              build.onLoad({ filter: /.*/ }, () => {
  1594                return {
  1595                  errors: [{
  1596                    text: 'some error',
  1597                    location: {
  1598                      file: 'file1',
  1599                      namespace: 'ns1',
  1600                      line: 1,
  1601                      column: 2,
  1602                      length: 3,
  1603                      lineText: 'some text',
  1604                      suggestion: '',
  1605                    },
  1606                    notes: [{
  1607                      text: 'some note',
  1608                      location: {
  1609                        file: 'file2',
  1610                        namespace: 'ns2',
  1611                        line: 4,
  1612                        column: 5,
  1613                        length: 6,
  1614                        lineText: 'more text',
  1615                        suggestion: '',
  1616                      },
  1617                    }],
  1618                    detail: theError,
  1619                  }],
  1620                };
  1621              })
  1622            },
  1623          }],
  1624        })
  1625        throw new Error('Expected an error to be thrown')
  1626      } catch (e) {
  1627        assert.strictEqual(e.warnings.length, 0)
  1628        assert.strictEqual(e.errors.length, 1)
  1629        assert.deepStrictEqual(e.errors[0], {
  1630          id: '',
  1631          pluginName: 'the-plugin',
  1632          text: 'some error',
  1633          location: {
  1634            file: 'ns1:file1',
  1635            namespace: 'ns1',
  1636            line: 1,
  1637            column: 2,
  1638            length: 3,
  1639            lineText: 'some text',
  1640            suggestion: '',
  1641          },
  1642          notes: [{
  1643            text: 'some note',
  1644            location: {
  1645              file: 'ns2:file2',
  1646              namespace: 'ns2',
  1647              line: 4,
  1648              column: 5,
  1649              length: 6,
  1650              lineText: 'more text',
  1651              suggestion: '',
  1652            },
  1653          }],
  1654          detail: theError,
  1655        })
  1656      }
  1657    },
  1658  
  1659    async specificDetailForOnLoadPluginReturnWarning({ esbuild }) {
  1660      const theError = new Error('theError');
  1661      const result = await esbuild.build({
  1662        entryPoints: ['entry'],
  1663        write: false,
  1664        logLevel: 'silent',
  1665        plugins: [{
  1666          name: 'the-plugin',
  1667          setup(build) {
  1668            build.onResolve({ filter: /.*/ }, () => ({ path: 'abc', namespace: 'xyz' }))
  1669            build.onLoad({ filter: /.*/ }, () => {
  1670              return {
  1671                contents: '', warnings: [{
  1672                  text: 'some warning',
  1673                  location: {
  1674                    file: 'file1',
  1675                    namespace: 'ns1',
  1676                    line: 1,
  1677                    column: 2,
  1678                    length: 3,
  1679                    lineText: 'some text',
  1680                    suggestion: '',
  1681                  },
  1682                  notes: [{
  1683                    text: 'some note',
  1684                    location: {
  1685                      file: 'file2',
  1686                      namespace: 'ns2',
  1687                      line: 4,
  1688                      column: 5,
  1689                      length: 6,
  1690                      lineText: 'more text',
  1691                      suggestion: '',
  1692                    },
  1693                  }],
  1694                  detail: theError,
  1695                }],
  1696              };
  1697            })
  1698          },
  1699        }],
  1700      })
  1701      assert.strictEqual(result.warnings.length, 1)
  1702      assert.deepStrictEqual(result.warnings[0], {
  1703        id: '',
  1704        pluginName: 'the-plugin',
  1705        text: 'some warning',
  1706        location: {
  1707          file: 'ns1:file1',
  1708          namespace: 'ns1',
  1709          line: 1,
  1710          column: 2,
  1711          length: 3,
  1712          lineText: 'some text',
  1713          suggestion: '',
  1714        },
  1715        notes: [{
  1716          text: 'some note',
  1717          location: {
  1718            file: 'ns2:file2',
  1719            namespace: 'ns2',
  1720            line: 4,
  1721            column: 5,
  1722            length: 6,
  1723            lineText: 'more text',
  1724            suggestion: '',
  1725          },
  1726        }],
  1727        detail: theError,
  1728      })
  1729    },
  1730  
  1731    async pluginDataResolveToLoad({ esbuild }) {
  1732      const theObject = {}
  1733      const result = await esbuild.build({
  1734        entryPoints: ['entry'],
  1735        write: false,
  1736        plugins: [{
  1737          name: 'plugin',
  1738          setup(build) {
  1739            build.onResolve({ filter: /.*/ }, () => ({
  1740              path: 'abc',
  1741              namespace: 'xyz',
  1742              pluginData: theObject,
  1743            }))
  1744            build.onLoad({ filter: /.*/ }, args => {
  1745              assert.strictEqual(args.pluginData, theObject)
  1746              return { contents: 'foo()' };
  1747            })
  1748          },
  1749        }],
  1750      })
  1751      assert.strictEqual(result.outputFiles[0].text, 'foo();\n')
  1752    },
  1753  
  1754    async pluginDataResolveToLoadNested({ esbuild }) {
  1755      const theObject = {}
  1756      const result = await esbuild.build({
  1757        entryPoints: ['entry'],
  1758        write: false,
  1759        bundle: true,
  1760        format: 'esm',
  1761        plugins: [{
  1762          name: 'plugin',
  1763          setup(build) {
  1764            build.onResolve({ filter: /.*/ }, args => {
  1765              if (args.path === 'entry') return { path: 'entry', namespace: 'xyz' }
  1766              return {
  1767                path: 'nested',
  1768                namespace: 'xyz',
  1769                pluginData: theObject,
  1770              }
  1771            })
  1772            build.onLoad({ filter: /.*/ }, args => {
  1773              if (args.path === 'entry') return { contents: 'import "nested"' };
  1774              assert.strictEqual(args.pluginData, theObject)
  1775              return { contents: 'foo()' };
  1776            })
  1777          },
  1778        }],
  1779      })
  1780      assert.strictEqual(result.outputFiles[0].text, '// xyz:nested\nfoo();\n')
  1781    },
  1782  
  1783    async pluginDataLoadToResolve({ esbuild }) {
  1784      const theObject = {}
  1785      const result = await esbuild.build({
  1786        entryPoints: ['entry'],
  1787        write: false,
  1788        plugins: [{
  1789          name: 'plugin',
  1790          setup(build) {
  1791            build.onResolve({ filter: /.*/ }, args => {
  1792              if (args === 'import') {
  1793                assert.strictEqual(args.pluginData, theObject)
  1794                return { external: true }
  1795              }
  1796              return { path: 'abc', namespace: 'xyz' }
  1797            })
  1798            build.onLoad({ filter: /.*/ }, () => ({
  1799              contents: 'import("import")',
  1800              pluginData: theObject,
  1801            }))
  1802          },
  1803        }],
  1804      })
  1805      assert.strictEqual(result.outputFiles[0].text, 'import("import");\n')
  1806    },
  1807  
  1808    async resolveKindEntryPoint({ esbuild }) {
  1809      let resolveKind = '<missing>'
  1810      try {
  1811        await esbuild.build({
  1812          entryPoints: ['entry'],
  1813          bundle: true,
  1814          write: false,
  1815          logLevel: 'silent',
  1816          plugins: [{
  1817            name: 'plugin',
  1818            setup(build) {
  1819              build.onResolve({ filter: /.*/ }, args => {
  1820                resolveKind = args.kind
  1821              })
  1822            },
  1823          }],
  1824        })
  1825      } catch (e) {
  1826      }
  1827      assert.strictEqual(resolveKind, 'entry-point')
  1828    },
  1829  
  1830    async resolveKindImportStmt({ esbuild }) {
  1831      let resolveKind = '<missing>'
  1832      try {
  1833        await esbuild.build({
  1834          entryPoints: ['entry'],
  1835          bundle: true,
  1836          write: false,
  1837          logLevel: 'silent',
  1838          plugins: [{
  1839            name: 'plugin',
  1840            setup(build) {
  1841              build.onResolve({ filter: /.*/ }, args => {
  1842                if (args.importer === '') return { path: args.path, namespace: 'ns' }
  1843                else resolveKind = args.kind
  1844              })
  1845              build.onLoad({ filter: /.*/, namespace: 'ns' }, () => {
  1846                return { contents: `import 'test'` }
  1847              })
  1848            },
  1849          }],
  1850        })
  1851      } catch (e) {
  1852      }
  1853      assert.strictEqual(resolveKind, 'import-statement')
  1854    },
  1855  
  1856    async resolveKindRequireCall({ esbuild }) {
  1857      let resolveKind = '<missing>'
  1858      try {
  1859        await esbuild.build({
  1860          entryPoints: ['entry'],
  1861          bundle: true,
  1862          write: false,
  1863          logLevel: 'silent',
  1864          plugins: [{
  1865            name: 'plugin',
  1866            setup(build) {
  1867              build.onResolve({ filter: /.*/ }, args => {
  1868                if (args.importer === '') return { path: args.path, namespace: 'ns' }
  1869                else resolveKind = args.kind
  1870              })
  1871              build.onLoad({ filter: /.*/, namespace: 'ns' }, () => {
  1872                return { contents: `require('test')` }
  1873              })
  1874            },
  1875          }],
  1876        })
  1877      } catch (e) {
  1878      }
  1879      assert.strictEqual(resolveKind, 'require-call')
  1880    },
  1881  
  1882    async resolveKindDynamicImport({ esbuild }) {
  1883      let resolveKind = '<missing>'
  1884      try {
  1885        await esbuild.build({
  1886          entryPoints: ['entry'],
  1887          bundle: true,
  1888          write: false,
  1889          logLevel: 'silent',
  1890          plugins: [{
  1891            name: 'plugin',
  1892            setup(build) {
  1893              build.onResolve({ filter: /.*/ }, args => {
  1894                if (args.importer === '') return { path: args.path, namespace: 'ns' }
  1895                else resolveKind = args.kind
  1896              })
  1897              build.onLoad({ filter: /.*/, namespace: 'ns' }, () => {
  1898                return { contents: `import('test')` }
  1899              })
  1900            },
  1901          }],
  1902        })
  1903      } catch (e) {
  1904      }
  1905      assert.strictEqual(resolveKind, 'dynamic-import')
  1906    },
  1907  
  1908    async resolveKindRequireResolve({ esbuild }) {
  1909      let resolveKind = '<missing>'
  1910      try {
  1911        await esbuild.build({
  1912          entryPoints: ['entry'],
  1913          bundle: true,
  1914          write: false,
  1915          logLevel: 'silent',
  1916          platform: 'node',
  1917          plugins: [{
  1918            name: 'plugin',
  1919            setup(build) {
  1920              build.onResolve({ filter: /.*/ }, args => {
  1921                if (args.importer === '') return { path: args.path, namespace: 'ns' }
  1922                else resolveKind = args.kind
  1923              })
  1924              build.onLoad({ filter: /.*/, namespace: 'ns' }, () => {
  1925                return { contents: `require.resolve('test')` }
  1926              })
  1927            },
  1928          }],
  1929        })
  1930      } catch (e) {
  1931      }
  1932      assert.strictEqual(resolveKind, 'require-resolve')
  1933    },
  1934  
  1935    async resolveKindAtImport({ esbuild }) {
  1936      let resolveKind = '<missing>'
  1937      try {
  1938        await esbuild.build({
  1939          entryPoints: ['entry'],
  1940          bundle: true,
  1941          write: false,
  1942          logLevel: 'silent',
  1943          plugins: [{
  1944            name: 'plugin',
  1945            setup(build) {
  1946              build.onResolve({ filter: /.*/ }, args => {
  1947                if (args.importer === '') return { path: args.path, namespace: 'ns' }
  1948                else resolveKind = args.kind
  1949              })
  1950              build.onLoad({ filter: /.*/, namespace: 'ns' }, () => {
  1951                return { contents: `@import "test";`, loader: 'css' }
  1952              })
  1953            },
  1954          }],
  1955        })
  1956      } catch (e) {
  1957      }
  1958      assert.strictEqual(resolveKind, 'import-rule')
  1959    },
  1960  
  1961    async resolveKindComposesFrom({ esbuild }) {
  1962      let resolveKind = '<missing>'
  1963      try {
  1964        await esbuild.build({
  1965          entryPoints: ['entry'],
  1966          bundle: true,
  1967          write: false,
  1968          logLevel: 'silent',
  1969          plugins: [{
  1970            name: 'plugin',
  1971            setup(build) {
  1972              build.onResolve({ filter: /.*/ }, args => {
  1973                if (args.importer === '') return { path: args.path, namespace: 'ns' }
  1974                else resolveKind = args.kind
  1975              })
  1976              build.onLoad({ filter: /.*/, namespace: 'ns' }, () => {
  1977                return { contents: `.foo { composes: bar from 'entry' }`, loader: 'local-css' }
  1978              })
  1979            },
  1980          }],
  1981        })
  1982      } catch (e) {
  1983      }
  1984      assert.strictEqual(resolveKind, 'composes-from')
  1985    },
  1986  
  1987    async resolveKindURLToken({ esbuild }) {
  1988      let resolveKind = '<missing>'
  1989      try {
  1990        await esbuild.build({
  1991          entryPoints: ['entry'],
  1992          bundle: true,
  1993          write: false,
  1994          logLevel: 'silent',
  1995          plugins: [{
  1996            name: 'plugin',
  1997            setup(build) {
  1998              build.onResolve({ filter: /.*/ }, args => {
  1999                if (args.importer === '') return { path: args.path, namespace: 'ns' }
  2000                else resolveKind = args.kind
  2001              })
  2002              build.onLoad({ filter: /.*/, namespace: 'ns' }, () => {
  2003                return { contents: `div { background: url('test') }`, loader: 'css' }
  2004              })
  2005            },
  2006          }],
  2007        })
  2008      } catch (e) {
  2009      }
  2010      assert.strictEqual(resolveKind, 'url-token')
  2011    },
  2012  
  2013    async warnIfUnusedNoWarning({ esbuild }) {
  2014      const build = await esbuild.build({
  2015        entryPoints: ['entry'],
  2016        bundle: true,
  2017        write: false,
  2018        logLevel: 'silent',
  2019        plugins: [{
  2020          name: 'plugin',
  2021          setup(build) {
  2022            build.onResolve({ filter: /.*/ }, args => {
  2023              if (args.importer === '') return { path: args.path, namespace: 'entry' }
  2024              else return { path: args.path, namespace: 'bare-import' }
  2025            })
  2026            build.onLoad({ filter: /.*/, namespace: 'entry' }, () => {
  2027              return {
  2028                contents: `
  2029                  import "base64"
  2030                  import "binary"
  2031                  import "dataurl"
  2032                  import "json"
  2033                  import "text"
  2034                `,
  2035              }
  2036            })
  2037            build.onLoad({ filter: /.*/, namespace: 'bare-import' }, args => {
  2038              return { contents: `[1, 2, 3]`, loader: args.path }
  2039            })
  2040          },
  2041        }],
  2042      })
  2043      assert.strictEqual(build.warnings.length, 0)
  2044    },
  2045  
  2046    async onResolvePreserveOriginalEntryPointNameIssue945({ esbuild, testDir }) {
  2047      const build = await esbuild.build({
  2048        entryPoints: ['first'],
  2049        write: false,
  2050        logLevel: 'silent',
  2051        outdir: testDir,
  2052        plugins: [{
  2053          name: 'plugin',
  2054          setup(build) {
  2055            build.onResolve({ filter: /.*/ }, () => {
  2056              return { path: 'second', namespace: 'what' }
  2057            })
  2058            build.onLoad({ filter: /.*/ }, () => {
  2059              return { contents: `` }
  2060            })
  2061          },
  2062        }],
  2063      })
  2064      assert.strictEqual(build.outputFiles[0].path, path.join(testDir, 'first.js'))
  2065    },
  2066  
  2067    async dynamicImportDuplicateChunkIssue1099({ esbuild, testDir }) {
  2068      const outdir = path.join(testDir, 'out')
  2069      await mkdirAsync(path.join(testDir, 'hi'), { recursive: true })
  2070      await writeFileAsync(path.join(testDir, 'index.js'), `import x from 'manifest'; console.log(x.name(), x.hi())`)
  2071      await writeFileAsync(path.join(testDir, 'name.js'), `import x from 'manifest'; console.log(x.index(), x.hi())`)
  2072      await writeFileAsync(path.join(testDir, 'hi', 'name.js'), `import x from 'manifest'; console.log(x.index(), x.name())`)
  2073      await esbuild.build({
  2074        entryPoints: [path.join(testDir, 'index.js')],
  2075        outdir,
  2076        bundle: true,
  2077        splitting: true,
  2078        format: 'esm',
  2079        plugins: [{
  2080          name: 'plugin',
  2081          setup(build) {
  2082            build.onResolve({ filter: /^manifest$/ }, () => {
  2083              return { path: 'manifest', namespace: 'Manifest' }
  2084            })
  2085            build.onLoad({ namespace: 'Manifest', filter: /.*/ }, () => {
  2086              return {
  2087                resolveDir: testDir,
  2088                contents: `
  2089                  export const index = () => import('./index')
  2090                  export const name = () => import('./name')
  2091                  export const hi = () => import('./hi/name')
  2092                  export default {index, name, hi}
  2093                `,
  2094              }
  2095            })
  2096          },
  2097        }],
  2098      })
  2099    },
  2100  
  2101    async fileLoaderCustomNamespaceIssue1404({ esbuild, testDir }) {
  2102      const input = path.join(testDir, 'in.data')
  2103      const outdir = path.join(testDir, 'out')
  2104      await writeFileAsync(input, `some data`)
  2105      await esbuild.build({
  2106        entryPoints: [path.basename(input)],
  2107        absWorkingDir: testDir,
  2108        logLevel: 'silent',
  2109        outdir,
  2110        assetNames: '[name]',
  2111        plugins: [{
  2112          name: 'plugin',
  2113          setup(build) {
  2114            build.onResolve({ filter: /\.data$/ }, args => {
  2115              return {
  2116                path: args.path,
  2117                namespace: 'ns',
  2118              }
  2119            })
  2120            build.onLoad({ filter: /.*/, namespace: 'ns' }, async (args) => {
  2121              const data = await readFileAsync(path.join(testDir, args.path), 'utf8')
  2122              return {
  2123                contents: data.split('').reverse().join(''),
  2124                loader: 'file',
  2125              }
  2126            })
  2127          },
  2128        }],
  2129      })
  2130      assert.strictEqual(await readFileAsync(input, 'utf8'), `some data`)
  2131      assert.strictEqual(require(path.join(outdir, 'in.js')), `./in.data`)
  2132    },
  2133  
  2134    async esbuildProperty({ esbuild }) {
  2135      let esbuildFromBuild
  2136      await esbuild.build({
  2137        entryPoints: ['xyz'],
  2138        write: false,
  2139        plugins: [{
  2140          name: 'plugin',
  2141          setup(build) {
  2142            esbuildFromBuild = build.esbuild
  2143            build.onResolve({ filter: /.*/ }, () => ({ path: 'foo', namespace: 'bar' }))
  2144            build.onLoad({ filter: /.*/ }, () => ({ contents: '' }))
  2145          },
  2146        }],
  2147      })
  2148      assert.deepStrictEqual({ ...esbuildFromBuild }, { ...esbuild })
  2149    },
  2150  
  2151    async onResolveInvalidPathSuffix({ esbuild }) {
  2152      try {
  2153        await esbuild.build({
  2154          entryPoints: ['foo'],
  2155          logLevel: 'silent',
  2156          plugins: [{
  2157            name: 'plugin',
  2158            setup(build) {
  2159              build.onResolve({ filter: /.*/ }, () => ({ path: 'bar', suffix: '%what' }))
  2160            },
  2161          }],
  2162        })
  2163        throw new Error('Expected an error to be thrown')
  2164      } catch (e) {
  2165        assert.strictEqual(e.message, `Build failed with 1 error:
  2166  error: Invalid path suffix "%what" returned from plugin (must start with "?" or "#")`)
  2167      }
  2168    },
  2169  
  2170    async onResolveWithInternalOnLoadAndQuerySuffix({ testDir, esbuild }) {
  2171      const entry = path.join(testDir, 'entry.js')
  2172      await writeFileAsync(entry, `console.log('entry')`)
  2173      const onResolveSet = new Set()
  2174      const onLoadSet = new Set()
  2175      await esbuild.build({
  2176        stdin: {
  2177          resolveDir: testDir,
  2178          contents: `
  2179            import "foo%a"
  2180            import "foo%b"
  2181          `,
  2182        },
  2183        bundle: true,
  2184        write: false,
  2185        plugins: [{
  2186          name: 'plugin',
  2187          setup(build) {
  2188            build.onResolve({ filter: /.*/ }, args => {
  2189              onResolveSet.add({ path: args.path, suffix: args.suffix })
  2190              if (args.path.startsWith('foo%')) {
  2191                return {
  2192                  path: entry,
  2193                  suffix: '?' + args.path.slice(args.path.indexOf('%') + 1),
  2194                }
  2195              }
  2196            })
  2197            build.onLoad({ filter: /.*/ }, args => {
  2198              onLoadSet.add({ path: args.path, suffix: args.suffix })
  2199            })
  2200          },
  2201        }],
  2202      })
  2203      const order = (a, b) => {
  2204        a = JSON.stringify(a)
  2205        b = JSON.stringify(b)
  2206        return (a > b) - (a < b)
  2207      }
  2208      const observed = JSON.stringify({
  2209        onResolve: [...onResolveSet].sort(order),
  2210        onLoad: [...onLoadSet].sort(order),
  2211      }, null, 2)
  2212      const expected = JSON.stringify({
  2213        onResolve: [
  2214          { path: 'foo%a' },
  2215          { path: 'foo%b' },
  2216        ],
  2217        onLoad: [
  2218          { path: path.join(testDir, 'entry.js'), suffix: '?a' },
  2219          { path: path.join(testDir, 'entry.js'), suffix: '?b' },
  2220        ],
  2221      }, null, 2)
  2222      if (observed !== expected) throw new Error(`Observed ${observed}, expected ${expected}`)
  2223    },
  2224  
  2225    async onLoadWithInternalOnResolveAndQuerySuffix({ testDir, esbuild }) {
  2226      const entry = path.join(testDir, 'entry.js')
  2227      await writeFileAsync(entry, `console.log('entry')`)
  2228      const onResolveSet = new Set()
  2229      const onLoadSet = new Set()
  2230      await esbuild.build({
  2231        stdin: {
  2232          resolveDir: testDir,
  2233          contents: `
  2234            import "./entry?a"
  2235            import "./entry?b"
  2236          `,
  2237        },
  2238        bundle: true,
  2239        write: false,
  2240        plugins: [{
  2241          name: 'plugin',
  2242          setup(build) {
  2243            build.onResolve({ filter: /.*/ }, args => {
  2244              onResolveSet.add({ path: args.path, suffix: args.suffix })
  2245            })
  2246            build.onLoad({ filter: /.*/ }, args => {
  2247              onLoadSet.add({ path: args.path, suffix: args.suffix })
  2248            })
  2249          },
  2250        }],
  2251      })
  2252      const order = (a, b) => {
  2253        a = JSON.stringify(a)
  2254        b = JSON.stringify(b)
  2255        return (a > b) - (a < b)
  2256      }
  2257      const observed = JSON.stringify({
  2258        onResolve: [...onResolveSet].sort(order),
  2259        onLoad: [...onLoadSet].sort(order),
  2260      }, null, 2)
  2261      const expected = JSON.stringify({
  2262        onResolve: [
  2263          { path: './entry?a' },
  2264          { path: './entry?b' },
  2265        ],
  2266        onLoad: [
  2267          { path: path.join(testDir, 'entry.js'), suffix: '?a' },
  2268          { path: path.join(testDir, 'entry.js'), suffix: '?b' },
  2269        ],
  2270      }, null, 2)
  2271      if (observed !== expected) throw new Error(`Observed ${observed}, expected ${expected}`)
  2272    },
  2273  
  2274    async externalSideEffectsFalse({ esbuild }) {
  2275      const build = await esbuild.build({
  2276        entryPoints: ['entry'],
  2277        bundle: true,
  2278        write: false,
  2279        platform: 'node',
  2280        format: 'esm',
  2281        plugins: [{
  2282          name: 'plugin',
  2283          setup(build) {
  2284            build.onResolve({ filter: /.*/ }, args => {
  2285              if (args.importer === '') return { path: args.path, namespace: 'entry' }
  2286              else return { path: args.path, external: true, sideEffects: args.path !== 'noSideEffects' }
  2287            })
  2288            build.onLoad({ filter: /.*/, namespace: 'entry' }, () => {
  2289              return {
  2290                contents: `
  2291                  import "sideEffects"
  2292                  import "noSideEffects"
  2293                `,
  2294              }
  2295            })
  2296          },
  2297        }],
  2298      })
  2299      assert.strictEqual(build.outputFiles[0].text, `// entry:entry\nimport "sideEffects";\n`)
  2300    },
  2301  
  2302    async callResolveTooEarlyError({ esbuild }) {
  2303      try {
  2304        await esbuild.build({
  2305          entryPoints: [],
  2306          logLevel: 'silent',
  2307          plugins: [{
  2308            name: 'plugin',
  2309            async setup(build) {
  2310              await build.resolve('foo', { kind: 'entry-point' })
  2311            },
  2312          }],
  2313        })
  2314        throw new Error('Expected an error to be thrown')
  2315      } catch (e) {
  2316        assert(e.message.includes('Cannot call "resolve" before plugin setup has completed'), e.message)
  2317      }
  2318    },
  2319  
  2320    async callResolveTooLateError({ esbuild }) {
  2321      let resolve
  2322      await esbuild.build({
  2323        entryPoints: [],
  2324        plugins: [{
  2325          name: 'plugin',
  2326          async setup(build) {
  2327            resolve = build.resolve
  2328          },
  2329        }],
  2330      })
  2331      try {
  2332        const result = await resolve('foo', { kind: 'entry-point' })
  2333        console.log(result.errors)
  2334        throw new Error('Expected an error to be thrown')
  2335      } catch (e) {
  2336        assert(e.message.includes('Cannot call \"resolve\" on an inactive build'), e.message)
  2337      }
  2338    },
  2339  
  2340    async callResolveBadKindError({ esbuild }) {
  2341      try {
  2342        await esbuild.build({
  2343          entryPoints: ['entry'],
  2344          logLevel: 'silent',
  2345          plugins: [{
  2346            name: 'plugin',
  2347            async setup(build) {
  2348              build.onResolve({ filter: /^entry$/ }, async () => {
  2349                return await build.resolve('foo', { kind: 'what' })
  2350              })
  2351            },
  2352          }],
  2353        })
  2354        throw new Error('Expected an error to be thrown')
  2355      } catch (e) {
  2356        assert(e.message.includes('Invalid kind: "what"'), e.message)
  2357      }
  2358    },
  2359  
  2360    // Test that user options are taken into account
  2361    async callResolveUserOptionsExternal({ esbuild, testDir }) {
  2362      const result = await esbuild.build({
  2363        stdin: { contents: `import "foo"` },
  2364        write: false,
  2365        bundle: true,
  2366        external: ['bar'],
  2367        format: 'esm',
  2368        plugins: [{
  2369          name: 'plugin',
  2370          async setup(build) {
  2371            build.onResolve({ filter: /^foo$/ }, async () => {
  2372              const result = await build.resolve('bar', {
  2373                resolveDir: testDir,
  2374                kind: 'import-statement',
  2375              })
  2376              assert(result.external)
  2377              return { path: 'baz', external: true }
  2378            })
  2379          },
  2380        }],
  2381      })
  2382      assert.strictEqual(result.outputFiles[0].text, `// <stdin>\nimport "baz";\n`)
  2383    },
  2384  
  2385    async callResolveBuiltInHandler({ esbuild, testDir }) {
  2386      const srcDir = path.join(testDir, 'src')
  2387      const input = path.join(srcDir, 'input.js')
  2388      await mkdirAsync(srcDir, { recursive: true })
  2389      await writeFileAsync(input, `console.log(123)`)
  2390      const result = await esbuild.build({
  2391        entryPoints: ['entry'],
  2392        write: false,
  2393        plugins: [{
  2394          name: 'plugin',
  2395          setup(build) {
  2396            build.onResolve({ filter: /^entry$/ }, async () => {
  2397              return await build.resolve('./' + path.basename(input), {
  2398                resolveDir: srcDir,
  2399                kind: 'import-statement',
  2400              })
  2401            })
  2402          },
  2403        }],
  2404      })
  2405      assert.strictEqual(result.outputFiles[0].text, `console.log(123);\n`)
  2406    },
  2407  
  2408    async callResolvePluginHandler({ esbuild, testDir }) {
  2409      const srcDir = path.join(testDir, 'src')
  2410      const input = path.join(srcDir, 'input.js')
  2411      await mkdirAsync(srcDir, { recursive: true })
  2412      await writeFileAsync(input, `console.log(123)`)
  2413      const result = await esbuild.build({
  2414        entryPoints: ['entry'],
  2415        write: false,
  2416        plugins: [{
  2417          name: 'plugin',
  2418          setup(build) {
  2419            build.onResolve({ filter: /^entry$/ }, async () => {
  2420              return await build.resolve('foo', {
  2421                importer: 'foo-importer',
  2422                namespace: 'foo-namespace',
  2423                resolveDir: 'foo-resolveDir',
  2424                pluginData: 'foo-pluginData',
  2425                kind: 'dynamic-import',
  2426              })
  2427            })
  2428            build.onResolve({ filter: /^foo$/ }, async (args) => {
  2429              assert.strictEqual(args.path, 'foo')
  2430              assert.strictEqual(args.importer, 'foo-importer')
  2431              assert.strictEqual(args.namespace, 'foo-namespace')
  2432              assert.strictEqual(args.resolveDir, path.join(process.cwd(), 'foo-resolveDir'))
  2433              assert.strictEqual(args.pluginData, 'foo-pluginData')
  2434              assert.strictEqual(args.kind, 'dynamic-import')
  2435              return { path: input }
  2436            })
  2437          },
  2438        }],
  2439      })
  2440      assert.strictEqual(result.outputFiles[0].text, `console.log(123);\n`)
  2441    },
  2442  
  2443    async injectWithVirtualFile({ esbuild, testDir }) {
  2444      const input = path.join(testDir, 'input.js')
  2445      await writeFileAsync(input, `console.log(test)`)
  2446      const result = await esbuild.build({
  2447        entryPoints: [input],
  2448        write: false,
  2449        inject: ['plugin-file'],
  2450        plugins: [{
  2451          name: 'plugin',
  2452          setup(build) {
  2453            build.onResolve({ filter: /^plugin-file$/ }, () => {
  2454              return { namespace: 'plugin', path: 'path' }
  2455            })
  2456            build.onLoad({ filter: /^path$/, namespace: 'plugin' }, () => {
  2457              return { contents: `export let test = 'injected'` }
  2458            })
  2459          },
  2460        }],
  2461      })
  2462      assert.strictEqual(result.outputFiles[0].text, `var test2 = "injected";\nconsole.log(test2);\n`)
  2463    },
  2464  
  2465    async tsconfigRawAffectsVirtualFiles({ esbuild }) {
  2466      const result = await esbuild.build({
  2467        entryPoints: ['entry'],
  2468        tsconfigRaw: {
  2469          compilerOptions: {
  2470            jsxFactory: 'jay_ess_ex',
  2471          },
  2472        },
  2473        write: false,
  2474        plugins: [{
  2475          name: 'name',
  2476          setup(build) {
  2477            build.onResolve({ filter: /entry/ }, () => {
  2478              return { path: 'foo', namespace: 'ns' }
  2479            })
  2480            build.onLoad({ filter: /foo/ }, () => {
  2481              return { loader: 'tsx', contents: 'console.log(<div/>)' }
  2482            })
  2483          },
  2484        }],
  2485      })
  2486      assert.strictEqual(result.outputFiles[0].text, 'console.log(/* @__PURE__ */ jay_ess_ex("div", null));\n')
  2487    },
  2488  
  2489    async importAttributesOnResolve({ esbuild }) {
  2490      const result = await esbuild.build({
  2491        entryPoints: ['entry'],
  2492        bundle: true,
  2493        format: 'esm',
  2494        charset: 'utf8',
  2495        write: false,
  2496        plugins: [{
  2497          name: 'name',
  2498          setup(build) {
  2499            build.onResolve({ filter: /.*/ }, args => {
  2500              if (args.with.type === 'cheese') return { path: 'cheese', namespace: 'ns' }
  2501              if (args.with.pizza === 'true') return { path: 'pizza', namespace: 'ns' }
  2502              return { path: args.path, namespace: 'ns' }
  2503            })
  2504            build.onLoad({ filter: /.*/ }, args => {
  2505              const entry = `
  2506                import a from 'foo' with { type: 'cheese' }
  2507                import b from 'foo' with { pizza: 'true' }
  2508                console.log(a, b)
  2509              `
  2510              if (args.path === 'entry') return { contents: entry }
  2511              if (args.path === 'cheese') return { contents: `export default "🧀"` }
  2512              if (args.path === 'pizza') return { contents: `export default "🍕"` }
  2513            })
  2514          },
  2515        }],
  2516      })
  2517      assert.strictEqual(result.outputFiles[0].text, `// ns:cheese
  2518  var cheese_default = "🧀";
  2519  
  2520  // ns:pizza
  2521  var pizza_default = "🍕";
  2522  
  2523  // ns:entry
  2524  console.log(cheese_default, pizza_default);
  2525  `)
  2526    },
  2527  
  2528    async importAttributesOnLoad({ esbuild }) {
  2529      const result = await esbuild.build({
  2530        entryPoints: ['entry'],
  2531        bundle: true,
  2532        format: 'esm',
  2533        charset: 'utf8',
  2534        write: false,
  2535        plugins: [{
  2536          name: 'name',
  2537          setup(build) {
  2538            build.onResolve({ filter: /.*/ }, args => {
  2539              return { path: args.path, namespace: 'ns' }
  2540            })
  2541            build.onLoad({ filter: /.*/ }, args => {
  2542              const entry = `
  2543                import a from 'foo' with { type: 'cheese' }
  2544                import b from 'foo' with { pizza: 'true' }
  2545                console.log(a, b)
  2546              `
  2547              if (args.path === 'entry') return { contents: entry }
  2548              if (args.with.type === 'cheese') return { contents: `export default "🧀"` }
  2549              if (args.with.pizza === 'true') return { contents: `export default "🍕"` }
  2550            })
  2551          },
  2552        }],
  2553      })
  2554      assert.strictEqual(result.outputFiles[0].text, `// ns:foo with { type: 'cheese' }
  2555  var foo_default = "🧀";
  2556  
  2557  // ns:foo with { pizza: 'true' }
  2558  var foo_default2 = "🍕";
  2559  
  2560  // ns:entry
  2561  console.log(foo_default, foo_default2);
  2562  `)
  2563    },
  2564  
  2565    async importAttributesResolve({ esbuild }) {
  2566      const log = []
  2567      await esbuild.build({
  2568        entryPoints: [],
  2569        bundle: true,
  2570        format: 'esm',
  2571        charset: 'utf8',
  2572        write: false,
  2573        plugins: [{
  2574          name: 'name',
  2575          setup(build) {
  2576            build.onResolve({ filter: /.*/ }, args => {
  2577              log.push(args)
  2578              return { external: true }
  2579            })
  2580            build.onStart(() => {
  2581              build.resolve('foo', {
  2582                kind: 'require-call',
  2583                with: { type: 'cheese' },
  2584              })
  2585              build.resolve('bar', {
  2586                kind: 'import-statement',
  2587                with: { pizza: 'true' },
  2588              })
  2589            })
  2590          },
  2591        }],
  2592      })
  2593      assert.strictEqual(log.length, 2)
  2594      assert.strictEqual(log[0].with.type, 'cheese')
  2595      assert.strictEqual(log[1].with.pizza, 'true')
  2596    },
  2597  
  2598    async internalCrashIssue3634({ esbuild }) {
  2599      await esbuild.build({
  2600        entryPoints: [],
  2601        bundle: true,
  2602        plugins: [{
  2603          name: 'abc',
  2604          setup(build) {
  2605            build.onStart(async () => {
  2606              const result = await build.resolve('/foo', {
  2607                kind: 'require-call',
  2608                resolveDir: 'bar',
  2609              })
  2610              assert.strictEqual(result.errors.length, 1)
  2611            })
  2612          }
  2613        }],
  2614      })
  2615    },
  2616  }
  2617  
  2618  const makeRebuildUntilPlugin = () => {
  2619    let onEnd
  2620  
  2621    return {
  2622      rebuildUntil: (mutator, condition) => new Promise((resolve, reject) => {
  2623        let timeout = setTimeout(() => reject(new Error('Timeout after 30 seconds')), 30 * 1000)
  2624        onEnd = result => {
  2625          try { if (result && condition(result)) clearTimeout(timeout), resolve(result) }
  2626          catch (e) { clearTimeout(timeout), reject(e) }
  2627        }
  2628        mutator()
  2629      }),
  2630  
  2631      plugin: {
  2632        name: 'rebuildUntil',
  2633        setup(build) {
  2634          build.onEnd(result => onEnd && onEnd(result))
  2635        },
  2636      },
  2637    }
  2638  }
  2639  
  2640  // These tests have to run synchronously
  2641  let syncTests = {
  2642    async pluginWithWatchMode({ esbuild, testDir }) {
  2643      const srcDir = path.join(testDir, 'src')
  2644      const outfile = path.join(testDir, 'out.js')
  2645      const input = path.join(srcDir, 'in.js')
  2646      const example = path.join(srcDir, 'example.js')
  2647      await mkdirAsync(srcDir, { recursive: true })
  2648      await writeFileAsync(input, `import {x} from "./example.js"; exports.x = x`)
  2649      await writeFileAsync(example, `export let x = 1`)
  2650  
  2651      const { rebuildUntil, plugin } = makeRebuildUntilPlugin()
  2652      const ctx = await esbuild.context({
  2653        entryPoints: [input],
  2654        outfile,
  2655        format: 'cjs',
  2656        logLevel: 'silent',
  2657        bundle: true,
  2658        plugins: [
  2659          {
  2660            name: 'some-plugin',
  2661            setup(build) {
  2662              build.onLoad({ filter: /example\.js$/ }, async (args) => {
  2663                const contents = await fs.promises.readFile(args.path, 'utf8')
  2664                return { contents }
  2665              })
  2666            },
  2667          },
  2668          plugin,
  2669        ],
  2670      })
  2671  
  2672      try {
  2673        // First build
  2674        const result = await ctx.rebuild()
  2675        let code = await readFileAsync(outfile, 'utf8')
  2676        let exports = {}
  2677        new Function('exports', code)(exports)
  2678        assert.strictEqual(result.outputFiles, void 0)
  2679        assert.strictEqual(exports.x, 1)
  2680        await ctx.watch()
  2681  
  2682        // First rebuild: edit
  2683        {
  2684          const result2 = await rebuildUntil(
  2685            () => setTimeout(() => writeFileAtomic(example, `export let x = 2`), 250),
  2686            () => fs.readFileSync(outfile, 'utf8') !== code,
  2687          )
  2688          code = await readFileAsync(outfile, 'utf8')
  2689          exports = {}
  2690          new Function('exports', code)(exports)
  2691          assert.strictEqual(result2.outputFiles, void 0)
  2692          assert.strictEqual(exports.x, 2)
  2693        }
  2694      } finally {
  2695        await ctx.dispose()
  2696      }
  2697    },
  2698  
  2699    async pluginWithWatchFiles({ esbuild, testDir }) {
  2700      const srcDir = path.join(testDir, 'src')
  2701      const otherDir = path.join(testDir, 'other')
  2702      const outfile = path.join(testDir, 'out.js')
  2703      const input = path.join(srcDir, 'in.js')
  2704      const example = path.join(otherDir, 'example.js')
  2705      await mkdirAsync(srcDir, { recursive: true })
  2706      await mkdirAsync(otherDir, { recursive: true })
  2707      await writeFileAsync(input, `import {x} from "<virtual>"; exports.x = x`)
  2708      await writeFileAsync(example, `export let x = 1`)
  2709  
  2710      const { rebuildUntil, plugin } = makeRebuildUntilPlugin()
  2711      const ctx = await esbuild.context({
  2712        entryPoints: [input],
  2713        outfile,
  2714        format: 'cjs',
  2715        logLevel: 'silent',
  2716        bundle: true,
  2717        plugins: [
  2718          {
  2719            name: 'some-plugin',
  2720            setup(build) {
  2721              build.onResolve({ filter: /^<virtual>$/ }, args => {
  2722                return { path: args.path, namespace: 'ns' }
  2723              })
  2724              build.onLoad({ filter: /^<virtual>$/, namespace: 'ns' }, async (args) => {
  2725                const contents = await fs.promises.readFile(example, 'utf8')
  2726                return { contents, watchFiles: [example] }
  2727              })
  2728            },
  2729          },
  2730          plugin,
  2731        ],
  2732      })
  2733  
  2734      try {
  2735        // First build
  2736        const result = await ctx.rebuild()
  2737        let code = await readFileAsync(outfile, 'utf8')
  2738        let exports = {}
  2739        new Function('exports', code)(exports)
  2740        assert.strictEqual(result.outputFiles, void 0)
  2741        assert.strictEqual(exports.x, 1)
  2742        await ctx.watch()
  2743  
  2744        // First rebuild: edit
  2745        {
  2746          const result2 = await rebuildUntil(
  2747            () => setTimeout(() => writeFileAtomic(example, `export let x = 2`), 250),
  2748            () => fs.readFileSync(outfile, 'utf8') !== code,
  2749          )
  2750          code = await readFileAsync(outfile, 'utf8')
  2751          exports = {}
  2752          new Function('exports', code)(exports)
  2753          assert.strictEqual(result2.outputFiles, void 0)
  2754          assert.strictEqual(exports.x, 2)
  2755        }
  2756      } finally {
  2757        await ctx.dispose()
  2758      }
  2759    },
  2760  
  2761    async pluginWithWatchDir({ esbuild, testDir }) {
  2762      const srcDir = path.join(testDir, 'src')
  2763      const otherDir = path.join(testDir, 'other')
  2764      const outfile = path.join(testDir, 'out.js')
  2765      const input = path.join(srcDir, 'in.js')
  2766      await mkdirAsync(srcDir, { recursive: true })
  2767      await mkdirAsync(otherDir, { recursive: true })
  2768      await writeFileAsync(input, `import {x} from "<virtual>"; exports.x = x`)
  2769  
  2770      const { rebuildUntil, plugin } = makeRebuildUntilPlugin()
  2771      const ctx = await esbuild.context({
  2772        entryPoints: [input],
  2773        outfile,
  2774        format: 'cjs',
  2775        logLevel: 'silent',
  2776        bundle: true,
  2777        plugins: [
  2778          {
  2779            name: 'some-plugin',
  2780            setup(build) {
  2781              build.onResolve({ filter: /^<virtual>$/ }, args => {
  2782                return { path: args.path, namespace: 'ns' }
  2783              })
  2784              build.onLoad({ filter: /^<virtual>$/, namespace: 'ns' }, async () => {
  2785                const entries = await fs.promises.readdir(otherDir, 'utf8')
  2786                return { contents: `export let x = ${entries.length}`, watchDirs: [otherDir] }
  2787              })
  2788            },
  2789          },
  2790          plugin,
  2791        ],
  2792      })
  2793  
  2794      try {
  2795        const result = await ctx.rebuild()
  2796        let code = await readFileAsync(outfile, 'utf8')
  2797        let exports = {}
  2798        new Function('exports', code)(exports)
  2799        assert.strictEqual(result.outputFiles, void 0)
  2800        assert.strictEqual(exports.x, 0)
  2801        await ctx.watch()
  2802  
  2803        // First rebuild: edit
  2804        {
  2805          const result2 = await rebuildUntil(
  2806            () => setTimeout(() => writeFileAtomic(path.join(otherDir, 'file.txt'), `...`), 250),
  2807            () => fs.readFileSync(outfile, 'utf8') !== code,
  2808          )
  2809          code = await readFileAsync(outfile, 'utf8')
  2810          exports = {}
  2811          new Function('exports', code)(exports)
  2812          assert.strictEqual(result2.outputFiles, void 0)
  2813          assert.strictEqual(exports.x, 1)
  2814        }
  2815      } finally {
  2816        await ctx.dispose()
  2817      }
  2818    },
  2819  
  2820    async onStartCallback({ esbuild, testDir }) {
  2821      const input = path.join(testDir, 'in.js')
  2822      await writeFileAsync(input, ``)
  2823  
  2824      let onStartTimes = 0
  2825      let errorToThrow = null
  2826      let valueToReturn = null
  2827  
  2828      const ctx = await esbuild.context({
  2829        entryPoints: [input],
  2830        write: false,
  2831        logLevel: 'silent',
  2832        plugins: [
  2833          {
  2834            name: 'some-plugin',
  2835            setup(build) {
  2836              build.onStart(() => {
  2837                if (errorToThrow) throw errorToThrow
  2838                if (valueToReturn) return valueToReturn
  2839                onStartTimes++
  2840              })
  2841            },
  2842          },
  2843        ],
  2844      })
  2845      try {
  2846        assert.strictEqual(onStartTimes, 0)
  2847  
  2848        await ctx.rebuild()
  2849        assert.strictEqual(onStartTimes, 1)
  2850  
  2851        await ctx.rebuild()
  2852        assert.strictEqual(onStartTimes, 2)
  2853  
  2854        errorToThrow = new Error('throw test')
  2855        try {
  2856          await ctx.rebuild()
  2857          throw new Error('Expected an error to be thrown')
  2858        } catch (e) {
  2859          assert.notStrictEqual(e.errors, void 0)
  2860          assert.strictEqual(e.errors.length, 1)
  2861          assert.strictEqual(e.errors[0].pluginName, 'some-plugin')
  2862          assert.strictEqual(e.errors[0].text, 'throw test')
  2863        } finally {
  2864          errorToThrow = null
  2865        }
  2866  
  2867        valueToReturn = { errors: [{ text: 'return test', location: { file: 'foo.js', line: 2 } }] }
  2868        try {
  2869          await ctx.rebuild()
  2870          throw new Error('Expected an error to be thrown')
  2871        } catch (e) {
  2872          assert.notStrictEqual(e.errors, void 0)
  2873          assert.strictEqual(e.errors.length, 1)
  2874          assert.strictEqual(e.errors[0].pluginName, 'some-plugin')
  2875          assert.strictEqual(e.errors[0].text, 'return test')
  2876          assert.notStrictEqual(e.errors[0].location, null)
  2877          assert.strictEqual(e.errors[0].location.file, 'foo.js')
  2878          assert.strictEqual(e.errors[0].location.line, 2)
  2879        } finally {
  2880          valueToReturn = null
  2881        }
  2882  
  2883        assert.strictEqual(onStartTimes, 2)
  2884        valueToReturn = new Promise(resolve => setTimeout(() => {
  2885          onStartTimes++
  2886          resolve()
  2887        }, 500))
  2888        await ctx.rebuild()
  2889        assert.strictEqual(onStartTimes, 3)
  2890        valueToReturn = null
  2891      } finally {
  2892        await ctx.dispose()
  2893      }
  2894    },
  2895  
  2896    async onStartCallbackWithDelay({ esbuild }) {
  2897      await esbuild.build({
  2898        entryPoints: ['foo'],
  2899        write: false,
  2900        logLevel: 'silent',
  2901        plugins: [
  2902          {
  2903            name: 'some-plugin',
  2904            setup(build) {
  2905              let isStarted = false
  2906              build.onStart(async () => {
  2907                await new Promise(r => setTimeout(r, 1000))
  2908                isStarted = true
  2909              })
  2910  
  2911              // Verify that "onStart" is finished before "onResolve" and "onLoad" run
  2912              build.onResolve({ filter: /foo/ }, () => {
  2913                assert.strictEqual(isStarted, true)
  2914                return { path: 'foo', namespace: 'foo' }
  2915              })
  2916              build.onLoad({ filter: /foo/ }, () => {
  2917                assert.strictEqual(isStarted, true)
  2918                return { contents: '' }
  2919              })
  2920            },
  2921          },
  2922        ],
  2923      })
  2924    },
  2925  
  2926    async onEndCallback({ esbuild, testDir }) {
  2927      const input = path.join(testDir, 'in.js')
  2928      await writeFileAsync(input, ``)
  2929  
  2930      let onEndTimes = 0
  2931      let errorToThrow = null
  2932      let valueToReturn = null
  2933      let mutateFn = null
  2934  
  2935      const ctx = await esbuild.context({
  2936        entryPoints: [input],
  2937        write: false,
  2938        logLevel: 'silent',
  2939        plugins: [
  2940          {
  2941            name: 'some-plugin',
  2942            setup(build) {
  2943              build.onEnd(result => {
  2944                if (errorToThrow) throw errorToThrow
  2945                if (valueToReturn) return valueToReturn
  2946                if (mutateFn) mutateFn(result)
  2947                onEndTimes++
  2948              })
  2949            },
  2950          },
  2951        ],
  2952      })
  2953      try {
  2954        assert.strictEqual(onEndTimes, 0)
  2955  
  2956        await ctx.rebuild()
  2957        assert.strictEqual(onEndTimes, 1)
  2958  
  2959        await ctx.rebuild()
  2960        assert.strictEqual(onEndTimes, 2)
  2961  
  2962        errorToThrow = new Error('throw test')
  2963        try {
  2964          await ctx.rebuild()
  2965          throw new Error('Expected an error to be thrown')
  2966        } catch (e) {
  2967          assert.notStrictEqual(e.errors, void 0)
  2968          assert.strictEqual(e.errors.length, 1)
  2969          assert.strictEqual(e.errors[0].pluginName, 'some-plugin')
  2970          assert.strictEqual(e.errors[0].text, 'throw test')
  2971        } finally {
  2972          errorToThrow = null
  2973        }
  2974  
  2975        assert.strictEqual(onEndTimes, 2)
  2976        valueToReturn = new Promise(resolve => setTimeout(() => {
  2977          onEndTimes++
  2978          resolve()
  2979        }, 500))
  2980        await ctx.rebuild()
  2981        assert.strictEqual(onEndTimes, 3)
  2982        valueToReturn = null
  2983  
  2984        mutateFn = result => result.warnings.push(true)
  2985        const result2 = await ctx.rebuild()
  2986        assert.deepStrictEqual(result2.warnings, [true])
  2987        mutateFn = () => { }
  2988        const result3 = await ctx.rebuild()
  2989        assert.deepStrictEqual(result3.warnings, [])
  2990  
  2991        // Adding an error this way does not fail the build (we don't scan the build object for modifications)
  2992        mutateFn = result => result.errors.push({ text: 'test failure' })
  2993        await ctx.rebuild()
  2994        mutateFn = () => { }
  2995  
  2996        // Instead, plugins should return any additional errors from the "onEnd" callback itself
  2997        valueToReturn = { errors: [{ text: 'test failure 2' }] }
  2998        try {
  2999          await ctx.rebuild()
  3000          throw new Error('Expected an error to be thrown')
  3001        } catch (e) {
  3002          assert.notStrictEqual(e.errors, void 0)
  3003          assert.strictEqual(e.errors.length, 1)
  3004          assert.strictEqual(e.errors[0].pluginName, 'some-plugin')
  3005          assert.strictEqual(e.errors[0].text, 'test failure 2')
  3006          assert.strictEqual(e.errors[0].location, null)
  3007        }
  3008      } finally {
  3009        await ctx.dispose()
  3010      }
  3011    },
  3012  
  3013    async onEndCallbackMutateContents({ esbuild, testDir }) {
  3014      const input = path.join(testDir, 'in.js')
  3015      await writeFileAsync(input, `x=y`)
  3016  
  3017      let onEndTimes = 0
  3018  
  3019      const result = await esbuild.build({
  3020        entryPoints: [input],
  3021        write: false,
  3022        plugins: [
  3023          {
  3024            name: 'some-plugin',
  3025            setup(build) {
  3026              build.onEnd(result => {
  3027                onEndTimes++
  3028  
  3029                assert.deepStrictEqual(result.outputFiles[0].contents, new Uint8Array([120, 32, 61, 32, 121, 59, 10]))
  3030                assert.deepStrictEqual(result.outputFiles[0].text, 'x = y;\n')
  3031  
  3032                result.outputFiles[0].contents = new Uint8Array([120, 61, 121])
  3033                assert.deepStrictEqual(result.outputFiles[0].contents, new Uint8Array([120, 61, 121]))
  3034                assert.deepStrictEqual(result.outputFiles[0].text, 'x=y')
  3035  
  3036                result.outputFiles[0].contents = new Uint8Array([121, 61, 120])
  3037                assert.deepStrictEqual(result.outputFiles[0].contents, new Uint8Array([121, 61, 120]))
  3038                assert.deepStrictEqual(result.outputFiles[0].text, 'y=x')
  3039              })
  3040            },
  3041          },
  3042        ],
  3043      })
  3044  
  3045      assert.deepStrictEqual(onEndTimes, 1)
  3046      assert.deepStrictEqual(result.outputFiles.length, 1)
  3047      assert.deepStrictEqual(result.outputFiles[0].contents, new Uint8Array([121, 61, 120]))
  3048      assert.deepStrictEqual(result.outputFiles[0].text, 'y=x')
  3049    },
  3050  
  3051    async onStartOnEndWatchMode({ esbuild, testDir }) {
  3052      const srcDir = path.join(testDir, 'src')
  3053      const outfile = path.join(testDir, 'out.js')
  3054      const input = path.join(srcDir, 'in.js')
  3055      const example = path.join(srcDir, 'example.js')
  3056      await mkdirAsync(srcDir, { recursive: true })
  3057      await writeFileAsync(input, `import {x} from "./example.js"; exports.x = x`)
  3058      await writeFileAsync(example, `export let x = 1`)
  3059  
  3060      let onStartCalls = 0
  3061      let onEndCalls = 0
  3062  
  3063      const { rebuildUntil, plugin } = makeRebuildUntilPlugin()
  3064      const ctx = await esbuild.context({
  3065        entryPoints: [input],
  3066        outfile,
  3067        format: 'cjs',
  3068        logLevel: 'silent',
  3069        bundle: true,
  3070        metafile: true,
  3071        plugins: [
  3072          {
  3073            name: 'some-plugin',
  3074            setup(build) {
  3075              build.onStart(() => {
  3076                onStartCalls++
  3077              })
  3078              build.onEnd(result => {
  3079                assert.notStrictEqual(result.metafile, void 0)
  3080                onEndCalls++
  3081              })
  3082  
  3083              build.onLoad({ filter: /example\.js$/ }, async (args) => {
  3084                const contents = await fs.promises.readFile(args.path, 'utf8')
  3085                return { contents }
  3086              })
  3087            },
  3088          },
  3089          plugin,
  3090        ],
  3091      })
  3092  
  3093      try {
  3094        assert.strictEqual(onStartCalls, 0)
  3095        assert.strictEqual(onEndCalls, 0)
  3096  
  3097        const result = await rebuildUntil(
  3098          () => ctx.watch(),
  3099          () => true,
  3100        )
  3101  
  3102        assert.notStrictEqual(onStartCalls, 0)
  3103        assert.notStrictEqual(onEndCalls, 0)
  3104  
  3105        const onStartAfterWatch = onStartCalls
  3106        const onEndAfterWatch = onEndCalls
  3107  
  3108        let code = await readFileAsync(outfile, 'utf8')
  3109        let exports = {}
  3110        new Function('exports', code)(exports)
  3111        assert.strictEqual(result.outputFiles, void 0)
  3112        assert.strictEqual(exports.x, 1)
  3113  
  3114        // First rebuild: edit
  3115        {
  3116          const result2 = await rebuildUntil(
  3117            () => setTimeout(() => writeFileAtomic(example, `export let x = 2`), 250),
  3118            () => fs.readFileSync(outfile, 'utf8') !== code,
  3119          )
  3120          code = await readFileAsync(outfile, 'utf8')
  3121          exports = {}
  3122          new Function('exports', code)(exports)
  3123          assert.strictEqual(result2.outputFiles, void 0)
  3124          assert.strictEqual(exports.x, 2)
  3125        }
  3126  
  3127        assert.notStrictEqual(onStartCalls, onStartAfterWatch)
  3128        assert.notStrictEqual(onEndCalls, onEndAfterWatch)
  3129      } finally {
  3130        await ctx.dispose()
  3131      }
  3132    },
  3133  
  3134    async pluginServeWriteTrueOnEnd({ esbuild, testDir }) {
  3135      const outfile = path.join(testDir, 'out.js')
  3136      const input = path.join(testDir, 'in.js')
  3137      await writeFileAsync(input, `console.log(1+2`)
  3138  
  3139      let latestResult
  3140      const ctx = await esbuild.context({
  3141        entryPoints: [input],
  3142        outfile,
  3143        logLevel: 'silent',
  3144        plugins: [
  3145          {
  3146            name: 'some-plugin',
  3147            setup(build) {
  3148              build.onEnd(result => {
  3149                latestResult = result
  3150              })
  3151            },
  3152          },
  3153        ],
  3154      })
  3155  
  3156      try {
  3157        const server = await ctx.serve()
  3158  
  3159        // Fetch once
  3160        try {
  3161          await fetch(server.host, server.port, '/out.js')
  3162          throw new Error('Expected an error to be thrown')
  3163        } catch (err) {
  3164          assert.strictEqual(err.statusCode, 503)
  3165        }
  3166        assert.strictEqual(latestResult.errors.length, 1)
  3167        assert.strictEqual(latestResult.errors[0].text, 'Expected ")" but found end of file')
  3168  
  3169        // Fix the error
  3170        await writeFileAsync(input, `console.log(1+2)`)
  3171  
  3172        // Fetch again
  3173        const buffer = await fetchUntilSuccessOrTimeout(server.host, server.port, '/out.js')
  3174        assert.strictEqual(buffer.toString(), 'console.log(1 + 2);\n')
  3175        assert.strictEqual(latestResult.errors.length, 0)
  3176        assert.strictEqual(latestResult.outputFiles, undefined)
  3177        assert.strictEqual(fs.readFileSync(outfile, 'utf8'), 'console.log(1 + 2);\n')
  3178      } finally {
  3179        await ctx.dispose()
  3180      }
  3181    },
  3182  
  3183    async pluginServeWriteFalseOnEnd({ esbuild, testDir }) {
  3184      const outfile = path.join(testDir, 'out.js')
  3185      const input = path.join(testDir, 'in.js')
  3186      await writeFileAsync(input, `console.log(1+2`)
  3187  
  3188      let latestResult
  3189      const ctx = await esbuild.context({
  3190        entryPoints: [input],
  3191        outfile,
  3192        logLevel: 'silent',
  3193        write: false,
  3194        plugins: [
  3195          {
  3196            name: 'some-plugin',
  3197            setup(build) {
  3198              build.onEnd(result => {
  3199                latestResult = result
  3200              })
  3201            },
  3202          },
  3203        ],
  3204      })
  3205  
  3206      try {
  3207        const server = await ctx.serve()
  3208  
  3209        // Fetch once
  3210        try {
  3211          await fetch(server.host, server.port, '/out.js')
  3212          throw new Error('Expected an error to be thrown')
  3213        } catch (err) {
  3214          assert.strictEqual(err.statusCode, 503)
  3215        }
  3216        assert.strictEqual(latestResult.errors.length, 1)
  3217        assert.strictEqual(latestResult.errors[0].text, 'Expected ")" but found end of file')
  3218        assert.strictEqual(latestResult.outputFiles.length, 0)
  3219  
  3220        // Fix the error
  3221        await writeFileAsync(input, `console.log(1+2)`)
  3222  
  3223        // Fetch again
  3224        const buffer = await fetchUntilSuccessOrTimeout(server.host, server.port, '/out.js')
  3225        assert.strictEqual(buffer.toString(), 'console.log(1 + 2);\n')
  3226        assert.strictEqual(latestResult.errors.length, 0)
  3227        assert.strictEqual(latestResult.outputFiles.length, 1)
  3228        assert.strictEqual(latestResult.outputFiles[0].text, 'console.log(1 + 2);\n')
  3229        assert.strictEqual(fs.existsSync(outfile), false)
  3230      } finally {
  3231        await ctx.dispose()
  3232      }
  3233    },
  3234  
  3235    async pluginOnDisposeAfterSuccessfulBuild({ esbuild, testDir }) {
  3236      const input = path.join(testDir, 'in.js')
  3237      await writeFileAsync(input, `1+2`)
  3238  
  3239      let onDisposeCalled
  3240      let onDisposePromise = new Promise(resolve => onDisposeCalled = resolve)
  3241      await esbuild.build({
  3242        entryPoints: [input],
  3243        write: false,
  3244        plugins: [{
  3245          name: 'x', setup(build) {
  3246            build.onDispose(onDisposeCalled)
  3247          }
  3248        }]
  3249      })
  3250      await onDisposePromise
  3251    },
  3252  
  3253    async pluginOnDisposeAfterFailedBuild({ esbuild, testDir }) {
  3254      const input = path.join(testDir, 'in.js')
  3255  
  3256      let onDisposeCalled
  3257      let onDisposePromise = new Promise(resolve => onDisposeCalled = resolve)
  3258      try {
  3259        await esbuild.build({
  3260          entryPoints: [input],
  3261          write: false,
  3262          logLevel: 'silent',
  3263          plugins: [{
  3264            name: 'x', setup(build) {
  3265              build.onDispose(onDisposeCalled)
  3266            }
  3267          }]
  3268        })
  3269        throw new Error('Expected an error to be thrown')
  3270      } catch (err) {
  3271        if (!err.errors || err.errors.length !== 1)
  3272          throw err
  3273      }
  3274      await onDisposePromise
  3275    },
  3276  
  3277    async pluginOnDisposeWithUnusedContext({ esbuild, testDir }) {
  3278      const input = path.join(testDir, 'in.js')
  3279      await writeFileAsync(input, `1+2`)
  3280  
  3281      let onDisposeCalled
  3282      let onDisposePromise = new Promise(resolve => onDisposeCalled = resolve)
  3283      let ctx = await esbuild.context({
  3284        entryPoints: [input],
  3285        write: false,
  3286        plugins: [{
  3287          name: 'x', setup(build) {
  3288            build.onDispose(onDisposeCalled)
  3289          }
  3290        }]
  3291      })
  3292      await ctx.dispose()
  3293      await onDisposePromise
  3294    },
  3295  
  3296    async pluginOnDisposeWithRebuild({ esbuild, testDir }) {
  3297      const input = path.join(testDir, 'in.js')
  3298      await writeFileAsync(input, `1+2`)
  3299  
  3300      let onDisposeCalled
  3301      let onDisposeWasCalled = false
  3302      let onDisposePromise = new Promise(resolve => {
  3303        onDisposeCalled = () => {
  3304          onDisposeWasCalled = true
  3305          resolve()
  3306        }
  3307      })
  3308      let ctx = await esbuild.context({
  3309        entryPoints: [input],
  3310        write: false,
  3311        plugins: [{
  3312          name: 'x', setup(build) {
  3313            build.onDispose(onDisposeCalled)
  3314          }
  3315        }]
  3316      })
  3317  
  3318      let result = await ctx.rebuild()
  3319      assert.strictEqual(result.outputFiles.length, 1)
  3320      assert.strictEqual(onDisposeWasCalled, false)
  3321  
  3322      await ctx.dispose()
  3323      await onDisposePromise
  3324      assert.strictEqual(onDisposeWasCalled, true)
  3325    },
  3326  }
  3327  
  3328  async function main() {
  3329    const esbuild = installForTests()
  3330  
  3331    // Create a fresh test directory
  3332    removeRecursiveSync(rootTestDir)
  3333    fs.mkdirSync(rootTestDir)
  3334  
  3335    // Time out these tests after 5 minutes. This exists to help debug test hangs in CI.
  3336    let minutes = 5
  3337    let timeout = setTimeout(() => {
  3338      console.error(`❌ plugin tests timed out after ${minutes} minutes, exiting...`)
  3339      process.exit(1)
  3340    }, minutes * 60 * 1000)
  3341  
  3342    // Run all tests concurrently
  3343    const runTest = async ([name, fn]) => {
  3344      let testDir = path.join(rootTestDir, name)
  3345      try {
  3346        await mkdirAsync(testDir)
  3347        await fn({ esbuild, testDir })
  3348        removeRecursiveSync(testDir)
  3349        return true
  3350      } catch (e) {
  3351        console.error(`❌ ${name}: ${e && e.message || e}`)
  3352        return false
  3353      }
  3354    }
  3355    const tests = Object.entries(pluginTests)
  3356    let allTestsPassed = (await Promise.all(tests.map(runTest))).every(success => success)
  3357  
  3358    for (let test of Object.entries(syncTests)) {
  3359      if (!await runTest(test)) {
  3360        allTestsPassed = false
  3361      }
  3362    }
  3363  
  3364    if (!allTestsPassed) {
  3365      console.error(`❌ plugin tests failed`)
  3366      process.exit(1)
  3367    } else {
  3368      console.log(`✅ plugin tests passed`)
  3369      removeRecursiveSync(rootTestDir)
  3370    }
  3371  
  3372    clearTimeout(timeout);
  3373  }
  3374  
  3375  main().catch(e => setTimeout(() => { throw e }))