Add semantic token highlighting for 'that' variable and comment file references in VS Code extension Add Phone_Text_Input and Currency_Input components with formatting utilities Implement client widgets, form standardization, and soft delete functionality Add modal scroll lock and update documentation Implement comprehensive modal system with form integration and validation Fix modal component instantiation using jQuery plugin API Implement modal system with responsive sizing, queuing, and validation support Implement form submission with validation, error handling, and loading states Implement country/state selectors with dynamic data loading and Bootstrap styling Revert Rsx::Route() highlighting in Blade/PHP files Target specific PHP scopes for Rsx::Route() highlighting in Blade Expand injection selector for Rsx::Route() highlighting Add custom syntax highlighting for Rsx::Route() and Rsx.Route() calls Update jqhtml packages to v2.2.165 Add bundle path validation for common mistakes (development mode only) Create Ajax_Select_Input widget and Rsx_Reference_Data controller Create Country_Select_Input widget with default country support Initialize Tom Select on Select_Input widgets Add Tom Select bundle for enhanced select dropdowns Implement ISO 3166 geographic data system for country/region selection Implement widget-based form system with disabled state support 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
420 lines
9.9 KiB
JavaScript
420 lines
9.9 KiB
JavaScript
/*
|
|
MIT License http://www.opensource.org/licenses/mit-license.php
|
|
Author Tobias Koppers @sokra
|
|
*/
|
|
|
|
"use strict";
|
|
|
|
const { dirname, join, 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
|
|
* @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
|
|
* @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
|
|
* @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
|
|
* @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
|
|
* @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
|
|
* @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);
|
|
}
|
|
|
|
/** @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, [
|
|
...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;
|
|
|
|
/**
|
|
* @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;
|