Implement JQHTML function cache ID system and fix bundle compilation Implement underscore prefix for system tables Fix JS syntax linter to support decorators and grant exception to Task system SPA: Update planning docs and wishlists with remaining features SPA: Document Navigation API abandonment and future enhancements Implement SPA browser integration with History API (Phase 1) Convert contacts view page to SPA action Convert clients pages to SPA actions and document conversion procedure SPA: Merge GET parameters and update documentation Implement SPA route URL generation in JavaScript and PHP Implement SPA bootstrap controller architecture Add SPA routing manual page (rsx:man spa) Add SPA routing documentation to CLAUDE.md Phase 4 Complete: Client-side SPA routing implementation Update get_routes() consumers for unified route structure Complete SPA Phase 3: PHP-side route type detection and is_spa flag Restore unified routes structure and Manifest_Query class Refactor route indexing and add SPA infrastructure Phase 3 Complete: SPA route registration in manifest Implement SPA Phase 2: Extract router code and test decorators Rename Jqhtml_Component to Component and complete SPA foundation setup 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
364 lines
12 KiB
JavaScript
364 lines
12 KiB
JavaScript
/*
|
|
MIT License http://www.opensource.org/licenses/mit-license.php
|
|
Author Tobias Koppers @sokra
|
|
*/
|
|
|
|
"use strict";
|
|
|
|
const util = require("util");
|
|
const ExternalModule = require("./ExternalModule");
|
|
const ContextElementDependency = require("./dependencies/ContextElementDependency");
|
|
const CssImportDependency = require("./dependencies/CssImportDependency");
|
|
const CssUrlDependency = require("./dependencies/CssUrlDependency");
|
|
const HarmonyImportDependency = require("./dependencies/HarmonyImportDependency");
|
|
const ImportDependency = require("./dependencies/ImportDependency");
|
|
const { cachedSetProperty, resolveByProperty } = require("./util/cleverMerge");
|
|
|
|
/** @typedef {import("enhanced-resolve").ResolveContext} ResolveContext */
|
|
/** @typedef {import("../declarations/WebpackOptions").ResolveOptions} ResolveOptions */
|
|
/** @typedef {import("../declarations/WebpackOptions").ExternalsType} ExternalsType */
|
|
/** @typedef {import("../declarations/WebpackOptions").ExternalItemValue} ExternalItemValue */
|
|
/** @typedef {import("../declarations/WebpackOptions").ExternalItemObjectKnown} ExternalItemObjectKnown */
|
|
/** @typedef {import("../declarations/WebpackOptions").ExternalItemObjectUnknown} ExternalItemObjectUnknown */
|
|
/** @typedef {import("../declarations/WebpackOptions").Externals} Externals */
|
|
/** @typedef {import("./ExternalModule").DependencyMeta} DependencyMeta */
|
|
/** @typedef {import("./ModuleFactory").IssuerLayer} IssuerLayer */
|
|
/** @typedef {import("./ModuleFactory").ModuleFactoryCreateDataContextInfo} ModuleFactoryCreateDataContextInfo */
|
|
/** @typedef {import("./NormalModuleFactory")} NormalModuleFactory */
|
|
|
|
/** @typedef {((context: string, request: string, callback: (err?: Error | null, result?: string | false, resolveRequest?: import('enhanced-resolve').ResolveRequest) => void) => void)} ExternalItemFunctionDataGetResolveCallbackResult */
|
|
/** @typedef {((context: string, request: string) => Promise<string>)} ExternalItemFunctionDataGetResolveResult */
|
|
/** @typedef {(options?: ResolveOptions) => ExternalItemFunctionDataGetResolveCallbackResult | ExternalItemFunctionDataGetResolveResult} ExternalItemFunctionDataGetResolve */
|
|
|
|
/**
|
|
* @typedef {object} ExternalItemFunctionData
|
|
* @property {string} context the directory in which the request is placed
|
|
* @property {ModuleFactoryCreateDataContextInfo} contextInfo contextual information
|
|
* @property {string} dependencyType the category of the referencing dependency
|
|
* @property {ExternalItemFunctionDataGetResolve} getResolve get a resolve function with the current resolver options
|
|
* @property {string} request the request as written by the user in the require/import expression/statement
|
|
*/
|
|
|
|
/** @typedef {((data: ExternalItemFunctionData, callback: (err?: (Error | null), result?: ExternalItemValue) => void) => void)} ExternalItemFunctionCallback */
|
|
/** @typedef {((data: import("../lib/ExternalModuleFactoryPlugin").ExternalItemFunctionData) => Promise<ExternalItemValue>)} ExternalItemFunctionPromise */
|
|
|
|
const UNSPECIFIED_EXTERNAL_TYPE_REGEXP = /^[a-z0-9-]+ /;
|
|
const EMPTY_RESOLVE_OPTIONS = {};
|
|
|
|
// TODO webpack 6 remove this
|
|
const callDeprecatedExternals = util.deprecate(
|
|
/**
|
|
* @param {EXPECTED_FUNCTION} externalsFunction externals function
|
|
* @param {string} context context
|
|
* @param {string} request request
|
|
* @param {(err: Error | null | undefined, value: ExternalValue | undefined, ty: ExternalsType | undefined) => void} cb cb
|
|
*/
|
|
(externalsFunction, context, request, cb) => {
|
|
// eslint-disable-next-line no-useless-call
|
|
externalsFunction.call(null, context, request, cb);
|
|
},
|
|
"The externals-function should be defined like ({context, request}, cb) => { ... }",
|
|
"DEP_WEBPACK_EXTERNALS_FUNCTION_PARAMETERS"
|
|
);
|
|
|
|
/** @typedef {ExternalItemObjectKnown & ExternalItemObjectUnknown} ExternalItemObject */
|
|
|
|
/**
|
|
* @template {ExternalItemObject} T
|
|
* @typedef {WeakMap<T, Map<IssuerLayer, Omit<T, "byLayer">>>} ExternalWeakCache
|
|
*/
|
|
|
|
/** @type {ExternalWeakCache<ExternalItemObject>} */
|
|
const cache = new WeakMap();
|
|
|
|
/**
|
|
* @param {ExternalItemObject} obj obj
|
|
* @param {IssuerLayer} layer layer
|
|
* @returns {Omit<ExternalItemObject, "byLayer">} result
|
|
*/
|
|
const resolveLayer = (obj, layer) => {
|
|
let map = cache.get(obj);
|
|
if (map === undefined) {
|
|
map = new Map();
|
|
cache.set(obj, map);
|
|
} else {
|
|
const cacheEntry = map.get(layer);
|
|
if (cacheEntry !== undefined) return cacheEntry;
|
|
}
|
|
const result = resolveByProperty(obj, "byLayer", layer);
|
|
map.set(layer, result);
|
|
return result;
|
|
};
|
|
|
|
/** @typedef {string | string[] | boolean | Record<string, string | string[]>} ExternalValue */
|
|
|
|
const PLUGIN_NAME = "ExternalModuleFactoryPlugin";
|
|
|
|
class ExternalModuleFactoryPlugin {
|
|
/**
|
|
* @param {ExternalsType} type default external type
|
|
* @param {Externals} externals externals config
|
|
*/
|
|
constructor(type, externals) {
|
|
this.type = type;
|
|
this.externals = externals;
|
|
}
|
|
|
|
/**
|
|
* @param {NormalModuleFactory} normalModuleFactory the normal module factory
|
|
* @returns {void}
|
|
*/
|
|
apply(normalModuleFactory) {
|
|
const globalType = this.type;
|
|
normalModuleFactory.hooks.factorize.tapAsync(
|
|
PLUGIN_NAME,
|
|
(data, callback) => {
|
|
const context = data.context;
|
|
const contextInfo = data.contextInfo;
|
|
const dependency = data.dependencies[0];
|
|
const dependencyType = data.dependencyType;
|
|
|
|
/** @typedef {(err?: Error | null, externalModule?: ExternalModule) => void} HandleExternalCallback */
|
|
|
|
/**
|
|
* @param {ExternalValue} value the external config
|
|
* @param {ExternalsType | undefined} type type of external
|
|
* @param {HandleExternalCallback} callback callback
|
|
* @returns {void}
|
|
*/
|
|
const handleExternal = (value, type, callback) => {
|
|
if (value === false) {
|
|
// Not externals, fallback to original factory
|
|
return callback();
|
|
}
|
|
/** @type {ExternalValue} */
|
|
let externalConfig = value === true ? dependency.request : value;
|
|
// When no explicit type is specified, extract it from the externalConfig
|
|
if (type === undefined) {
|
|
if (
|
|
typeof externalConfig === "string" &&
|
|
UNSPECIFIED_EXTERNAL_TYPE_REGEXP.test(externalConfig)
|
|
) {
|
|
const idx = externalConfig.indexOf(" ");
|
|
type =
|
|
/** @type {ExternalsType} */
|
|
(externalConfig.slice(0, idx));
|
|
externalConfig = externalConfig.slice(idx + 1);
|
|
} else if (
|
|
Array.isArray(externalConfig) &&
|
|
externalConfig.length > 0 &&
|
|
UNSPECIFIED_EXTERNAL_TYPE_REGEXP.test(externalConfig[0])
|
|
) {
|
|
const firstItem = externalConfig[0];
|
|
const idx = firstItem.indexOf(" ");
|
|
type = /** @type {ExternalsType} */ (firstItem.slice(0, idx));
|
|
externalConfig = [
|
|
firstItem.slice(idx + 1),
|
|
...externalConfig.slice(1)
|
|
];
|
|
}
|
|
}
|
|
|
|
const resolvedType = type || globalType;
|
|
|
|
// TODO make it pluggable/add hooks to `ExternalModule` to allow output modules own externals?
|
|
/** @type {DependencyMeta | undefined} */
|
|
let dependencyMeta;
|
|
|
|
if (
|
|
dependency instanceof HarmonyImportDependency ||
|
|
dependency instanceof ImportDependency ||
|
|
dependency instanceof ContextElementDependency
|
|
) {
|
|
const externalType =
|
|
dependency instanceof HarmonyImportDependency
|
|
? "module"
|
|
: dependency instanceof ImportDependency
|
|
? "import"
|
|
: undefined;
|
|
|
|
dependencyMeta = {
|
|
attributes: dependency.attributes,
|
|
externalType
|
|
};
|
|
} else if (dependency instanceof CssImportDependency) {
|
|
dependencyMeta = {
|
|
layer: dependency.layer,
|
|
supports: dependency.supports,
|
|
media: dependency.media
|
|
};
|
|
}
|
|
|
|
if (
|
|
resolvedType === "asset" &&
|
|
dependency instanceof CssUrlDependency
|
|
) {
|
|
dependencyMeta = { sourceType: "css-url" };
|
|
}
|
|
|
|
callback(
|
|
null,
|
|
new ExternalModule(
|
|
externalConfig,
|
|
resolvedType,
|
|
dependency.request,
|
|
dependencyMeta
|
|
)
|
|
);
|
|
};
|
|
|
|
/**
|
|
* @param {Externals} externals externals config
|
|
* @param {HandleExternalCallback} callback callback
|
|
* @returns {void}
|
|
*/
|
|
const handleExternals = (externals, callback) => {
|
|
if (typeof externals === "string") {
|
|
if (externals === dependency.request) {
|
|
return handleExternal(dependency.request, undefined, callback);
|
|
}
|
|
} else if (Array.isArray(externals)) {
|
|
let i = 0;
|
|
const next = () => {
|
|
/** @type {boolean | undefined} */
|
|
let asyncFlag;
|
|
/**
|
|
* @param {(Error | null)=} err err
|
|
* @param {ExternalModule=} module module
|
|
* @returns {void}
|
|
*/
|
|
const handleExternalsAndCallback = (err, module) => {
|
|
if (err) return callback(err);
|
|
if (!module) {
|
|
if (asyncFlag) {
|
|
asyncFlag = false;
|
|
return;
|
|
}
|
|
return next();
|
|
}
|
|
callback(null, module);
|
|
};
|
|
|
|
do {
|
|
asyncFlag = true;
|
|
if (i >= externals.length) return callback();
|
|
handleExternals(externals[i++], handleExternalsAndCallback);
|
|
} while (!asyncFlag);
|
|
asyncFlag = false;
|
|
};
|
|
|
|
next();
|
|
return;
|
|
} else if (externals instanceof RegExp) {
|
|
if (externals.test(dependency.request)) {
|
|
return handleExternal(dependency.request, undefined, callback);
|
|
}
|
|
} else if (typeof externals === "function") {
|
|
/**
|
|
* @param {Error | null | undefined} err err
|
|
* @param {ExternalValue=} value value
|
|
* @param {ExternalsType=} type type
|
|
* @returns {void}
|
|
*/
|
|
const cb = (err, value, type) => {
|
|
if (err) return callback(err);
|
|
if (value !== undefined) {
|
|
handleExternal(value, type, callback);
|
|
} else {
|
|
callback();
|
|
}
|
|
};
|
|
if (externals.length === 3) {
|
|
// TODO webpack 6 remove this
|
|
callDeprecatedExternals(
|
|
externals,
|
|
context,
|
|
dependency.request,
|
|
cb
|
|
);
|
|
} else {
|
|
const promise = externals(
|
|
{
|
|
context,
|
|
request: dependency.request,
|
|
dependencyType,
|
|
contextInfo,
|
|
getResolve: (options) => (context, request, callback) => {
|
|
/** @type {ResolveContext} */
|
|
const resolveContext = {
|
|
fileDependencies: data.fileDependencies,
|
|
missingDependencies: data.missingDependencies,
|
|
contextDependencies: data.contextDependencies
|
|
};
|
|
let resolver = normalModuleFactory.getResolver(
|
|
"normal",
|
|
dependencyType
|
|
? cachedSetProperty(
|
|
data.resolveOptions || EMPTY_RESOLVE_OPTIONS,
|
|
"dependencyType",
|
|
dependencyType
|
|
)
|
|
: data.resolveOptions
|
|
);
|
|
if (options) resolver = resolver.withOptions(options);
|
|
if (callback) {
|
|
resolver.resolve(
|
|
{},
|
|
context,
|
|
request,
|
|
resolveContext,
|
|
callback
|
|
);
|
|
} else {
|
|
return new Promise((resolve, reject) => {
|
|
resolver.resolve(
|
|
{},
|
|
context,
|
|
request,
|
|
resolveContext,
|
|
(err, result) => {
|
|
if (err) reject(err);
|
|
else resolve(result);
|
|
}
|
|
);
|
|
});
|
|
}
|
|
}
|
|
},
|
|
cb
|
|
);
|
|
if (promise && promise.then) {
|
|
promise.then((r) => cb(null, r), cb);
|
|
}
|
|
}
|
|
return;
|
|
} else if (typeof externals === "object") {
|
|
const resolvedExternals = resolveLayer(
|
|
externals,
|
|
/** @type {IssuerLayer} */
|
|
(contextInfo.issuerLayer)
|
|
);
|
|
if (
|
|
Object.prototype.hasOwnProperty.call(
|
|
resolvedExternals,
|
|
dependency.request
|
|
)
|
|
) {
|
|
return handleExternal(
|
|
resolvedExternals[dependency.request],
|
|
undefined,
|
|
callback
|
|
);
|
|
}
|
|
}
|
|
callback();
|
|
};
|
|
|
|
handleExternals(this.externals, callback);
|
|
}
|
|
);
|
|
}
|
|
}
|
|
|
|
module.exports = ExternalModuleFactoryPlugin;
|