2

I'm attempting to write a custom webpack loader that loads demo javascript files together with their source for documentation purposes. It should

  • Load the indicated javascript file (e.g. Simple.demo.js)
  • Attach the raw source of the file to the default export as a new field

When this is done, I should be able to configure the loader in my webpack config:

{
    test: /\.demo.js$/,
    exclude: /node_modules/,
    use: [{
       loader: "babel-loader",
    }, {
       loader: resolve("./demo-loader.js"),
    }]
}

…then access the source code for my demo after import:

import SimpleDemo from 'demo-loader!./Simple.demo.js';                  
console.log(SimpleDemo.__source__);
SimpleDemo(); //run the demo code

Attempt 1

I have successfully loaded the raw source and done a named export, using the following loader:

const { readFile } = require("fs");
const escapeSource = require("js-string-escape");

module.exports = function withSourceLoader(content, map, meta) {
    const onComplete = this.async();
    const fileName = this.resource;
    readFile(fileName, (error, fileContents) => {
        if ( error ) {
            return onComplete(error);
        }
        const exportSource = `export const rawSource = "${ escapeSource(fileContents)}";`;
        const newContent = `${ content }\n\n${ exportSource }`;

        onComplete(null, newContent, map, meta);
    });
};

This lets me do

import SimpleDemo, { rawSource } from 'demo-loader!./Simple.demo.js`;                  
console.log(rawSource);
SimpleDemo(); //run the demo code

…but I want to pass the whole SimpleDemo as an object to another function, so I'd like to import them as a single block (there will be lots of demos).

Attempt 2

Because I don't know the internal variable name used for the default export in the demo file (without doing a full AST parse), I can't just append an extra line to the file to modify it. Instead I tried to write a wrapper file that imports and re-exports the default, like this:

const { readFile } = require("fs");
const { basename } = require("path");
const escapeSource = require("js-string-escape");

module.exports = function withSourceLoader(content, map, meta) {
    const onComplete = this.async();
    const fileName = this.resource;
    readFile(fileName, (error, fileContents) => {
        if ( error ) {
            return onComplete(error);
        }
        const newContent = `
             const rawSource = "${ escapeSource(fileContents) }";
             import DemoDefault from './${ basename(fileName) }';
             DemoDefault.__source__ = rawSource;
             export default DemoDefault;
        `;

        onComplete(null, newContent, map, meta);
    });
};

My hope was that this new import directive would get resolved by webpack (I'm aware that this could lead to an infinite regression, because it should just trigger the same loader again based on filename). This doesn't seem to happen, however - I just get

TypeError: _Simple_demo_js__WEBPACK_IMPORTED_MODULE_0__.default is undefined

which appears to be because webpack has already built its dependency tree and doesn't want to add anything else to it. I have also tried using a require() instead of import, this doesn't improve matters.

Am I going about this the wrong way? I wanted to use something simple with chaining, by combining the output of other loaders, but the API doesn't seem to have a way to do that.

Duncan Thacker
  • 5,073
  • 1
  • 10
  • 20

1 Answers1

1

Figured out how to do this using a webpack "pitching" loader:

const { readFile } = require("fs");
const escapeSource = require("js-string-escape");
const { stringifyRequest } = require("loader-utils");

module.exports = function empty() { /* do nothing */ };

module.exports.pitch = function withSourceLoader(remainingRequest) {
    const onComplete = this.async();
    const filename = this.resource;
    readFile(filename, (error, fileContents) => {
        if ( error ) {
            return onComplete(error);
        }
        const newContent = `        
            module.exports = require(${stringifyRequest(this, "!!" + remainingRequest)});
            const rawSource = "${ escapeSource(fileContents) }";
            module.exports.default.__source__ = rawSource;
        `;

        onComplete(null, newContent);
    });
};

Still not 100% clear on the magic behind how my require() call resolves, but it works great! I plan to publish as a module later this week (with some other features).

Duncan Thacker
  • 5,073
  • 1
  • 10
  • 20