mirror of https://github.com/webpack/webpack.git
				
				
				
			
		
			
				
	
	
		
			426 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
			
		
		
	
	
			426 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
/*
 | 
						|
	MIT License http://www.opensource.org/licenses/mit-license.php
 | 
						|
	Author Tobias Koppers @sokra
 | 
						|
*/
 | 
						|
 | 
						|
"use strict";
 | 
						|
 | 
						|
const { join, dirname, readJson } = require("../util/fs");
 | 
						|
 | 
						|
/** @typedef {import("../util/fs").InputFileSystem} InputFileSystem */
 | 
						|
/** @typedef {import("../util/fs").JsonObject} JsonObject */
 | 
						|
/** @typedef {import("../util/fs").JsonPrimitive} JsonPrimitive */
 | 
						|
 | 
						|
// Extreme shorthand only for github. eg: foo/bar
 | 
						|
const RE_URL_GITHUB_EXTREME_SHORT = /^[^/@:.\s][^/@:\s]*\/[^@:\s]*[^/@:\s]#\S+/;
 | 
						|
 | 
						|
// Short url with specific protocol. eg: github:foo/bar
 | 
						|
const RE_GIT_URL_SHORT = /^(github|gitlab|bitbucket|gist):\/?[^/.]+\/?/i;
 | 
						|
 | 
						|
// Currently supported protocols
 | 
						|
const RE_PROTOCOL =
 | 
						|
	/^((git\+)?(ssh|https?|file)|git|github|gitlab|bitbucket|gist):$/i;
 | 
						|
 | 
						|
// Has custom protocol
 | 
						|
const RE_CUSTOM_PROTOCOL = /^((git\+)?(ssh|https?|file)|git):\/\//i;
 | 
						|
 | 
						|
// Valid hash format for npm / yarn ...
 | 
						|
const RE_URL_HASH_VERSION = /#(?:semver:)?(.+)/;
 | 
						|
 | 
						|
// Simple hostname validate
 | 
						|
const RE_HOSTNAME = /^(?:[^/.]+(\.[^/]+)+|localhost)$/;
 | 
						|
 | 
						|
// For hostname with colon. eg: ssh://user@github.com:foo/bar
 | 
						|
const RE_HOSTNAME_WITH_COLON =
 | 
						|
	/([^/@#:.]+(?:\.[^/@#:.]+)+|localhost):([^#/0-9]+)/;
 | 
						|
 | 
						|
// Reg for url without protocol
 | 
						|
const RE_NO_PROTOCOL = /^([^/@#:.]+(?:\.[^/@#:.]+)+)/;
 | 
						|
 | 
						|
// RegExp for version string
 | 
						|
const VERSION_PATTERN_REGEXP = /^([\d^=v<>~]|[*xX]$)/;
 | 
						|
 | 
						|
// Specific protocol for short url without normal hostname
 | 
						|
const PROTOCOLS_FOR_SHORT = [
 | 
						|
	"github:",
 | 
						|
	"gitlab:",
 | 
						|
	"bitbucket:",
 | 
						|
	"gist:",
 | 
						|
	"file:"
 | 
						|
];
 | 
						|
 | 
						|
// Default protocol for git url
 | 
						|
const DEF_GIT_PROTOCOL = "git+ssh://";
 | 
						|
 | 
						|
// thanks to https://github.com/npm/hosted-git-info/blob/latest/git-host-info.js
 | 
						|
const extractCommithashByDomain = {
 | 
						|
	/**
 | 
						|
	 * @param {string} pathname pathname
 | 
						|
	 * @param {string} hash hash
 | 
						|
	 * @returns {string | undefined} hash
 | 
						|
	 */
 | 
						|
	"github.com": (pathname, hash) => {
 | 
						|
		let [, user, project, type, commithash] = pathname.split("/", 5);
 | 
						|
		if (type && type !== "tree") {
 | 
						|
			return;
 | 
						|
		}
 | 
						|
 | 
						|
		commithash = !type ? hash : `#${commithash}`;
 | 
						|
 | 
						|
		if (project && project.endsWith(".git")) {
 | 
						|
			project = project.slice(0, -4);
 | 
						|
		}
 | 
						|
 | 
						|
		if (!user || !project) {
 | 
						|
			return;
 | 
						|
		}
 | 
						|
 | 
						|
		return commithash;
 | 
						|
	},
 | 
						|
	/**
 | 
						|
	 * @param {string} pathname pathname
 | 
						|
	 * @param {string} hash hash
 | 
						|
	 * @returns {string | undefined} hash
 | 
						|
	 */
 | 
						|
	"gitlab.com": (pathname, hash) => {
 | 
						|
		const path = pathname.slice(1);
 | 
						|
		if (path.includes("/-/") || path.includes("/archive.tar.gz")) {
 | 
						|
			return;
 | 
						|
		}
 | 
						|
 | 
						|
		const segments = path.split("/");
 | 
						|
		let project = /** @type {string} */ (segments.pop());
 | 
						|
		if (project.endsWith(".git")) {
 | 
						|
			project = project.slice(0, -4);
 | 
						|
		}
 | 
						|
 | 
						|
		const user = segments.join("/");
 | 
						|
		if (!user || !project) {
 | 
						|
			return;
 | 
						|
		}
 | 
						|
 | 
						|
		return hash;
 | 
						|
	},
 | 
						|
	/**
 | 
						|
	 * @param {string} pathname pathname
 | 
						|
	 * @param {string} hash hash
 | 
						|
	 * @returns {string | undefined} hash
 | 
						|
	 */
 | 
						|
	"bitbucket.org": (pathname, hash) => {
 | 
						|
		let [, user, project, aux] = pathname.split("/", 4);
 | 
						|
		if (["get"].includes(aux)) {
 | 
						|
			return;
 | 
						|
		}
 | 
						|
 | 
						|
		if (project && project.endsWith(".git")) {
 | 
						|
			project = project.slice(0, -4);
 | 
						|
		}
 | 
						|
 | 
						|
		if (!user || !project) {
 | 
						|
			return;
 | 
						|
		}
 | 
						|
 | 
						|
		return hash;
 | 
						|
	},
 | 
						|
	/**
 | 
						|
	 * @param {string} pathname pathname
 | 
						|
	 * @param {string} hash hash
 | 
						|
	 * @returns {string | undefined} hash
 | 
						|
	 */
 | 
						|
	"gist.github.com": (pathname, hash) => {
 | 
						|
		let [, user, project, aux] = pathname.split("/", 4);
 | 
						|
		if (aux === "raw") {
 | 
						|
			return;
 | 
						|
		}
 | 
						|
 | 
						|
		if (!project) {
 | 
						|
			if (!user) {
 | 
						|
				return;
 | 
						|
			}
 | 
						|
 | 
						|
			project = user;
 | 
						|
		}
 | 
						|
 | 
						|
		if (project.endsWith(".git")) {
 | 
						|
			project = project.slice(0, -4);
 | 
						|
		}
 | 
						|
 | 
						|
		return hash;
 | 
						|
	}
 | 
						|
};
 | 
						|
 | 
						|
/**
 | 
						|
 * extract commit hash from parsed url
 | 
						|
 * @inner
 | 
						|
 * @param {URL} urlParsed parsed url
 | 
						|
 * @returns {string} commithash
 | 
						|
 */
 | 
						|
function getCommithash(urlParsed) {
 | 
						|
	let { hostname, pathname, hash } = urlParsed;
 | 
						|
	hostname = hostname.replace(/^www\./, "");
 | 
						|
 | 
						|
	try {
 | 
						|
		hash = decodeURIComponent(hash);
 | 
						|
		// eslint-disable-next-line no-empty
 | 
						|
	} catch (_err) {}
 | 
						|
 | 
						|
	if (
 | 
						|
		extractCommithashByDomain[
 | 
						|
			/** @type {keyof extractCommithashByDomain} */ (hostname)
 | 
						|
		]
 | 
						|
	) {
 | 
						|
		return (
 | 
						|
			extractCommithashByDomain[
 | 
						|
				/** @type {keyof extractCommithashByDomain} */ (hostname)
 | 
						|
			](pathname, hash) || ""
 | 
						|
		);
 | 
						|
	}
 | 
						|
 | 
						|
	return hash;
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * make url right for URL parse
 | 
						|
 * @inner
 | 
						|
 * @param {string} gitUrl git url
 | 
						|
 * @returns {string} fixed url
 | 
						|
 */
 | 
						|
function correctUrl(gitUrl) {
 | 
						|
	// like:
 | 
						|
	// proto://hostname.com:user/repo -> proto://hostname.com/user/repo
 | 
						|
	return gitUrl.replace(RE_HOSTNAME_WITH_COLON, "$1/$2");
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * make url protocol right for URL parse
 | 
						|
 * @inner
 | 
						|
 * @param {string} gitUrl git url
 | 
						|
 * @returns {string} fixed url
 | 
						|
 */
 | 
						|
function correctProtocol(gitUrl) {
 | 
						|
	// eg: github:foo/bar#v1.0. Should not add double slash, in case of error parsed `pathname`
 | 
						|
	if (RE_GIT_URL_SHORT.test(gitUrl)) {
 | 
						|
		return gitUrl;
 | 
						|
	}
 | 
						|
 | 
						|
	// eg: user@github.com:foo/bar
 | 
						|
	if (!RE_CUSTOM_PROTOCOL.test(gitUrl)) {
 | 
						|
		return `${DEF_GIT_PROTOCOL}${gitUrl}`;
 | 
						|
	}
 | 
						|
 | 
						|
	return gitUrl;
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * extract git dep version from hash
 | 
						|
 * @inner
 | 
						|
 * @param {string} hash hash
 | 
						|
 * @returns {string} git dep version
 | 
						|
 */
 | 
						|
function getVersionFromHash(hash) {
 | 
						|
	const matched = hash.match(RE_URL_HASH_VERSION);
 | 
						|
 | 
						|
	return (matched && matched[1]) || "";
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * if string can be decoded
 | 
						|
 * @inner
 | 
						|
 * @param {string} str str to be checked
 | 
						|
 * @returns {boolean} if can be decoded
 | 
						|
 */
 | 
						|
function canBeDecoded(str) {
 | 
						|
	try {
 | 
						|
		decodeURIComponent(str);
 | 
						|
	} catch (_err) {
 | 
						|
		return false;
 | 
						|
	}
 | 
						|
 | 
						|
	return true;
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * get right dep version from git url
 | 
						|
 * @inner
 | 
						|
 * @param {string} gitUrl git url
 | 
						|
 * @returns {string} dep version
 | 
						|
 */
 | 
						|
function getGitUrlVersion(gitUrl) {
 | 
						|
	const oriGitUrl = gitUrl;
 | 
						|
	// github extreme shorthand
 | 
						|
	gitUrl = RE_URL_GITHUB_EXTREME_SHORT.test(gitUrl)
 | 
						|
		? `github:${gitUrl}`
 | 
						|
		: correctProtocol(gitUrl);
 | 
						|
 | 
						|
	gitUrl = correctUrl(gitUrl);
 | 
						|
 | 
						|
	let parsed;
 | 
						|
	try {
 | 
						|
		parsed = new URL(gitUrl);
 | 
						|
		// eslint-disable-next-line no-empty
 | 
						|
	} catch (_err) {}
 | 
						|
 | 
						|
	if (!parsed) {
 | 
						|
		return "";
 | 
						|
	}
 | 
						|
 | 
						|
	const { protocol, hostname, pathname, username, password } = parsed;
 | 
						|
	if (!RE_PROTOCOL.test(protocol)) {
 | 
						|
		return "";
 | 
						|
	}
 | 
						|
 | 
						|
	// pathname shouldn't be empty or URL malformed
 | 
						|
	if (!pathname || !canBeDecoded(pathname)) {
 | 
						|
		return "";
 | 
						|
	}
 | 
						|
 | 
						|
	// without protocol, there should have auth info
 | 
						|
	if (RE_NO_PROTOCOL.test(oriGitUrl) && !username && !password) {
 | 
						|
		return "";
 | 
						|
	}
 | 
						|
 | 
						|
	if (!PROTOCOLS_FOR_SHORT.includes(protocol.toLowerCase())) {
 | 
						|
		if (!RE_HOSTNAME.test(hostname)) {
 | 
						|
			return "";
 | 
						|
		}
 | 
						|
 | 
						|
		const commithash = getCommithash(parsed);
 | 
						|
		return getVersionFromHash(commithash) || commithash;
 | 
						|
	}
 | 
						|
 | 
						|
	// for protocol short
 | 
						|
	return getVersionFromHash(gitUrl);
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * @param {string} str maybe required version
 | 
						|
 * @returns {boolean} true, if it looks like a version
 | 
						|
 */
 | 
						|
function isRequiredVersion(str) {
 | 
						|
	return VERSION_PATTERN_REGEXP.test(str);
 | 
						|
}
 | 
						|
 | 
						|
module.exports.isRequiredVersion = isRequiredVersion;
 | 
						|
 | 
						|
/**
 | 
						|
 * @see https://docs.npmjs.com/cli/v7/configuring-npm/package-json#urls-as-dependencies
 | 
						|
 * @param {string} versionDesc version to be normalized
 | 
						|
 * @returns {string} normalized version
 | 
						|
 */
 | 
						|
function normalizeVersion(versionDesc) {
 | 
						|
	versionDesc = (versionDesc && versionDesc.trim()) || "";
 | 
						|
 | 
						|
	if (isRequiredVersion(versionDesc)) {
 | 
						|
		return versionDesc;
 | 
						|
	}
 | 
						|
 | 
						|
	// add handle for URL Dependencies
 | 
						|
	return getGitUrlVersion(versionDesc.toLowerCase());
 | 
						|
}
 | 
						|
 | 
						|
module.exports.normalizeVersion = normalizeVersion;
 | 
						|
 | 
						|
/** @typedef {{ data: JsonObject, path: string }} DescriptionFile */
 | 
						|
 | 
						|
/**
 | 
						|
 * @param {InputFileSystem} fs file system
 | 
						|
 * @param {string} directory directory to start looking into
 | 
						|
 * @param {string[]} descriptionFiles possible description filenames
 | 
						|
 * @param {(err?: Error | null, descriptionFile?: DescriptionFile, paths?: string[]) => void} callback callback
 | 
						|
 * @param {(descriptionFile?: DescriptionFile) => boolean} satisfiesDescriptionFileData file data compliance check
 | 
						|
 * @param {Set<string>} checkedFilePaths set of file paths that have been checked
 | 
						|
 */
 | 
						|
const getDescriptionFile = (
 | 
						|
	fs,
 | 
						|
	directory,
 | 
						|
	descriptionFiles,
 | 
						|
	callback,
 | 
						|
	satisfiesDescriptionFileData,
 | 
						|
	checkedFilePaths = new Set()
 | 
						|
) => {
 | 
						|
	let i = 0;
 | 
						|
 | 
						|
	const satisfiesDescriptionFileDataInternal = {
 | 
						|
		check: satisfiesDescriptionFileData,
 | 
						|
		checkedFilePaths
 | 
						|
	};
 | 
						|
 | 
						|
	const tryLoadCurrent = () => {
 | 
						|
		if (i >= descriptionFiles.length) {
 | 
						|
			const parentDirectory = dirname(fs, directory);
 | 
						|
			if (!parentDirectory || parentDirectory === directory) {
 | 
						|
				return callback(
 | 
						|
					null,
 | 
						|
					undefined,
 | 
						|
					Array.from(satisfiesDescriptionFileDataInternal.checkedFilePaths)
 | 
						|
				);
 | 
						|
			}
 | 
						|
			return getDescriptionFile(
 | 
						|
				fs,
 | 
						|
				parentDirectory,
 | 
						|
				descriptionFiles,
 | 
						|
				callback,
 | 
						|
				satisfiesDescriptionFileDataInternal.check,
 | 
						|
				satisfiesDescriptionFileDataInternal.checkedFilePaths
 | 
						|
			);
 | 
						|
		}
 | 
						|
		const filePath = join(fs, directory, descriptionFiles[i]);
 | 
						|
		readJson(fs, filePath, (err, data) => {
 | 
						|
			if (err) {
 | 
						|
				if ("code" in err && err.code === "ENOENT") {
 | 
						|
					i++;
 | 
						|
					return tryLoadCurrent();
 | 
						|
				}
 | 
						|
				return callback(err);
 | 
						|
			}
 | 
						|
			if (!data || typeof data !== "object" || Array.isArray(data)) {
 | 
						|
				return callback(
 | 
						|
					new Error(`Description file ${filePath} is not an object`)
 | 
						|
				);
 | 
						|
			}
 | 
						|
			if (
 | 
						|
				typeof satisfiesDescriptionFileDataInternal.check === "function" &&
 | 
						|
				!satisfiesDescriptionFileDataInternal.check({ data, path: filePath })
 | 
						|
			) {
 | 
						|
				i++;
 | 
						|
				satisfiesDescriptionFileDataInternal.checkedFilePaths.add(filePath);
 | 
						|
				return tryLoadCurrent();
 | 
						|
			}
 | 
						|
			callback(null, { data, path: filePath });
 | 
						|
		});
 | 
						|
	};
 | 
						|
	tryLoadCurrent();
 | 
						|
};
 | 
						|
module.exports.getDescriptionFile = getDescriptionFile;
 | 
						|
 | 
						|
/**
 | 
						|
 * @param {JsonObject} data description file data i.e.: package.json
 | 
						|
 * @param {string} packageName name of the dependency
 | 
						|
 * @returns {string | undefined} normalized version
 | 
						|
 */
 | 
						|
const getRequiredVersionFromDescriptionFile = (data, packageName) => {
 | 
						|
	const dependencyTypes = [
 | 
						|
		"optionalDependencies",
 | 
						|
		"dependencies",
 | 
						|
		"peerDependencies",
 | 
						|
		"devDependencies"
 | 
						|
	];
 | 
						|
 | 
						|
	for (const dependencyType of dependencyTypes) {
 | 
						|
		const dependency = /** @type {JsonObject} */ (data[dependencyType]);
 | 
						|
		if (
 | 
						|
			dependency &&
 | 
						|
			typeof dependency === "object" &&
 | 
						|
			packageName in dependency
 | 
						|
		) {
 | 
						|
			return normalizeVersion(
 | 
						|
				/** @type {Exclude<JsonPrimitive, null | boolean| number>} */ (
 | 
						|
					dependency[packageName]
 | 
						|
				)
 | 
						|
			);
 | 
						|
		}
 | 
						|
	}
 | 
						|
};
 | 
						|
module.exports.getRequiredVersionFromDescriptionFile =
 | 
						|
	getRequiredVersionFromDescriptionFile;
 |