JuroOravec/mini-extract-plugin

View on GitHub
test/fixtures/mini-css-extract-plugin/hooks.ts

Summary

Maintainability
C
1 day
Test Coverage
import { Template } from 'webpack';
import { SourceMapSource, OriginalSource, ConcatSource } from 'webpack-sources';

import { types, util } from '../../../src';

import type { MiniCssExtractPlugin, Taps } from './types';
import { getChunkObject } from './util';

const hooks: Partial<Taps> = {
  compilation: (ctx) => {
    const {
      classOptions: { pluginName, type, moduleType },
      compilation: { mainTemplate },
      options,
    } = ctx;

    mainTemplate.hooks.localVars.tap(pluginName, (source, chunk, hash) => {
      const chunkMap = getChunkObject(moduleType, chunk);

      if (Object.keys(chunkMap).length > 0) {
        return Template.asString([
          source,
          '',
          `// object to store loaded ${type} chunks`,
          `var ${type}InstalledChunks = {`,
          Template.indent(
            (chunk.ids || [])
              .map((id) => `${JSON.stringify(id)}: 0`)
              .join(',\n'),
          ),
          '}',
        ]);
      }

      return source;
    });

    mainTemplate.hooks.requireEnsure.tap(pluginName, (source, chunk, hash) => {
      const chunkMap = getChunkObject(moduleType, chunk);

      if (Object.keys(chunkMap).length > 0) {
        // @ts-ignore
        const chunkMaps = chunk.getChunkMaps();
        const { crossOriginLoading } = mainTemplate.outputOptions;
        // @ts-ignore
        const linkHrefPath = mainTemplate.getAssetPath(
          JSON.stringify(options.chunkFilename),
          {
            // @ts-ignore
            hash: `" + ${mainTemplate.renderCurrentHashCode(hash)} + "`,
            hashWithLength: (length: number) =>
              // @ts-ignore
              `" + ${mainTemplate.renderCurrentHashCode(hash, length)} + "`,
            chunk: {
              id: '" + chunkId + "',
              hash: `" + ${JSON.stringify(chunkMaps.hash)}[chunkId] + "`,
              hashWithLength(length: number) {
                const shortChunkHashMap = Object.create(null);

                for (const chunkId of Object.keys(chunkMaps.hash)) {
                  if (typeof chunkMaps.hash[chunkId] === 'string') {
                    shortChunkHashMap[chunkId] = chunkMaps.hash[
                      chunkId
                    ].substring(0, length);
                  }
                }

                return `" + ${JSON.stringify(shortChunkHashMap)}[chunkId] + "`;
              },
              contentHash: {
                [moduleType]: `" + ${JSON.stringify(
                  chunkMaps.contentHash[moduleType],
                )}[chunkId] + "`,
              },
              contentHashWithLength: {
                [moduleType]: (length: number) => {
                  const shortContentHashMap: { [key: string]: any } = {};
                  const contentHash = chunkMaps.contentHash[moduleType];

                  for (const chunkId of Object.keys(contentHash)) {
                    if (typeof contentHash[chunkId] === 'string') {
                      shortContentHashMap[chunkId] = contentHash[
                        chunkId
                      ].substring(0, length);
                    }
                  }

                  return `" + ${JSON.stringify(
                    shortContentHashMap,
                  )}[chunkId] + "`;
                },
              },
              name: `" + (${JSON.stringify(
                chunkMaps.name,
              )}[chunkId]||chunkId) + "`,
            },
            contentHashType: moduleType,
          },
        );

        return Template.asString([
          source,
          '',
          `// ${pluginName} ${type} loading`,
          `var ${type}Chunks = ${JSON.stringify(chunkMap)};`,
          `if(${type}InstalledChunks[chunkId]) promises.push(${type}InstalledChunks[chunkId]);`,
          `else if(${type}InstalledChunks[chunkId] !== 0 && ${type}Chunks[chunkId]) {`,
          Template.indent([
            `promises.push(${type}InstalledChunks[chunkId] = new Promise(function(resolve, reject) {`,
            Template.indent([
              `var href = ${linkHrefPath};`,
              `var fullhref = ${mainTemplate.requireFn}.p + href;`,
              'var existingLinkTags = document.getElementsByTagName("link");',
              'for(var i = 0; i < existingLinkTags.length; i++) {',
              Template.indent([
                'var tag = existingLinkTags[i];',
                'var dataHref = tag.getAttribute("data-href") || tag.getAttribute("href");',
                'if(tag.rel === "stylesheet" && (dataHref === href || dataHref === fullhref)) return resolve();',
              ]),
              '}',
              'var existingStyleTags = document.getElementsByTagName("style");',
              'for(var i = 0; i < existingStyleTags.length; i++) {',
              Template.indent([
                'var tag = existingStyleTags[i];',
                'var dataHref = tag.getAttribute("data-href");',
                'if(dataHref === href || dataHref === fullhref) return resolve();',
              ]),
              '}',
              'var linkTag = document.createElement("link");',
              'linkTag.rel = "stylesheet";',
              `linkTag.type = "text/${type}";`,
              'linkTag.onload = resolve;',
              'linkTag.onerror = function(event) {',
              Template.indent([
                'var request = event && event.target && event.target.src || fullhref;',
                `var err = new Error("Loading ${type} chunk " + chunkId + " failed.\\n(" + request + ")");`,
                `err.code = "${type}_CHUNK_LOAD_FAILED";`,
                'err.request = request;',
                `delete ${type}InstalledChunks[chunkId]`,
                'linkTag.parentNode.removeChild(linkTag)',
                'reject(err);',
              ]),
              '};',
              'linkTag.href = fullhref;',
              crossOriginLoading
                ? Template.asString([
                    `if (linkTag.href.indexOf(window.location.origin + '/') !== 0) {`,
                    Template.indent(
                      `linkTag.crossOrigin = ${JSON.stringify(
                        crossOriginLoading,
                      )};`,
                    ),
                    '}',
                  ])
                : '',
              'var head = document.getElementsByTagName("head")[0];',
              'head.appendChild(linkTag);',
            ]),
            '}).then(function() {',
            Template.indent([`${type}InstalledChunks[chunkId] = 0;`]),
            '}));',
          ]),
          '}',
        ]);
      }

      return source;
    });
  },

  dependency: (context, { exports: exported }) => {
    const { childCompilation, classOptions } = context;
    const deps: types.GetHookReturnType<
      'dependency',
      MiniCssExtractPlugin
    > = [];
    for (const [id, content, media, sourceMap] of exported) {
      const mod = util.module.findById(childCompilation.modules, id);
      if (!mod) continue;
      deps.push({
        miniExtractType: classOptions.type,
        moduleType: classOptions.moduleType,
        identifier: mod.identifier(),
        context: mod.context,
        content,
        media,
        sourceMap,
      });
    }
    return deps;
  },

  merge: (ctx, modules) => {
    const {
      compilation: { requestShortener },
    } = ctx;
    const source = new ConcatSource();
    const externalsSource = new ConcatSource();
    for (const mod of modules) {
      if (/^@import url/.test(mod.content)) {
        // HACK for IE
        // http://stackoverflow.com/a/14676665/1458162
        let { content } = mod;

        if (mod.media) {
          // insert media into the @import
          // this is rar
          // TODO improve this and parse the CSS to support multiple medias
          content = content.replace(/;|\s*$/, mod.media);
        }

        externalsSource.add(content);
        externalsSource.add('\n');
      } else {
        if (mod.media) source.add(`@media ${mod.media} {\n`);

        if (mod.sourceMap) {
          source.add(
            new SourceMapSource(
              mod.content,
              mod.readableIdentifier(requestShortener),
              // @ts-ignore
              mod.sourceMap,
            ),
          );
        } else {
          source.add(
            new OriginalSource(
              mod.content,
              mod.readableIdentifier(requestShortener),
            ),
          );
        }
        source.add('\n');

        if (mod.media) source.add('}\n');
      }
    }
    return new ConcatSource(externalsSource, source);
  },
};

export default hooks;