Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 74 additions & 30 deletions RequireJsLoaderPlugin.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
'use strict';

const ConcatSource = require('webpack-sources').ConcatSource;
const { ConcatSource } = require('webpack-sources');

let RequireJsLoaderPlugin = function () {
};

function isNormalModule(module) {
return Boolean(module && typeof module.request === 'string' && module.rawRequest != null);
}

function normalizeRequest(rawRequest) {
return String(rawRequest).replace(/^mixins!/, '').replace(/\.js$/, '');
}

function gatherRequireJsImports(modules) {
let needsImport = [];
for (let module of modules) {
// If the requirejs-loader was used, then we need to wrap and import this module.
// It's safe to use mixins! in all cases, and necessary for anything where require('mixins').hasMixins(module) is true.
// TODO: Clean up this check.
if (module.request && module.request.indexOf('requirejs-loader') !== -1) {
needsImport.push('mixins!' + module.rawRequest);
if (isNormalModule(module) && String(module.request).indexOf('requirejs-loader') !== -1) {
needsImport.push('mixins!' + normalizeRequest(module.rawRequest));
}
}

Expand All @@ -28,32 +33,71 @@ function generateEpilog(imports) {
return `});`;
}

function registerHook(object, oldName, newName, cb) {
if (object.hooks) {
object.hooks[newName].tap('RequireJsLoader', cb);
RequireJsLoaderPlugin.prototype.apply = function (compiler) {
const isWebpack5 = Boolean(compiler.webpack);

if (isWebpack5) {
compiler.hooks.compilation.tap('RequireJsLoaderPlugin', (compilation) => {
const Compilation = compiler.webpack.Compilation;

compilation.hooks.processAssets.tap(
{
name: 'RequireJsLoaderPlugin',
stage: Compilation.PROCESS_ASSETS_STAGE_SUMMARIZE,
},
() => {
const processedFiles = new Set();

for (const chunk of compilation.chunks) {
const chunkModules = Array.from(
compilation.chunkGraph.getChunkModulesIterable(chunk)
);

const needsImport = gatherRequireJsImports(chunkModules);

if (needsImport.length === 0) continue;

const prolog = generateProlog(needsImport);
const epilog = generateEpilog(needsImport);

for (const filename of chunk.files) {
if (!filename.endsWith('.js')) continue;
if (processedFiles.has(filename)) continue;

processedFiles.add(filename);
compilation.updateAsset(
filename,
(old) => new ConcatSource(prolog, '\n', old, '\n', epilog)
);
}
}
}
);
});
} else {
object.plugin(oldName, cb);
}
}
compiler.hooks.compilation.tap('RequireJsLoaderPlugin', (compilation) => {
compilation.hooks.chunkAsset.tap('RequireJsLoaderPlugin', (chunk, filename) => {
if (!filename.endsWith('.js')) return;

RequireJsLoaderPlugin.prototype.apply = function (compiler) {
registerHook(compiler, 'compilation', 'compilation', (compilation, data) => {
registerHook(compilation, 'chunk-asset', 'chunkAsset', (chunk, filename) => {
// Avoid applying imports twice.
if ('--requirejs-export:done' in chunk) {
return;
}

const modules = chunk.modulesIterable ? Array.from(chunk.modulesIterable) : modules;
const needsImport = gatherRequireJsImports(modules);
if (needsImport.length != 0) {
let prolog = generateProlog(needsImport);
let epilog = generateEpilog(needsImport);

compilation.assets[filename] = new ConcatSource(prolog, "\n", compilation.assets[filename], "\n", epilog);
}
// Avoid applying imports twice.
if ('--requirejs-export:done' in chunk) {
return;
}

const modules = chunk.modulesIterable ? Array.from(chunk.modulesIterable) : [];
const needsImport = gatherRequireJsImports(modules);

if (needsImport.length !== 0) {
let prolog = generateProlog(needsImport);
let epilog = generateEpilog(needsImport);

compilation.assets[filename] = new ConcatSource(prolog, "\n", compilation.assets[filename], "\n", epilog);
}

chunk['--requirejs-export:done'] = true;
});
});
});
}
};

module.exports = RequireJsLoaderPlugin;
module.exports = RequireJsLoaderPlugin;
50 changes: 40 additions & 10 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,49 @@
'use strict';

const path = require('path');
const ConcatSource = require('webpack-sources').ConcatSource;

module.exports = function () {
};

module.exports.pitch = function (remainingRequest) {
this.cacheable && this.cacheable();
this.cacheable && this.cacheable();

const options = (this.getOptions ? this.getOptions() : this.query) || {};

// This loader is normally applied via a `module.rules` entry rather than an
// inline `mixins!` prefix, so `remainingRequest` here resolves to the absolute
// filesystem path of the file, not the bare specifier (e.g. "jquery", or a
// RequireJS-style module id) that was originally written in source. We need
// that original, unresolved specifier to build a `window.require(...)` id
// RequireJS will actually recognize. `this._module.rawRequest` is the only
// way to recover it; it's a documented-but-discouraged loader context
// property webpack has flagged for eventual removal, but there's currently
// no public replacement that preserves the as-written request, so we keep
// using it and only fall back to `remainingRequest` if it's unavailable.
const rawRequest = (this._module && this._module.rawRequest) || remainingRequest;
const requestName = String(rawRequest).replace(/^mixins!/, '').replace(/\.js$/, '');

if (options.debug) {
console.log('[requirejs-loader] pitch', {
resourcePath: this.resourcePath,
remainingRequest,
rawRequest,
requestName,
});
}

let output;
if (requestName === 'jquery') {
const jqueryRequest = JSON.stringify('mixins!' + requestName);
output = `module.exports = window.jQuery || window.$ || (typeof window.require === 'function' ? window.require(${jqueryRequest}) : undefined);`;
} else {
const jsonName = JSON.stringify('mixins!' + requestName);
output = `module.exports = window.require(${jsonName});`;
}

if (typeof options.onTransform === 'function') {
output = options.onTransform(output, this.resourcePath) || output;
}

// Route through window.require.
// It's safe to use mixins! in all cases, and necessary for anything where require('mixins').hasMixins(module) is true.
// TODO: We use rawRequest to grab the original request (including text! or etc.)
const jsonName = JSON.stringify('mixins!' + this._module.rawRequest);
return `module.exports = window.require(${jsonName});`;
return output;
};

module.exports.RequireJsLoaderPlugin = require('./RequireJsLoaderPlugin.js');
module.exports.RequireJsLoaderPlugin = require('./RequireJsLoaderPlugin.js');
8 changes: 6 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
{
"name": "@sdinteractive/requirejs-loader",
"version": "3.0.0",
"description": "webpack loader to import requirejs browser published modules.",
"version": "4.0.0",
"description": "webpack loader to import requirejs browser published modules. Supports webpack 4 and webpack 5.",
"main": "index.js",
"peerDependencies": {
"webpack": "^4.0.0 || ^5.0.0",
"webpack-sources": "^1.4.3 || ^2.0.0 || ^3.0.0"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
Expand Down