305 lines
7.6 KiB
JavaScript
305 lines
7.6 KiB
JavaScript
|
"use strict";
|
||
|
|
||
|
const {
|
||
|
validate
|
||
|
} = require("schema-utils");
|
||
|
|
||
|
const mime = require("mime-types");
|
||
|
|
||
|
const middleware = require("./middleware");
|
||
|
|
||
|
const getFilenameFromUrl = require("./utils/getFilenameFromUrl");
|
||
|
|
||
|
const setupHooks = require("./utils/setupHooks");
|
||
|
|
||
|
const setupWriteToDisk = require("./utils/setupWriteToDisk");
|
||
|
|
||
|
const setupOutputFileSystem = require("./utils/setupOutputFileSystem");
|
||
|
|
||
|
const ready = require("./utils/ready");
|
||
|
|
||
|
const schema = require("./options.json");
|
||
|
|
||
|
const noop = () => {};
|
||
|
/** @typedef {import("schema-utils/declarations/validate").Schema} Schema */
|
||
|
|
||
|
/** @typedef {import("webpack").Compiler} Compiler */
|
||
|
|
||
|
/** @typedef {import("webpack").MultiCompiler} MultiCompiler */
|
||
|
|
||
|
/** @typedef {import("webpack").Configuration} Configuration */
|
||
|
|
||
|
/** @typedef {import("webpack").Stats} Stats */
|
||
|
|
||
|
/** @typedef {import("webpack").MultiStats} MultiStats */
|
||
|
|
||
|
/**
|
||
|
* @typedef {Object} ExtendedServerResponse
|
||
|
* @property {{ webpack?: { devMiddleware?: Context<IncomingMessage, ServerResponse> } }} [locals]
|
||
|
*/
|
||
|
|
||
|
/** @typedef {import("http").IncomingMessage} IncomingMessage */
|
||
|
|
||
|
/** @typedef {import("http").ServerResponse & ExtendedServerResponse} ServerResponse */
|
||
|
|
||
|
/**
|
||
|
* @callback NextFunction
|
||
|
* @param {any} [err]
|
||
|
* @return {void}
|
||
|
*/
|
||
|
|
||
|
/**
|
||
|
* @typedef {NonNullable<Configuration["watchOptions"]>} WatchOptions
|
||
|
*/
|
||
|
|
||
|
/**
|
||
|
* @typedef {Compiler["watching"]} Watching
|
||
|
*/
|
||
|
|
||
|
/**
|
||
|
* @typedef {ReturnType<Compiler["watch"]>} MultiWatching
|
||
|
*/
|
||
|
|
||
|
/**
|
||
|
* @typedef {Compiler["outputFileSystem"] & { createReadStream?: import("fs").createReadStream, statSync?: import("fs").statSync, lstat?: import("fs").lstat, readFileSync?: import("fs").readFileSync }} OutputFileSystem
|
||
|
*/
|
||
|
|
||
|
/** @typedef {ReturnType<Compiler["getInfrastructureLogger"]>} Logger */
|
||
|
|
||
|
/**
|
||
|
* @callback Callback
|
||
|
* @param {Stats | MultiStats} [stats]
|
||
|
*/
|
||
|
|
||
|
/**
|
||
|
* @template {IncomingMessage} Request
|
||
|
* @template {ServerResponse} Response
|
||
|
* @typedef {Object} Context
|
||
|
* @property {boolean} state
|
||
|
* @property {Stats | MultiStats | undefined} stats
|
||
|
* @property {Callback[]} callbacks
|
||
|
* @property {Options<Request, Response>} options
|
||
|
* @property {Compiler | MultiCompiler} compiler
|
||
|
* @property {Watching | MultiWatching} watching
|
||
|
* @property {Logger} logger
|
||
|
* @property {OutputFileSystem} outputFileSystem
|
||
|
*/
|
||
|
|
||
|
/**
|
||
|
* @template {IncomingMessage} Request
|
||
|
* @template {ServerResponse} Response
|
||
|
* @typedef {Record<string, string | number> | Array<{ key: string, value: number | string }> | ((req: Request, res: Response, context: Context<Request, Response>) => void | undefined | Record<string, string | number>) | undefined} Headers
|
||
|
*/
|
||
|
|
||
|
/**
|
||
|
* @template {IncomingMessage} Request
|
||
|
* @template {ServerResponse} Response
|
||
|
* @typedef {Object} Options
|
||
|
* @property {{[key: string]: string}} [mimeTypes]
|
||
|
* @property {boolean | ((targetPath: string) => boolean)} [writeToDisk]
|
||
|
* @property {string} [methods]
|
||
|
* @property {Headers<Request, Response>} [headers]
|
||
|
* @property {NonNullable<Configuration["output"]>["publicPath"]} [publicPath]
|
||
|
* @property {Configuration["stats"]} [stats]
|
||
|
* @property {boolean} [serverSideRender]
|
||
|
* @property {OutputFileSystem} [outputFileSystem]
|
||
|
* @property {boolean | string} [index]
|
||
|
*/
|
||
|
|
||
|
/**
|
||
|
* @template {IncomingMessage} Request
|
||
|
* @template {ServerResponse} Response
|
||
|
* @callback Middleware
|
||
|
* @param {Request} req
|
||
|
* @param {Response} res
|
||
|
* @param {NextFunction} next
|
||
|
* @return {Promise<void>}
|
||
|
*/
|
||
|
|
||
|
/**
|
||
|
* @callback GetFilenameFromUrl
|
||
|
* @param {string} url
|
||
|
* @returns {string | undefined}
|
||
|
*/
|
||
|
|
||
|
/**
|
||
|
* @callback WaitUntilValid
|
||
|
* @param {Callback} callback
|
||
|
*/
|
||
|
|
||
|
/**
|
||
|
* @callback Invalidate
|
||
|
* @param {Callback} callback
|
||
|
*/
|
||
|
|
||
|
/**
|
||
|
* @callback Close
|
||
|
* @param {(err: Error | null | undefined) => void} callback
|
||
|
*/
|
||
|
|
||
|
/**
|
||
|
* @template {IncomingMessage} Request
|
||
|
* @template {ServerResponse} Response
|
||
|
* @typedef {Object} AdditionalMethods
|
||
|
* @property {GetFilenameFromUrl} getFilenameFromUrl
|
||
|
* @property {WaitUntilValid} waitUntilValid
|
||
|
* @property {Invalidate} invalidate
|
||
|
* @property {Close} close
|
||
|
* @property {Context<Request, Response>} context
|
||
|
*/
|
||
|
|
||
|
/**
|
||
|
* @template {IncomingMessage} Request
|
||
|
* @template {ServerResponse} Response
|
||
|
* @typedef {Middleware<Request, Response> & AdditionalMethods<Request, Response>} API
|
||
|
*/
|
||
|
|
||
|
/**
|
||
|
* @template {IncomingMessage} Request
|
||
|
* @template {ServerResponse} Response
|
||
|
* @param {Compiler | MultiCompiler} compiler
|
||
|
* @param {Options<Request, Response>} [options]
|
||
|
* @returns {API<Request, Response>}
|
||
|
*/
|
||
|
|
||
|
|
||
|
function wdm(compiler, options = {}) {
|
||
|
validate(
|
||
|
/** @type {Schema} */
|
||
|
schema, options, {
|
||
|
name: "Dev Middleware",
|
||
|
baseDataPath: "options"
|
||
|
});
|
||
|
const {
|
||
|
mimeTypes
|
||
|
} = options;
|
||
|
|
||
|
if (mimeTypes) {
|
||
|
const {
|
||
|
types
|
||
|
} = mime; // mimeTypes from user provided options should take priority
|
||
|
// over existing, known types
|
||
|
// @ts-ignore
|
||
|
|
||
|
mime.types = { ...types,
|
||
|
...mimeTypes
|
||
|
};
|
||
|
}
|
||
|
/**
|
||
|
* @type {Context<Request, Response>}
|
||
|
*/
|
||
|
|
||
|
|
||
|
const context = {
|
||
|
state: false,
|
||
|
// eslint-disable-next-line no-undefined
|
||
|
stats: undefined,
|
||
|
callbacks: [],
|
||
|
options,
|
||
|
compiler,
|
||
|
// @ts-ignore
|
||
|
// eslint-disable-next-line no-undefined
|
||
|
watching: undefined,
|
||
|
logger: compiler.getInfrastructureLogger("webpack-dev-middleware"),
|
||
|
// @ts-ignore
|
||
|
// eslint-disable-next-line no-undefined
|
||
|
outputFileSystem: undefined
|
||
|
};
|
||
|
setupHooks(context);
|
||
|
|
||
|
if (options.writeToDisk) {
|
||
|
setupWriteToDisk(context);
|
||
|
}
|
||
|
|
||
|
setupOutputFileSystem(context); // Start watching
|
||
|
|
||
|
if (
|
||
|
/** @type {Compiler} */
|
||
|
context.compiler.watching) {
|
||
|
context.watching =
|
||
|
/** @type {Compiler} */
|
||
|
context.compiler.watching;
|
||
|
} else {
|
||
|
/**
|
||
|
* @type {WatchOptions | WatchOptions[]}
|
||
|
*/
|
||
|
let watchOptions;
|
||
|
/**
|
||
|
* @param {Error | null | undefined} error
|
||
|
*/
|
||
|
|
||
|
const errorHandler = error => {
|
||
|
if (error) {
|
||
|
// TODO: improve that in future
|
||
|
// For example - `writeToDisk` can throw an error and right now it is ends watching.
|
||
|
// We can improve that and keep watching active, but it is require API on webpack side.
|
||
|
// Let's implement that in webpack@5 because it is rare case.
|
||
|
context.logger.error(error);
|
||
|
}
|
||
|
};
|
||
|
|
||
|
if (Array.isArray(
|
||
|
/** @type {MultiCompiler} */
|
||
|
context.compiler.compilers)) {
|
||
|
watchOptions =
|
||
|
/** @type {MultiCompiler} */
|
||
|
context.compiler.compilers.map(
|
||
|
/**
|
||
|
* @param {Compiler} childCompiler
|
||
|
* @returns {WatchOptions}
|
||
|
*/
|
||
|
childCompiler => childCompiler.options.watchOptions || {});
|
||
|
context.watching =
|
||
|
/** @type {MultiWatching} */
|
||
|
context.compiler.watch(
|
||
|
/** @type {WatchOptions}} */
|
||
|
watchOptions, errorHandler);
|
||
|
} else {
|
||
|
watchOptions =
|
||
|
/** @type {Compiler} */
|
||
|
context.compiler.options.watchOptions || {};
|
||
|
context.watching =
|
||
|
/** @type {Watching} */
|
||
|
context.compiler.watch(watchOptions, errorHandler);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
const instance =
|
||
|
/** @type {API<Request, Response>} */
|
||
|
middleware(context); // API
|
||
|
|
||
|
/** @type {API<Request, Response>} */
|
||
|
|
||
|
instance.getFilenameFromUrl =
|
||
|
/**
|
||
|
* @param {string} url
|
||
|
* @returns {string|undefined}
|
||
|
*/
|
||
|
url => getFilenameFromUrl(context, url);
|
||
|
/** @type {API<Request, Response>} */
|
||
|
|
||
|
|
||
|
instance.waitUntilValid = (callback = noop) => {
|
||
|
ready(context, callback);
|
||
|
};
|
||
|
/** @type {API<Request, Response>} */
|
||
|
|
||
|
|
||
|
instance.invalidate = (callback = noop) => {
|
||
|
ready(context, callback);
|
||
|
context.watching.invalidate();
|
||
|
};
|
||
|
/** @type {API<Request, Response>} */
|
||
|
|
||
|
|
||
|
instance.close = (callback = noop) => {
|
||
|
context.watching.close(callback);
|
||
|
};
|
||
|
/** @type {API<Request, Response>} */
|
||
|
|
||
|
|
||
|
instance.context = context;
|
||
|
return instance;
|
||
|
}
|
||
|
|
||
|
module.exports = wdm;
|