rollup#OutputChunk TypeScript Examples

The following examples show how to use rollup#OutputChunk. You can vote up the ones you like or vote down the ones you don't like, and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: report.ts    From reskript with MIT License 6 votes vote down vote up
drawBuildReport = (outputs: RollupOutput[]) => {
    const toAsset = (value: OutputAsset | OutputChunk): Asset => {
        return {
            name: value.fileName,
            size: Buffer.byteLength(value.type === 'asset' ? value.source : value.code),
            initial: value.type === 'chunk' && value.isEntry,
        };
    };
    const assets = outputs.flatMap(v => v.output.map(toAsset));
    drawAssetReport(assets);
}
Example #2
Source File: index.test.ts    From reskript with MIT License 6 votes vote down vote up
build = async (options: Pick<VirtualEntryOptions, 'favicon'>) => {
    const entries = [
        createEntry('index'),
        createEntry('about'),
    ];
    const entryOptions: VirtualEntryOptions = {
        ...options,
        entries,
        publicPath: '/',
        defaultEntry: entries[0],
        buildTarget: 'stable',
        customizeMiddleware: () => {},
    };
    const config: InlineConfig = {
        root: path.join(currentDirectory, 'fixtures'),
        logLevel: 'warn',
        build: {
            rollupOptions: {
                input: Object.entries(entries).reduce(
                    (input, [name, {entry}]) => Object.assign(input, {[name]: entry}),
                    {} as Record<string, string>
                ),
            },
        },
        plugins: [
            virtualEntry(entryOptions),
        ],
    };
    const bundle = await vite.build(config) as RollupOutput;
    return {
        assets: bundle.output.filter((v: any): v is OutputAsset => v.type === 'asset'),
        chunks: bundle.output.filter((v: any): v is OutputChunk => v.type === 'chunk'),
    };
}
Example #3
Source File: plugin.ts    From vite-plugin-legacy with MIT License 5 votes vote down vote up
async function createLegacyChunk(
  mainChunk: OutputChunk,
  config: PluginConfig,
  viteConfig: ViteConfig
): Promise<OutputChunk> {
  const viteBuild = viteConfig.build

  // Transform the modern bundle into a dinosaur.
  const transformed = await babel.transformAsync(mainChunk.code, {
    configFile: false,
    inputSourceMap: mainChunk.map ?? undefined,
    sourceMaps: viteBuild.sourcemap,
    presets: [[require('@babel/preset-env'), getBabelEnv(config)]],
    plugins: !config.corejs
      ? [require('@babel/plugin-transform-regenerator')]
      : null,
  })

  const { code, map } = transformed || {}
  if (!code) {
    throw Error('[vite-plugin-legacy] Failed to transform modern bundle')
  }

  const legacyPath = mainChunk.fileName.replace(/\.js$/, '.legacy.js')
  const legacyPlugins: RollupPlugin[] = []

  // core-js imports are CommonJS modules.
  if (config.corejs)
    legacyPlugins.push(
      commonJS({
        sourceMap: !!viteBuild.sourcemap,
      })
    )

  // Provide our transformed code to Rollup.
  legacyPlugins.push({
    name: 'vite-legacy:resolve',
    resolveId(id) {
      if (id == legacyPath) return id
      if (/^(core-js|regenerator-runtime)\//.test(id)) {
        return require.resolve(id)
      }
    },
    load(id) {
      if (id == legacyPath) {
        return { code, map }
      }
    },
  })

  // Use rollup-plugin-terser even if "minify" option is esbuild.
  if (viteBuild.minify)
    legacyPlugins.push(
      require('rollup-plugin-terser').terser(viteBuild.terserOptions)
    )

  const rollup = require('rollup').rollup as typeof import('rollup').rollup

  // Prepare the module graph.
  const bundle = await rollup({
    input: legacyPath,
    plugins: legacyPlugins,
  })

  // Generate the legacy bundle.
  const { output } = await bundle.generate({
    entryFileNames: legacyPath,
    format: 'iife',
    sourcemap: viteBuild.sourcemap,
    sourcemapExcludeSources: true,
    inlineDynamicImports: true,
  })

  return output[0]
}
Example #4
Source File: rollup-plugin-html.test.ts    From web with MIT License 5 votes vote down vote up
function getChunk(output: Output, name: string) {
  return output.find(o => o.fileName === name && o.type === 'chunk') as OutputChunk;
}
Example #5
Source File: plugins.test.ts    From backstage with Apache License 2.0 4 votes vote down vote up
describe('forwardFileImports', () => {
  it('should be created', () => {
    const plugin = forwardFileImports({ include: /\.png$/ });
    expect(plugin.name).toBe('forward-file-imports');
  });

  it('should call through to original external option', async () => {
    const plugin = forwardFileImports({ include: /\.png$/ });
    const external = jest.fn((id: string) => id.endsWith('external'));

    const options = (await plugin.options?.call(context, { external }))!;
    if (typeof options.external !== 'function') {
      throw new Error('options.external is not a function');
    }

    expect(external).toHaveBeenCalledTimes(0);
    expect(options.external('./my-module', '/dev/src/index.ts', false)).toBe(
      false,
    );
    expect(external).toHaveBeenCalledTimes(1);
    expect(options.external('./my-external', '/dev/src/index.ts', false)).toBe(
      true,
    );
    expect(external).toHaveBeenCalledTimes(2);
    expect(options.external('./my-image.png', '/dev/src/index.ts', false)).toBe(
      true,
    );
    expect(external).toHaveBeenCalledTimes(3);
    expect(options.external('./my-image.png', '/dev/src/index.ts', true)).toBe(
      true,
    );
    expect(external).toHaveBeenCalledTimes(4);

    expect(() =>
      (options as any).external('./my-image.png', undefined, false),
    ).toThrow('Unknown importer of file module ./my-image.png');
  });

  it('should handle original external array', async () => {
    const plugin = forwardFileImports({ include: /\.png$/ });

    const options = (await plugin.options?.call(context, {
      external: ['my-external'],
    }))!;
    if (typeof options.external !== 'function') {
      throw new Error('options.external is not a function');
    }

    expect(options.external('my-module', '/dev/src/index.ts', false)).toBe(
      false,
    );
    expect(options.external('my-external', '/dev/src/index.ts', false)).toBe(
      true,
    );
    expect(options.external('my-image.png', '/dev/src/index.ts', false)).toBe(
      true,
    );
  });

  describe('with mock fs', () => {
    beforeEach(() => {
      mockFs({
        '/dev/src/my-module.ts': '',
        '/dev/src/dir/my-image.png': 'my-image',
      });
    });

    afterEach(() => {
      mockFs.restore();
    });

    it('should extract files', async () => {
      const plugin = forwardFileImports({ include: /\.png$/ });

      const options = (await plugin.options?.call(context, {}))!;
      if (typeof options.external !== 'function') {
        throw new Error('options.external is not a function');
      }

      expect(options.external('./my-module', '/dev/src/index.ts', false)).toBe(
        false,
      );
      expect(
        options.external('./my-image.png', '/dev/src/dir/index.ts', false),
      ).toBe(true);

      const outPath = '/dev/dist/dir/my-image.png';
      await expect(fs.pathExists(outPath)).resolves.toBe(false);

      await plugin.generateBundle?.call(
        context,
        { dir: '/dev/dist' } as NormalizedOutputOptions,
        {
          ['index.js']: {
            type: 'chunk',
            facadeModuleId: '/dev/src/index.ts',
          } as OutputChunk,
        },
        false, // isWrite = false -> no write
      );
      await expect(fs.pathExists(outPath)).resolves.toBe(false);

      await plugin.generateBundle?.call(
        context,
        { dir: '/dev/dist' } as NormalizedOutputOptions,
        {
          // output assets should not cause a write
          ['index.js']: { type: 'asset' } as OutputAsset,
          // missing facadeModuleId should not cause a write either
          ['index2.js']: { type: 'chunk' } as OutputChunk,
        },
        true,
      );
      await expect(fs.pathExists(outPath)).resolves.toBe(false);

      // output chunk + isWrite -> generate files
      await plugin.generateBundle?.call(
        context,
        { dir: '/dev/dist' } as NormalizedOutputOptions,
        {
          ['index.js']: {
            type: 'chunk',
            facadeModuleId: '/dev/src/index.ts',
          } as OutputChunk,
        },
        true,
      );
      await expect(fs.pathExists(outPath)).resolves.toBe(true);

      // should not break when triggering another write
      await plugin.generateBundle?.call(
        context,
        { file: '/dev/dist/my-output.js' } as NormalizedOutputOptions,
        {
          ['index.js']: {
            type: 'chunk',
            facadeModuleId: '/dev/src/index.ts',
          } as OutputChunk,
        },
        true,
      );
    });
  });
});
Example #6
Source File: plugins.ts    From backstage with Apache License 2.0 4 votes vote down vote up
/**
 * This rollup plugin leaves all encountered asset imports as-is, but
 * copies the imported files into the output directory.
 *
 * For example `import ImageUrl from './my-image.png'` inside `src/MyComponent` will
 * cause `src/MyComponent/my-image.png` to be copied to the output directory at the
 * path `dist/MyComponent/my-image.png`. The import itself will stay, but be resolved,
 * resulting in something like `import ImageUrl from './MyComponent/my-image.png'`
 */
export function forwardFileImports(options: ForwardFileImportsOptions): Plugin {
  const filter = createFilter(options.include, options.exclude);

  // We collect the absolute paths to all files we want to bundle into the
  // output dir here. Resolving to relative paths in the output dir happens later.
  const exportedFiles = new Set<string>();

  // We keep track of output directories that we've already copied files
  // into, so that we don't duplicate that work
  const generatedFor = new Set<string>();

  return {
    name: 'forward-file-imports',
    async generateBundle(outputOptions, bundle, isWrite) {
      if (!isWrite) {
        return;
      }

      const dir = outputOptions.dir || dirname(outputOptions.file!);
      if (generatedFor.has(dir)) {
        return;
      }

      for (const output of Object.values(bundle)) {
        if (output.type !== 'chunk') {
          continue;
        }
        const chunk = output as OutputChunk;

        // This'll be an absolute path pointing to the initial index file of the
        // build, and we use it to find the location of the `src` dir
        if (!chunk.facadeModuleId) {
          continue;
        }
        generatedFor.add(dir);

        // We're assuming that the index file is at the root of the source dir, and
        // that all assets exist within that dir.
        const srcRoot = dirname(chunk.facadeModuleId);

        // Copy all the files we found into the dist dir
        await Promise.all(
          Array.from(exportedFiles).map(async exportedFile => {
            const outputPath = relativePath(srcRoot, exportedFile);
            const targetFile = resolvePath(dir, outputPath);

            await fs.ensureDir(dirname(targetFile));
            await fs.copyFile(exportedFile, targetFile);
          }),
        );
        return;
      }
    },
    options(inputOptions) {
      const origExternal = inputOptions.external;

      // We decorate any existing `external` option with our own way of determining
      // if a module should be external. The can't use `resolveId`, since asset files
      // aren't passed there, might be some better way to do this though.
      const external: InputOptions['external'] = (id, importer, isResolved) => {
        // Call to inner external option
        if (
          typeof origExternal === 'function' &&
          origExternal(id, importer, isResolved)
        ) {
          return true;
        }

        if (Array.isArray(origExternal) && origExternal.includes(id)) {
          return true;
        }

        // The piece that we're adding
        if (!filter(id)) {
          return false;
        }

        // Sanity check, dunno if this can happen
        if (!importer) {
          throw new Error(`Unknown importer of file module ${id}`);
        }

        // Resolve relative imports to the full file URL, for deduping and copying later
        const fullId = isResolved ? id : resolvePath(dirname(importer), id);
        exportedFiles.add(fullId);

        // Treating this module as external from here, meaning rollup won't try to
        // put it in the output bundle, but still keep track of the relative imports
        // as needed in the output code.
        return true;
      };

      return { ...inputOptions, external };
    },
  };
}
Example #7
Source File: rollup-plugin-html.test.ts    From web with MIT License 4 votes vote down vote up
describe('rollup-plugin-html', () => {
  it('can build with an input path as input', async () => {
    const config = {
      plugins: [
        rollupPluginHTML({
          input: require.resolve('./fixtures/rollup-plugin-html/index.html'),
          rootDir,
        }),
      ],
    };
    const bundle = await rollup(config);
    const { output } = await bundle.generate(outputConfig);
    expect(output.length).to.equal(4);
    const { code: entryA } = getChunk(output, 'entrypoint-a.js');
    const { code: entryB } = getChunk(output, 'entrypoint-b.js');
    expect(entryA).to.include("console.log('entrypoint-a.js');");
    expect(entryB).to.include("console.log('entrypoint-b.js');");
    expect(stripNewlines(getAsset(output, 'index.html').source)).to.equal(
      '<html><head></head><body><h1>hello world</h1>' +
        '<script type="module" src="./entrypoint-a.js"></script>' +
        '<script type="module" src="./entrypoint-b.js"></script>' +
        '</body></html>',
    );
  });

  it('can build with html file as rollup input', async () => {
    const config = {
      input: require.resolve('./fixtures/rollup-plugin-html/index.html'),
      plugins: [rollupPluginHTML({ rootDir })],
    };
    const bundle = await rollup(config);
    const { output } = await bundle.generate(outputConfig);
    expect(output.length).to.equal(4);
    const { code: entryA } = getChunk(output, 'entrypoint-a.js');
    const { code: entryB } = getChunk(output, 'entrypoint-b.js');
    expect(entryA).to.include("console.log('entrypoint-a.js');");
    expect(entryB).to.include("console.log('entrypoint-b.js');");
    expect(stripNewlines(getAsset(output, 'index.html').source)).to.equal(
      '<html><head></head><body><h1>hello world</h1>' +
        '<script type="module" src="./entrypoint-a.js"></script>' +
        '<script type="module" src="./entrypoint-b.js"></script>' +
        '</body></html>',
    );
  });

  it('will retain attributes on script tags', async () => {
    const config = {
      input: require.resolve('./fixtures/rollup-plugin-html/retain-attributes.html'),
      plugins: [rollupPluginHTML({ rootDir })],
    };
    const bundle = await rollup(config);
    const { output } = await bundle.generate(outputConfig);
    expect(output.length).to.equal(4);
    const { code: entryA } = getChunk(output, 'entrypoint-a.js');
    const { code: entryB } = getChunk(output, 'entrypoint-b.js');
    expect(entryA).to.include("console.log('entrypoint-a.js');");
    expect(entryB).to.include("console.log('entrypoint-b.js');");
    expect(stripNewlines(getAsset(output, 'retain-attributes.html').source)).to.equal(
      '<html><head></head><body><h1>hello world</h1>' +
        '<script type="module" src="./entrypoint-a.js" keep-this-attribute=""></script>' +
        '<script type="module" src="./entrypoint-b.js"></script>' +
        '</body></html>',
    );
  });

  it('can build with pure html file as rollup input', async () => {
    const config = {
      input: require.resolve('./fixtures/rollup-plugin-html/pure-index.html'),
      plugins: [rollupPluginHTML({ rootDir })],
    };
    const bundle = await rollup(config);
    const { output } = await bundle.generate(outputConfig);
    expect(stripNewlines(getAsset(output, 'pure-index.html').source)).to.equal(
      '<html><head></head><body><h1>hello world</h1></body></html>',
    );
  });

  it('can build with multiple pure html inputs', async () => {
    const config = {
      plugins: [
        rollupPluginHTML({
          input: [
            require.resolve('./fixtures/rollup-plugin-html/pure-index.html'),
            require.resolve('./fixtures/rollup-plugin-html/pure-index2.html'),
          ],
          rootDir,
        }),
      ],
    };
    const bundle = await rollup(config);
    const { output } = await bundle.generate(outputConfig);
    expect(stripNewlines(getAsset(output, 'pure-index.html').source)).to.equal(
      '<html><head></head><body><h1>hello world</h1></body></html>',
    );
    expect(stripNewlines(getAsset(output, 'pure-index2.html').source)).to.equal(
      '<html><head></head><body><h1>hey there</h1></body></html>',
    );
  });

  it('can build with html string as input', async () => {
    const config = {
      plugins: [
        rollupPluginHTML({
          input: {
            name: 'index.html',
            html: '<h1>Hello world</h1><script type="module" src="./entrypoint-a.js"></script>',
          },
          rootDir,
        }),
      ],
    };

    const bundle = await rollup(config);
    const { output } = await bundle.generate(outputConfig);
    expect(output.length).to.equal(2);
    expect(stripNewlines(getAsset(output, 'index.html').source)).to.equal(
      '<html><head></head><body><h1>Hello world</h1>' +
        '<script type="module" src="./entrypoint-a.js"></script></body></html>',
    );
  });

  it('resolves paths relative to virtual html filename', async () => {
    const config = {
      plugins: [
        rollupPluginHTML({
          input: {
            name: 'pages/index.html',
            html: '<h1>Hello world</h1><script type="module" src="../entrypoint-a.js"></script>',
          },
          rootDir,
        }),
      ],
    };

    const bundle = await rollup(config);
    const { output } = await bundle.generate(outputConfig);
    expect(output.length).to.equal(2);
    expect(stripNewlines(getAsset(output, 'pages/index.html').source)).to.equal(
      '<html><head></head><body><h1>Hello world</h1>' +
        '<script type="module" src="../entrypoint-a.js"></script></body></html>',
    );
  });

  it('can build with inline modules', async () => {
    const config = {
      plugins: [
        rollupPluginHTML({
          rootDir,
          input: {
            name: 'index.html',
            html: '<h1>Hello world</h1><script type="module">import "./entrypoint-a.js";</script>',
          },
        }),
      ],
    };

    const bundle = await rollup(config);
    const { output } = await bundle.generate(outputConfig);
    expect(output.length).to.equal(2);
    const hash = '5ec680a4efbb48ae254268ab1defe610';
    const { code: appCode } = getChunk(output, `inline-module-${hash}.js`);
    expect(appCode).to.include("console.log('entrypoint-a.js');");
    expect(stripNewlines(getAsset(output, 'index.html').source)).to.equal(
      '<html><head></head><body><h1>Hello world</h1>' +
        `<script type="module" src="./inline-module-${hash}.js"></script>` +
        '</body></html>',
    );
  });

  it('resolves inline module imports relative to the HTML file', async () => {
    const config = {
      plugins: [
        rollupPluginHTML({
          input: require.resolve('./fixtures/rollup-plugin-html/foo/foo.html'),
          rootDir,
        }),
      ],
    };
    const bundle = await rollup(config);
    const { output } = await bundle.generate(outputConfig);
    expect(output.length).to.equal(2);
    const { code: appCode } = getChunk(output, 'inline-module-1b13383486c70d87f4e2585ff87b147c.js');
    expect(appCode).to.include("console.log('foo');");
  });

  it('can build transforming final output', async () => {
    const config = {
      input: require.resolve('./fixtures/rollup-plugin-html/entrypoint-a.js'),
      plugins: [
        rollupPluginHTML({
          rootDir,
          input: {
            html: '<h1>Hello world</h1><script type="module" src="./entrypoint-a.js"></script>',
          },
          transformHtml(html) {
            return html.replace('Hello world', 'Goodbye world');
          },
        }),
      ],
    };
    const bundle = await rollup(config);
    const { output } = await bundle.generate(outputConfig);
    expect(output.length).to.equal(2);
    expect(getAsset(output, 'index.html').source).to.equal(
      '<html><head></head><body><h1>Goodbye world</h1>' +
        '<script type="module" src="./entrypoint-a.js"></script></body></html>',
    );
  });

  it('can build with a public path', async () => {
    const config = {
      input: require.resolve('./fixtures/rollup-plugin-html/entrypoint-a.js'),
      plugins: [
        rollupPluginHTML({
          rootDir,
          input: {
            html: '<h1>Hello world</h1><script type="module" src="./entrypoint-a.js"></script>',
          },
          publicPath: '/static/',
        }),
      ],
    };
    const bundle = await rollup(config);
    const { output } = await bundle.generate(outputConfig);
    expect(output.length).to.equal(2);
    expect(getAsset(output, 'index.html').source).to.equal(
      '<html><head></head><body><h1>Hello world</h1>' +
        '<script type="module" src="/static/entrypoint-a.js"></script></body></html>',
    );
  });

  it('can build with a public path with a file in a directory', async () => {
    const config = {
      input: require.resolve('./fixtures/rollup-plugin-html/entrypoint-a.js'),
      plugins: [
        rollupPluginHTML({
          rootDir,
          input: {
            name: 'pages/index.html',
            html: '<h1>Hello world</h1><script type="module" src="../entrypoint-a.js"></script>',
          },
          publicPath: '/static/',
        }),
      ],
    };
    const bundle = await rollup(config);
    const { output } = await bundle.generate(outputConfig);
    expect(output.length).to.equal(2);
    expect(getAsset(output, 'pages/index.html').source).to.equal(
      '<html><head></head><body><h1>Hello world</h1>' +
        '<script type="module" src="/static/entrypoint-a.js"></script></body></html>',
    );
  });

  it('can build with multiple build outputs', async () => {
    const plugin = rollupPluginHTML({
      rootDir,
      input: {
        html: '<h1>Hello world</h1><script type="module" src="./entrypoint-a.js"></script>',
      },
      publicPath: '/static/',
    });
    const config = {
      input: require.resolve('./fixtures/rollup-plugin-html/entrypoint-a.js'),
      plugins: [plugin],
    };
    const build = await rollup(config);
    const bundleA = build.generate({
      format: 'system',
      dir: 'dist',
      plugins: [plugin.api.addOutput('legacy')],
    });
    const bundleB = build.generate({
      format: 'es',
      dir: 'dist',
      plugins: [plugin.api.addOutput('modern')],
    });
    const { output: outputA } = await bundleA;
    const { output: outputB } = await bundleB;
    expect(outputA.length).to.equal(1);
    expect(outputB.length).to.equal(2);
    const { code: entrypointA1 } = getChunk(outputA, 'entrypoint-a.js');
    const { code: entrypointA2 } = getChunk(outputB, 'entrypoint-a.js');
    expect(entrypointA1).to.include("console.log('entrypoint-a.js');");
    expect(entrypointA1).to.include("console.log('module-a.js');");
    expect(entrypointA2).to.include("console.log('entrypoint-a.js');");
    expect(entrypointA2).to.include("console.log('module-a.js');");
    expect(getAsset(outputA, 'index.html')).to.not.exist;
    expect(getAsset(outputB, 'index.html').source).to.equal(
      '<html><head></head><body><h1>Hello world</h1>' +
        '<script>System.import("/static/entrypoint-a.js");</script>' +
        '<script type="module" src="/static/entrypoint-a.js"></script></body></html>',
    );
  });

  it('can build with index.html as input and an extra html file as output', async () => {
    const config = {
      plugins: [
        rollupPluginHTML({
          rootDir,
          input: {
            html: '<h1>Hello world</h1><script type="module" src="./entrypoint-a.js"></script>',
          },
        }),
        rollupPluginHTML({
          rootDir,
          input: {
            name: 'foo.html',
            html: '<html><body><h1>foo.html</h1></body></html>',
          },
        }),
      ],
    };
    const bundle = await rollup(config);
    const { output } = await bundle.generate(outputConfig);
    expect(output.length).to.equal(4);
    expect(getChunk(output, 'entrypoint-a.js')).to.exist;
    expect(getAsset(output, 'index.html').source).to.equal(
      '<html><head></head><body><h1>Hello world</h1>' +
        '<script type="module" src="./entrypoint-a.js"></script></body></html>',
    );
    expect(getAsset(output, 'foo.html').source).to.equal(
      '<html><head></head><body><h1>foo.html</h1></body></html>',
    );
  });

  it('can build with multiple html inputs', async () => {
    const config = {
      plugins: [
        rollupPluginHTML({
          rootDir,
          input: [
            {
              name: 'page-a.html',
              html: `<h1>Page A</h1><script type="module" src="./entrypoint-a.js"></script>`,
            },
            {
              name: 'page-b.html',
              html: `<h1>Page B</h1><script type="module" src="./entrypoint-b.js"></script>`,
            },
            {
              name: 'page-c.html',
              html: `<h1>Page C</h1><script type="module" src="./entrypoint-c.js"></script>`,
            },
          ],
        }),
      ],
    };
    const bundle = await rollup(config);
    const { output } = await bundle.generate(outputConfig);
    expect(output.length).to.equal(7);
    expect(getChunk(output, 'entrypoint-a.js')).to.exist;
    expect(getChunk(output, 'entrypoint-b.js')).to.exist;
    expect(getChunk(output, 'entrypoint-c.js')).to.exist;
    expect(getAsset(output, 'page-a.html').source).to.equal(
      '<html><head></head><body><h1>Page A</h1><script type="module" src="./entrypoint-a.js"></script></body></html>',
    );
    expect(getAsset(output, 'page-b.html').source).to.equal(
      '<html><head></head><body><h1>Page B</h1><script type="module" src="./entrypoint-b.js"></script></body></html>',
    );
    expect(getAsset(output, 'page-c.html').source).to.equal(
      '<html><head></head><body><h1>Page C</h1><script type="module" src="./entrypoint-c.js"></script></body></html>',
    );
  });

  it('can use a glob to build multiple pages', async () => {
    const config = {
      plugins: [
        rollupPluginHTML({
          rootDir,
          input: 'pages/**/*.html',
        }),
      ],
    };

    const bundle = await rollup(config);
    const { output } = await bundle.generate(outputConfig);
    const pageA = getAsset(output, 'page-a.html').source;
    const pageB = getAsset(output, 'page-b.html').source;
    const pageC = getAsset(output, 'page-c.html').source;
    expect(output.length).to.equal(7);
    expect(getChunk(output, 'page-a.js')).to.exist;
    expect(getChunk(output, 'page-b.js')).to.exist;
    expect(getChunk(output, 'page-c.js')).to.exist;
    expect(pageA).to.include('<p>page-a.html</p>');
    expect(pageA).to.include('<script type="module" src="./page-a.js"></script>');
    expect(pageA).to.include('<script type="module" src="./shared.js"></script>');
    expect(pageB).to.include('<p>page-b.html</p>');
    expect(pageB).to.include('<script type="module" src="./page-b.js"></script>');
    expect(pageB).to.include('<script type="module" src="./shared.js"></script>');
    expect(pageC).to.include('<p>page-c.html</p>');
    expect(pageC).to.include('<script type="module" src="./page-c.js"></script>');
    expect(pageC).to.include('<script type="module" src="./shared.js"></script>');
  });

  it('can exclude globs', async () => {
    const config = {
      plugins: [
        rollupPluginHTML({
          input: 'exclude/**/*.html',
          exclude: '**/partial.html',
          rootDir,
        }),
      ],
    };

    const bundle = await rollup(config);
    const { output } = await bundle.generate(outputConfig);
    expect(output.length).to.equal(2);
  });

  it('creates unique inline script names', async () => {
    const config = {
      plugins: [
        rollupPluginHTML({
          rootDir,
          input: [
            {
              name: 'foo/index.html',
              html: '<h1>Page A</h1><script type="module">console.log("A")</script>',
            },
            {
              name: 'bar/index.html',
              html: '<h1>Page B</h1><script type="module">console.log("B")</script>',
            },
            {
              name: 'x.html',
              html: '<h1>Page C</h1><script type="module">console.log("C")</script>',
            },
          ],
        }),
      ],
    };
    const bundle = await rollup(config);
    const { output } = await bundle.generate(outputConfig);
    expect(output.length).to.equal(6);
    expect(getChunk(output, 'inline-module-b8667c926d8a16ee8b4499492c1726ed.js')).to.exist;
    expect(getChunk(output, 'inline-module-c91911481b66e7483731d4de5df616a6.js')).to.exist;
    expect(getChunk(output, 'inline-module-fbf0242ebea027b7392472c19328791d.js')).to.exist;
    expect(getAsset(output, 'foo/index.html').source).to.equal(
      '<html><head></head><body><h1>Page A</h1><script type="module" src="../inline-module-b8667c926d8a16ee8b4499492c1726ed.js"></script></body></html>',
    );
    expect(getAsset(output, 'bar/index.html').source).to.equal(
      '<html><head></head><body><h1>Page B</h1><script type="module" src="../inline-module-c91911481b66e7483731d4de5df616a6.js"></script></body></html>',
    );
    expect(getAsset(output, 'x.html').source).to.equal(
      '<html><head></head><body><h1>Page C</h1><script type="module" src="./inline-module-fbf0242ebea027b7392472c19328791d.js"></script></body></html>',
    );
  });

  it('deduplicates common modules', async () => {
    const config = {
      plugins: [
        rollupPluginHTML({
          rootDir,
          input: [
            {
              name: 'a.html',
              html: '<h1>Page A</h1><script type="module">console.log("A")</script>',
            },
            {
              name: 'b.html',
              html: '<h1>Page B</h1><script type="module">console.log("A")</script>',
            },
            {
              name: 'c.html',
              html: '<h1>Page C</h1><script type="module">console.log("A")</script>',
            },
          ],
        }),
      ],
    };
    const bundle = await rollup(config);
    const { output } = await bundle.generate(outputConfig);
    expect(output.length).to.equal(4);
    expect(getChunk(output, 'inline-module-b8667c926d8a16ee8b4499492c1726ed.js')).to.exist;
    expect(getAsset(output, 'a.html').source).to.equal(
      '<html><head></head><body><h1>Page A</h1><script type="module" src="./inline-module-b8667c926d8a16ee8b4499492c1726ed.js"></script></body></html>',
    );
    expect(getAsset(output, 'b.html').source).to.equal(
      '<html><head></head><body><h1>Page B</h1><script type="module" src="./inline-module-b8667c926d8a16ee8b4499492c1726ed.js"></script></body></html>',
    );
    expect(getAsset(output, 'c.html').source).to.equal(
      '<html><head></head><body><h1>Page C</h1><script type="module" src="./inline-module-b8667c926d8a16ee8b4499492c1726ed.js"></script></body></html>',
    );
  });

  it('outputs the hashed entrypoint name', async () => {
    const config = {
      plugins: [
        rollupPluginHTML({
          rootDir,
          input: {
            html:
              '<h1>Hello world</h1>' + `<script type="module" src="./entrypoint-a.js"></script>`,
          },
        }),
      ],
    };
    const bundle = await rollup(config);
    const { output } = await bundle.generate({
      ...outputConfig,
      entryFileNames: '[name]-[hash].js',
    });
    expect(output.length).to.equal(2);
    const entrypoint = output.find(f =>
      // @ts-ignore
      f.facadeModuleId.endsWith('entrypoint-a.js'),
    ) as OutputChunk;
    // ensure it's actually hashed
    expect(entrypoint.fileName).to.not.equal('entrypoint-a.js');
    // get hashed name dynamically
    expect(getAsset(output, 'index.html').source).to.equal(
      `<html><head></head><body><h1>Hello world</h1><script type="module" src="./${entrypoint.fileName}"></script></body></html>`,
    );
  });

  it('outputs import path relative to the final output html', async () => {
    const config = {
      plugins: [
        rollupPluginHTML({
          rootDir,
          input: {
            name: 'pages/index.html',
            html: '<h1>Hello world</h1><script type="module" src="../entrypoint-a.js"></script>',
          },
        }),
      ],
    };
    const bundle = await rollup(config);
    const { output } = await bundle.generate(outputConfig);
    expect(output.length).to.equal(2);
    expect(getAsset(output, 'pages/index.html').source).to.equal(
      '<html><head></head><body><h1>Hello world</h1><script type="module" src="../entrypoint-a.js"></script></body></html>',
    );
  });

  it('can change HTML root directory', async () => {
    const config = {
      plugins: [
        rollupPluginHTML({
          rootDir: path.join(__dirname, 'fixtures'),
          input: {
            name: 'rollup-plugin-html/pages/index.html',
            html: '<h1>Hello world</h1><script type="module" src="../entrypoint-a.js"></script>',
          },
        }),
      ],
    };
    const bundle = await rollup(config);
    const { output } = await bundle.generate(outputConfig);
    expect(output.length).to.equal(2);
    expect(getAsset(output, 'rollup-plugin-html/pages/index.html').source).to.equal(
      '<html><head></head><body><h1>Hello world</h1><script type="module" src="../../entrypoint-a.js"></script></body></html>',
    );
  });

  it('can get the input with getInputs()', async () => {
    // default filename
    const pluginA = rollupPluginHTML({ input: { html: 'Hello world' } });
    // filename inferred from input filename
    const pluginB = rollupPluginHTML({
      input: require.resolve('./fixtures/rollup-plugin-html/my-page.html'),
    });
    // filename explicitly set
    const pluginC = rollupPluginHTML({
      input: {
        name: 'pages/my-other-page.html',
        path: require.resolve('./fixtures/rollup-plugin-html/index.html'),
      },
    });
    await rollup({
      input: require.resolve('./fixtures/rollup-plugin-html/entrypoint-a.js'),
      plugins: [pluginA],
    });
    await rollup({ plugins: [pluginB] });
    await rollup({ plugins: [pluginC] });
    expect(pluginA.api.getInputs()[0].name).to.equal('index.html');
    expect(pluginB.api.getInputs()[0].name).to.equal('my-page.html');
    expect(pluginC.api.getInputs()[0].name).to.equal('pages/my-other-page.html');
  });

  it('supports other plugins injecting a transform function', async () => {
    const config = {
      plugins: [
        rollupPluginHTML({
          rootDir,
          input: require.resolve('./fixtures/rollup-plugin-html/index.html'),
        }),
        {
          name: 'other-plugin',
          buildStart(options) {
            if (!options.plugins) throw new Error('no plugins');
            const plugin = options.plugins.find(pl => {
              if (pl.name === '@web/rollup-plugin-html') {
                return pl!.api.getInputs()[0].name === 'index.html';
              }
              return false;
            });
            plugin!.api.addHtmlTransformer((html: string) =>
              html.replace('</body>', '<!-- injected --></body>'),
            );
          },
        } as Plugin,
      ],
    };
    const bundle = await rollup(config);
    const { output } = await bundle.generate(outputConfig);
    expect(output.length).to.equal(4);
    const { code: entryA } = getChunk(output, 'entrypoint-a.js');
    const { code: entryB } = getChunk(output, 'entrypoint-b.js');
    expect(entryA).to.include("console.log('entrypoint-a.js');");
    expect(entryB).to.include("console.log('entrypoint-b.js');");
    expect(stripNewlines(getAsset(output, 'index.html').source)).to.equal(
      '<html><head></head><body><h1>hello world</h1>' +
        '<script type="module" src="./entrypoint-a.js"></script>' +
        '<script type="module" src="./entrypoint-b.js"></script>' +
        '<!-- injected --></body></html>',
    );
  });

  it('includes referenced assets in the bundle', async () => {
    const config = {
      plugins: [
        rollupPluginHTML({
          input: {
            html: `<html>
<head>
<link rel="apple-touch-icon" sizes="180x180" href="./image-a.png" />
<link rel="icon" type="image/png" sizes="32x32" href="./image-b.png" />
<link rel="manifest" href="./webmanifest.json" />
<link rel="mask-icon" href="./image-a.svg" color="#3f93ce" />
<link rel="stylesheet" href="./styles.css" />
<link rel="stylesheet" href="./foo/x.css" />
<link rel="stylesheet" href="./foo/bar/y.css" />
</head>
<body>
<img src="./image-c.png" />
<div>
<img src="./image-b.svg" />
</div>
</body>
</html>`,
          },
          rootDir: path.join(__dirname, 'fixtures', 'assets'),
        }),
      ],
    };

    const bundle = await rollup(config);
    const { output } = await bundle.generate(outputConfig);
    expect(output.length).to.equal(11);
    const expectedAssets = [
      'image-c.png',
      'webmanifest.json',
      'image-a.svg',
      'styles.css',
      'x.css',
      'y.css',
      'image-b.svg',
    ];

    for (const name of expectedAssets) {
      const asset = getAsset(output, name);
      expect(asset).to.exist;
      expect(asset.source).to.exist;
    }

    const outputHtml = getAsset(output, 'index.html').source;
    expect(outputHtml).to.include(
      '<link rel="apple-touch-icon" sizes="180x180" href="assets/image-a.png">',
    );
    expect(outputHtml).to.include(
      '<link rel="icon" type="image/png" sizes="32x32" href="assets/image-b.png">',
    );
    expect(outputHtml).to.include('<link rel="manifest" href="assets/webmanifest.json">');
    expect(outputHtml).to.include(
      '<link rel="mask-icon" href="assets/image-a.svg" color="#3f93ce">',
    );
    expect(outputHtml).to.include('<link rel="stylesheet" href="assets/styles-ed723e17.css">');
    expect(outputHtml).to.include('<link rel="stylesheet" href="assets/x-58ef5070.css">');
    expect(outputHtml).to.include('<link rel="stylesheet" href="assets/y-4f2d398e.css">');
    expect(outputHtml).to.include('<img src="assets/image-c-23edadf6.png">');
    expect(outputHtml).to.include('<img src="assets/image-b-ee32b49e.svg">');
  });

  it('deduplicates static assets with similar names', async () => {
    const config = {
      plugins: [
        rollupPluginHTML({
          input: {
            html: `<html>
<head>
<link rel="icon" type="image/png" sizes="32x32" href="./foo.svg" />
<link rel="mask-icon" href="./x/foo.svg" color="#3f93ce" />
</head>
</html>`,
          },
          rootDir: path.join(__dirname, 'fixtures', 'assets'),
        }),
      ],
    };

    const bundle = await rollup(config);
    const { output } = await bundle.generate(outputConfig);

    expect(stripNewlines(getAsset(output, 'index.html').source)).to.equal(
      '<html><head>' +
        '<link rel="icon" type="image/png" sizes="32x32" href="assets/foo.svg">' +
        '<link rel="mask-icon" href="assets/foo1.svg" color="#3f93ce">' +
        '</head><body></body></html>',
    );
  });

  it('static and hashed asset nodes can reference the same files', async () => {
    const config = {
      plugins: [
        rollupPluginHTML({
          input: {
            html: `<html>
<head>
<link rel="icon" type="image/png" sizes="32x32" href="./foo.svg">
<img src="./foo.svg">
</head>
</html>`,
          },
          rootDir: path.join(__dirname, 'fixtures', 'assets'),
        }),
      ],
    };

    const bundle = await rollup(config);
    const { output } = await bundle.generate(outputConfig);

    expect(stripNewlines(getAsset(output, 'index.html').source)).to.equal(
      '<html><head><link rel="icon" type="image/png" sizes="32x32" href="assets/foo.svg"></head>' +
        '<body><img src="assets/foo-81034cb4.svg"></body></html>',
    );
  });

  it('deduplicates common assets', async () => {
    const config = {
      plugins: [
        rollupPluginHTML({
          input: {
            html: `<html>
<body>
<link rel="stylesheet" href="./image-a.png">
<img src="./image-a.png">
<img src="./image-a.png">
</body>
</html>`,
          },
          rootDir: path.join(__dirname, 'fixtures', 'assets'),
        }),
      ],
    };

    const bundle = await rollup(config);
    const { output } = await bundle.generate(outputConfig);

    expect(stripNewlines(getAsset(output, 'index.html').source)).to.equal(
      '<html><head></head><body>' +
        '<link rel="stylesheet" href="assets/image-a-9c3a45f9.png">' +
        '<img src="assets/image-a-9c3a45f9.png">' +
        '<img src="assets/image-a-9c3a45f9.png">' +
        '</body></html>',
    );
  });

  it('deduplicates common assets across HTML files', async () => {
    const config = {
      plugins: [
        rollupPluginHTML({
          input: [
            {
              name: 'page-a.html',
              html: `<html>
  <body>
  <img src="./image-a.png">
  </body>
  </html>`,
            },
            {
              name: 'page-b.html',
              html: `<html>
  <body>
  <link rel="stylesheet" href="./image-a.png">
  </body>
  </html>`,
            },
            {
              name: 'page-c.html',
              html: `<html>
  <body>
  <link rel="stylesheet" href="./image-a.png">
  <img src="./image-a.png">
  </body>
  </html>`,
            },
          ],
          rootDir: path.join(__dirname, 'fixtures', 'assets'),
        }),
      ],
    };

    const bundle = await rollup(config);
    const { output } = await bundle.generate(outputConfig);

    expect(stripNewlines(getAsset(output, 'page-a.html').source)).to.equal(
      '<html><head></head><body>' +
        '  <img src="assets/image-a-9c3a45f9.png">' +
        '    </body></html>',
    );

    expect(stripNewlines(getAsset(output, 'page-b.html').source)).to.equal(
      '<html><head></head><body>' +
        '  <link rel="stylesheet" href="assets/image-a-9c3a45f9.png">' +
        '    </body></html>',
    );

    expect(stripNewlines(getAsset(output, 'page-c.html').source)).to.equal(
      '<html><head></head><body>' +
        '  <link rel="stylesheet" href="assets/image-a-9c3a45f9.png">' +
        '  <img src="assets/image-a-9c3a45f9.png">' +
        '    </body></html>',
    );
  });

  it('can turn off extracting assets', async () => {
    const config = {
      plugins: [
        rollupPluginHTML({
          extractAssets: false,
          input: {
            html: `<html>
<body>
<img src="./image-c.png" />
<link rel="stylesheet" href="./styles.css" />
<img src="./image-b.svg" />
</body>
</html>`,
          },
          rootDir: path.join(__dirname, 'fixtures', 'assets'),
        }),
      ],
    };

    const bundle = await rollup(config);
    const { output } = await bundle.generate(outputConfig);

    expect(output.length).to.equal(2);
    expect(stripNewlines(getAsset(output, 'index.html').source)).to.equal(
      '<html><head></head><body><img src="./image-c.png"><link rel="stylesheet" href="./styles.css"><img src="./image-b.svg"></body></html>',
    );
  });

  it('can inject a CSP meta tag for inline scripts', async () => {
    const config = {
      plugins: [
        rollupPluginHTML({
          input: require.resolve('./fixtures/rollup-plugin-html/csp-page-a.html'),
          rootDir,
          strictCSPInlineScripts: true,
        }),
      ],
    };
    const bundle = await rollup(config);
    const { output } = await bundle.generate(outputConfig);
    expect(output.length).to.equal(4);
    const { code: entryA } = getChunk(output, 'entrypoint-a.js');
    const { code: entryB } = getChunk(output, 'entrypoint-b.js');
    expect(entryA).to.include("console.log('entrypoint-a.js');");
    expect(entryB).to.include("console.log('entrypoint-b.js');");
    expect(stripNewlines(getAsset(output, 'csp-page-a.html').source)).to.equal(
      '<html><head>' +
        "<meta http-equiv=\"Content-Security-Policy\" content=\"script-src 'self' 'sha256-k0fj3IHUtZNziFbz6LL40uxkFlr28beNcMKKtp5+EwE=' 'sha256-UJadfRwzUCb1ajAJFfAPl8NTvtyiHtltKG/12veER70=';\">" +
        '</head><body><h1>hello world</h1>' +
        "<script>console.log('foo');</script>" +
        "<script>console.log('bar');</script>" +
        '<script type="module" src="./entrypoint-a.js"></script>' +
        '<script type="module" src="./entrypoint-b.js"></script>' +
        '</body></html>',
    );
  });

  it('can add to an existing CSP meta tag for inline scripts', async () => {
    const config = {
      plugins: [
        rollupPluginHTML({
          input: require.resolve('./fixtures/rollup-plugin-html/csp-page-b.html'),
          rootDir,
          strictCSPInlineScripts: true,
        }),
      ],
    };
    const bundle = await rollup(config);
    const { output } = await bundle.generate(outputConfig);
    expect(output.length).to.equal(4);
    const { code: entryA } = getChunk(output, 'entrypoint-a.js');
    const { code: entryB } = getChunk(output, 'entrypoint-b.js');
    expect(entryA).to.include("console.log('entrypoint-a.js');");
    expect(entryB).to.include("console.log('entrypoint-b.js');");
    expect(stripNewlines(getAsset(output, 'csp-page-b.html').source)).to.equal(
      '<html><head>' +
        "<meta http-equiv=\"Content-Security-Policy\" content=\"default-src 'self'; prefetch-src 'self'; upgrade-insecure-requests; style-src 'self' 'unsafe-inline'; script-src 'self' 'sha256-k0fj3IHUtZNziFbz6LL40uxkFlr28beNcMKKtp5+EwE=' 'sha256-UJadfRwzUCb1ajAJFfAPl8NTvtyiHtltKG/12veER70=';\">" +
        '</head><body><h1>hello world</h1>' +
        "<script>console.log('foo');</script>" +
        "<script>console.log('bar');</script>" +
        '<script type="module" src="./entrypoint-a.js"></script>' +
        '<script type="module" src="./entrypoint-b.js"></script>' +
        '</body></html>',
    );
  });

  it('can add to an existing CSP meta tag for inline scripts even if script-src is already there', async () => {
    const config = {
      plugins: [
        rollupPluginHTML({
          input: require.resolve('./fixtures/rollup-plugin-html/csp-page-c.html'),
          rootDir,
          strictCSPInlineScripts: true,
        }),
      ],
    };
    const bundle = await rollup(config);
    const { output } = await bundle.generate(outputConfig);
    expect(output.length).to.equal(4);
    const { code: entryA } = getChunk(output, 'entrypoint-a.js');
    const { code: entryB } = getChunk(output, 'entrypoint-b.js');
    expect(entryA).to.include("console.log('entrypoint-a.js');");
    expect(entryB).to.include("console.log('entrypoint-b.js');");
    expect(stripNewlines(getAsset(output, 'csp-page-c.html').source)).to.equal(
      '<html><head>' +
        "<meta http-equiv=\"Content-Security-Policy\" content=\"default-src 'self'; prefetch-src 'self'; upgrade-insecure-requests; style-src 'self' 'unsafe-inline'; script-src 'self' 'sha256-k0fj3IHUtZNziFbz6LL40uxkFlr28beNcMKKtp5+EwE=' 'sha256-UJadfRwzUCb1ajAJFfAPl8NTvtyiHtltKG/12veER70=';\">" +
        '</head><body><h1>hello world</h1>' +
        "<script>console.log('foo');</script>" +
        "<script>console.log('bar');</script>" +
        '<script type="module" src="./entrypoint-a.js"></script>' +
        '<script type="module" src="./entrypoint-b.js"></script>' +
        '</body></html>',
    );
  });

  it('can inject a service worker registration script if injectServiceWorker and serviceWorkerPath are provided', async () => {
    const serviceWorkerPath = path.join(
      // @ts-ignore
      path.resolve(outputConfig.dir),
      'service-worker.js',
    );

    const config = {
      plugins: [
        rollupPluginHTML({
          input: '**/*.html',
          rootDir: path.join(__dirname, 'fixtures', 'inject-service-worker'),
          flattenOutput: false,
          injectServiceWorker: true,
          serviceWorkerPath,
        }),
      ],
    };
    const bundle = await rollup(config);
    const { output } = await bundle.generate(outputConfig);

    function extractServiceWorkerPath(src: string) {
      const registerOpen = src.indexOf(".register('");
      const registerClose = src.indexOf("')", registerOpen + 11);
      return src.substring(registerOpen + 11, registerClose);
    }

    expect(extractServiceWorkerPath(getAsset(output, 'index.html').source)).to.equal(
      'service-worker.js',
    );
    expect(
      extractServiceWorkerPath(getAsset(output, path.join('sub-with-js', 'index.html')).source),
    ).to.equal(`../service-worker.js`);
    expect(
      extractServiceWorkerPath(getAsset(output, path.join('sub-pure-html', 'index.html')).source),
    ).to.equal(`../service-worker.js`);
  });

  it('does support a absolutePathPrefix to allow for sub folder deployments', async () => {
    const config = {
      plugins: [
        rollupPluginHTML({
          input: {
            html: `<html>
<body>
<img src="/my-prefix/x/foo.svg" />
<link rel="stylesheet" href="../styles.css" />
<img src="../image-b.svg" />
</body>
</html>`,
            name: 'x/index.html',
          },
          rootDir: path.join(__dirname, 'fixtures', 'assets'),
          absolutePathPrefix: '/my-prefix/',
        }),
      ],
    };

    const bundle = await rollup(config);
    const { output } = await bundle.generate(outputConfig);

    expect(stripNewlines(getAsset(output, 'x/index.html').source)).to.equal(
      [
        '<html><head></head><body>',
        '<img src="../assets/foo-c9db7cc0.svg">',
        '<link rel="stylesheet" href="../assets/styles-ed723e17.css">',
        '<img src="../assets/image-b-ee32b49e.svg">',
        '</body></html>',
      ].join(''),
    );
  });
});