maxmilton/rollup-plugin-css

View on GitHub
src/index.ts

Summary

Maintainability
A
0 mins
Test Coverage
F
0%
import { createFilter, type FilterPattern } from '@rollup/pluginutils';
import { transform } from 'lightningcss';
import type { Targets } from 'lightningcss/node/targets';
import { basename, extname } from 'node:path';
import type { Plugin, SourceMap } from 'rollup';
import { SourceMapConsumer, SourceNode, type RawSourceMap } from 'source-map';

export interface PluginOptions {
  /**
   * Files to exclude from processing.
   *
   * @default []
   */
  exclude?: FilterPattern | undefined;
  /**
   * Files to include in processing.
   *
   * @default /\.x?css$/
   */
  include?: FilterPattern | undefined;
  /** Minify CSS. */
  minify?: boolean | undefined;
  targets?: Targets | undefined;
  /**
   * Name of combined CSS file to emit.
   *
   * When not defined the asset name will be inferred from `rollup#output.name`
   * or `rollup#output.file`.
   */
  name?: string | undefined;
}

function rollupPlugin({
  exclude = [],
  name,
  include = /\.x?css$/,
  minify,
  targets,
}: PluginOptions = {}): Plugin {
  const filter = createFilter(include, exclude);
  const styles = new Map<string, string>();
  const sourcemaps = new Map<string, SourceMap>();
  let useSourceMaps: boolean | 'inline' | 'hidden';

  return {
    name: 'css',

    renderStart(outputOptions) {
      useSourceMaps = outputOptions.sourcemap;
    },

    transform(code, id) {
      if (!filter(id)) return null;

      styles.set(id, code);

      if (useSourceMaps) {
        sourcemaps.set(id, this.getCombinedSourcemap());
      }

      return {
        code: '',
        map: { mappings: '' },
      };
    },

    async generateBundle(outputOpts) {
      if (styles.size === 0) return;

      let css = '';

      for (const id of styles.keys()) {
        css += styles.get(id) || '';
      }

      if (!css) return;

      const inferredName =
        name ||
        outputOpts.name ||
        (outputOpts.file && basename(outputOpts.file));

      if (!inferredName) {
        // eslint-disable-next-line consistent-return
        return this.error(
          'Unable to infer output CSS asset name; add "name" to plugin options or rollup output options',
        );
      }

      const assetName = inferredName.replace(extname(inferredName), '');
      const combinedCss = String(css);
      let minifiedMap;

      if (minify) {
        const minified = transform({
          filename: assetName,
          code: Buffer.from(css),
          minify: true,
          sourceMap: !!outputOpts.sourcemap,
          targets: targets as Targets, // cast to workaround poorly typed package
        });

        for (const warning of minified.warnings) {
          this.warn(warning.message);
        }

        css = minified.code.toString();
        minifiedMap = minified.map?.toString();
      }

      const assetFileId = this.emitFile({
        name: `${assetName}.css`,
        source: css,
        type: 'asset',
      });

      if (outputOpts.sourcemap) {
        const mapNodes = [];

        for (const id of sourcemaps.keys()) {
          mapNodes.push(
            SourceNode.fromStringWithSourceMap(
              styles.get(id)!,
              // eslint-disable-next-line no-await-in-loop
              await new SourceMapConsumer(sourcemaps.get(id) as RawSourceMap),
            ),
          );
        }

        if (minifiedMap) {
          mapNodes.push(
            SourceNode.fromStringWithSourceMap(
              combinedCss,
              await new SourceMapConsumer(minifiedMap),
            ),
          );
        }

        const root = new SourceNode(null, null, null, mapNodes);
        const out = root.toStringWithSourceMap({
          file: `${assetName}.css`,
        });

        if (outputOpts.sourcemap === 'inline') {
          css += `\n/*# sourceMappingURL=data:application/json;base64,${Buffer.from(
            out.map.toString(),
            'utf8',
          ).toString('base64')} */`;
        } else {
          const assetFileName = this.getFileName(assetFileId);

          this.emitFile({
            fileName: `${assetFileName}.map`,
            source: out.map.toString(),
            type: 'asset',
          });

          if (outputOpts.sourcemap !== 'hidden') {
            css += `\n/*# sourceMappingURL=${assetFileName}.map */`;

            // FIXME: Can't getFileName before setting CSS source; Overwriting
            // like this causes a warning: The emitted file "app-c3ca2dbc.css"
            // overwrites a previously emitted file of the same name.
            this.emitFile({
              fileName: assetFileName,
              source: css,
              type: 'asset',
            });
            //  ↳ Can't getFileName before setAssetSource :(
            // this.setAssetSource(assetFileId, css);
          }
        }
      }
    },
  };
}

export { rollupPlugin as css };