267 lines
		
	
	
		
			7.9 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			267 lines
		
	
	
		
			7.9 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /*
 | |
| 	MIT License http://www.opensource.org/licenses/mit-license.php
 | |
| 	Author Haijie Xie @hai-x
 | |
| */
 | |
| 
 | |
| "use strict";
 | |
| 
 | |
| const { pathToFileURL } = require("url");
 | |
| const CommentCompilationWarning = require("../CommentCompilationWarning");
 | |
| const RuntimeGlobals = require("../RuntimeGlobals");
 | |
| const UnsupportedFeatureWarning = require("../UnsupportedFeatureWarning");
 | |
| const ConstDependency = require("../dependencies/ConstDependency");
 | |
| const ContextDependencyHelpers = require("../dependencies/ContextDependencyHelpers");
 | |
| const URLContextDependency = require("../dependencies/URLContextDependency");
 | |
| const URLDependency = require("../dependencies/URLDependency");
 | |
| const BasicEvaluatedExpression = require("../javascript/BasicEvaluatedExpression");
 | |
| const { approve } = require("../javascript/JavascriptParserHelpers");
 | |
| const InnerGraph = require("../optimize/InnerGraph");
 | |
| 
 | |
| /** @typedef {import("estree").MemberExpression} MemberExpression */
 | |
| /** @typedef {import("estree").NewExpression} NewExpressionNode */
 | |
| /** @typedef {import("../../declarations/WebpackOptions").JavascriptParserOptions} JavascriptParserOptions */
 | |
| /** @typedef {import("../ContextModule").ContextMode} ContextMode */
 | |
| /** @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 = "URLParserPlugin";
 | |
| 
 | |
| /**
 | |
|  * @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;
 | |
| };
 | |
| 
 | |
| /**
 | |
|  * @type {WeakMap<NewExpressionNode, BasicEvaluatedExpression | undefined>}
 | |
|  */
 | |
| const getEvaluatedExprCache = new WeakMap();
 | |
| 
 | |
| /**
 | |
|  * @param {NewExpressionNode} expr expression
 | |
|  * @param {Parser} parser parser parser
 | |
|  * @returns {BasicEvaluatedExpression | undefined} basic evaluated expression
 | |
|  */
 | |
| const getEvaluatedExpr = (expr, parser) => {
 | |
| 	let result = getEvaluatedExprCache.get(expr);
 | |
| 	if (result !== undefined) return result;
 | |
| 
 | |
| 	/**
 | |
| 	 * @returns {BasicEvaluatedExpression | undefined} basic evaluated expression
 | |
| 	 */
 | |
| 	const evaluate = () => {
 | |
| 		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);
 | |
| 	};
 | |
| 
 | |
| 	result = evaluate();
 | |
| 	getEvaluatedExprCache.set(expr, result);
 | |
| 
 | |
| 	return result;
 | |
| };
 | |
| 
 | |
| class URLParserPlugin {
 | |
| 	/**
 | |
| 	 * @param {JavascriptParserOptions} options options
 | |
| 	 */
 | |
| 	constructor(options) {
 | |
| 		this.options = options;
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * @param {JavascriptParser} parser the parser
 | |
| 	 * @returns {void}
 | |
| 	 */
 | |
| 	apply(parser) {
 | |
| 		const relative = this.options.url === "relative";
 | |
| 
 | |
| 		parser.hooks.canRename.for("URL").tap(PLUGIN_NAME, approve);
 | |
| 		parser.hooks.evaluateNewExpression.for("URL").tap(PLUGIN_NAME, (expr) => {
 | |
| 			const evaluatedExpr = getEvaluatedExpr(expr, parser);
 | |
| 			const request = evaluatedExpr && evaluatedExpr.asString();
 | |
| 
 | |
| 			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 evaluatedExpr = getEvaluatedExpr(expr, parser);
 | |
| 			if (!evaluatedExpr) return;
 | |
| 
 | |
| 			let request;
 | |
| 
 | |
| 			// static URL
 | |
| 			if ((request = evaluatedExpr.asString())) {
 | |
| 				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;
 | |
| 			}
 | |
| 
 | |
| 			if (this.options.dynamicUrl === false) return;
 | |
| 
 | |
| 			// context URL
 | |
| 			let include;
 | |
| 			let exclude;
 | |
| 
 | |
| 			if (importOptions) {
 | |
| 				if (importOptions.webpackInclude !== undefined) {
 | |
| 					if (
 | |
| 						!importOptions.webpackInclude ||
 | |
| 						!(importOptions.webpackInclude instanceof RegExp)
 | |
| 					) {
 | |
| 						parser.state.module.addWarning(
 | |
| 							new UnsupportedFeatureWarning(
 | |
| 								`\`webpackInclude\` expected a regular expression, but received: ${importOptions.webpackInclude}.`,
 | |
| 								/** @type {DependencyLocation} */ (expr.loc)
 | |
| 							)
 | |
| 						);
 | |
| 					} else {
 | |
| 						include = importOptions.webpackInclude;
 | |
| 					}
 | |
| 				}
 | |
| 				if (importOptions.webpackExclude !== undefined) {
 | |
| 					if (
 | |
| 						!importOptions.webpackExclude ||
 | |
| 						!(importOptions.webpackExclude instanceof RegExp)
 | |
| 					) {
 | |
| 						parser.state.module.addWarning(
 | |
| 							new UnsupportedFeatureWarning(
 | |
| 								`\`webpackExclude\` expected a regular expression, but received: ${importOptions.webpackExclude}.`,
 | |
| 								/** @type {DependencyLocation} */ (expr.loc)
 | |
| 							)
 | |
| 						);
 | |
| 					} else {
 | |
| 						exclude = importOptions.webpackExclude;
 | |
| 					}
 | |
| 				}
 | |
| 			}
 | |
| 			const dep = ContextDependencyHelpers.create(
 | |
| 				URLContextDependency,
 | |
| 				/** @type {Range} */ (expr.range),
 | |
| 				evaluatedExpr,
 | |
| 				expr,
 | |
| 				this.options,
 | |
| 				{
 | |
| 					include,
 | |
| 					exclude,
 | |
| 					mode: "sync",
 | |
| 					typePrefix: "new URL with import.meta.url",
 | |
| 					category: "url"
 | |
| 				},
 | |
| 				parser
 | |
| 			);
 | |
| 			if (!dep) return;
 | |
| 			dep.loc = /** @type {DependencyLocation} */ (expr.loc);
 | |
| 			dep.optional = Boolean(parser.scope.inTry);
 | |
| 			parser.state.current.addDependency(dep);
 | |
| 			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 evaluatedExpr = getEvaluatedExpr(expr, parser);
 | |
| 			const request = evaluatedExpr && evaluatedExpr.asString();
 | |
| 
 | |
| 			if (request) return true;
 | |
| 		});
 | |
| 	}
 | |
| }
 | |
| 
 | |
| module.exports = URLParserPlugin;
 |