rollup#Plugin TypeScript Examples

The following examples show how to use rollup#Plugin. 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: rollup.ts    From Cromwell with MIT License 6 votes vote down vote up
scssExternalPlugin = (): Plugin => {
    return {
        name: 'cromwell-scssExternalPlugin',
        resolveId(source) {
            if (/\.s?css$/.test(source)) {
                return { id: source, external: true };
            }
        }
    }

}
Example #2
Source File: mdxPlugin.ts    From web with MIT License 6 votes vote down vote up
export function mdxPlugin(): Plugin {
  return {
    name: 'mdx',
    transform(code, id) {
      if (id.endsWith('.mdx')) {
        return transformMdxToCsf(code, id);
      }
    },
  };
}
Example #3
Source File: mdjsPlugin.ts    From web with MIT License 6 votes vote down vote up
export function mdjsPlugin(type: string): Plugin {
  return {
    name: 'md',
    transform(code, id) {
      if (id.endsWith('.md')) {
        return mdjsToCsf(code, id, type);
      }
    },
  };
}
Example #4
Source File: injectExportsOrderPlugin.ts    From web with MIT License 6 votes vote down vote up
export function injectExportsOrderPlugin(storyFilePaths: string[]): Plugin {
  return {
    name: 'mdx',
    transform(code, id) {
      if (storyFilePaths.includes(id)) {
        return injectExportsOrder(code, id);
      }
    },
  };
}
Example #5
Source File: index.ts    From rollup-plugin-swc with MIT License 6 votes vote down vote up
function minify(options: JsMinifyOptions = {}): Plugin {
  return {
    name: 'swc-minify',

    renderChunk(code: string) {
      return swcMinify(code, options);
    }
  };
}
Example #6
Source File: dts.ts    From icepkg with MIT License 5 votes vote down vote up
// dtsPlugin is used to generate declartion file when transforming
function dtsPlugin(entry: string, generateTypesForJs?: UserConfig['generateTypesForJs']): Plugin {
  const ids: string[] = [];
  return {
    name: 'ice-pkg:dts',
    transform(_, id) {
      if (dtsFilter(id)) {
        if (!cachedContents[id]) {
          cachedContents[id] = {
            srcCode: _,
            updated: true,
            ext: extname(id) as FileExt,
            filePath: id,
          };
        } else if (cachedContents[id].srcCode === _) {
          cachedContents[id].updated = false;
        } else {
          cachedContents[id].srcCode = _;
          cachedContents[id].updated = true;
        }

        ids.push(id);
      }
      // Always return null to escape transforming
      return null;
    },

    buildEnd() {
      const compileIds = ids
        .filter(
          (id) =>
            isTypescriptOnly(cachedContents[id].ext, id)
          || (generateTypesForJs && isEcmascriptOnly(cachedContents[id].ext, id)),
        );

      // should re-run typescript programs
      const shouldUpdateDts = compileIds.some((id) => cachedContents[id].updated);

      let dtsFiles;
      if (shouldUpdateDts) {
        const compileFiles = compileIds.map((id) => ({
          ext: cachedContents[id].ext,
          filePath: id,
          srcCode: cachedContents[id].srcCode,
        }));
        dtsFiles = dtsCompile(compileFiles);
      } else {
        dtsFiles = ids.map((id) => cachedContents[id]);
      }

      dtsFiles.forEach((file) => {
        this.emitFile({
          type: 'asset',
          fileName: relative(entry, file.dtsPath),
          source: file.dtsContent,
        });

        cachedContents[file.filePath] = {
          ...cachedContents[file.filePath],
          ...file,
        };
      });
    },
  };
}
Example #7
Source File: index.ts    From tailchat with GNU General Public License v3.0 5 votes vote down vote up
export default function sourceRef(): Plugin {
  return {
    name: 'source-ref',
    transform(code, id) {
      const filepath = id;

      const ast = parse(code, {
        sourceType: 'module',
        plugins: ['jsx', 'typescript'],
      });

      traverse(ast, {
        JSXOpeningElement(path) {
          const location = path.node.loc;
          if (!location) {
            return;
          }

          if (Array.isArray(location)) {
            return;
          }

          const name = path.node.name;
          if (isJSXIdentifier(name) && name.name === 'Fragment') {
            return;
          }
          if (
            isJSXMemberExpression(name) &&
            name.property.name === 'Fragment'
          ) {
            return;
          }

          const line = location.start.line;
          const col = location.start.column;

          const attrs = path.node.attributes;
          for (let i = 0; i < attrs.length; i++) {
            const attr = attrs[i];
            if (attr.type === 'JSXAttribute' && attr.name.name === TRACE_ID) {
              // existed
              return;
            }
          }

          const traceId = `${filepath}:${line}:${col}`;

          attrs.push(
            jsxAttribute(jsxIdentifier(TRACE_ID), stringLiteral(traceId))
          );
        },
      });

      const res = generate(ast);

      return { code: res.code, map: res.map };
    },
  };
}
Example #8
Source File: rollup-globals.ts    From Cromwell with MIT License 5 votes vote down vote up
function createPlugin(globals,
    { include, exclude, dynamicWrapper = defaultDynamicWrapper, createVars }
        : {
            include?: string[] | string;
            exclude?: string[] | string;
            dynamicWrapper?: typeof defaultDynamicWrapper;
            createVars?: boolean;
        } = {}): Plugin {
    if (!globals) {
        throw new TypeError("Missing mandatory option 'globals'");
    }
    let getName = globals;
    const globalsType = typeof globals;
    const isGlobalsObj = globalsType === "object";
    if (isGlobalsObj) {
        getName = function (name) {
            if (Object.prototype.hasOwnProperty.call(globals, name)) {
                return globals[name];
            }
        };
    } else if (globalsType !== "function") {
        throw new TypeError(`Unexpected type of 'globals', got '${globalsType}'`);
    }
    const dynamicWrapperType = typeof dynamicWrapper;
    if (dynamicWrapperType !== "function") {
        throw new TypeError(`Unexpected type of 'dynamicWrapper', got '${dynamicWrapperType}'`);
    }
    const filter = createFilter(include, exclude);
    return {
        name: "rollup-plugin-external-globals",
        transform(code, id) {
            if ((id[0] !== "\0" && !filter(id)) || (isGlobalsObj && Object.keys(globals).every(id => !code.includes(id)))) {
                return;
            }
            
            const ast = this.parse(code);
            const magicCode = new MagicString(code);
            const isTouched = importToGlobals({
                ast,
                code: magicCode,
                getName,
                getDynamicWrapper: dynamicWrapper,
                createVars,
            });
            return isTouched ? {
                code: magicCode.toString(),
                map: magicCode.generateMap()
            } : undefined;
        }
    };
}
Example #9
Source File: index.ts    From rollup-plugin-rename-node-modules with MIT License 5 votes vote down vote up
plugin = (
  moduleName: string | ((fileName: string) => string) = "external",
  sourceMaps = true
): Plugin => {
  const replace =
    typeof moduleName === "string"
      ? (fileName: string) => fileName.replace(/node_modules/g, moduleName)
      : moduleName;
  return {
    name: "rename-external-node-modules",
    generateBundle(_, bundle) {
      const changedFiles: string[] = [];
      Object.entries(bundle).forEach(([fileName, chunkInfo]) => {
        if (fileName.includes("node_modules")) {
          const newFileName = replace(fileName);
          chunkInfo.fileName = newFileName;
          changedFiles.push(fileName);
        }
        if ("code" in chunkInfo) {
          if (chunkInfo.imports.some((i) => i.includes("node_modules"))) {
            const magicString = new MagicString(chunkInfo.code);
            const ast = this.parse(chunkInfo.code, {
              ecmaVersion: "latest",
              sourceType: "module",
            });

            walk(ast, {
              enter(node) {
                if (importNodeTypes.includes(node.type as NodeType)) {
                  const req: any =
                    getRequireSource(node) || getImportSource(node);

                  if (req && req.value.includes("node_modules")) {
                    const { start, end } = req;
                    // compute a new path relative to the bundle root
                    const bundlePath = replace(
                      path.join(path.dirname(fileName), req.value)
                    );
                    // and then format the path relative to the updated chunk path
                    const newPath = path.relative(
                      path.dirname(chunkInfo.fileName),
                      bundlePath
                    );

                    // add ./ to files relative to project root
                    const normalizedPath =
                      newPath.startsWith("./") || newPath.startsWith("../")
                        ? newPath
                        : `./${newPath}`;

                    magicString.overwrite(start, end, `'${normalizedPath}'`);
                  }
                }
              },
            });

            if (sourceMaps) {
              chunkInfo.map = magicString.generateMap();
            }
            chunkInfo.code = magicString.toString();
          }
        }
      });
      for (const fileName of changedFiles) {
        const file = bundle[fileName];
        const newFileName = file.fileName;
        delete bundle[fileName];
        bundle[newFileName] = file;
      }
    },
  };
}
Example #10
Source File: pluginContainer.ts    From icepkg with MIT License 4 votes vote down vote up
export async function createPluginContainer(
  { plugins, logger, root, output, build: { rollupOptions } },
  moduleGraph?: any,
  watcher?: FSWatcher,
): Promise<PluginContainer> {
  let ids = 0; // counter for generating unique emitted asset IDs
  const files = new Map();

  const isDebug = process.env.DEBUG;

  const seenResolves: Record<string, true | undefined> = {};

  const debugResolve = createLogger('resolve');
  const debugPluginResolve = createDebugger('plugin-resolve');
  const debugPluginTransform = createDebugger('plugin-transform');

  const watchFiles = new Set<string>();

  // get rollup version
  const rollupPkgPath = resolve(require.resolve('rollup'), '../../package.json');
  const minimalContext: MinimalPluginContext = {
    meta: {
      rollupVersion: safeRequire(rollupPkgPath)
        .version,
      // rollupVersion: '2.3.4',
      watchMode: true,
    },
  };

  function warnIncompatibleMethod(method: string, plugin: string) {
    logger.warn(
      colors.cyan(`[plugin:${plugin}] `) +
        colors.yellow(
          `context method ${colors.bold(
            `${method}()`,
          )} is not supported in serve mode. This plugin is likely not vite-compatible.`,
        ),
    );
  }

  // throw when an unsupported ModuleInfo property is accessed,
  // so that incompatible plugins fail in a non-cryptic way.
  const ModuleInfoProxy: ProxyHandler<ModuleInfo> = {
    get(info: any, key: string) {
      if (key in info) {
        return info[key];
      }
      throw Error(
        `[vite] The "${key}" property of ModuleInfo is not supported.`,
      );
    },
  };

  // same default value of "moduleInfo.meta" as in Rollup
  const EMPTY_OBJECT = Object.freeze({});

  function getModuleInfo(id: string) {
    const module = moduleGraph?.getModuleById(id);
    if (!module) {
      return null;
    }
    if (!module.info) {
      module.info = new Proxy(
        // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
        { id, meta: module.meta || EMPTY_OBJECT } as ModuleInfo,
        ModuleInfoProxy,
      );
    }
    return module.info;
  }

  function updateModuleInfo(id: string, { meta }: { meta?: object | null }) {
    if (meta) {
      const moduleInfo = getModuleInfo(id);
      if (moduleInfo) {
        moduleInfo.meta = { ...moduleInfo.meta, ...meta };
      }
    }
  }

  // we should create a new context for each async hook pipeline so that the
  // active plugin in that pipeline can be tracked in a concurrency-safe manner.
  // using a class to make creating new contexts more efficient
  class Context implements PluginContext {
    meta = minimalContext.meta;
    ssr = false;
    _activePlugin: Plugin | null;
    _activeId: string | null = null;
    _activeCode: string | null = null;
    _resolveSkips?: Set<Plugin>;
    _addedImports: Set<string> | null = null;

    constructor(initialPlugin?: Plugin) {
      this._activePlugin = initialPlugin || null;
    }

    parse(code: string, opts: any = {}) {
      return parser.parse(code, {
        sourceType: 'module',
        ecmaVersion: 'latest',
        locations: true,
        ...opts,
      });
    }

    async resolve(
      id: string,
      importer?: string,
      options?: { skipSelf?: boolean },
    ) {
      let skip: Set<Plugin> | undefined;
      if (options?.skipSelf && this._activePlugin) {
        skip = new Set(this._resolveSkips);
        skip.add(this._activePlugin);
      }
      let out = await container.resolveId(id, importer, { skip, ssr: this.ssr });
      if (typeof out === 'string') out = { id: out };
      return out as ResolvedId | null;
    }

    getModuleInfo(id: string) {
      return getModuleInfo(id);
    }

    getModuleIds() {
      return moduleGraph
        ? moduleGraph.idToModuleMap.keys()
        : Array.prototype[Symbol.iterator]();
    }

    addWatchFile(id: string) {
      watchFiles.add(id);
      (this._addedImports || (this._addedImports = new Set())).add(id);
      if (watcher) ensureWatchedFile(watcher, id, root);
    }

    getWatchFiles() {
      return [...watchFiles];
    }

    emitFile(assetOrFile: EmittedFile) {
      function resolveFileName(fileName: string) {
        if (!fileName) return;
        if (isAbsolute(fileName)) return fileName;
        return resolve(root, output, fileName);
      }

      const { type, fileName } = assetOrFile;
      const name = type === 'chunk' ? assetOrFile.name || assetOrFile.id : assetOrFile.name;
      const source = type === 'asset' && assetOrFile.source;
      const filename = resolveFileName(fileName);

      const id = String(++ids);
      files.set(id, { id, name, filename });

      if (type === 'chunk') {
        consola.warn(`type ${type} of this.emitFile is not supported in transform mode. This plugin is likely not compatible`);
      } else if (source) {
        fs.writeFileSync(filename, source);
      }
      return id;
    }

    setAssetSource(assetId: string, source: string | Uint8Array) {
      const asset = files.get(String(assetId));
      if (asset.type === 'chunk') {
        return;
      }
      asset.source = source;
      fs.writeFile(asset.filename, source);
    }

    getFileName() {
      warnIncompatibleMethod('getFileName', this._activePlugin!.name);
      return '';
    }

    warn(
      e: string | RollupError,
      position?: number | { column: number; line: number },
    ) {
      const err = formatError(e, position, this);
      const msg = err.message;
      // const msg = buildErrorMessage(
      //   err,
      //   [colors.yellow(`warning: ${err.message}`)],
      //   false,
      // );
      logger.warn(msg, {
        clear: true,
        timestamp: true,
      });
    }

    error(
      e: string | RollupError,
      position?: number | { column: number; line: number },
    ): never {
      // error thrown here is caught by the transform middleware and passed on
      // the the error middleware.
      throw formatError(e, position, this);
    }
  }

  function formatError(
    e: string | RollupError,
    position: number | { column: number; line: number } | undefined,
    ctx: Context,
  ) {
    let err = (
      typeof e === 'string' ? new Error(e) : e
    ) as postcss.CssSyntaxError & RollupError;

    if (err.file && err.name === 'CssSyntaxError') {
      err.id = normalizePath(err.file);
    }
    if (ctx._activePlugin) err.plugin = ctx._activePlugin.name;
    if (ctx._activeId && !err.id) err.id = ctx._activeId;

    if (ctx._activeCode && !err.pluginCode) {
      err.pluginCode = ctx._activeCode;

      const pos =
        position != null
          ? position
          : err.pos != null
            ? err.pos
            : // some rollup plugins, e.g. json, sets position instead of pos
            (err as any).position;

      if (pos != null) {
        let errLocation;
        try {
          errLocation = numberToPos(ctx._activeCode, pos);
        } catch (err2) {
          logger.error(
            colors.red(
              `Error in error handler:\n${err2.stack || err2.message}\n`,
            ),
            // print extra newline to separate the two errors
            { error: err2 },
          );
          throw err;
        }
        err.loc = err.loc || {
          file: err.id,
          ...errLocation,
        };
        err.frame = err.frame || generateCodeFrame(ctx._activeCode, pos);
      } else if (err.loc) {
        // css preprocessors may report errors in an included file
        if (!err.frame) {
          let code = ctx._activeCode;
          if (err.loc.file) {
            err.id = normalizePath(err.loc.file);
            try {
              code = fs.readFileSync(err.loc.file, 'utf-8');
            } catch {
              // empty
            }
          }
          err.frame = generateCodeFrame(code, err.loc);
        }
      } else if ((err as any).line && (err as any).column) {
        err.loc = {
          file: err.id,
          line: (err as any).line,
          column: (err as any).column,
        };
        err.frame = err.frame || generateCodeFrame(err.id!, err.loc);
      }

      if (err.loc && ctx instanceof TransformContext) {
        const rawSourceMap = ctx._getCombinedSourcemap();
        if (rawSourceMap) {
          const consumer = new SourceMapConsumer(rawSourceMap as any);
          const { source, line, column } = consumer.originalPositionFor({
            line: Number(err.loc.line),
            column: Number(err.loc.column),
            bias: SourceMapConsumer.GREATEST_LOWER_BOUND,
          });
          if (source) {
            err.loc = { file: source, line, column };
          }
        }
      }
    }

    // Be consistent of rollup https://github.com/rollup/rollup/blob/master/src/utils/pluginUtils.ts#L7
    if (!(err instanceof Error)) {
      err = Object.assign(new Error(err.message), err);
    }

    return err;
  }

  class TransformContext extends Context {
    filename: string;
    originalCode: string;
    originalSourcemap: SourceMap | null = null;
    sourcemapChain: Array<NonNullable<SourceDescription['map']>> = [];
    combinedMap: SourceMap | null = null;

    constructor(filename: string, code: string, inMap?: SourceMap | string) {
      super();
      this.filename = filename;
      this.originalCode = code;
      if (inMap) {
        this.sourcemapChain.push(inMap);
      }
    }

    _getCombinedSourcemap(createIfNull = false) {
      let { combinedMap } = this;
      for (let m of this.sourcemapChain) {
        if (typeof m === 'string') m = JSON.parse(m);
        if (!('version' in (m as SourceMap))) {
          // empty, nullified source map
          this.combinedMap = null;
          combinedMap = this.combinedMap;
          this.sourcemapChain.length = 0;
          break;
        }
        if (!combinedMap) {
          combinedMap = m as SourceMap;
        } else {
          combinedMap = combineSourcemaps(this.filename, [
            {
              ...(m as RawSourceMap),
              sourcesContent: combinedMap.sourcesContent,
            },
            combinedMap as RawSourceMap,
          ]) as SourceMap;
        }
      }
      if (!combinedMap) {
        return createIfNull
          ? new MagicString(this.originalCode).generateMap({
            includeContent: true,
            hires: true,
            source: this.filename,
          })
          : null;
      }
      if (combinedMap !== this.combinedMap) {
        this.combinedMap = combinedMap;
        this.sourcemapChain.length = 0;
      }
      return this.combinedMap;
    }

    getCombinedSourcemap() {
      return this._getCombinedSourcemap(true) as SourceMap;
    }
  }

  let closed = false;

  const container: PluginContainer = {
    options: await (async () => {
      let options = rollupOptions;
      for (const plugin of plugins) {
        if (!plugin.options) continue;
        options =
          (await plugin.options.call(minimalContext, options)) || options;
      }
      if (options.acornInjectPlugins) {
        parser = acorn.Parser.extend(options.acornInjectPlugins as any);
      }
      return {
        acorn,
        acornInjectPlugins: [],
        ...options,
      };
    })(),

    getModuleInfo,

    async buildStart() {
      await Promise.all(
        plugins.map((plugin) => {
          if (plugin.buildStart) {
            return plugin.buildStart.call(
              new Context(plugin) as any,
              container.options as NormalizedInputOptions,
            );
          }
          return null;
        }),
      );
    },

    async resolveId(rawId, importer = join(root, 'index.html'), options) {
      const skip = options?.skip;
      const ssr = options?.ssr;
      const ctx = new Context();
      ctx.ssr = !!ssr;
      ctx._resolveSkips = skip;
      const resolveStart = isDebug ? performance.now() : 0;

      let id: string | null = null;
      const partial: Partial<PartialResolvedId> = {};
      for (const plugin of plugins) {
        if (!plugin.resolveId) continue;
        if (skip?.has(plugin)) continue;

        ctx._activePlugin = plugin;

        const pluginResolveStart = isDebug ? performance.now() : 0;
        const result = await plugin.resolveId.call(
          ctx as any,
          rawId,
          importer,
          { ssr },
        );
        if (!result) continue;

        if (typeof result === 'string') {
          id = result;
        } else {
          id = result.id;
          Object.assign(partial, result);
        }

        isDebug &&
          debugPluginResolve.info(
            timeFrom(pluginResolveStart),
            plugin.name,
            // prettifyUrl(id, root),
          );

        // resolveId() is hookFirst - first non-null result is returned.
        break;
      }

      if (isDebug && rawId !== id && !rawId.startsWith(FS_PREFIX)) {
        const key = rawId + id;
        // avoid spamming
        if (!seenResolves[key]) {
          seenResolves[key] = true;
          debugResolve.info(
            `${timeFrom(resolveStart)}`,
            `${colors.cyan(rawId)} -> ${colors.dim(
              id,
            )}`,
          );
        }
      }

      if (id) {
        partial.id = isExternalUrl(id) ? id : normalizePath(id);
        return partial as PartialResolvedId;
      } else {
        return null;
      }
    },

    async load(id, options) {
      const ssr = options?.ssr;
      const ctx = new Context();
      ctx.ssr = !!ssr;
      for (const plugin of plugins) {
        if (!plugin.load) continue;
        ctx._activePlugin = plugin;
        // eslint-disable-next-line no-await-in-loop
        const result = await plugin.load.call(ctx as any, id, { ssr });
        if (result != null) {
          if (isObject(result)) {
            updateModuleInfo(id, result);
          }
          return result;
        }
      }
      return null;
    },

    async transform(code, id, options) {
      const inMap = options?.inMap;
      const ssr = options?.ssr;
      const ctx = new TransformContext(id, code, inMap as SourceMap);
      ctx.ssr = !!ssr;
      let meta = null;
      for (const plugin of plugins) {
        if (!plugin.transform) continue;
        ctx._activePlugin = plugin;
        ctx._activeId = id;
        ctx._activeCode = code;
        const start = isDebug ? performance.now() : 0;
        let result: TransformResult | string | undefined;
        try {
          // eslint-disable-next-line no-await-in-loop
          result = await plugin.transform.call(ctx as any, code, id, { ssr });
        } catch (e) {
          ctx.error(e);
        }
        if (!result) continue;
        isDebug &&
          debugPluginTransform(
            timeFrom(start),
            plugin.name,
            // prettifyUrl(id, root),
          );
        if (isObject(result)) {
          if (result.code !== undefined) {
            code = result.code;
            if (result.map) {
              ctx.sourcemapChain.push(result.map);
            }

            // Add to support custom meta info
            if (result.meta) {
              meta = result.meta;
            }
          }
          updateModuleInfo(id, result);
        } else {
          code = result;
        }
      }
      return {
        code,
        map: ctx._getCombinedSourcemap(),
        meta,
      };
    },

    async close() {
      if (closed) return;
      const ctx = new Context();
      await Promise.all(
        plugins.map((p) => p.buildEnd && p.buildEnd.call(ctx as any)),
      );
      await Promise.all(
        plugins.map((p) => p.closeBundle && p.closeBundle.call(ctx as any)),
      );
      closed = true;
    },
  };

  return container;
}
Example #11
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 #12
Source File: createRollupConfig.ts    From web with MIT License 4 votes vote down vote up
export function createRollupConfig(params: CreateRollupConfigParams): RollupOptions {
  const { outputDir, indexFilename, indexHtmlString, storyFilePaths } = params;

  const options: RollupOptions = {
    preserveEntrySignatures: false,
    onwarn,
    output: {
      entryFileNames: '[hash].js',
      chunkFileNames: '[hash].js',
      assetFileNames: '[hash][extname]',
      format: 'system',
      dir: outputDir,
    },
    plugins: [
      resolve({
        moduleDirectories: ['node_modules', 'web_modules'],
      }),
      babel({
        babelHelpers: 'bundled',
        babelrc: false,
        configFile: false,
        extensions: [...DEFAULT_EXTENSIONS, 'md', 'mdx'],
        exclude: `${prebuiltDir}/**`,
        presets: [
          [
            require.resolve('@babel/preset-env'),
            {
              targets: [
                'last 3 Chrome major versions',
                'last 3 ChromeAndroid major versions',
                'last 3 Firefox major versions',
                'last 3 Edge major versions',
                'last 3 Safari major versions',
                'last 3 iOS major versions',
                'ie 11',
              ],
              useBuiltIns: false,
              shippedProposals: true,
              modules: false,
              bugfixes: true,
            },
          ],
        ],
        plugins: [
          [require.resolve('babel-plugin-bundled-import-meta'), { importStyle: 'baseURI' }],
          [
            require.resolve('babel-plugin-template-html-minifier'),
            {
              modules: {
                // this is web component specific, but has no effect on other project styles
                'lit-html': ['html'],
                'lit-element': ['html', { name: 'css', encapsulation: 'style' }],
                '@web/storybook-prebuilt/web-components': [
                  'html',
                  { name: 'css', encapsulation: 'style' },
                ],
                '@web/storybook-prebuilt/web-components.js': [
                  'html',
                  { name: 'css', encapsulation: 'style' },
                ],
                '@open-wc/testing': ['html', { name: 'css', encapsulation: 'style' }],
                '@open-wc/testing-helpers': ['html', { name: 'css', encapsulation: 'style' }],
              },
              logOnError: true,
              failOnError: false,
              strictCSS: true,
              htmlMinifier: {
                collapseWhitespace: true,
                conservativeCollapse: true,
                removeComments: true,
                caseSensitive: true,
                minifyCSS: true,
              },
            },
          ],
        ],
      }) as Plugin,
      html({ input: { name: indexFilename, html: indexHtmlString } }) as Plugin,
      polyfillsLoader({
        polyfills: {
          coreJs: true,
          fetch: true,
          abortController: true,
          regeneratorRuntime: 'always',
          webcomponents: true,
          intersectionObserver: true,
          resizeObserver: true,
        },
      }) as Plugin,
      terser({ format: { comments: false } }) as Plugin,
    ],
  };

  if (storyFilePaths && storyFilePaths.length > 0) {
    // plugins we need to inject only in the preview
    options.plugins!.unshift(injectExportsOrderPlugin(storyFilePaths));
    options.plugins!.unshift(mdxPlugin());
    options.plugins!.unshift(mdjsPlugin(params.type));
  }

  return options;
}
Example #13
Source File: index.ts    From rollup-plugin-swc with MIT License 4 votes vote down vote up
function swc(options: PluginOptions = {}): Plugin {
  const filter = createFilter(
    options.include || INCLUDE_REGEXP,
    options.exclude || EXCLUDE_REGEXP
  );

  return {
    name: 'swc',

    async resolveId(importee, importer) {
      // ignore IDs with null character, these belong to other plugins
      if (importee.startsWith('\0')) {
        return null;
      }

      if (importer && importee[0] === '.') {
        const resolved = resolve(
          importer ? dirname(importer) : process.cwd(),
          importee
        );

        let file = await resolveFile(resolved);
        if (file) return file;
        if (!file && await fileExists(resolved) && (await fs.promises.stat(resolved)).isDirectory()) {
          file = await resolveFile(resolved, true);
          if (file) return file;
        }
      }
    },

    async transform(code: string, id: string) {
      if (!filter(id)) {
        return null;
      }

      const ext = extname(id);

      if (!ACCEPTED_EXTENSIONS.includes(ext)) return null;

      const isTypeScript = ext === '.ts' || ext === '.tsx';
      const isTsx = ext === '.tsx';
      const isJsx = ext === '.jsx';

      const tsconfigOptions
        = options.tsconfig === false
          ? {}
          : await getOptions(dirname(id), options.tsconfig);

      const swcOptionsFromTsConfig: SwcConfig = {
        jsc: {
          externalHelpers: tsconfigOptions.importHelpers,
          parser: {
            syntax: isTypeScript ? 'typescript' : 'ecmascript',
            tsx: isTypeScript ? isTsx : undefined,
            jsx: !isTypeScript ? isJsx : undefined,
            decorators: tsconfigOptions.experimentalDecorators
          },
          transform: {
            decoratorMetadata: tsconfigOptions.emitDecoratorMetadata,
            react: {
              pragma: tsconfigOptions.jsxFactory,
              pragmaFrag: tsconfigOptions.jsxFragmentFactory
            }
          },
          target: tsconfigOptions.target?.toLowerCase() as JscTarget | undefined,
          baseUrl: tsconfigOptions.baseUrl,
          paths: tsconfigOptions.paths
        }
      };

      const swcOption = deepmerge.all<SwcConfig>([
        swcOptionsFromTsConfig,
        options,
        {
          jsc: {
            minify: undefined // Disable minify ob transform, do it on renderChunk
          },
          // filename: id,
          include: undefined, // Rollup's filter is not compatible with swc
          exclude: undefined,
          tsconfig: undefined, // swc has no tsconfig option
          minify: false // Disable minify on transform, do it on renderChunk
        }
      ]);

      /**
       * swc cannot transform module ids with "\0", which is the identifier of rollup virtual module
       *
       * FIXME: This is a temporary workaround, remove when swc fixes it (DO NOT FORGET TO BUMP PEER DEPS VERSION AS WELL!)
       *
       * @see https://rollupjs.org/guide/en/#conventions
       * @see https://github.com/rollup/plugins/blob/02fb349d315f0ffc55970fba5de20e23f8ead881/packages/commonjs/src/helpers.js#L15
       * @see https://github.com/SukkaW/rollup-plugin-swc/pull/1
       * @see https://github.com/swc-project/swc/issues/2853
       */
      const { code: transformedCode, ...rest } = await swcTransform(
        code,
        swcOption
      );

      return {
        ...rest,
        code: transformedCode
      };
    },

    renderChunk(code: string) {
      if (options.minify || options.jsc?.minify?.mangle || options.jsc?.minify?.compress) {
        return swcMinify(code, options.jsc?.minify);
      }

      return null;
    }
  };
}
Example #14
Source File: rollupPlugin.ts    From elderjs with MIT License 4 votes vote down vote up
export default function elderjsRollup({
  elderConfig,
  svelteConfig,
  type = 'ssr',
  startDevServer = false,
}: IElderjsRollupConfig): Partial<Plugin> {
  let styleCssHash;
  let styleCssMapHash;

  const { childProcess, startWatcher, startOrRestartServer } = devServer({ elderConfig, forceStart: false });

  return {
    name: 'rollup-plugin-elder',

    watchChange(id) {
      // clean out dependency relationships on a file change.
      const prior = cache.get('dependencies');
      prior[id] = new Set();

      if (!dependencyCache) dependencyCache = {};
      dependencyCache = prior;
    },

    /**
     * Essentially what is happening here is that we need to say we're going to
     * emit these files before we know the content.
     * We are given a hash that we later use to populate them with data.
     */
    buildStart() {
      // kill server to prevent failures.
      if (childProcess) childProcess.kill('SIGINT');

      // create placeholder files to be filled later.
      if (type === 'ssr') {
        styleCssHash = this.emitFile({
          type: 'asset',
          name: 'svelte.css',
        });

        if (isDev) {
          styleCssMapHash = this.emitFile({
            type: 'asset',
            name: 'svelte.css.map',
          });
        }
      }

      // cleaning up folders that need to be deleted.
      if (type === 'ssr') {
        del.sync(elderConfig.$$internal.ssrComponents);
        del.sync(path.resolve(elderConfig.$$internal.distElder, `.${sep}assets${sep}`));
        del.sync(path.resolve(elderConfig.$$internal.distElder, `.${sep}props${sep}`));
      } else if (type === 'client') {
        del.sync(path.resolve(elderConfig.$$internal.distElder, `.${sep}svelte${sep}`));
      }
    },

    resolveId: resolveFn,
    load: loadCss,
    async transform(code, id) {
      const thisTransformFn = transformFn.bind(this);
      const r = await thisTransformFn({
        svelteConfig,
        elderConfig,
        type,
      })(code, id);

      if (!r) return;

      (r.warnings || []).forEach((warning) => {
        if (warning.code === 'css-unused-selector') return;
        this.warn(warning);
      });
      // eslint-disable-next-line consistent-return
      return r.output;
    },

    // eslint-disable-next-line consistent-return
    async renderChunk(code, chunk) {
      if (chunk.isEntry) {
        if (type === 'ssr') {
          const trackedDeps = getDependencies(chunk.facadeModuleId);

          const cssOutput = await minifyCss(trackedDeps, elderConfig);
          code += `\nmodule.exports._css = ${devalue(cssOutput.styles)};`;
          code += `\nmodule.exports._cssMap = ${devalue(encodeSourceMap(cssOutput.sourceMap))};`;
          code += `\nmodule.exports._cssIncluded = ${JSON.stringify(
            cssOutput.included.map((d) => path.relative(elderConfig.rootDir, d)),
          )}`;

          return { code, map: null };
        }
      }
    },

    /**
     * generateBundle is used to write all of the CSS to the file system
     * @param options
     * @param bundle
     * @param isWrite
     */

    // eslint-disable-next-line consistent-return
    async generateBundle() {
      // IMPORTANT!!!
      // all css is only available on the ssr version...
      // but we need to move the css to the client folder.
      if (type === 'ssr') {
        const { styles, sourceMap } = await minifyCss([...this.getModuleIds()], elderConfig);
        if (styleCssMapHash) {
          // set the source later when we have it.
          this.setAssetSource(styleCssMapHash, sourceMap.toString());
          const sourceMapFile = this.getFileName(styleCssMapHash);
          const sourceMapFileRel = `/${path.relative(
            elderConfig.distDir,
            path.resolve(elderConfig.$$internal.distElder, sourceMapFile),
          )}`;
          this.setAssetSource(styleCssHash, `${styles}\n /*# sourceMappingURL=${sourceMapFileRel} */`);
        } else if (styleCssHash) {
          this.setAssetSource(styleCssHash, styles);
        }
      }
    },

    writeBundle() {
      if (type === 'ssr') {
        // copy over assets from the ssr folder to the client folder
        const ssrAssets = path.resolve(elderConfig.rootDir, `.${sep}___ELDER___${sep}compiled${sep}assets`);
        const clientAssets = path.resolve(elderConfig.$$internal.distElder, `.${sep}assets${sep}`);
        fs.ensureDirSync(clientAssets);
        const open = fs.readdirSync(ssrAssets);
        if (open.length > 0) {
          open.forEach((name) => {
            fs.copyFileSync(path.join(ssrAssets, name), path.join(clientAssets, name));
          });
        }
      }

      cache.set('dependencies', dependencyCache);

      if (startDevServer && type === 'client') {
        startWatcher();
        startOrRestartServer();
      }
    },
  };
}
Example #15
Source File: rollupPluginHTML.ts    From web with MIT License 4 votes vote down vote up
export function rollupPluginHTML(pluginOptions: RollupPluginHTMLOptions = {}): RollupPluginHtml {
  const multiOutputNames: string[] = [];
  let inputs: InputData[] = [];
  let generatedBundles: GeneratedBundle[] = [];
  let externalTransformHtmlFns: TransformHtmlFunction[] = [];
  let defaultInjectDisabled = false;
  let serviceWorkerPath = '';
  let injectServiceWorker = false;
  let absolutePathPrefix: string;
  let strictCSPInlineScripts = false;

  function reset() {
    inputs = [];
    generatedBundles = [];
    externalTransformHtmlFns = [];
  }

  return {
    name: '@web/rollup-plugin-html',

    options(inputOptions) {
      reset();

      inputs = getInputData(pluginOptions, inputOptions.input);
      const moduleImports: ScriptModuleTag[] = [];

      for (const input of inputs) {
        moduleImports.push(...input.moduleImports);
      }

      if (moduleImports.length === 0) {
        // if there are only pages with pure HTML we need to make sure there is at
        // least some input for rollup
        moduleImports.push(NOOP_IMPORT);
      }

      if (pluginOptions.serviceWorkerPath) {
        serviceWorkerPath = pluginOptions.serviceWorkerPath;
      }
      if (pluginOptions.injectServiceWorker) {
        injectServiceWorker = pluginOptions.injectServiceWorker;
      }
      if (pluginOptions.absolutePathPrefix) {
        absolutePathPrefix = pluginOptions.absolutePathPrefix;
      }
      if (pluginOptions.strictCSPInlineScripts) {
        strictCSPInlineScripts = pluginOptions.strictCSPInlineScripts;
      }

      if (pluginOptions.input == null) {
        // we are reading rollup input, so replace whatever was there
        return { ...inputOptions, input: moduleImports.map(mod => mod.importPath) };
      } else {
        // we need to add modules to existing rollup input
        return addRollupInput(inputOptions, moduleImports);
      }
    },

    /** Watch input files when running in watch mode */
    buildStart() {
      // watch filesf
      for (const input of inputs) {
        if (input.filePath) {
          this.addWatchFile(input.filePath);
        }

        for (const asset of input.assets) {
          this.addWatchFile(asset.filePath);
        }
      }
    },

    /** Notifies rollup that we will be handling these modules */
    resolveId(id) {
      if (id === NOOP_IMPORT.importPath) {
        return NOOP_IMPORT.importPath;
      }

      for (const input of inputs) {
        if (input.inlineModules.find(mod => mod.importPath === id)) {
          return id;
        }
      }
    },

    /** Provide code for modules we are handling */
    load(id) {
      if (id === NOOP_IMPORT.importPath) {
        return 'export default "noop"';
      }

      for (const input of inputs) {
        const foundMod = input.inlineModules.find(mod => mod.importPath === id);
        if (foundMod) {
          return foundMod.code;
        }
      }
    },

    /**
     * Emits output html file if we are doing a single output build.
     * @param {OutputOptions} options
     * @param {OutputBundle} bundle
     */
    async generateBundle(options, bundle) {
      if (multiOutputNames.length !== 0) {
        // we are generating multiple build outputs, which is handled by child plugins
        return;
      }

      if (!options.dir) {
        throw createError('Rollup output options must have a dir option set to emit an HTML file.');
      }
      generatedBundles.push({ name: 'default', options, bundle });

      const emittedAssets = await emitAssets.call(this, inputs, pluginOptions);
      const outputs = await createHTMLOutput({
        outputDir: path.resolve(options.dir),
        inputs,
        emittedAssets,
        generatedBundles,
        externalTransformHtmlFns,
        pluginOptions,
        defaultInjectDisabled,
        serviceWorkerPath,
        injectServiceWorker,
        absolutePathPrefix,
        strictCSPInlineScripts,
      });

      for (const output of outputs) {
        this.emitFile(output);
      }
    },

    api: {
      getInputs() {
        return inputs;
      },

      addHtmlTransformer(transformHtmlFunction: TransformHtmlFunction) {
        externalTransformHtmlFns.push(transformHtmlFunction);
      },

      disableDefaultInject() {
        defaultInjectDisabled = true;
      },

      addOutput(name: string) {
        if (!name || multiOutputNames.includes(name)) {
          throw createError('Each output must have a unique name');
        }

        multiOutputNames.push(name);

        return {
          name: `@web/rollup-plugin-html-multi-output-${multiOutputNames.length}`,

          async generateBundle(options, bundle) {
            if (!options.dir) {
              throw createError(`Output ${name} must have a dir option set.`);
            }

            generatedBundles.push({ name, options, bundle });

            if (generatedBundles.length === multiOutputNames.length) {
              // this is the last build, emit the HTML files
              const outputDirs = new Set(generatedBundles.map(b => b.options.dir));
              if (outputDirs.size !== 1) {
                throw createError(
                  'Unable to emit HTML output. Multiple rollup build outputs have a different output directory set.',
                );
              }

              const emittedAssets = await emitAssets.call(this, inputs, pluginOptions);
              const outputs = await createHTMLOutput({
                outputDir: path.resolve(options.dir),
                inputs,
                emittedAssets,
                generatedBundles,
                externalTransformHtmlFns,
                pluginOptions,
                defaultInjectDisabled,
                serviceWorkerPath,
                injectServiceWorker,
                absolutePathPrefix,
                strictCSPInlineScripts,
              });

              for (const output of outputs) {
                this.emitFile(output);
              }
            }
          },
        } as Plugin;
      },
    },
  };
}
Example #16
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(''),
    );
  });
});
Example #17
Source File: rollupPluginPolyfillsLoader.ts    From web with MIT License 4 votes vote down vote up
export function polyfillsLoader(pluginOptions: RollupPluginPolyfillsLoaderConfig = {}): Plugin {
  let generatedFiles: GeneratedFile[] | undefined;

  return {
    name: '@web/rollup-plugin-polyfills-loader',

    buildStart(options) {
      generatedFiles = undefined;
      if (!options.plugins) {
        throw createError('Could not find any installed plugins');
      }

      const htmlPlugins = options.plugins.filter(
        p => p.name === '@web/rollup-plugin-html',
      ) as RollupPluginHtml[];
      const [htmlPlugin] = htmlPlugins;

      if (!htmlPlugin) {
        throw createError(
          'Could not find any instance of @web/rollup-plugin-html in rollup build.',
        );
      }

      htmlPlugin.api.disableDefaultInject();
      htmlPlugin.api.addHtmlTransformer(async (html, { bundle, bundles, htmlFileName }) => {
        const config = createPolyfillsLoaderConfig(pluginOptions, bundle, bundles);
        const relativePathToPolyfills = path.relative(
          path.dirname(htmlFileName),
          path.dirname(pluginOptions.polyfillsDir || './polyfills'),
        );
        let htmlString = html;

        if (shouldInjectLoader(config)) {
          const result = await injectPolyfillsLoader(html, { ...config, relativePathToPolyfills });
          htmlString = result.htmlString;
          generatedFiles = result.polyfillFiles;
        } else {
          // we don't need to inject a polyfills loader, so we just inject the scripts directly
          const scripts = config
            .modern!.files.map((f: File) => {
              let attributes = '';
              if (f.attributes && f.attributes.length > 0) {
                attributes = ' ';
                attributes += f.attributes
                  .map(attribute => `${attribute.name}="${attribute.value}"`)
                  .join(' ');
              }
              return `<script type="module" src="${f.path}"${attributes}></script>\n`;
            })
            .join('');
          htmlString = htmlString.replace('</body>', `\n${scripts}\n</body>`);
        }

        // preload all entrypoints as well as their direct dependencies
        const { entrypoints } =
          pluginOptions.legacyOutput && pluginOptions.modernOutput
            ? bundles[pluginOptions.modernOutput.name]
            : bundle;

        let preloaded = [];
        for (const entrypoint of entrypoints) {
          preloaded.push(entrypoint.importPath);

          // js files (incl. chunks) will always be in the root directory
          const pathToRoot = path.posix.dirname(entrypoint.importPath);
          for (const chunkPath of entrypoint.chunk.imports) {
            preloaded.push(path.posix.join(pathToRoot, chunkPath));
          }
        }
        preloaded = [...new Set(preloaded)];

        const type =
          pluginOptions.modernOutput?.type ?? formatToFileType(bundle?.options.format ?? 'esm');
        const crossorigin = type === fileTypes.MODULE ? ' crossorigin="anonymous"' : '';
        const shim = type === fileTypes.MODULESHIM;
        const rel = `${shim ? 'module' : ''}preload${shim ? '-shim' : ''}`;
        const as = shim ? '' : ' as="script"';
        return htmlString.replace(
          '</head>',
          `\n${preloaded
            .map(i => `<link rel="${rel}" href="${i}"${as}${crossorigin} />\n`)
            .join('')}</head>`,
        );
      });
    },

    generateBundle(_, bundle) {
      if (generatedFiles) {
        for (const file of generatedFiles) {
          // if the polyfills loader is used multiple times, this polyfill might already be output
          // so we guard against that. polyfills are already hashed, so there is no need to worry
          // about clashing
          if (!(file.path in bundle)) {
            this.emitFile({
              type: 'asset',
              name: file.path,
              fileName: file.path,
              source: file.content,
            });
          }
        }
      }
    },
  };
}
Example #18
Source File: rollup.ts    From Cromwell with MIT License 4 votes vote down vote up
rollupPluginCromwellFrontend = async (settings?: {
    packageJsonPath?: string;
    generateMeta?: boolean;
    generatePagesMeta?: boolean;
    pagesMetaInfo?: TPagesMetaInfo;
    buildDir?: string;
    srcDir?: string;
    pagesDir?: string;
    watch?: boolean;
    moduleInfo?: TPackageCromwellConfig;
    moduleConfig?: TModuleConfig;
    frontendDeps?: TFrontendDependency[];
    dependencyOptions?: RollupOptions[];
    type?: 'themePages' | 'themeAdminPanel';
}): Promise<Plugin> => {

    const scriptsInfo: Record<string, TScriptMetaInfo> = {};
    const importsInfo: Record<string, {
        externals: Record<string, string[]>;
        internals: string[];
    }> = {};

    if (settings?.frontendDeps) settings.frontendDeps = await parseFrontendDeps(settings.frontendDeps);

    // Defer stylesheetsLoader until compile info available. Look for: stylesheetsLoaderStarter()
    let stylesheetsLoaderStarter;
    if (settings?.srcDir && settings?.buildDir) {
        (async () => {
            await new Promise(resolve => {
                stylesheetsLoaderStarter = resolve;
            })
            if (settings?.srcDir && settings?.buildDir)
                initStylesheetsLoader(settings?.srcDir, settings?.buildDir, settings?.pagesMetaInfo?.basePath, settings?.watch);
        })();

    }

    const packageJson: TPackageJson = require(resolve(process.cwd(), 'package.json'));

    const plugin: Plugin = {
        name: 'cromwell-frontend',
        options(options: RollupOptions) {
            if (!options.plugins) options.plugins = [];

            options.plugins.push(externalGlobals((id: string) => {
                if (settings?.moduleInfo?.type === 'theme' && settings?.pagesMetaInfo?.paths) {
                    // Disable for Theme pages
                    return;
                }

                const extStr = `${cromwellStoreModulesPath}["${id}"]`;
                if (id.startsWith('next/')) {
                    return extStr;
                }
                const isExt = resolveExternal(id, settings?.frontendDeps);
                if (isExt) return extStr;
            }, {
                include: '**/*.+(ts|tsx|js|jsx)',
                createVars: true,
            }));

            return options;
        },
        resolveId(source) {
            if (settings?.moduleInfo?.type === 'theme' && settings?.pagesMetaInfo?.paths) {
                // If bundle frontend pages mark css as external to leave it to Next.js Webpack
                if (/\.s?css$/.test(source)) {
                    return { id: source, external: true };
                }

                // All node_modules are external to leave them to next.js
                if (isExternalForm(source)) {
                    return { id: source, external: true };
                }
            }

            if (resolveExternal(source, settings?.frontendDeps)) {
                // Mark external frontendDependencies from package.json
                return { id: source, external: true };
            }

            if (isExternalForm(source)) {
                // Other node_modules...

                if (source.startsWith('next/')) {
                    // Leave Next.js modules external for the next step 
                    return { id: source, external: true };
                }

                if (/\.s?css$/.test(source)) {
                    // Bundle css with AdminPanel pages
                    return { id: require.resolve(source), external: false };
                }

                // Bundled by Rollup by for Themes and Plugins
                return { id: require.resolve(source), external: false };
            }

            return null;
        },
    };

    if (settings?.generateMeta !== false) {
        plugin.transform = function (code, id): string | null {
            id = normalizePath(id).replace('\x00', '');
            if (!/\.(m?jsx?|tsx?)$/.test(id)) return null;
            const ast = this.parse(code);
            
            walk(ast, {
                enter(node: any) {
                    if (node.type === 'ImportDeclaration') {
                        if (!node.specifiers || !node.source) return;
                        const source = node.source.value.replace('\x00', '');

                        if (!importsInfo[id]) importsInfo[id] = {
                            externals: {},
                            internals: []
                        };

                        if (!isExternalForm(source)) {
                            const absolutePath = resolve(dirname(id), source);
                            const globStr = `${absolutePath}.+(ts|tsx|js|jsx)`;
                            const targetFiles = glob.sync(globStr);
                            if (targetFiles[0]) importsInfo[id].internals.push(targetFiles[0]);
                            return;
                        }

                        // Add external
                        if (resolveExternal(source, settings?.frontendDeps)) {
                            if (!importsInfo[id].externals[source]) importsInfo[id].externals[source] = [];

                            node.specifiers.forEach(spec => {
                                if (spec.type === 'ImportDefaultSpecifier' || spec.type === 'ImportNamespaceSpecifier') {
                                    importsInfo[id].externals[source].push('default')
                                }
                                if (spec.type === 'ImportSpecifier' && spec.imported) {
                                    importsInfo[id].externals[source].push(spec.imported.name)
                                }
                            })
                        }
                    }
                }
            });

            return null;
        };

        plugin.generateBundle = async function (options, bundle) {

            if (settings?.pagesMetaInfo && settings.buildDir && settings.pagesDir) {
                settings.pagesMetaInfo = await collectPages({
                    buildDir: settings.buildDir,
                    pagesDir: settings.pagesDir,
                });
            }

            Object.values(bundle).forEach((info: any) => {

                const mergeBindings = (bindings1, bindings2): Record<string, string[]> => {
                    const mergedBindings = Object.assign({}, bindings1);

                    Object.keys(bindings2).forEach(modDep => {
                        if (mergedBindings[modDep]) {
                            mergedBindings[modDep] = [...mergedBindings[modDep], ...bindings2[modDep]];
                            mergedBindings[modDep] = Array.from(new Set(mergedBindings[modDep]));
                        }
                        else mergedBindings[modDep] = bindings2[modDep];
                    })

                    return mergedBindings;
                }

                const importBindingsCache: Record<string, Record<string, string[]>> = {}
                const getImportBingingsForModule = (modId: string): Record<string, string[]> => {
                    modId = normalizePath(modId);
                    if (importBindingsCache[modId]) return importBindingsCache[modId];

                    let importedBindings = {};
                    importBindingsCache[modId] = {};
                    if (importsInfo[modId]) {
                        Object.keys(importsInfo[modId].externals).forEach(libName => {
                            if (!isExternalForm(libName)) return;

                            const importsSpecs = importsInfo[modId].externals[libName];
                            importsSpecs?.forEach(spec => {
                                if (!importedBindings[libName]) importedBindings[libName] = [];
                                if (!importedBindings[libName].includes(spec)) {
                                    importedBindings[libName].push(spec);
                                }
                            });
                        });

                        importsInfo[modId].internals.forEach(internal => {
                            const internalBinds = getImportBingingsForModule(internal);
                            importedBindings = mergeBindings(importedBindings, internalBinds);
                        })
                    }
                    importBindingsCache[modId] = importedBindings;
                    return importedBindings;
                }

                let totalImportedBindings = {};

                if (info.modules) {
                    Object.keys(info.modules).forEach(modId => {
                        const modBindings = getImportBingingsForModule(modId);
                        totalImportedBindings = mergeBindings(totalImportedBindings, modBindings);
                    })
                }

                const versionedImportedBindings = {};
                Object.keys(totalImportedBindings).forEach(dep => {
                    let ver = getNodeModuleVersion(dep, process.cwd());
                    if (!ver) {
                        ver = getDepVersion(packageJson, dep);
                    }
                    if (ver) {
                        if (totalImportedBindings[dep].includes('default')) totalImportedBindings[dep] = ['default'];
                        versionedImportedBindings[`${dep}@${ver}`] = totalImportedBindings[dep];
                    }
                })

                const metaInfo: TScriptMetaInfo = {
                    name: settings?.moduleInfo?.name + '/' + info.fileName + '_' + cryptoRandomString({ length: 8 }),
                    externalDependencies: versionedImportedBindings,
                    // importsInfo
                    // info
                };

                scriptsInfo[info.fileName] = metaInfo;

                this.emitFile({
                    type: 'asset',
                    fileName: getMetaInfoPath(info.fileName),
                    source: JSON.stringify(metaInfo, null, 2)
                });

                if (settings?.pagesMetaInfo?.paths && settings.buildDir && settings.generatePagesMeta) {
                    settings.pagesMetaInfo.paths = settings.pagesMetaInfo.paths.map(paths => {

                        if (info.fileName && paths.srcFullPath === normalizePath(info.facadeModuleId)) {
                            paths.localPath = info.fileName;

                            // Get base path chunk that appended in build dir relatively src dir.
                            // Since we use preserveModules: true, node_modules can appear in build dir.
                            // That will relatively move rollup's options.output.dir on a prefix 
                            // We don't know this prefix (basePath) before compile, so we calc it here: 
                            if (!settings?.pagesMetaInfo?.basePath && settings?.srcDir && info.facadeModuleId) {
                                let baseFileName: any = normalizePath(info.facadeModuleId).replace(normalizePath(settings.srcDir), '').split('.');
                                baseFileName.length > 1 ? baseFileName = baseFileName.slice(0, -1).join('.') : baseFileName.join('.');

                                let basePath: any = normalizePath(info.fileName).split('.');
                                basePath.length > 1 ? basePath = basePath.slice(0, -1).join('.') : basePath.join('.');
                                if ((basePath as string).startsWith('/')) basePath = (basePath as string).substring(1);
                                if ((baseFileName as string).startsWith('/')) baseFileName = (baseFileName as string).substring(1);

                                basePath = normalizePath(basePath).replace(baseFileName, '');
                                if (settings?.pagesMetaInfo) {
                                    settings.pagesMetaInfo.basePath = basePath;
                                    if (stylesheetsLoaderStarter) {
                                        stylesheetsLoaderStarter();
                                        stylesheetsLoaderStarter = null;
                                    }
                                }
                            }

                            delete paths.srcFullPath;
                        }

                        return paths;
                    })
                }
            });

            if (settings?.pagesMetaInfo?.paths && settings.srcDir && settings.buildDir && settings.generatePagesMeta) {
                await generatePagesMeta(settings.pagesMetaInfo, settings.buildDir)
            }
        }

        plugin.writeBundle = function () {
            if (hasToRunRendererGenerator) {
                hasToRunRendererGenerator = false;
                const { generator } = require('@cromwell/renderer/build/generator');
                generator({
                    scriptName: "build",
                    targetThemeName: settings?.moduleInfo?.name,
                });
            }
        }
    }

    return plugin;
}
Example #19
Source File: index.ts    From vanilla-extract with MIT License 4 votes vote down vote up
export function vanillaExtractPlugin({
  identifiers,
  cwd = process.cwd(),
}: Options = {}): Plugin {
  const emittedFiles = new Map<string, string>();
  const isProduction = process.env.NODE_ENV === 'production';

  return {
    name: 'vanilla-extract',
    buildStart() {
      emittedFiles.clear();
    },
    async transform(_code, id) {
      if (!cssFileFilter.test(id)) {
        return null;
      }

      const index = id.indexOf('?');
      const filePath = index === -1 ? id : id.substring(0, index);

      const { source, watchFiles } = await compile({
        filePath,
        cwd,
      });

      for (const file of watchFiles) {
        this.addWatchFile(file);
      }

      const output = await processVanillaFile({
        source,
        filePath,
        identOption: identifiers ?? (isProduction ? 'short' : 'debug'),
      });
      return {
        code: output,
        map: { mappings: '' },
      };
    },
    async resolveId(id) {
      if (!virtualCssFileFilter.test(id)) {
        return null;
      }

      // Emit an asset for every virtual css file
      const { fileName, source } = await getSourceFromVirtualCssFile(id);
      if (!emittedFiles.get(fileName)) {
        const assetId = this.emitFile({
          type: 'asset',
          name: fileName,
          source,
        });
        emittedFiles.set(fileName, assetId);
      }

      // Resolve to a temporary external module
      return {
        id: fileName,
        external: true,
      };
    },
    renderChunk(code, chunkInfo) {
      // For all imports in this chunk that we have emitted files for...
      const importsToReplace = chunkInfo.imports.filter((fileName) =>
        emittedFiles.get(fileName),
      );
      if (!importsToReplace.length) {
        return null;
      }

      // ...replace import paths with relative paths to emitted css files
      const chunkPath = dirname(chunkInfo.fileName);
      const output = importsToReplace.reduce((codeResult, importPath) => {
        const assetId = emittedFiles.get(importPath)!;
        const assetName = this.getFileName(assetId);
        const fixedImportPath = `./${normalize(
          relative(chunkPath, assetName),
        )}`;
        return codeResult.replace(importPath, fixedImportPath);
      }, code);
      return {
        code: output,
        map: chunkInfo.map ?? null,
      };
    },
  };
}