Skip to content
Merged
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
4 changes: 4 additions & 0 deletions declarations/WebpackOptions.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3299,6 +3299,10 @@ export interface JavascriptParserOptions {
* Specifies global preload for dynamic import.
*/
dynamicImportPreload?: number | boolean;
/**
* Enable/disable parsing of dynamic URL.
*/
dynamicUrl?: boolean;
/**
* Specifies the behavior of invalid export names in "import ... from ..." and "export ... from ...".
*/
Expand Down
51 changes: 51 additions & 0 deletions declarations/plugins/schemes/VirtualUrlPlugin.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* This file was automatically generated.
* DO NOT MODIFY BY HAND.
* Run `yarn fix:special` to update
*/

export type VirtualUrlPluginOptions = VirtualUrlOptions;
/**
* A virtual module can be a string, a function, or a VirtualModule object.
*/
export type VirtualModuleContent =
| string
| ((
loaderContext: import("webpack").LoaderContext<EXPECTED_ANY>
) => Promise<string> | string)
| VirtualModule;

/**
* Options for building virtual resources.
*/
export interface VirtualUrlOptions {
/**
* The virtual modules configuration.
*/
modules: {
[k: string]: VirtualModuleContent;
};
/**
* The URL scheme to use for virtual resources.
*/
scheme?: string;
}
/**
* A virtual module definition.
*/
export interface VirtualModule {
/**
* The source function that provides the virtual content.
*/
source: (
loaderContext: import("webpack").LoaderContext<EXPECTED_ANY>
) => Promise<string> | string;
/**
* The module type.
*/
type?: string;
/**
* Optional version function or value for cache invalidation.
*/
version?: true | string | (() => string | undefined);
}
1 change: 1 addition & 0 deletions lib/config/defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -585,6 +585,7 @@ const applyJavascriptParserOptionsDefaults = (
D(parserOptions, "dynamicImportPreload", false);
D(parserOptions, "dynamicImportFetchPriority", false);
D(parserOptions, "createRequire", isNode);
D(parserOptions, "dynamicUrl", true);
if (futureDefaults) D(parserOptions, "exportsPresence", "error");
};

Expand Down
4 changes: 3 additions & 1 deletion lib/dependencies/ImportParserPlugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ const ImportWeakDependency = require("./ImportWeakDependency");
/** @typedef {import("../javascript/JavascriptParser").ImportExpression} ImportExpression */
/** @typedef {import("../javascript/JavascriptParser").Range} Range */

const PLUGIN_NAME = "ImportParserPlugin";

class ImportParserPlugin {
/**
* @param {JavascriptParserOptions} options options
Expand All @@ -44,7 +46,7 @@ class ImportParserPlugin {
*/
const exportsFromEnumerable = enumerable =>
Array.from(enumerable, e => [e]);
parser.hooks.importCall.tap("ImportParserPlugin", expr => {
parser.hooks.importCall.tap(PLUGIN_NAME, expr => {
const param = parser.evaluateExpression(expr.source);

let chunkName = null;
Expand Down
65 changes: 65 additions & 0 deletions lib/dependencies/URLContextDependency.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
MIT License http://www.opensource.org/licenses/mit-license.php
Author Haijie Xie @hai-x
*/

"use strict";

const makeSerializable = require("../util/makeSerializable");
const ContextDependency = require("./ContextDependency");
const ContextDependencyTemplateAsRequireCall = require("./ContextDependencyTemplateAsRequireCall");

/** @typedef {import("../ContextModule").ContextOptions} ContextOptions */
/** @typedef {import("../javascript/JavascriptParser").Range} Range */
/** @typedef {import("../serialization/ObjectMiddleware").ObjectDeserializerContext} ObjectDeserializerContext */
/** @typedef {import("../serialization/ObjectMiddleware").ObjectSerializerContext} ObjectSerializerContext */

/** @typedef {ContextOptions & { request: string }} ContextDependencyOptions */

class URLContextDependency extends ContextDependency {
/**
* @param {ContextDependencyOptions} options options
* @param {Range} range range
* @param {Range} valueRange value range
*/
constructor(options, range, valueRange) {
super(options);
this.range = range;
this.valueRange = valueRange;
}

get type() {
return "new URL() context";
}

get category() {
return "url";
}

/**
* @param {ObjectSerializerContext} context context
*/
serialize(context) {
const { write } = context;
write(this.valueRange);
super.serialize(context);
}

/**
* @param {ObjectDeserializerContext} context context
*/
deserialize(context) {
const { read } = context;
this.valueRange = read();
super.deserialize(context);
}
}

makeSerializable(
URLContextDependency,
"webpack/lib/dependencies/URLContextDependency"
);

URLContextDependency.Template = ContextDependencyTemplateAsRequireCall;

module.exports = URLContextDependency;
174 changes: 17 additions & 157 deletions lib/dependencies/URLPlugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,29 +5,19 @@

"use strict";

const { pathToFileURL } = require("url");
const CommentCompilationWarning = require("../CommentCompilationWarning");
const {
JAVASCRIPT_MODULE_TYPE_AUTO,
JAVASCRIPT_MODULE_TYPE_ESM
} = require("../ModuleTypeConstants");
const RuntimeGlobals = require("../RuntimeGlobals");
const UnsupportedFeatureWarning = require("../UnsupportedFeatureWarning");
const BasicEvaluatedExpression = require("../javascript/BasicEvaluatedExpression");
const { approve } = require("../javascript/JavascriptParserHelpers");
const InnerGraph = require("../optimize/InnerGraph");
const ConstDependency = require("./ConstDependency");
const URLDependency = require("./URLDependency");

/** @typedef {import("estree").MemberExpression} MemberExpression */
/** @typedef {import("estree").NewExpression} NewExpressionNode */
const URLContextDependency = require("../dependencies/URLContextDependency");
const URLDependency = require("../dependencies/URLDependency");
const URLExpressionParserPlugin = require("../url/URLExpressionParserPlugin");

/** @typedef {import("../../declarations/WebpackOptions").JavascriptParserOptions} JavascriptParserOptions */
/** @typedef {import("../Compiler")} Compiler */
/** @typedef {import("../Dependency").DependencyLocation} DependencyLocation */
/** @typedef {import("../NormalModule")} NormalModule */
/** @typedef {import("../javascript/JavascriptParser")} JavascriptParser */
/** @typedef {import("../javascript/JavascriptParser")} Parser */
/** @typedef {import("../javascript/JavascriptParser").Range} Range */

const PLUGIN_NAME = "URLPlugin";

Expand All @@ -38,168 +28,38 @@ class URLPlugin {
apply(compiler) {
compiler.hooks.compilation.tap(
PLUGIN_NAME,
(compilation, { normalModuleFactory }) => {
(compilation, { normalModuleFactory, contextModuleFactory }) => {
compilation.dependencyFactories.set(URLDependency, normalModuleFactory);
compilation.dependencyTemplates.set(
URLDependency,
new URLDependency.Template()
);

/**
* @param {NormalModule} module module
* @returns {URL} file url
*/
const getUrl = module => pathToFileURL(module.resource);

/**
* @param {Parser} parser parser parser
* @param {MemberExpression} arg arg
* @returns {boolean} true when it is `meta.url`, otherwise false
*/
const isMetaUrl = (parser, arg) => {
const chain = parser.extractMemberExpressionChain(arg);

if (
chain.members.length !== 1 ||
chain.object.type !== "MetaProperty" ||
chain.object.meta.name !== "import" ||
chain.object.property.name !== "meta" ||
chain.members[0] !== "url"
)
return false;

return true;
};
compilation.dependencyFactories.set(
URLContextDependency,
contextModuleFactory
);
compilation.dependencyTemplates.set(
URLContextDependency,
new URLContextDependency.Template()
);

/**
* @param {Parser} parser parser parser
* @param {JavascriptParserOptions} parserOptions parserOptions
* @returns {void}
*/
const parserCallback = (parser, parserOptions) => {
const handler = (parser, parserOptions) => {
if (parserOptions.url === false) return;
const relative = parserOptions.url === "relative";

/**
* @param {NewExpressionNode} expr expression
* @returns {undefined | string} request
*/
const getUrlRequest = expr => {
if (expr.arguments.length !== 2) return;

const [arg1, arg2] = expr.arguments;

if (
arg2.type !== "MemberExpression" ||
arg1.type === "SpreadElement"
)
return;

if (!isMetaUrl(parser, arg2)) return;

return parser.evaluateExpression(arg1).asString();
};

parser.hooks.canRename.for("URL").tap(PLUGIN_NAME, approve);
parser.hooks.evaluateNewExpression
.for("URL")
.tap(PLUGIN_NAME, expr => {
const request = getUrlRequest(expr);
if (!request) return;
const url = new URL(request, getUrl(parser.state.module));

return new BasicEvaluatedExpression()
.setString(url.toString())
.setRange(/** @type {Range} */ (expr.range));
});
parser.hooks.new.for("URL").tap(PLUGIN_NAME, _expr => {
const expr = /** @type {NewExpressionNode} */ (_expr);
const { options: importOptions, errors: commentErrors } =
parser.parseCommentOptions(/** @type {Range} */ (expr.range));

if (commentErrors) {
for (const e of commentErrors) {
const { comment } = e;
parser.state.module.addWarning(
new CommentCompilationWarning(
`Compilation error while processing magic comment(-s): /*${comment.value}*/: ${e.message}`,
/** @type {DependencyLocation} */ (comment.loc)
)
);
}
}

if (importOptions && importOptions.webpackIgnore !== undefined) {
if (typeof importOptions.webpackIgnore !== "boolean") {
parser.state.module.addWarning(
new UnsupportedFeatureWarning(
`\`webpackIgnore\` expected a boolean, but received: ${importOptions.webpackIgnore}.`,
/** @type {DependencyLocation} */ (expr.loc)
)
);
return;
} else if (importOptions.webpackIgnore) {
if (expr.arguments.length !== 2) return;

const [, arg2] = expr.arguments;

if (
arg2.type !== "MemberExpression" ||
!isMetaUrl(parser, arg2)
)
return;

const dep = new ConstDependency(
RuntimeGlobals.baseURI,
/** @type {Range} */ (arg2.range),
[RuntimeGlobals.baseURI]
);
dep.loc = /** @type {DependencyLocation} */ (expr.loc);
parser.state.module.addPresentationalDependency(dep);

return true;
}
}

const request = getUrlRequest(expr);

if (!request) return;

const [arg1, arg2] = expr.arguments;
const dep = new URLDependency(
request,
[
/** @type {Range} */ (arg1.range)[0],
/** @type {Range} */ (arg2.range)[1]
],
/** @type {Range} */ (expr.range),
relative
);
dep.loc = /** @type {DependencyLocation} */ (expr.loc);
parser.state.current.addDependency(dep);
InnerGraph.onUsage(parser.state, e => (dep.usedByExports = e));
return true;
});
parser.hooks.isPure.for("NewExpression").tap(PLUGIN_NAME, _expr => {
const expr = /** @type {NewExpressionNode} */ (_expr);
const { callee } = expr;
if (callee.type !== "Identifier") return;
const calleeInfo = parser.getFreeInfoFromVariable(callee.name);
if (!calleeInfo || calleeInfo.name !== "URL") return;

const request = getUrlRequest(expr);

if (request) return true;
});
new URLExpressionParserPlugin(parserOptions).apply(parser);
};

normalModuleFactory.hooks.parser
.for(JAVASCRIPT_MODULE_TYPE_AUTO)
.tap(PLUGIN_NAME, parserCallback);
.tap(PLUGIN_NAME, handler);

normalModuleFactory.hooks.parser
.for(JAVASCRIPT_MODULE_TYPE_ESM)
.tap(PLUGIN_NAME, parserCallback);
.tap(PLUGIN_NAME, handler);
}
);
}
Expand Down
3 changes: 3 additions & 0 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -641,6 +641,9 @@ module.exports = mergeExports(fn, {
schemes: {
get HttpUriPlugin() {
return require("./schemes/HttpUriPlugin");
},
get VirtualUrlPlugin() {
return require("./schemes/VirtualUrlPlugin");
}
},
ids: {
Expand Down
Loading
Loading