Webpack'ing "sass" (dart-sass), uses require dynamically, hard code dependencies into context?

Viewed 799

Problem

The "sass" bundle contains a module, sass.dart.js: https://runpkg.com/?sass@1.21.0/sass.dart.js

Amongst other things, this contains dynamic usage of require (many lines redacted):

var self = Object.create(global);
self.require = require;

//...

u($,"N0","Hq",function(){return self.require("readline")})
u($,"Mr","cN",function(){return self.require("fs")})
u($,"MN","Ho",function(){return self.require("chokidar")})

This use of require prevents static analysis by webpack, resulting in the error in the output (also in this gist) and a webpackEmptyContext being created in the bundle. This naturally doesn't work :)

Options Tried

I've seen the ContextReplacementPlugin, I don't think this applies here as the usage is not a direct require call.

I've seen noParse, but that feels like the nuclear approach, and I don't know what the potential negative side effects may be.

Reproduction code

Also in this gist

index.js:

const sass = require("sass");
console.log(sass);

webpack.config.js:

module.exports = (env) => ({
    entry: "./index.js",
    mode: "development",
    target: "node"
});

package.json:

{
  "devDependencies": {
    "webpack": "^4.34.0",
    "webpack-cli": "^3.3.4"
  },
  "dependencies": {
    "sass": "^1.21.0"
  },
  "scripts": {
    "build": "webpack-cli"
  }
}

Goal

How can I configure the context for this particular module to specifically list readline/fs/chokidar as dependencies, possibly to produce some context that only provides those values?

I don't mind if the solution is a little brittle and would require pinning of the version of dart-sass.

1 Answers

We've arrived at a "solution" that implements a custom, dart-sass specific loader to prepend code to the module to bypass the static analysis.

sass-prepend-loader.js:

const SASS_PREPEND_CODE = `require = (function () {
  var fs = require("fs");
  var readline = require("readline");
  return function (mod) {
    switch (mod) {
      case "fs": return fs;
      case "readline": return readline;
      default: throw new Error("Unexpected sass dependency");
    }
  };
})();`

module.exports = function(source) {
    this.cacheable();
    this.callback(null, `${SASS_PREPEND_CODE}\n${source}`);
};

webpack.config.js (excerpt):

module: {
    rules: [
        {
            resource: path.resolve(__dirname, "node_modules", "sass", "sass.dart.js"),
            loader: path.resolve(__dirname, "sass-prepend-loader.js"),
        }
    ],
},

This is tailored specifically to the current "sass" version, 1.20.3, and possibly also behaviour in current webpack. Note: we're not passing through the "chokidar" dependency, our use case does not require it.

It works by assigning to require with a version that can only serve up "fs" and "readline". Webpack replaces the replace called with __webpack_require__ calls (satisfying its desire to statically analyse) and allows require (now shadowed) to stick around.

Extract from dist/index.js:

/***/ (function(module, exports, __webpack_require__) {

(function(module, exports, __webpack_require__) {

eval("/* WEBPACK VAR INJECTION */(function(__filename) {var require;require = (function () {\n  var fs = __webpack_require__(/*! fs */ \"fs\");\n  var readline = __webpack_require__(/*! readline */ \"readline\");\n  return function (mod) {\n    switch (mod) {\n      case \"fs\": return fs;\n      case \"readline\": return readline;\n      default: throw new Error(\"Unexpected sass dependency\");\n    }\n  };\n})();\n// make sure to keep this as 'var'\n// we don't want block scoping\nvar self = Object.create(global);\n\nself.scheduleImmediate = self.setImmediate\n    ? function (cb) {\n        global.setImmediate(cb);\n      }\n    : function(cb) {\n        setTimeout(cb, 0);\n      };\n\nself.require = require;

//...

self.require(\"fs\")

Hope that this is a temporary solution and that we can arrive at a better solution from one of these avenues:

Related