src/methods/pitch.ts
import loaderUtils from 'loader-utils';
import validateOptions from 'schema-utils';
import webpack, { Compiler } from 'webpack';
// @ts-ignore
import NodeTemplatePlugin from 'webpack/lib/node/NodeTemplatePlugin';
// @ts-ignore
import NodeTargetPlugin from 'webpack/lib/node/NodeTargetPlugin';
// @ts-ignore
import LibraryTemplatePlugin from 'webpack/lib/LibraryTemplatePlugin';
// @ts-ignore
import SingleEntryPlugin from 'webpack/lib/SingleEntryPlugin';
// @ts-ignore
import LimitChunkCountPlugin from 'webpack/lib/optimize/LimitChunkCountPlugin';
import type { PitchContext } from '../types/context';
import type { MiniExtractPlugin } from '../types/subclassing';
import type {
GetModule,
GetDependencyOptions,
} from '../types/subclassing-util';
import debug from '../lib/debug';
import hotLoader from '../lib/hot-loader';
import * as moduleLib from '../lib/module';
import { callTap } from '../lib/hook';
export default async function pitch<
MEP extends MiniExtractPlugin = MiniExtractPlugin
>(
this: MEP,
loaderContext: any,
remainingRequest: string,
precedingRequest: string,
data: object,
) {
debug('Started pitch method');
type Module = GetModule<MEP>;
type DepOpts = GetDependencyOptions<MEP>;
const callback = loaderContext.async();
const pitchContext: PitchContext<MEP> = {
plugin: this,
classOptions: this.classOptions,
options: this.options,
loaderContext,
remainingRequest,
precedingRequest,
data,
};
await callTap({
name: 'pitch',
hooks: this.hooks,
args: [pitchContext, undefined, undefined],
});
const options = loaderUtils.getOptions(loaderContext) || {};
validateOptions(
this.classOptions.loaderOptionsSchema,
options,
// @ts-ignore
`${this.classOptions.displayName} Loader`,
);
const loaders = loaderContext.loaders.slice(loaderContext.loaderIndex + 1);
loaderContext.addDependency(loaderContext.resourcePath);
const childFilename = '*';
const publicPath =
typeof options.publicPath === 'string'
? options.publicPath === '' || options.publicPath.endsWith('/')
? options.publicPath
: `${options.publicPath}/`
: typeof options.publicPath === 'function'
? options.publicPath(
loaderContext.resourcePath,
loaderContext.rootContext,
)
: loaderContext._compilation.outputOptions.publicPath;
const outputOptions = {
filename: childFilename,
publicPath,
};
const childCompiler: Compiler = loaderContext._compilation.createChildCompiler(
`${this.classOptions.pluginName} ${remainingRequest}`,
outputOptions,
);
new NodeTemplatePlugin(outputOptions).apply(childCompiler);
new LibraryTemplatePlugin(null, 'commonjs2').apply(childCompiler);
new NodeTargetPlugin().apply(childCompiler);
new SingleEntryPlugin(
loaderContext.context,
`!!${remainingRequest}`,
this.classOptions.pluginName,
).apply(childCompiler);
new LimitChunkCountPlugin({ maxChunks: 1 }).apply(childCompiler);
const pitchCompilerContext = {
...pitchContext,
childCompiler,
};
await callTap({
name: 'childCompiler',
hooks: this.hooks,
args: [pitchCompilerContext, undefined, undefined],
});
childCompiler.hooks.thisCompilation.tap(
`${this.classOptions.pluginName} loader`,
(compilation) => {
debug('Started pitch.childCompiler.thisCompilation');
compilation.hooks.normalModuleLoader.tap(
`${this.classOptions.pluginName} loader`,
(childLoaderContext, module) => {
debug(
'Started pitch.childCompiler.thisCompilation.normalModuleLoader',
);
const mod: Module = module as any;
// eslint-disable-next-line no-param-reassign
childLoaderContext.emitFile = loaderContext.emitFile;
if (mod.request === remainingRequest) {
// eslint-disable-next-line no-param-reassign
// @ts-ignore
module.loaders = loaders.map((loader) => {
return {
loader: loader.path,
options: loader.options,
ident: loader.ident,
};
});
}
debug('Done pitch.childCompiler.thisCompilation.normalModuleLoader');
},
);
debug('Done pitch.childCompiler.thisCompilation');
},
);
let source: string;
childCompiler.hooks.afterCompile.tapPromise(
this.classOptions.pluginName,
async (compilation) => {
debug('Started pitch.childCompiler.afterCompile');
const pitchCompilationContext = {
...pitchCompilerContext,
childCompilation: compilation,
};
// Let hooks modify the source
source = await callTap({
name: 'source',
hooks: this.hooks,
args: [pitchCompilationContext, undefined, undefined],
default: compilation.assets[childFilename]?.source(),
});
// Remove all chunk assets
compilation.chunks.forEach((chunk) => {
chunk.files.forEach((file: any) => {
delete compilation.assets[file]; // eslint-disable-line no-param-reassign
});
});
debug('Done pitch.childCompiler.afterCompile');
},
);
// @ts-ignore
childCompiler.runAsChild(
(
err: Error,
entries: any,
compilation: webpack.compilation.Compilation,
) => {
debug('Started pitch.childCompiler.runAsChild');
const pitchCompilationContext = {
...pitchCompilerContext,
childCompilation: compilation,
};
callTap({
name: 'childCompilation',
hooks: this.hooks,
args: [pitchCompilationContext, undefined, undefined],
});
const addDependencies = (dependencies: DepOpts[]) => {
if (!Array.isArray(dependencies) && dependencies != null) {
throw new Error(
`Exported value was not extracted as an array: ${JSON.stringify(
dependencies,
)}`,
);
}
const identifierCountMap = new Map();
for (const dependency of dependencies) {
const count = identifierCountMap.get(dependency.identifier) || 0;
loaderContext._module.addDependency(
new this.classOptions.dependencyClass(
dependency,
dependency.context,
count,
),
);
identifierCountMap.set(dependency.identifier, count + 1);
}
};
if (err) return callback(err);
if (compilation.errors.length > 0) {
return callback(compilation.errors[0]);
}
compilation.fileDependencies.forEach((dep) => {
loaderContext.addDependency(dep);
}, loaderContext);
compilation.contextDependencies.forEach((dep) => {
loaderContext.addContextDependency(dep);
}, loaderContext);
if (!source) {
return callback(new Error("Didn't get a result from child compiler"));
}
let locals;
try {
debug(`Evaluating source of module "${remainingRequest}"`);
let exports = moduleLib.evalCode(
loaderContext,
source,
remainingRequest,
);
// eslint-disable-next-line no-underscore-dangle
exports = exports.__esModule ? exports.default : exports;
locals = exports && exports.locals;
const exportsData = !Array.isArray(exports)
? [[null, exports]]
: exports;
const loaderModuleContext = { source, locals, exports: exportsData };
const dependencies: DepOpts[] = [];
// Get dependencies from hooks if any
let deps: DepOpts[] = callTap({
name: 'dependency',
hooks: this.hooks,
args: [pitchCompilationContext, loaderModuleContext, undefined],
});
// Default behaviour
if (deps === undefined) {
deps = [];
for (const [id, content] of exportsData) {
const mod = moduleLib.findById(compilation.modules, id);
if (!mod) continue;
deps.push({
identifier: mod.identifier(),
context: mod.context,
content,
miniExtractType: this.classOptions.type,
moduleType: this.classOptions.moduleType,
});
}
}
dependencies.push(...deps);
addDependencies(dependencies);
} catch (e) {
return callback(e);
}
const esModule =
typeof options.esModule !== 'undefined' ? options.esModule : false;
const result = locals
? `\n${
esModule ? 'export default' : 'module.exports ='
} ${JSON.stringify(locals)};`
: '';
let resultSource = `// extracted by ${this.classOptions.pluginName}`;
resultSource += options.hmr
? hotLoader(result, {
context: loaderContext.context,
options,
locals,
type: this.classOptions.type,
})
: result;
resultSource = callTap({
name: 'extracted',
hooks: this.hooks,
args: [pitchCompilationContext, resultSource, undefined],
default: resultSource,
});
debug('Done pitch.childCompiler.runAsChild');
debug('Calling loaderContext async callback in pitch');
return callback(null, resultSource);
},
);
debug('Done pitch method');
}