mini-css-extract-plugin icon indicating copy to clipboard operation
mini-css-extract-plugin copied to clipboard

Support passthrough dependency request.

Open ScriptedAlchemy opened this issue 3 years ago • 14 comments

Feature Proposal

Feature Use Case

I want to use webpack to bundle npm packages and component libraries that come with css/scss. Currently, mini-css treats css resources as side effects. Which makes sense when webpack is the consumer build, but does not make sense if webpack is a library build.

Id like to still process styles, but leave the require statement as an external.

example: require('./button.scss') -> becomes document.createElement(handleSideEffectDep) for library builds id like to require('./button.scss') -> require('./dist/248.css') so that the consumer build manages the side effect and i can ship component libraries that require their own css, but do not handle DOM injection.

Currently you would have to use style-loader which is not optimal. The alternative avaliable is using something like rollup which does not attempt to bundle side effects of a library, instead it treats it like an external once emitted. Then the consumer build is responsible for loading the side effect and its runtime requirements to inject styles.

Current output:

;// CONCATENATED MODULE: ./src/component.css
// extracted by mini-css-extract-plugin
/* harmony default export */ const component = ({"test-css-loader":"gj_ovF"});

desired output

;// CONCATENATED MODULE: ./src/component.css
// extracted by mini-css-extract-plugin
require('dist/248.css') // REFERENCE MODULE: ./src/component.css
/* harmony default export */ const component = ({"test-css-loader":"gj_ovF"});

If i add a very primitive loader to the start of the loader chain, it kind of works (though resolution paths would be incorrect. But that could be fixed.

module.exports = function(content,map,meta) {
    // console.log(this.resourcePath);
    // console.log(content);
    // content.source = 'test'
    const result = `__non_webpack_require__('${this.resourcePath}');
    ${content}`

    console.log(result)
    this.callback(null,result)
}

that would process the file as i need, and still load a require() into the file as a side effect - however this should point to main.css to pair with main.mjs/js

Please paste the results of npx webpack-cli info here, and mention other relevant information

ScriptedAlchemy avatar May 17 '22 22:05 ScriptedAlchemy

Seemingly related but I feel this could be conveniently supported by minicss. Something along the lines of an option to use ExternalModule instead of CSSModule within the plugin

https://github.com/webpack-contrib/mini-css-extract-plugin/issues/95

ScriptedAlchemy avatar May 18 '22 01:05 ScriptedAlchemy

Sounds good, I think we can do it under option @sokra @vankop What do you think?

alexander-akait avatar May 18 '22 19:05 alexander-akait

Yeah like on the Loader Options

type: "external" or external: true which would indicate "require/import the emitted asset around where locals usually is written to. This would only be intended for package authoring, not really for "last mile" builds

ScriptedAlchemy avatar May 18 '22 19:05 ScriptedAlchemy

Yes, let's wait answers and we will decide how best to design/implement it

alexander-akait avatar May 18 '22 20:05 alexander-akait

Fantastic! Fully willing to contribute on this one as well!

ScriptedAlchemy avatar May 19 '22 01:05 ScriptedAlchemy

How will this work during SSR?? @ScriptedAlchemy

ajayjaggi97 avatar May 23 '22 16:05 ajayjaggi97

Consumer build would be in charge of last mile. So css loader / mini css would strip the imports out and just leave the local exports. Same way it works today. Just slight differentiation between packaging via final build by consumer

ScriptedAlchemy avatar May 23 '22 18:05 ScriptedAlchemy

Update on this.

My team took my loader idea mentioned above and added an extra loader before mini css. Then using contextify we are able to inject a require or import() depending on if its esm or cjs.

Not sure how we could implement import since im depending on magic comments or non webpack require to prevent it getting extracted again.

Bit hacky but it seems to work for the time being while this issue is being investigated

ScriptedAlchemy avatar May 26 '22 04:05 ScriptedAlchemy

@ScriptedAlchemy Feel free to send a PR (with any solutions) so we can dicussion on code

alexander-akait avatar May 26 '22 14:05 alexander-akait

Hello! This is what we wrote on loading the field's css output. I took the mainFields value to construct the css path, since I think that's what this plugin is using to generate the css file. We have a couple checks just for browser targets and modules in order to pass the right fields in. Seems to work for our case, and would love feedback on it!

module.exports = function (content) {
  const { utils, _compiler: compiler } = this;

  if (!compiler.options.output.library && compiler.options.target === "web") {
    this.callback(null, content);
    return;
  }

  const mainFields = compiler.options.resolve.mainFields;
  const outputPath = (field) =>
    utils.contextify(compiler.outputPath, `./${field}.css`);

  let result;

  if (compiler.options.output.module) {
    result = mainFields.map((fieldString) => {
      return `import(/* webpackIgnore: true */ '${outputPath(fieldString)}')`;
    });
  } else {
    result = mainFields.map((fieldString) => {
      return `__non_webpack_require__('${outputPath(fieldString)}')`;
    });
  }
  result.push(content);

  this.callback(null, result.join(";"));
};

mikeechen avatar May 26 '22 18:05 mikeechen

@alexander-akait any suggestion on how we could implement. I’m happy to send a PR if you have a hint or sample of what/where I should start

ScriptedAlchemy avatar Jun 10 '22 21:06 ScriptedAlchemy

@ScriptedAlchemy Do you have any solution? If yes, let's add the option and feel free to send a PR with code that you have. Exampe above is good, we need the same

alexander-akait avatar Jun 11 '22 14:06 alexander-akait

Excellent I'll start tomorrow with my team. I see it as need for emit asset, use mini css loader to handle exports. Then take js and emit it, then inject the require into the file with the css module maps.

ScriptedAlchemy avatar Jun 30 '22 09:06 ScriptedAlchemy

Okay im going to try and factor this into mini-css tomorrow. But this loader "mostly" works - its messy and needs cleanup, but I just got the desired result out of the build. Note i set externals to /external\.css/ so webpack doesnt go into an import loop

const AUTO_PUBLIC_PATH = "__mini_css_extract_plugin_public_path_auto__";
const BASE_URI = "webpack://";
const { getOptions, interpolateName } = require("loader-utils");
const path = require("path");
const { normalizePath } = require("./utils");
const handleExports = (exports) => {
  const result = exports.default || exports;
  return {
    content: result.toString(),
    locals: result.locals,
  };
};
async function pitch(content) {
  const options = {};
  const callback = this.async();
  let { publicPath, module } =
    /** @type {Compilation} */
    (this._compilation).outputOptions;
  let loaderArray = this.request.split("!");
  loaderArray.shift();
  const nonCircularLoader = loaderArray.join("!");

  const result = await this.importModule(
    `${this.resourcePath}.webpack[javascript/auto]!=!!!${nonCircularLoader}`
  );
  const handledResult = handleExports(result);

  let loaderResult = [];

  const context = options.context || this.rootContext;
  const name = options.name || "[contenthash].external.css";

  const url = interpolateName(this, name, {
    context,
    content,
    regExp: options.regExp,
  });
  if (typeof options.emitFile === "undefined" || options.emitFile) {
    const assetInfo = {};

    if (typeof name === "string") {
      let normalizedName = name;

      const idx = normalizedName.indexOf("?");

      if (idx >= 0) {
        normalizedName = normalizedName.substr(0, idx);
      }

      const isImmutable = /\[([^:\]]+:)?(hash|contenthash)(:[^\]]+)?]/gi.test(
        normalizedName
      );

      if (isImmutable === true) {
        assetInfo.immutable = true;
      }
    }

    assetInfo.sourceFilename = normalizePath(
      path.relative(this.rootContext, this.resourcePath)
    );

    console.log(assetInfo);
    console.log(url);

    this.emitFile(url, handledResult.content, null, assetInfo);
  }
  if (module) {
    loaderResult.push(`import '${url}'`);
    loaderResult.push("export default " + JSON.stringify(handledResult.locals));
  } else {
    loaderResult.push(`require('${url}')`);
    loaderResult.push(
      "module.exports = " + JSON.stringify(handledResult.locals)
    );
  }
  this.callback(null, loaderResult.join(";"));
}
module.exports = {
  default:   pitch, ///TODO: actually use loader.pitch instead of loader
};

ScriptedAlchemy avatar Jul 08 '22 22:07 ScriptedAlchemy