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