import assert from 'assert';
import {
  bundle,
  bundler,
  assertBundles,
  removeDistDirectory,
  distDir,
  getNextBuild,
  run,
  inputFS,
  outputFS,
  overlayFS,
  ncp,
} from '@parcel/test-utils';
import path from 'path';

describe('html', function () {
  beforeEach(async () => {
    await removeDistDirectory();
  });

  let subscription;
  afterEach(async () => {
    if (subscription) {
      await subscription.unsubscribe();
      subscription = null;
    }
  });

  it('should support bundling HTML', async () => {
    let b = await bundle(path.join(__dirname, '/integration/html/index.html'));

    assertBundles(b, [
      {
        type: 'css',
        assets: ['index.html'],
      },
      {
        name: 'index.html',
        assets: ['index.html'],
      },
      {
        name: 'index.html',
        assets: ['index.html'],
      },
      {
        type: 'png',
        assets: ['100x100.png'],
      },
      {
        type: 'svg',
        assets: ['icons.svg'],
      },
      {
        type: 'css',
        assets: ['index.css'],
      },
      {
        type: 'html',
        assets: ['other.html'],
      },
      {
        type: 'js',
        assets: ['index.js'],
      },
    ]);

    let files = await outputFS.readdir(distDir);
    let html = await outputFS.readFile(
      path.join(distDir, 'index.html'),
      'utf8',
    );
    for (let file of files) {
      if (file !== 'index.html' && path.extname(file) !== '.map') {
        assert(html.includes(file));
      }
    }

    assert(html.includes('#hash_link'));
    assert(html.includes('mailto:someone@acme.com'));
    assert(html.includes('tel:+33636757575'));
    assert(html.includes('https://unpkg.com/parcel-bundler'));

    let iconsBundle = b.getBundles().find(b => b.name.startsWith('icons'));
    assert(
      html.includes('/' + path.basename(iconsBundle.filePath) + '#icon-code'),
    );

    let value = null;
    await run(b, {
      alert: v => (value = v),
    });
    assert.equal(value, 'Hi');
  });

  it('should support pkg#source array as entrypoints', async () => {
    let b = await bundle(
      path.join(__dirname, '/integration/html-pkg-source-array'),
    );

    assertBundles(b, [
      {
        name: 'a.html',
        assets: ['a.html'],
      },
      {
        name: 'b.html',
        assets: ['b.html'],
      },
    ]);

    assert(await outputFS.exists(path.join(distDir, 'a.html'), 'utf8'));
    assert(await outputFS.exists(path.join(distDir, 'b.html'), 'utf8'));
  });

  it('should find href attr when not first', async function () {
    let b = await bundle(
      path.join(__dirname, '/integration/html-attr-order/index.html'),
    );

    assertBundles(b, [
      {
        name: 'index.html',
        assets: ['index.html'],
      },
      {
        type: 'html',
        assets: ['other.html'],
      },
    ]);
  });

  it('should insert empty script tag for HMR at the end of the body', async function () {
    const b = await bundle(
      path.join(__dirname, '/integration/html-no-js/index.html'),
      {
        hmrOptions: {},
      },
    );

    assertBundles(b, [
      {
        type: 'js',
        assets: ['index.html'],
      },
      {
        name: 'index.html',
        assets: ['index.html'],
      },
    ]);

    const html = await outputFS.readFile(
      path.join(distDir, 'index.html'),
      'utf8',
    );

    assert(/<script src=".+?\.js"><\/script><\/body>/.test(html));
  });

  it('should insert empty script tag for HMR at the implied </body>', async function () {
    const b = await bundle(
      path.join(__dirname, '/integration/html-no-js/no-body.html'),
      {
        hmrOptions: {},
      },
    );

    assertBundles(b, [
      {
        type: 'js',
        assets: ['no-body.html'],
      },
      {
        name: 'no-body.html',
        assets: ['no-body.html'],
      },
    ]);

    const html = await outputFS.readFile(
      path.join(distDir, 'no-body.html'),
      'utf8',
    );

    assert(/<script src=".+?\.js"><\/script><\/html>/.test(html));
  });

  it('should insert empty script tag for HMR at the end of the file if both </body> and </html> are implied', async function () {
    const b = await bundle(
      path.join(__dirname, '/integration/html-no-js/no-body-or-html.html'),
      {
        hmrOptions: {},
      },
    );

    assertBundles(b, [
      {
        type: 'js',
        assets: ['no-body-or-html.html'],
      },
      {
        name: 'no-body-or-html.html',
        assets: ['no-body-or-html.html'],
      },
    ]);

    const html = await outputFS.readFile(
      path.join(distDir, 'no-body-or-html.html'),
      'utf8',
    );

    assert(/<script src=".+?\.js"><\/script>$/.test(html));
  });

  it('should support canonical links', async function () {
    let b = await bundle(
      path.join(__dirname, '/integration/html-canonical/index.html'),
    );

    assertBundles(b, [
      {
        name: 'index.html',
        assets: ['index.html'],
      },
    ]);

    let html = await outputFS.readFile(
      path.join(distDir, 'index.html'),
      'utf8',
    );

    assert(/<link rel="canonical" href="\.?\/index.html">/.test(html));
  });

  it('should support RSS feed links', async function () {
    let b = await bundle(
      path.join(__dirname, '/integration/html-feed/rss.html'),
    );

    assertBundles(b, [
      {
        name: 'rss.html',
        assets: ['rss.html'],
      },
      {
        name: 'feed.xml',
        assets: ['feed.xml'],
      },
    ]);
  });

  it('should support atom feed links', async function () {
    let b = await bundle(
      path.join(__dirname, '/integration/html-feed/atom.html'),
    );

    assertBundles(b, [
      {
        name: 'atom.html',
        assets: ['atom.html'],
      },
      {
        name: 'feed.xml',
        assets: ['feed.xml'],
      },
    ]);
  });

  it('should support meta tags', async function () {
    let b = await bundle(
      path.join(__dirname, '/integration/html-meta/index.html'),
    );

    assertBundles(b, [
      {
        name: 'index.html',
        assets: ['index.html'],
      },
      {
        name: 'logo.svg',
        assets: ['logo.svg'],
      },
    ]);

    let html = await outputFS.readFile(
      path.join(distDir, 'index.html'),
      'utf8',
    );
    assert(html.includes(`<meta name="msapplication-config" content="none">`));
    assert(html.includes(`<meta property="og:image" content="/logo.svg">`));
    assert(
      html.includes(
        `<meta name="twitter:image" content="https://parceljs.org/assets/logo.svg">`,
      ),
    );
  });

  it('should insert sibling CSS bundles for JS files in the HEAD', async function () {
    let b = await bundle(
      path.join(__dirname, '/integration/html-css/index.html'),
    );

    assertBundles(b, [
      {
        name: 'index.html',
        assets: ['index.html'],
      },
      {
        type: 'js',
        assets: ['index.js'],
      },
      {
        type: 'css',
        assets: ['index.css'],
      },
    ]);

    let html = await outputFS.readFile(
      path.join(distDir, 'index.html'),
      'utf8',
    );
    assert(
      /<link rel="stylesheet" href="[/\\]{1}index\.[a-f0-9]+\.css">/.test(html),
    );
  });

  it('should insert sibling bundles before body element if no HEAD', async function () {
    let b = await bundle(
      path.join(__dirname, '/integration/html-css-head/index.html'),
    );

    assertBundles(b, [
      {
        name: 'index.html',
        assets: ['index.html'],
      },
      {
        type: 'js',
        assets: ['index.js'],
      },
      {
        type: 'css',
        assets: ['index.css'],
      },
    ]);

    let html = await outputFS.readFile(
      path.join(distDir, 'index.html'),
      'utf8',
    );
    assert(
      /<html>\s*<link rel="stylesheet" href="[/\\]{1}index\.[a-f0-9]+\.css">\s*<body>/.test(
        html,
      ),
    );
  });

  it('should insert sibling bundles after doctype if no html', async function () {
    let b = await bundle(
      path.join(__dirname, '/integration/html-css-doctype/index.html'),
    );

    assertBundles(b, [
      {
        name: 'index.html',
        assets: ['index.html'],
      },
      {
        type: 'js',
        assets: ['index.js'],
      },
      {
        type: 'css',
        assets: ['index.css'],
      },
    ]);

    let html = await outputFS.readFile(
      path.join(distDir, 'index.html'),
      'utf8',
    );
    assert(
      /^\s*<!DOCTYPE html>\s*<link .*>\s*<script .*>\s*<\/script>\s*$/.test(
        html,
      ),
    );
  });

  it.skip('should insert sibling JS bundles for CSS files in the HEAD', async function () {
    let b = await bundle(
      path.join(__dirname, '/integration/html-css-js/index.html'),
      {
        hmr: true,
      },
    );

    assertBundles(b, [
      {
        name: 'index.html',
        assets: ['index.html'],
      },
      {
        type: 'css',
        assets: ['index.css'],
      },
      {
        type: 'js',
        assets: [
          'index.css',
          'bundle-url.js',
          'css-loader.js',
          'hmr-runtime.js',
        ],
      },
    ]);

    let html = await outputFS.readFile(
      path.join(distDir, 'index.html'),
      'utf8',
    );
    assert(/<script src="[/\\]{1}index\.[a-f0-9]+\.js">/.test(html));
  });

  it('should insert sibling bundles at correct location in tree when optional elements are absent', async function () {
    let b = await bundle(
      path.join(
        __dirname,
        '/integration/html-css-optional-elements/index.html',
      ),
    );

    assertBundles(b, [
      {
        name: 'index.html',
        assets: ['index.html'],
      },
      {
        type: 'js',
        assets: ['index.js'],
      },
      {
        type: 'css',
        assets: ['index.css'],
      },
      {
        type: 'js',
        assets: ['other.js'],
      },
    ]);

    let html = await outputFS.readFile(
      path.join(distDir, 'index.html'),
      'utf8',
    );

    assert(
      /^<link rel="stylesheet" href="[/\\]index\.[a-f0-9]+\.css">\s*<script src="[/\\]index\.[a-f0-9]+\.js" defer=""><\/script>\s*<h1>Hello/m.test(
        html,
      ),
    );
  });

  it('should combine sibling CSS from multiple script tags into one bundle', async function () {
    let b = await bundle(
      path.join(__dirname, '/integration/html-css-multi/index.html'),
    );

    assertBundles(b, [
      {
        name: 'index.html',
        assets: ['index.html'],
      },
      {
        type: 'js',
        assets: ['a.js'],
      },
      {
        type: 'js',
        assets: ['b.js'],
      },
      {
        type: 'css',
        assets: ['a.css', 'b.css'],
      },
    ]);

    let html = await outputFS.readFile(
      path.join(distDir, 'index.html'),
      'utf8',
    );

    assert.equal(
      html.match(
        /<link rel="stylesheet" href="[/\\]{1}index\.[a-f0-9]+?\.css">/g,
      ).length,
      1,
    );

    assert.equal(
      html.match(/<script src="[/\\]{1}index\.[a-f0-9]+?\.js" defer="">/g)
        .length,
      2,
    );
  });

  it('should deduplicate shared code between script tags', async function () {
    let b = await bundle(
      path.join(__dirname, '/integration/html-js-dedup/index.html'),
    );

    assertBundles(b, [
      {
        name: 'index.html',
        assets: ['index.html'],
      },
      {
        type: 'js',
        assets: ['component-1.js', 'obj.js', 'esmodule-helpers.js'],
      },
      {
        type: 'js',
        assets: ['component-2.js'],
      },
    ]);

    let o = [];
    await run(b, {
      output: v => o.push(v),
    });

    assert.deepEqual(o, ['component-1', 'component-2']);
  });

  it('should minify HTML in production mode', async function () {
    let inputFile = path.join(__dirname, '/integration/htmlnano/index.html');
    await bundle(inputFile, {
      defaultTargetOptions: {
        shouldOptimize: true,
      },
    });

    let inputSize = (await inputFS.stat(inputFile)).size;

    let outputFile = path.join(distDir, 'index.html');
    let outputSize = (await outputFS.stat(outputFile)).size;

    assert(inputSize > outputSize);

    let html = await outputFS.readFile(outputFile, 'utf8');
    assert(html.includes('Other page'));
  });

  it('should work with an empty html file', async function () {
    let inputFile = path.join(__dirname, '/integration/html-empty/index.html');
    await bundle(inputFile, {
      defaultTargetOptions: {
        shouldOptimize: false,
      },
    });

    let outputFile = path.join(distDir, 'index.html');
    let html = await outputFS.readFile(outputFile, 'utf8');
    assert.equal(html.length, 0);
  });

  it('should work with an invalid html file', async function () {
    let inputFile = path.join(
      __dirname,
      '/integration/html-invalid/index.html',
    );
    await bundle(inputFile, {
      defaultTargetOptions: {
        shouldOptimize: false,
      },
    });

    let outputFile = path.join(distDir, 'index.html');
    let html = await outputFS.readFile(outputFile, 'utf8');
    assert(html.includes('This is a paragraph'));
  });

  it("should work with html that doesn't include optional closing tags", async function () {
    let inputFile = path.join(
      __dirname,
      '/integration/html-optional-closing-tags/index.html',
    );
    await bundle(inputFile, {
      defaultTargetOptions: {
        shouldOptimize: false,
      },
    });

    let outputFile = path.join(distDir, 'index.html');
    let html = await outputFS.readFile(outputFile, 'utf8');
    assert(html.includes('Paragraph 1'));
  });

  it('should read .htmlnanorc.json and minify HTML in production mode', async function () {
    await bundle(
      path.join(__dirname, '/integration/htmlnano-config/index.html'),
      {
        defaultTargetOptions: {
          shouldOptimize: true,
        },
      },
    );

    let html = await outputFS.readFile(
      path.join(distDir, 'index.html'),
      'utf8',
    );

    // minifyJson
    assert(
      html.includes('<script type=application/json>{"user":"me"}</script>'),
    );

    // mergeStyles
    assert(html.includes('<style>h1{color:red}</style>'));

    assert(!html.includes('sourceMappingURL'));

    // minifySvg is false
    assert(
      html.includes(
        '<svg version=1.1 baseprofile=full width=300 height=200 xmlns=http://www.w3.org/2000/svg><rect width=100% height=100% fill=red></rect><circle cx=150 cy=100 r=80 fill=green></circle><text x=150 y=125 font-size=60 text-anchor=middle fill=white>SVG</text></svg>',
      ),
    );
  });

  it('should not minify default values inside HTML in production mode', async function () {
    let inputFile = path.join(
      __dirname,
      '/integration/htmlnano-defaults-form/index.html',
    );
    await bundle(inputFile, {
      defaultTargetOptions: {
        shouldOptimize: true,
      },
    });

    let inputSize = (await inputFS.stat(inputFile)).size;

    let outputFile = path.join(distDir, '/index.html');
    let outputSize = (await outputFS.stat(outputFile)).size;

    assert(inputSize > outputSize);

    let html = await outputFS.readFile(outputFile, 'utf8');
    assert(html.includes('<input type="text">'));
  });

  it('should not prepend the public path to assets with remote URLs', async function () {
    await bundle(path.join(__dirname, '/integration/html/index.html'));

    let html = await outputFS.readFile(
      path.join(distDir, 'index.html'),
      'utf8',
    );
    assert(
      html.includes('<script src="https://unpkg.com/parcel-bundler"></script>'),
    );
  });

  it('should not prepend the public path to hash links', async function () {
    await bundle(path.join(__dirname, '/integration/html/index.html'));

    let html = await outputFS.readFile(
      path.join(distDir, 'index.html'),
      'utf8',
    );
    assert(html.includes('<a href="#hash_link">'));
  });

  it('should detect virtual paths', async function () {
    let b = await bundle(
      path.join(__dirname, '/integration/html-virtualpath/index.html'),
    );

    assertBundles(b, [
      {
        name: 'index.html',
        assets: ['index.html'],
      },
      {
        type: 'html',
        assets: ['other.html'],
      },
    ]);
  });

  it('should not update root/main file in the bundles', async function () {
    await bundle(path.join(__dirname, '/integration/html-root/index.html'));

    let files = await outputFS.readdir(distDir);

    for (let file of files) {
      if (file !== 'index.html' && file.endsWith('.html')) {
        let html = await outputFS.readFile(path.join(distDir, file), 'utf8');
        assert(html.includes('index.html'));
      }
    }
  });

  it('should preserve the spacing in the HTML tags', async function () {
    await bundle(path.join(__dirname, '/integration/html/index.html'));

    let html = await outputFS.readFile(
      path.join(distDir, 'index.html'),
      'utf8',
    );
    assert(/<i>hello<\/i> <i>world<\/i>/.test(html));
  });

  it('should support child bundles of different types', async function () {
    let b = await bundle(
      path.join(
        __dirname,
        '/integration/child-bundle-different-types/index.html',
      ),
    );

    assertBundles(b, [
      {
        name: 'index.html',
        assets: ['index.html'],
      },
      {
        type: 'js',
        assets: ['main.js', 'util.js', 'other.js'],
      },
      {
        type: 'html',
        assets: ['other.html'],
      },
      {
        type: 'js',
        assets: ['index.js', 'util.js', 'other.js'],
      },
    ]);
  });

  it.skip('should support circular dependencies', async function () {
    let b = await bundle(
      path.join(__dirname, '/integration/circular/index.html'),
    );

    assertBundles(b, [
      {
        name: 'index.html',
        assets: ['index.html'],
      },
      {
        type: 'html',
        assets: ['about.html'],
      },
      {
        type: 'js',
        assets: ['about.js', 'index.js'],
      },
      {
        type: 'html',
        assets: ['test.html'],
      },
      {
        type: 'js',
        assets: ['about.js', 'index.js'],
      },
    ]);
  });

  it('should support bundling HTM', async function () {
    let b = await bundle(
      path.join(__dirname, '/integration/htm-extension/index.htm'),
    );

    assertBundles(b, [
      {
        name: 'index.html',
        assets: ['index.htm'],
        type: 'html',
      },
      {
        type: 'js',
        assets: ['index.js'],
      },
    ]);
  });

  it('should detect srcset attribute', async function () {
    let b = await bundle(
      path.join(__dirname, '/integration/html-srcset/index.html'),
    );

    assertBundles(b, [
      {
        name: 'index.html',
        assets: ['index.html'],
      },
      {
        type: 'png',
        assets: ['100x100.png'],
      },
      {
        type: 'png',
        assets: ['200x200.png'],
      },
      {
        type: 'png',
        assets: ['300x300.png'],
      },
    ]);
  });

  it('should detect srcset attribute of source element', async function () {
    let b = await bundle(
      path.join(__dirname, '/integration/html-source-srcset/index.html'),
    );

    assertBundles(b, [
      {
        name: 'index.html',
        assets: ['index.html'],
      },
      {
        type: 'png',
        assets: ['100x100.png'],
      },
      {
        type: 'png',
        assets: ['200x200.png'],
      },
      {
        type: 'png',
        assets: ['300x300.png'],
      },
    ]);
  });

  it('should detect imagesrcset attribute', async function () {
    let b = await bundle(
      path.join(__dirname, '/integration/html-imagesrcset/index.html'),
    );

    assertBundles(b, [
      {
        name: 'index.html',
        assets: ['index.html'],
      },
      {
        type: 'png',
        assets: ['100x100.png'],
      },
      {
        type: 'png',
        assets: ['200x200.png'],
      },
      {
        type: 'png',
        assets: ['300x300.png'],
      },
    ]);
  });

  it.skip('should support webmanifest', async function () {
    let b = await bundle(
      path.join(__dirname, '/integration/webmanifest/index.html'),
    );

    assertBundles(b, {
      name: 'index.html',
      assets: ['index.html'],
      childBundles: [
        {
          type: 'webmanifest',
          assets: ['manifest.webmanifest'],
          childBundles: [
            {
              type: 'txt',
              assets: ['some.txt'],
              childBundles: [],
            },
          ],
        },
      ],
    });
  });

  it.skip("should treat webmanifest as an entry module so it doesn't get content hashed", async function () {
    const b = await bundle(
      path.join(__dirname, '/integration/html-manifest/index.html'),
    );

    assertBundles(b, {
      name: 'index.html',
      assets: ['index.html'],
      childBundles: [
        {
          type: 'webmanifest',
          assets: ['manifest.webmanifest'],
        },
      ],
    });

    const html = await outputFS.readFile(
      path.join(__dirname, '/dist/index.html'),
      'utf8',
    );
    assert(html.includes('<link rel="manifest" href="/manifest.webmanifest">'));
  });

  it('should bundle svg files correctly', async function () {
    let b = await bundle(
      path.join(__dirname, '/integration/html-svg/index.html'),
    );

    assertBundles(b, [
      {
        name: 'index.html',
        assets: ['index.html'],
      },
      {
        type: 'svg',
        assets: ['file.svg'],
      },
    ]);
  });

  it('should ignore svgs referencing local symbols via <use xlink:href="#">', async function () {
    let b = await bundle(
      path.join(__dirname, '/integration/html-svg-local-symbol/index.html'),
      {
        mode: 'production',
      },
    );

    assertBundles(b, [
      {
        name: 'index.html',
        assets: ['index.html'],
      },
    ]);

    let contents = await outputFS.readFile(b.getBundles()[0].filePath, 'utf8');
    assert(
      contents.includes(
        '<svg><symbol id="all"><rect width="100" height="100"/></symbol></svg><svg xmlns:xlink="http://www.w3.org/1999/xlink"><use xlink:href="#all" href="#all"/></svg>',
      ),
    );
  });

  it('should bundle svg files using <image xlink:href=""> correctly', async function () {
    let b = await bundle(
      path.join(__dirname, '/integration/html-svg-image/index.html'),
    );

    assertBundles(b, [
      {
        name: 'index.html',
        assets: ['index.html'],
      },
      {
        type: 'svg',
        assets: ['file.svg'],
      },
    ]);
  });

  it("should support href attribute in <image /> in HTMLTransformer's collectDependencies", async function () {
    let b = await bundle(
      path.join(__dirname, '/integration/html-image-href-attr/index.html'),
    );

    assertBundles(b, [
      {
        name: 'index.html',
        assets: ['index.html'],
      },
      {
        type: 'png',
        assets: ['100x100.png'],
      },
    ]);
  });

  // Based on https://developer.mozilla.org/en-US/docs/Web/SVG/Element/script
  it('should bundle scripts inside svg', async function () {
    let b = await bundle(
      path.join(__dirname, '/integration/html-svg-script/index.html'),
    );

    assertBundles(b, [
      {
        name: 'index.html',
        assets: ['index.html'],
      },
      {
        type: 'js',
        assets: ['script-a.js'],
      },
      {
        type: 'js',
        assets: ['script-b.js'],
      },
    ]);
  });

  it('should support data attribute of object element', async function () {
    let b = await bundle(
      path.join(__dirname, '/integration/html-object/index.html'),
    );

    assertBundles(b, [
      {
        name: 'index.html',
        assets: ['index.html'],
      },
      {
        type: 'svg',
        assets: ['file.svg'],
      },
    ]);
  });

  it('should resolve assets containing spaces', async function () {
    let b = await bundle(
      path.join(__dirname, '/integration/resolve-spaces/index.html'),
    );

    assertBundles(b, [
      {
        name: 'index.html',
        assets: ['index.html'],
      },
      {
        type: 'html',
        assets: ['other page.html'],
      },
    ]);
  });

  it('should process inline JS', async function () {
    let b = await bundle(
      path.join(__dirname, '/integration/html-inline-js/index.html'),
      {
        defaultTargetOptions: {
          shouldOptimize: true,
        },
      },
    );

    // inline bundles are not output, but are apart of the bundleGraph
    assertBundles(b, [
      {type: 'js', assets: ['index.html']},
      {type: 'js', assets: ['index.html']},
      {type: 'js', assets: ['index.html']},
      {type: 'js', assets: ['index.html']},
      {name: 'index.html', assets: ['index.html']},
    ]);

    let files = await outputFS.readdir(distDir);
    // assert that the inline js files are not output
    assert(!files.some(filename => filename.includes('js')));

    let html = await outputFS.readFile(
      path.join(distDir, 'index.html'),
      'utf-8',
    );

    assert(!html.includes('someArgument'));
  });

  it('should process inline styles', async function () {
    let b = await bundle(
      path.join(__dirname, '/integration/html-inline-styles/index.html'),
      {
        defaultTargetOptions: {
          shouldOptimize: true,
        },
      },
    );

    assertBundles(b, [
      {
        type: 'css',
        assets: ['index.html'],
      },
      {
        type: 'css',
        assets: ['index.html'],
      },
      {
        type: 'css',
        assets: ['index.html'],
      },
      {
        type: 'css',
        assets: ['index.html'],
      },
      {
        type: 'jpg',
        assets: ['bg.jpg'],
      },
      {
        type: 'jpg',
        assets: ['img.jpg'],
      },
      {
        name: 'index.html',
        assets: ['index.html'],
      },
    ]);

    let bundles = b.getBundles();

    let html = await outputFS.readFile(
      bundles.find(bundle => bundle.type === 'html').filePath,
      'utf8',
    );

    let urls = [...html.matchAll(/url\(([^)]*)\)/g)].map(m => m[1]);
    assert.strictEqual(urls.length, 2);
    for (let url of urls) {
      assert(
        bundles.find(
          bundle =>
            bundle.bundleBehavior !== 'inline' &&
            path.basename(bundle.filePath) === url,
        ),
      );
    }
  });

  it('should process inline element styles', async function () {
    let b = await bundle(
      path.join(
        __dirname,
        '/integration/html-inline-styles-element/index.html',
      ),
      {shouldDisableCache: false},
    );

    assertBundles(b, [
      {
        type: 'css',
        assets: ['index.html'],
      },
      {
        type: 'css',
        assets: ['index.html'],
      },
      {
        type: 'css',
        assets: ['index.html'],
      },
      {
        name: 'index.html',
        assets: ['index.html'],
      },
    ]);
  });

  it('should process inline styles using lang', async function () {
    let b = await bundle(
      path.join(__dirname, '/integration/html-inline-sass/index.html'),
      {
        defaultTargetOptions: {
          shouldOptimize: true,
        },
      },
    );

    assertBundles(b, [
      {
        type: 'css',
        assets: ['index.html'],
      },
      {
        name: 'index.html',
        assets: ['index.html'],
      },
    ]);

    let html = await outputFS.readFile(
      path.join(distDir, 'index.html'),
      'utf8',
    );
    assert(html.includes('<style>.index{color:#00f}</style>'));
    assert(!html.includes('sourceMappingURL'));
  });

  it('should process inline non-js scripts', async function () {
    let b = await bundle(
      path.join(__dirname, '/integration/html-inline-coffeescript/index.html'),
      {
        defaultTargetOptions: {
          shouldOptimize: true,
        },
      },
    );

    assertBundles(b, [
      {
        type: 'js',
        assets: ['index.html'],
      },
      {
        name: 'index.html',
        assets: ['index.html'],
      },
    ]);

    let html = await outputFS.readFile(
      path.join(distDir, 'index.html'),
      'utf8',
    );
    assert(html.includes('alert("Hello, World!")'));
  });

  it('should handle inline css with @imports', async function () {
    let b = await bundle(
      path.join(__dirname, '/integration/html-inline-css-import/index.html'),
    );

    assertBundles(b, [
      {
        type: 'css',
        assets: ['index.html', 'test.css'],
      },
      {
        name: 'index.html',
        assets: ['index.html'],
      },
    ]);

    let html = await outputFS.readFile(
      path.join(distDir, 'index.html'),
      'utf8',
    );
    assert(!html.includes('@import'));
  });

  it('should not modify inline importmaps', async function () {
    let b = await bundle(
      path.join(__dirname, '/integration/html-inline-importmap/index.html'),
      {},
    );

    assertBundles(b, [
      {
        name: 'index.html',
        assets: ['index.html'],
      },
    ]);

    let html = await outputFS.readFile(b.getBundles()[0].filePath, 'utf8');
    assert(html.includes('/node_modules/lit1.3.0/'));
  });

  it('should expose top level declarations globally in inline <script> tags', async function () {
    let b = await bundle(
      path.join(__dirname, '/integration/html-inline-js-script/globals.html'),
    );

    let logs = [];
    let ctx = await run(
      b,
      {
        log(bar, baz) {
          logs.push([bar, baz]);
        },
      },
      {require: false},
    );
    let output = ctx.output;
    assert.equal(output.x, 2);
    assert.equal(output.y, 'undefined');
    assert.equal(output.z, 4);
    assert.equal(typeof output.bar, 'function');
    assert.equal(output.Test, 'undefined');
    assert.equal(typeof output.Foo, 'function');
    assert.equal(typeof output.baz, 'function');

    // x is a let, so is "global" but not part of the global object
    assert(!('x' in ctx));
    assert(!('y' in ctx));
    assert.equal(ctx.z, 4);
    assert.equal(typeof ctx.bar, 'function');
    assert(!('Test' in ctx));
    assert(!('Foo' in ctx));
    assert.equal(typeof ctx.baz, 'function');

    assert.deepEqual(logs, [
      ['undefined', 'function'],
      ['function', 'function'],
      ['function', 'function'],
    ]);
  });

  for (let scopeHoist of [false, true]) {
    it(
      'should expose top level declarations globally in inline <script> tags with dependencies with scopeHoist = ' +
        scopeHoist,
      async function () {
        let b = await bundle(
          path.join(
            __dirname,
            '/integration/html-inline-js-script/globals-dependencies.html',
          ),
          {
            defaultTargetOptions: {
              shouldScopeHoist: scopeHoist,
            },
          },
        );

        let logs = [];
        let ctx = await run(
          b,
          {
            log(bar, baz) {
              logs.push([bar, baz]);
            },
          },
          {require: false},
        );
        let output = ctx.output;
        assert.equal(output.x, 2);
        assert.equal(output.y, 'undefined');
        assert.equal(output.z, 4);
        assert.equal(typeof output.bar, 'function');
        assert.equal(output.Test, 'undefined');
        assert.equal(typeof output.Foo, 'function');
        assert.equal(typeof output.baz, 'function');

        // x is a let, so is "global" but not part of the global object
        assert(!('x' in ctx));
        assert(!('y' in ctx));
        assert.equal(ctx.z, 4);
        assert.equal(typeof ctx.bar, 'function');
        assert(!('Test' in ctx));
        assert(!('Foo' in ctx));
        assert.equal(typeof ctx.baz, 'function');

        assert.deepEqual(logs, [
          ['undefined', 'function'],
          ['function', 'function'],
          ['function', 'function'],
        ]);

        let contents = await outputFS.readFile(
          b.getBundles()[0].filePath,
          'utf8',
        );
        if (scopeHoist) {
          assert(contents.includes('import('));
        } else {
          assert(!contents.includes('import('));
        }
      },
    );
  }

  it('should error on imports in inline scripts without type="module"', async function () {
    let errored = false;
    try {
      await bundle(
        path.join(__dirname, '/integration/html-inline-js-script/error.html'),
      );
    } catch (err) {
      assert.equal(
        err.message,
        'Browser scripts cannot have imports or exports.',
      );
      assert.deepEqual(err.diagnostics, [
        {
          message: 'Browser scripts cannot have imports or exports.',
          origin: '@parcel/transformer-js',
          codeFrames: [
            {
              filePath: path.join(
                __dirname,
                '/integration/html-inline-js-script/error.html',
              ),
              codeHighlights: [
                {
                  message: null,
                  start: {
                    line: 5,
                    column: 7,
                  },
                  end: {
                    line: 5,
                    column: 24,
                  },
                },
              ],
            },
          ],
          hints: ['Add the type="module" attribute to the <script> tag.'],
          documentationURL:
            'https://parceljs.org/languages/javascript/#classic-scripts',
        },
      ]);

      errored = true;
    }

    assert(errored);
  });

  it('should allow imports and requires in inline <script> tags', async function () {
    let b = await bundle(
      path.join(__dirname, '/integration/html-inline-js-require/index.html'),
      {
        defaultTargetOptions: {
          shouldOptimize: true,
        },
      },
    );

    assertBundles(b, [
      {
        type: 'js',
        assets: ['index.html', 'test.js'],
      },
      {
        name: 'index.html',
        assets: ['index.html'],
      },
    ]);

    let html = await outputFS.readFile(
      path.join(distDir, 'index.html'),
      'utf8',
    );
    assert(html.includes('console.log("test")'));
  });

  it('should support protocol-relative urls', async function () {
    let b = await bundle(
      path.join(__dirname, '/integration/html-protocol-relative/index.html'),
    );

    assertBundles(b, [
      {
        name: 'index.html',
        assets: ['index.html'],
      },
      {
        type: 'css',
        assets: ['index.css'],
      },
    ]);

    for (let bundle of b.getBundles()) {
      let contents = await outputFS.readFile(bundle.filePath, 'utf8');
      assert(contents.includes('//unpkg.com/xyz'));
    }
  });

  it('should support inline <script type="module">', async function () {
    let b = await bundle(
      path.join(__dirname, '/integration/html-inline-js-module/index.html'),
      {
        defaultTargetOptions: {
          shouldScopeHoist: true,
        },
      },
    );

    assertBundles(b, [
      {
        type: 'js',
        assets: ['index.html'],
      },
      {
        name: 'index.html',
        assets: ['index.html'],
      },
    ]);

    let html = await outputFS.readFile(
      path.join(distDir, 'index.html'),
      'utf8',
    );
    assert(html.includes('<script type="module">'));
    assert(html.includes('document.write("Hello world")'));
  });

  it('should compile inline <script type="module"> to non-module if not all engines support esmodules', async function () {
    let b = await bundle(
      path.join(__dirname, '/integration/html-inline-js-module/index.html'),
      {
        defaultTargetOptions: {
          mode: 'production',
          shouldScopeHoist: true,
          engines: {
            browsers: '>= 0.25%',
          },
        },
      },
    );

    await assertBundles(b, [
      {
        type: 'js',
        assets: ['index.html'],
      },
      {
        name: 'index.html',
        assets: ['index.html'],
      },
    ]);

    let html = await outputFS.readFile(
      path.join(distDir, 'index.html'),
      'utf8',
    );
    assert(!html.includes('<script type="module">'));
    assert(html.includes('<script>'));
    assert(html.includes('document.write("Hello world")'));
  });

  it('should compile a module and nomodule script when not all engines support esmodules natively', async function () {
    let b = await bundle(
      path.join(__dirname, '/integration/html-js/index.html'),
      {
        defaultTargetOptions: {
          mode: 'production',
          shouldScopeHoist: true,
          engines: {
            browsers: '>= 0.25%',
          },
        },
      },
    );

    await assertBundles(b, [
      {
        type: 'js',
        assets: ['index.js', 'other.js'],
      },
      {
        type: 'js',
        assets: ['index.js', 'other.js'],
      },
      {
        name: 'index.html',
        assets: ['index.html'],
      },
    ]);

    let bundles = b.getBundles();
    let html = await outputFS.readFile(
      bundles.find(b => b.type === 'html').filePath,
      'utf8',
    );
    assert(html.includes('<script type="module" src='));
    assert(/<script src=".*?" nomodule/.test(html));

    let js = await outputFS.readFile(
      bundles.find(b => b.type === 'js' && b.env.outputFormat === 'esmodule')
        .filePath,
      'utf8',
    );
    assert(/class \$[a-f0-9]+\$var\$Useless \{/.test(js));

    js = await outputFS.readFile(
      bundles.find(b => b.type === 'js' && b.env.outputFormat === 'global')
        .filePath,
      'utf8',
    );
    assert(!/class \$[a-f0-9]+\$var\$Useless \{/.test(js));
  });

  it('should remove type="module" when not scope hoisting', async function () {
    let b = await bundle(
      path.join(__dirname, '/integration/html-js/index.html'),
    );

    await assertBundles(b, [
      {
        type: 'js',
        assets: ['esmodule-helpers.js', 'index.js', 'other.js'],
      },
      {
        name: 'index.html',
        assets: ['index.html'],
      },
    ]);

    let html = await outputFS.readFile(
      path.join(distDir, 'index.html'),
      'utf8',
    );
    assert(!html.includes('<script type="module"'));
    assert(html.includes('<script src='));
  });

  it('should not add a nomodule version when all browsers support esmodules', async function () {
    let b = await bundle(
      path.join(__dirname, '/integration/html-js/index.html'),
      {
        defaultTargetOptions: {
          mode: 'production',
          shouldScopeHoist: true,
          engines: {
            browsers: 'last 1 Chrome version',
          },
        },
      },
    );

    await assertBundles(b, [
      {
        type: 'js',
        assets: ['index.js', 'other.js'],
      },
      {
        name: 'index.html',
        assets: ['index.html'],
      },
    ]);

    let html = await outputFS.readFile(
      path.join(distDir, 'index.html'),
      'utf8',
    );
    assert(html.includes('<script type="module" src='));
    assert(!/<script src=".*?" nomodule/.test(html));
  });

  it('should error on imports in scripts without type="module"', async function () {
    let errored = false;
    try {
      await bundle(path.join(__dirname, '/integration/html-js/error.html'));
    } catch (err) {
      assert.equal(
        err.message,
        'Browser scripts cannot have imports or exports.',
      );
      assert.deepEqual(err.diagnostics, [
        {
          message: 'Browser scripts cannot have imports or exports.',
          origin: '@parcel/transformer-js',
          codeFrames: [
            {
              filePath: path.join(__dirname, '/integration/html-js/index.js'),
              codeHighlights: [
                {
                  message: null,
                  start: {
                    line: 1,
                    column: 1,
                  },
                  end: {
                    line: 1,
                    column: 29,
                  },
                },
              ],
            },
            {
              filePath: path.join(__dirname, '/integration/html-js/error.html'),
              codeHighlights: [
                {
                  message: 'The environment was originally created here',
                  start: {
                    line: 1,
                    column: 1,
                  },
                  end: {
                    line: 1,
                    column: 32,
                  },
                },
              ],
            },
          ],
          hints: ['Add the type="module" attribute to the <script> tag.'],
          documentationURL:
            'https://parceljs.org/languages/javascript/#classic-scripts',
        },
      ]);

      errored = true;
    }

    assert(errored);
  });

  it('should correctly bundle loaders for nested dynamic imports', async function () {
    let b = await bundle(
      path.join(
        __dirname,
        '/integration/html-js-shared-dynamic-nested/index.html',
      ),
      {
        mode: 'production',
        defaultTargetOptions: {
          shouldScopeHoist: true,
          engines: {
            browsers: '>= 0.25%',
          },
        },
      },
    );

    assertBundles(b, [
      {
        type: 'js',
        assets: [
          'bundle-manifest.js',
          'bundle-url.js',
          'cacheLoader.js',
          'index.js',
          'index.js',
          'index.js',
          'js-loader.js',
        ],
      },
      {
        type: 'js',
        assets: ['bundle-manifest.js', 'index.js', 'index.js', 'index.js'],
      },
      {
        name: 'index.html',
        assets: ['index.html'],
      },
      {
        type: 'js',
        assets: ['simpleHasher.js'],
      },
      {
        type: 'js',
        assets: ['simpleHasher.js'],
      },
    ]);

    let res = await run(b, {output: null}, {require: false});
    assert.deepEqual(await res.output, ['hasher', ['hasher', 'hasher']]);
  });

  it('should support shared bundles between multiple inline scripts', async function () {
    let b = await bundle(
      path.join(__dirname, '/integration/html-inline-js-shared/index.html'),
      {
        mode: 'production',
        defaultTargetOptions: {
          shouldScopeHoist: true,
          shouldOptimize: false,
        },
      },
    );

    assertBundles(b, [
      {
        type: 'js',
        assets: ['index.html'],
      },
      {
        type: 'js',
        assets: ['index.html'],
      },
      {
        type: 'js',
        assets: ['lodash.js'],
      },
      {
        name: 'index.html',
        assets: ['index.html'],
      },
    ]);

    let html = await outputFS.readFile(
      path.join(distDir, 'index.html'),
      'utf8',
    );
    assert(html.includes('<script type="module" src="'));
    assert(html.includes('<script type="module">'));
    assert(html.includes('.add(1, 2)'));
    assert(html.includes('.add(2, 3)'));
  });

  it('inserts sibling bundles into html in the correct order (no head)', async function () {
    let b = await bundle(
      path.join(__dirname, '/integration/html-js-shared/index.html'),
      {
        mode: 'production',
        defaultTargetOptions: {
          shouldScopeHoist: true,
        },
      },
    );

    assertBundles(b, [
      {
        type: 'js',
        assets: ['async.js'],
      },
      {
        type: 'js',
        assets: [
          'bundle-manifest.js',
          'get-worker-url.js',
          'index.js',
          'lodash.js',
        ],
      },
      {
        name: 'index.html',
        type: 'html',
        assets: ['index.html'],
      },
      // {
      //   type: 'js',
      //   assets: ['lodash.js'],
      // },
      {
        type: 'js',
        assets: ['worker.js', 'lodash.js'],
      },
    ]);

    // let lodashSibling = path.basename(
    //   b.getBundles().find(v => v.getEntryAssets().length === 0).filePath,
    // );

    let html = await outputFS.readFile(
      path.join(distDir, 'index.html'),
      'utf8',
    );

    let insertedBundles = [];
    let regex = /<script (?:type="[^"]+" )?src="([^"]*)"><\/script>/g;
    let match;
    while ((match = regex.exec(html)) !== null) {
      insertedBundles.push(path.basename(match[1]));
    }

    assert.equal(insertedBundles.length, 1);
    // assert.equal(insertedBundles.length, 2);
    // assert.equal(insertedBundles[0], lodashSibling);
  });

  it('inserts sibling bundles into html in the correct order (head)', async function () {
    let b = await bundle(
      path.join(__dirname, '/integration/html-js-shared-head/index.html'),
      {
        mode: 'production',
        defaultTargetOptions: {
          shouldScopeHoist: true,
        },
      },
    );

    assertBundles(b, [
      {
        type: 'js',
        assets: ['async.js'],
      },
      {
        type: 'js',
        assets: [
          'bundle-manifest.js',
          'get-worker-url.js',
          'index.js',
          'lodash.js',
        ],
      },
      {
        name: 'index.html',
        type: 'html',
        assets: ['index.html'],
      },
      // {
      //   type: 'js',
      //   assets: ['lodash.js'],
      // },
      {
        type: 'js',
        assets: ['worker.js', 'lodash.js'],
      },
    ]);

    // let lodashSibling = path.basename(
    //   b.getBundles().find(v => v.getEntryAssets().length === 0).filePath,
    // );

    let html = await outputFS.readFile(
      path.join(distDir, 'index.html'),
      'utf8',
    );

    let insertedBundles = [];
    let regex = /<script (?:type="[^"]+" )?src="([^"]*)"><\/script>/g;
    let match;
    while ((match = regex.exec(html)) !== null) {
      insertedBundles.push(path.basename(match[1]));
    }

    assert.equal(insertedBundles.length, 1);
    // assert.equal(insertedBundles.length, 2);
    // assert.equal(insertedBundles[0], lodashSibling);
  });

  it('inserts sibling bundles into html with nomodule or type=module', async function () {
    let b = await bundle(
      path.join(__dirname, '/integration/html-js-shared-nomodule/*.html'),
      {
        mode: 'production',
        defaultTargetOptions: {
          shouldScopeHoist: true,
          shouldOptimize: false,
        },
      },
    );

    assertBundles(b, [
      {
        name: 'a.html',
        assets: ['a.html'],
      },
      {
        type: 'js',
        assets: ['a.js'],
      },
      {
        type: 'js',
        assets: ['a.js'],
      },
      {
        name: 'b.html',
        assets: ['b.html'],
      },
      {
        type: 'js',
        assets: ['b.js'],
      },
      {
        type: 'js',
        assets: ['b.js'],
      },
      {
        type: 'js',
        assets: ['lib.js'],
      },
      {
        type: 'js',
        assets: ['lib.js'],
      },
    ]);

    for (let file of b
      .getBundles()
      .filter(b => b.type === 'html')
      .map(b => b.filePath)) {
      let html = await outputFS.readFile(file, 'utf8');

      let noModuleScripts = [];
      let moduleScripts = [];

      let regex = /<script ([^>]*)><\/script>/g;
      let match;
      while ((match = regex.exec(html)) !== null) {
        let attributes = new Map(match[1].split(' ').map(a => a.split('=')));
        let url = attributes.get('src').replace(/"/g, '');
        assert(url);
        if (attributes.get('type') === '"module"') {
          assert.strictEqual(attributes.size, 2);
          moduleScripts.push(path.basename(url));
        } else {
          assert.strictEqual(attributes.size, 3);
          assert(attributes.get('nomodule'));
          assert(attributes.get('defer'));
          noModuleScripts.push(path.basename(url));
        }
      }

      for (let scripts of [moduleScripts, noModuleScripts]) {
        assert.strictEqual(scripts.length, 2);
        assert(
          b
            .getBundles()
            .find(b => b.filePath.endsWith(scripts[0]))
            .getMainEntry() == null,
        );
        assert(
          b
            .getBundles()
            .find(b => b.filePath.endsWith(scripts[1]))
            .getMainEntry(),
        );
      }
    }
  });

  it('should isolate async scripts', async function () {
    let b = await bundle(
      path.join(__dirname, '/integration/html-async-script/index.html'),
      {
        mode: 'production',
        defaultTargetOptions: {
          shouldScopeHoist: true,
          shouldOptimize: false,
        },
      },
    );

    assertBundles(b, [
      {
        name: 'index.html',
        assets: ['index.html'],
      },
      {
        assets: ['a.js', 'c.js'],
      },
      {
        assets: ['b.js', 'c.js'],
      },
    ]);

    let output = [];
    await run(b, {
      output(o) {
        output.push(o);
      },
    });

    // could run in either order.
    assert(output.sort(), ['a', 'b', 'c']);
  });

  it('should isolate classic scripts from nomodule scripts', async function () {
    let b = await bundle(
      path.join(__dirname, '/integration/html-isolate-script/index.html'),
      {
        mode: 'production',
        defaultTargetOptions: {
          shouldScopeHoist: true,
          shouldOptimize: false,
        },
      },
    );

    assertBundles(b, [
      {
        name: 'index.html',
        assets: ['index.html'],
      },
      {
        assets: ['a.js', 'bundle-manifest.js'],
      },
      {
        assets: [
          'a.js',
          'bundle-manifest.js',
          'bundle-url.js',
          'cacheLoader.js',
          'js-loader.js',
        ],
      },
      {
        assets: [
          'b.js',
          'bundle-manifest.js',
          'bundle-url.js',
          'cacheLoader.js',
          'js-loader.js',
        ],
      },
      {
        assets: ['c.js'],
      },
      {
        assets: ['c.js'],
      },
    ]);

    let output = [];
    await run(b, {
      output(o) {
        output.push(o);
      },
    });

    // could run in either order.
    assert(output.sort(), ['a', 'b', 'c']);
  });

  it('should support multiple entries with shared sibling bundles', async function () {
    let b = await bundle(
      path.join(__dirname, '/integration/shared-sibling-entries/*.html'),
      {
        defaultTargetOptions: {
          shouldScopeHoist: true,
        },
      },
    );

    assertBundles(b, [
      {
        name: 'a.html',
        type: 'html',
        assets: ['a.html'],
      },
      {
        name: 'b.html',
        type: 'html',
        assets: ['b.html'],
      },
      {
        name: 'c.html',
        type: 'html',
        assets: ['c.html'],
      },
      {
        type: 'js',
        assets: ['a.html', 'shared.js'],
      },
      {
        type: 'js',
        assets: ['b.html', 'shared.js'],
      },
      {
        type: 'js',
        assets: ['c.html', 'shared.js'],
      },
      {
        type: 'css',
        assets: ['shared.css', 'other.css'],
      },
    ]);

    // Both HTML files should point to the sibling CSS file
    let html = await outputFS.readFile(path.join(distDir, 'a.html'), 'utf8');
    assert(/<link rel="stylesheet" href="\/a\.[a-z0-9]+\.css">/.test(html));

    html = await outputFS.readFile(path.join(distDir, 'b.html'), 'utf8');
    assert(/<link rel="stylesheet" href="\/a\.[a-z0-9]+\.css">/.test(html));

    html = await outputFS.readFile(path.join(distDir, 'c.html'), 'utf8');
    assert(/<link rel="stylesheet" href="\/a\.[a-z0-9]+\.css">/.test(html));
  });

  it('should insert JS sibling bundle script tags in the correct order', async function () {
    let b = await bundle(
      path.join(
        __dirname,
        'integration/scope-hoisting/es6/interop-async/index.html',
      ),
      {
        mode: 'production',
        defaultTargetOptions: {
          shouldScopeHoist: true,
          shouldOptimize: false,
        },
      },
    );
    let bundles = b.getBundles();

    let html = await outputFS.readFile(
      path.join(distDir, 'index.html'),
      'utf8',
    );

    let insertedBundles = [];
    let regex = /<script (?:type="[^"]+" )?src="([^"]*)"><\/script>/g;
    let match;
    while ((match = regex.exec(html)) !== null) {
      let bundle = bundles.find(
        b => path.basename(b.filePath) === path.basename(match[1]),
      );

      insertedBundles.push(bundle);
    }

    assert.equal(insertedBundles.length, 1);

    let res = await run(b, {output: null}, {require: false});
    assert.deepEqual(await res.output, ['client', 'client', 'viewer']);
  });

  it('should not point to unrelated sibling bundles', async function () {
    await bundle(
      path.join(
        __dirname,
        '/integration/shared-sibling-entries-multiple/*.html',
      ),
      {
        defaultTargetOptions: {
          shouldScopeHoist: true,
        },
      },
    );

    // a.html should point to a CSS bundle containing a.css as well as
    // reuse the b.css bundle from b.html.
    let html = await outputFS.readFile(path.join(distDir, 'a.html'), 'utf8');
    assert.equal(
      html.match(/<link rel="stylesheet" href="\/a\.[a-z0-9]+\.css">/g).length,
      1,
    );
    assert.equal(
      html.match(/<link rel="stylesheet" href="\/b\.[a-z0-9]+\.css">/g).length,
      1,
    );

    // a.html should reference a.js only
    assert.equal(html.match(/a\.[a-z0-9]+\.js/g).length, 1);

    assert.equal(html.match(/b\.[a-z0-9]+\.js/g), null);

    let css = await outputFS.readFile(
      path.join(distDir, html.match(/\/a\.[a-z0-9]+\.css/)[0]),
      'utf8',
    );
    assert(css.includes('.a {'));
    assert(!css.includes('.b {'));

    // b.html should point to a CSS bundle containing only b.css
    // It should not point to the bundle containing a.css from a.html
    html = await outputFS.readFile(path.join(distDir, 'b.html'), 'utf8');
    assert.equal(
      html.match(/<link rel="stylesheet" href="\/a\.[a-z0-9]+\.css">/g),
      null,
    );
    assert.equal(
      html.match(/<link rel="stylesheet" href="\/b\.[a-z0-9]+\.css">/g).length,
      1,
    );

    // b.html should reference b.js only
    assert.equal(html.match(/a\.[a-z0-9]+\.js/g), null);

    assert.equal(html.match(/b\.[a-z0-9]+\.js/g).length, 1);

    css = await outputFS.readFile(
      path.join(distDir, html.match(/\/b\.[a-z0-9]+\.css/)[0]),
      'utf8',
    );
    assert(!css.includes('.a {'));
    assert(css.includes('.b {'));
  });

  it('should support split bundles with many pages', async function () {
    await bundle(path.join(__dirname, '/integration/shared-many/*.html'), {
      mode: 'production',
    });

    let html = await outputFS.readFile(path.join(distDir, 'a.html'), 'utf8');
    assert.equal(html.match(/<script/g).length, 3);

    html = await outputFS.readFile(path.join(distDir, 'b.html'), 'utf8');
    assert.equal(html.match(/<script/g).length, 5);

    html = await outputFS.readFile(path.join(distDir, 'c.html'), 'utf8');
    assert.equal(html.match(/<script/g).length, 5);

    html = await outputFS.readFile(path.join(distDir, 'd.html'), 'utf8');
    assert.equal(html.match(/<script/g).length, 4);

    html = await outputFS.readFile(path.join(distDir, 'e.html'), 'utf8');
    assert.equal(html.match(/<script/g).length, 4);

    html = await outputFS.readFile(path.join(distDir, 'f.html'), 'utf8');
    assert.equal(html.match(/<script/g).length, 3);

    // b.html hitting the parallel request limit should not prevent g.html from being optimized
    html = await outputFS.readFile(path.join(distDir, 'g.html'), 'utf8');
    assert.equal(html.match(/<script/g).length, 5);
  });

  it('should not add CSS to a worker bundle group', async function () {
    let b = await bundle(
      path.join(__dirname, '/integration/shared-sibling-worker-css/index.html'),
    );

    assertBundles(b, [
      {
        type: 'css',
        assets: ['style.css'],
      },
      {
        type: 'html',
        assets: ['index.html'],
      },
      {
        type: 'js',
        assets: ['a.js', 'worker.js', 'esmodule-helpers.js'],
      },
      {
        type: 'js',
        assets: [
          'a.js',
          'bundle-url.js',
          'esmodule-helpers.js',
          'get-worker-url.js',
          'index.js',
        ],
      },
    ]);

    let htmlBundle = b.getBundles().find(b => b.type === 'html');
    let htmlSiblings = b.getReferencedBundles(htmlBundle);
    assert.equal(htmlSiblings.length, 2);
    assert(htmlSiblings.some(b => b.type === 'js'));
    assert(htmlSiblings.some(b => b.type === 'css'));

    let worker = b.getChildBundles(htmlSiblings.find(b => b.type === 'js'));
    assert.equal(worker.length, 1);
    let workerSiblings = b.getReferencedBundles(worker[0]);
    assert.equal(workerSiblings.length, 0);
  });

  it('should correctly add sibling bundles to all using bundles', async function () {
    let b = await bundle(
      path.join(__dirname, '/integration/shared-sibling/*.html'),
    );

    assertBundles(b, [
      {
        type: 'html',
        assets: ['form.html'],
      },
      {
        type: 'js',
        assets: ['form.js', 'a.js', 'a.module.css', 'esmodule-helpers.js'],
      },
      {
        type: 'css',
        assets: ['a.module.css'],
      },
      {
        type: 'html',
        assets: ['searchfield.html'],
      },
      {
        type: 'js',
        assets: [
          'searchfield.js',
          'a.js',
          'a.module.css',
          'b.js',
          'esmodule-helpers.js',
        ],
      },
      {
        type: 'html',
        assets: ['searchfield2.html'],
      },
      {
        type: 'js',
        assets: [
          'searchfield2.js',
          'a.js',
          'a.module.css',
          'b.js',
          'esmodule-helpers.js',
        ],
      },
    ]);

    for (let htmlBundle of b.getBundles().filter(b => b.type === 'html')) {
      let htmlSiblings = b
        .getReferencedBundles(htmlBundle, true)
        .map(b => b.type)
        .sort();
      assert.deepEqual(htmlSiblings, ['css', 'js']);
    }
  });

  it('should remove duplicate assets from sibling bundles', async function () {
    let bundleGraph = await bundle(
      path.join(__dirname, '/integration/shared-sibling-duplicate/*.html'),
      {mode: 'production'},
    );

    bundleGraph.traverseBundles(bundle => {
      bundle.traverseAssets(asset => {
        let bundles = bundleGraph.getBundlesWithAsset(asset);
        assert.equal(
          bundles.length,
          1,
          `asset ${asset.filePath} is duplicated`,
        );
      });
    });
  });

  it('should support split bundles with many pages with esmodule output', async function () {
    await bundle(path.join(__dirname, '/integration/shared-many-esm/*.html'), {
      defaultTargetOptions: {
        shouldScopeHoist: true,
      },
    });

    let checkHtml = async filename => {
      // Find all scripts referenced in the HTML file
      let html = await outputFS.readFile(path.join(distDir, filename), 'utf8');
      let re = /<script.*?src="(.*?)"/g;
      let match;
      let scripts = new Set();
      while ((match = re.exec(html))) {
        scripts.add(path.join(distDir, match[1]));
      }

      assert(scripts.size > 0, 'no scripts found');

      // Ensure that those scripts don't import anything other than what's in the HTML.
      for (let script of scripts) {
        let js = await outputFS.readFile(script, 'utf8');
        let re = /import .*? from "(.*?)"/g;
        let match;
        while ((match = re.exec(js))) {
          let imported = path.join(distDir, match[1]);
          assert(
            scripts.has(imported),
            `unknown script ${match[1]} imported in ${path.basename(script)}`,
          );
        }
      }
    };

    for (let letter of ['a', 'b', 'c', 'd', 'e', 'f', 'g']) {
      await checkHtml(letter + '.html');
    }
  });

  it('should include the correct paths when using multiple entries and referencing style from html and js', async function () {
    let b = await bundle(
      path.join(__dirname, '/integration/html-multi-entry/*.html'),
      {
        defaultTargetOptions: {
          shouldScopeHoist: true,
        },
      },
    );

    assertBundles(b, [
      {
        name: 'a.html',
        type: 'html',
        assets: ['a.html'],
      },
      {
        name: 'b.html',
        type: 'html',
        assets: ['b.html'],
      },
      {
        type: 'css',
        assets: ['style.css'],
      },
      {
        type: 'js',
        assets: ['index.js'],
      },
    ]);

    let firstHtmlFile = await outputFS.readFile(
      path.join(distDir, 'a.html'),
      'utf8',
    );

    let secondHtmlFile = await outputFS.readFile(
      path.join(distDir, 'b.html'),
      'utf8',
    );

    let bundles = b.getBundles();
    let cssBundle = path.basename(
      bundles.find(bundle => bundle.filePath.endsWith('.css')).filePath,
    );
    let jsBundle = path.basename(
      bundles.find(bundle => bundle.filePath.endsWith('.js')).filePath,
    );

    assert(
      firstHtmlFile.includes(cssBundle),
      `a.html should include a reference to ${cssBundle}`,
    );
    assert(
      secondHtmlFile.includes(cssBundle),
      `b.html should include a reference to ${cssBundle}`,
    );

    assert(
      firstHtmlFile.includes(jsBundle),
      `a.html should include a reference to ${jsBundle}`,
    );
    assert(
      secondHtmlFile.includes(jsBundle),
      `b.html should include a reference to ${jsBundle}`,
    );
  });

  it('should invalidate parent bundle when inline bundles change', async function () {
    // copy into memory fs
    await ncp(
      path.join(__dirname, '/integration/html-inline-js-require'),
      path.join(__dirname, '/html-inline-js-require'),
    );

    let distDir = path.join(outputFS.cwd(), 'dist');

    let b = await bundler(
      path.join(__dirname, '/html-inline-js-require/index.html'),
      {
        inputFS: overlayFS,
        shouldDisableCache: false,
        defaultTargetOptions: {
          distDir,
        },
      },
    );

    subscription = await b.watch();
    await getNextBuild(b);

    let html = await outputFS.readFile('/dist/index.html', 'utf8');
    assert(html.includes("console.log('test')"));

    await overlayFS.writeFile(
      path.join(__dirname, '/html-inline-js-require/test.js'),
      "console.log('foo')",
    );
    await getNextBuild(b);

    html = await outputFS.readFile(path.join(distDir, '/index.html'), 'utf8');
    assert(html.includes("console.log('foo')"));
  });

  it('should invalidate parent bundle when nested inline bundles change', async function () {
    // copy into memory fs
    await ncp(
      path.join(__dirname, '/integration/html-inline-js-nested'),
      path.join(__dirname, '/html-inline-js-nested'),
    );

    let distDir = path.join(outputFS.cwd(), 'dist');

    let b = await bundler(
      path.join(__dirname, '/html-inline-js-nested/index.html'),
      {
        inputFS: overlayFS,
        shouldDisableCache: false,
        defaultTargetOptions: {
          distDir,
        },
      },
    );

    subscription = await b.watch();
    await getNextBuild(b);

    let html = await outputFS.readFile('/dist/index.html', 'utf8');
    assert(html.includes('module.exports = "hello world"'));
    assert(html.includes('console.log'));

    await overlayFS.writeFile(
      path.join(__dirname, '/html-inline-js-nested/test.txt'),
      'foo bar',
    );
    await getNextBuild(b);

    html = await outputFS.readFile(path.join(distDir, 'index.html'), 'utf8');
    assert(!html.includes('module.exports = "hello world"'));
    assert(html.includes('module.exports = "foo bar"'));
    assert(html.includes('console.log'));
  });

  it('should inline data-urls', async () => {
    let b = await bundle(
      path.join(__dirname, '/integration/data-url/index.html'),
      {
        defaultTargetOptions: {
          sourceMaps: false,
        },
      },
    );

    let contents = await outputFS.readFile(
      b.getBundles().find(b => b.type === 'html').filePath,
      'utf8',
    );
    assert.equal(
      contents.trim(),
      `<img src="data:image/svg+xml,%3Csvg%20width%3D%22120%22%20height%3D%22120%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%3Cfilter%20id%3D%22blur-_.%21~%2a%22%3E%0A%20%20%20%20%3CfeGaussianBlur%20stdDeviation%3D%225%22%3E%3C%2FfeGaussianBlur%3E%0A%20%20%3C%2Ffilter%3E%0A%20%20%3Ccircle%20cx%3D%2260%22%20cy%3D%2260%22%20r%3D%2250%22%20fill%3D%22green%22%20filter%3D%22url%28%27%23blur-_.%21~%2a%27%29%22%3E%3C%2Fcircle%3E%0A%3C%2Fsvg%3E%0A">`,
    );
  });

  it('should print a diagnostic for invalid bundler options', async () => {
    let dir = path.join(__dirname, 'integration/invalid-bundler-config');
    let pkg = path.join(dir, 'package.json');
    let code = await inputFS.readFileSync(pkg, 'utf8');
    await assert.rejects(() => bundle(path.join(dir, 'index.html')), {
      name: 'BuildError',
      diagnostics: [
        {
          message: 'Invalid config for @parcel/bundler-default',
          origin: '@parcel/bundler-default',
          codeFrames: [
            {
              filePath: pkg,
              language: 'json',
              code,
              codeHighlights: [
                {
                  message: 'Did you mean "minBundleSize", "minBundles"?',
                  start: {
                    column: 30,
                    line: 3,
                  },
                  end: {
                    column: 45,
                    line: 3,
                  },
                },
              ],
            },
          ],
        },
      ],
    });
  });

  it('should escape inline script tags', async function () {
    let b = await bundle(
      path.join(__dirname, 'integration/html-inline-escape/script.html'),
    );
    let output;
    await run(b, {
      output(o) {
        output = o;
      },
    });

    assert.deepEqual(output, {
      a: '<script></script>',
      b: '<!-- test',
      c: '<SCRIPT></SCRIPT>',
    });
  });

  it('should escape quotes in inline style attributes and style tags', async function () {
    let b = await bundle(
      path.join(__dirname, 'integration/html-inline-escape/style.html'),
    );
    let output = await outputFS.readFile(b.getBundles()[0].filePath, 'utf8');
    let name = path.basename(
      b.getBundles().find(b => b.type === 'png').filePath,
      'utf8',
    );
    assert(output.includes(`url(&quot;${name}&quot;)`));
    assert(output.includes('<\\/style>'));
  });

  it('should work with bundle names that have colons in them', async function () {
    if (process.platform === 'win32') {
      return;
    }

    // Windows paths cannot contain colons and will fail to git clone, so write the file here (in memory).
    await overlayFS.mkdirp(path.join(__dirname, 'integration/url-colon'));
    await overlayFS.writeFile(
      path.join(__dirname, 'integration/url-colon/a:b:c.html'),
      '<p>Test</p>',
    );

    let b = await bundle(
      path.join(__dirname, 'integration/url-colon/relative.html'),
      {inputFS: overlayFS},
    );

    assertBundles(b, [
      {
        name: 'relative.html',
        assets: ['relative.html'],
      },
      {
        name: 'a:b:c.html',
        assets: ['a:b:c.html'],
      },
    ]);

    let output = await outputFS.readFile(b.getBundles()[0].filePath, 'utf8');
    assert(output.includes('/a:b:c.html'));

    b = await bundle(
      path.join(__dirname, 'integration/url-colon/absolute.html'),
      {inputFS: overlayFS},
    );

    assertBundles(b, [
      {
        name: 'absolute.html',
        assets: ['absolute.html'],
      },
      {
        name: 'a:b:c.html',
        assets: ['a:b:c.html'],
      },
    ]);

    output = await outputFS.readFile(b.getBundles()[0].filePath, 'utf8');
    assert(output.includes('/a:b:c.html'));
  });

  it('should normalize case of SVG elements and attributes when minified', async function () {
    let b = await bundle(
      path.join(__dirname, 'integration/html-svg-case/index.html'),
      {
        mode: 'production',
      },
    );

    let output = await outputFS.readFile(b.getBundles()[0].filePath, 'utf8');
    assert(output.includes('<x-custom stddeviation="0.5"'));
    assert(output.includes('<svg role="img" viewBox='));
    assert(output.includes('<filter'));
    assert(output.includes('<feGaussianBlur in="SourceGraphic" stdDeviation='));
  });

  it('should throw error with empty string reference to other resource', async function () {
    await assert.rejects(
      () =>
        bundle(
          path.join(__dirname, 'integration/html-empty-reference/index.html'),
          {
            mode: 'production',
          },
        ),
      {
        name: 'BuildError',
        diagnostics: [
          {
            message: "'src' should not be empty string",
            origin: '@parcel/transformer-html',
            codeFrames: [
              {
                filePath: path.join(
                  __dirname,
                  'integration/html-empty-reference/index.html',
                ),
                language: 'html',
                codeHighlights: [
                  {
                    start: {
                      column: 1,
                      line: 1,
                    },
                    end: {
                      column: 14,
                      line: 1,
                    },
                  },
                ],
              },
            ],
          },

          {
            message: "'src' should not be empty string",
            origin: '@parcel/transformer-html',
            codeFrames: [
              {
                filePath: path.join(
                  __dirname,
                  'integration/html-empty-reference/index.html',
                ),
                language: 'html',
                codeHighlights: [
                  {
                    start: {
                      column: 1,
                      line: 2,
                    },
                    end: {
                      column: 24,
                      line: 2,
                    },
                  },
                ],
              },
            ],
          },

          {
            message: "'href' should not be empty string",
            origin: '@parcel/transformer-html',
            codeFrames: [
              {
                filePath: path.join(
                  __dirname,
                  'integration/html-empty-reference/index.html',
                ),
                language: 'html',
                codeHighlights: [
                  {
                    start: {
                      column: 1,
                      line: 3,
                    },
                    end: {
                      column: 16,
                      line: 3,
                    },
                  },
                ],
              },
            ],
          },
        ],
      },
    );
  });
});
