Skip to content

Commit 4fb73da

Browse files
authored
Merge pull request #16945 from snitin315/feat/url-deps-mf
feat: add URL dependencies support to consume shared module via module federation
2 parents b471a6b + 3cd2303 commit 4fb73da

6 files changed

Lines changed: 1514 additions & 9 deletions

File tree

cspell.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -281,7 +281,8 @@
281281
"Xmodule",
282282
"xxhash",
283283
"xxhashjs",
284-
"Yann"
284+
"Yann",
285+
"commithash"
285286
],
286287
"ignoreRegExpList": [
287288
"/Author.+/",

lib/sharing/utils.js

Lines changed: 293 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,299 @@ const { join, dirname, readJson } = require("../util/fs");
99

1010
/** @typedef {import("../util/fs").InputFileSystem} InputFileSystem */
1111

12+
// Extreme shorthand only for github. eg: foo/bar
13+
const RE_URL_GITHUB_EXTREME_SHORT = /^[^/@:.\s][^/@:\s]*\/[^@:\s]*[^/@:\s]#\S+/;
14+
15+
// Short url with specific protocol. eg: github:foo/bar
16+
const RE_GIT_URL_SHORT = /^(github|gitlab|bitbucket|gist):\/?[^/.]+\/?/i;
17+
18+
// Currently supported protocols
19+
const RE_PROTOCOL =
20+
/^((git\+)?(ssh|https?|file)|git|github|gitlab|bitbucket|gist):$/i;
21+
22+
// Has custom protocol
23+
const RE_CUSTOM_PROTOCOL = /^((git\+)?(ssh|https?|file)|git):\/\//i;
24+
25+
// Valid hash format for npm / yarn ...
26+
const RE_URL_HASH_VERSION = /#(?:semver:)?(.+)/;
27+
28+
// Simple hostname validate
29+
const RE_HOSTNAME = /^(?:[^/.]+(\.[^/]+)+|localhost)$/;
30+
31+
// For hostname with colon. eg: ssh://user@github.com:foo/bar
32+
const RE_HOSTNAME_WITH_COLON =
33+
/([^/@#:.]+(?:\.[^/@#:.]+)+|localhost):([^#/0-9]+)/;
34+
35+
// Reg for url without protocol
36+
const RE_NO_PROTOCOL = /^([^/@#:.]+(?:\.[^/@#:.]+)+)/;
37+
38+
// RegExp for version string
39+
const VERSION_PATTERN_REGEXP = /^([\d^=v<>~]|[*xX]$)/;
40+
41+
// Specific protocol for short url without normal hostname
42+
const PROTOCOLS_FOR_SHORT = [
43+
"github:",
44+
"gitlab:",
45+
"bitbucket:",
46+
"gist:",
47+
"file:"
48+
];
49+
50+
// Default protocol for git url
51+
const DEF_GIT_PROTOCOL = "git+ssh://";
52+
53+
// thanks to https://github.com/npm/hosted-git-info/blob/latest/git-host-info.js
54+
const extractCommithashByDomain = {
55+
"github.com": (pathname, hash) => {
56+
let [, user, project, type, commithash] = pathname.split("/", 5);
57+
if (type && type !== "tree") {
58+
return;
59+
}
60+
61+
if (!type) {
62+
commithash = hash;
63+
} else {
64+
commithash = "#" + commithash;
65+
}
66+
67+
if (project && project.endsWith(".git")) {
68+
project = project.slice(0, -4);
69+
}
70+
71+
if (!user || !project) {
72+
return;
73+
}
74+
75+
return commithash;
76+
},
77+
"gitlab.com": (pathname, hash) => {
78+
const path = pathname.slice(1);
79+
if (path.includes("/-/") || path.includes("/archive.tar.gz")) {
80+
return;
81+
}
82+
83+
const segments = path.split("/");
84+
let project = segments.pop();
85+
if (project.endsWith(".git")) {
86+
project = project.slice(0, -4);
87+
}
88+
89+
const user = segments.join("/");
90+
if (!user || !project) {
91+
return;
92+
}
93+
94+
return hash;
95+
},
96+
"bitbucket.org": (pathname, hash) => {
97+
let [, user, project, aux] = pathname.split("/", 4);
98+
if (["get"].includes(aux)) {
99+
return;
100+
}
101+
102+
if (project && project.endsWith(".git")) {
103+
project = project.slice(0, -4);
104+
}
105+
106+
if (!user || !project) {
107+
return;
108+
}
109+
110+
return hash;
111+
},
112+
"gist.github.com": (pathname, hash) => {
113+
let [, user, project, aux] = pathname.split("/", 4);
114+
if (aux === "raw") {
115+
return;
116+
}
117+
118+
if (!project) {
119+
if (!user) {
120+
return;
121+
}
122+
123+
project = user;
124+
user = null;
125+
}
126+
127+
if (project.endsWith(".git")) {
128+
project = project.slice(0, -4);
129+
}
130+
131+
return hash;
132+
}
133+
};
134+
135+
/**
136+
* extract commit hash from parsed url
137+
*
138+
* @inner
139+
* @param {Object} urlParsed parsed url
140+
* @returns {string} commithash
141+
*/
142+
function getCommithash(urlParsed) {
143+
let { hostname, pathname, hash } = urlParsed;
144+
hostname = hostname.replace(/^www\./, "");
145+
146+
try {
147+
hash = decodeURIComponent(hash);
148+
// eslint-disable-next-line no-empty
149+
} catch (e) {}
150+
151+
if (extractCommithashByDomain[hostname]) {
152+
return extractCommithashByDomain[hostname](pathname, hash) || "";
153+
}
154+
155+
return hash;
156+
}
157+
158+
/**
159+
* make url right for URL parse
160+
*
161+
* @inner
162+
* @param {string} gitUrl git url
163+
* @returns {string} fixed url
164+
*/
165+
function correctUrl(gitUrl) {
166+
// like:
167+
// proto://hostname.com:user/repo -> proto://hostname.com/user/repo
168+
return gitUrl.replace(RE_HOSTNAME_WITH_COLON, "$1/$2");
169+
}
170+
171+
/**
172+
* make url protocol right for URL parse
173+
*
174+
* @inner
175+
* @param {string} gitUrl git url
176+
* @returns {string} fixed url
177+
*/
178+
function correctProtocol(gitUrl) {
179+
// eg: github:foo/bar#v1.0. Should not add double slash, in case of error parsed `pathname`
180+
if (RE_GIT_URL_SHORT.test(gitUrl)) {
181+
return gitUrl;
182+
}
183+
184+
// eg: user@github.com:foo/bar
185+
if (!RE_CUSTOM_PROTOCOL.test(gitUrl)) {
186+
return `${DEF_GIT_PROTOCOL}${gitUrl}`;
187+
}
188+
189+
return gitUrl;
190+
}
191+
192+
/**
193+
* extract git dep version from hash
194+
*
195+
* @inner
196+
* @param {string} hash hash
197+
* @returns {string} git dep version
198+
*/
199+
function getVersionFromHash(hash) {
200+
const matched = hash.match(RE_URL_HASH_VERSION);
201+
202+
return (matched && matched[1]) || "";
203+
}
204+
205+
/**
206+
* if string can be decoded
207+
*
208+
* @inner
209+
* @param {string} str str to be checked
210+
* @returns {boolean} if can be decoded
211+
*/
212+
function canBeDecoded(str) {
213+
try {
214+
decodeURIComponent(str);
215+
} catch (e) {
216+
return false;
217+
}
218+
219+
return true;
220+
}
221+
222+
/**
223+
* get right dep version from git url
224+
*
225+
* @inner
226+
* @param {string} gitUrl git url
227+
* @returns {string} dep version
228+
*/
229+
function getGitUrlVersion(gitUrl) {
230+
let oriGitUrl = gitUrl;
231+
// github extreme shorthand
232+
if (RE_URL_GITHUB_EXTREME_SHORT.test(gitUrl)) {
233+
gitUrl = "github:" + gitUrl;
234+
} else {
235+
gitUrl = correctProtocol(gitUrl);
236+
}
237+
238+
gitUrl = correctUrl(gitUrl);
239+
240+
let parsed;
241+
try {
242+
parsed = new URL(gitUrl);
243+
// eslint-disable-next-line no-empty
244+
} catch (e) {}
245+
246+
if (!parsed) {
247+
return "";
248+
}
249+
250+
const { protocol, hostname, pathname, username, password } = parsed;
251+
if (!RE_PROTOCOL.test(protocol)) {
252+
return "";
253+
}
254+
255+
// pathname shouldn't be empty or URL malformed
256+
if (!pathname || !canBeDecoded(pathname)) {
257+
return "";
258+
}
259+
260+
// without protocol, there should have auth info
261+
if (RE_NO_PROTOCOL.test(oriGitUrl) && !username && !password) {
262+
return "";
263+
}
264+
265+
if (!PROTOCOLS_FOR_SHORT.includes(protocol.toLowerCase())) {
266+
if (!RE_HOSTNAME.test(hostname)) {
267+
return "";
268+
}
269+
270+
const commithash = getCommithash(parsed);
271+
return getVersionFromHash(commithash) || commithash;
272+
}
273+
274+
// for protocol short
275+
return getVersionFromHash(gitUrl);
276+
}
277+
12278
/**
13279
* @param {string} str maybe required version
14280
* @returns {boolean} true, if it looks like a version
15281
*/
16-
exports.isRequiredVersion = str => {
17-
return /^([\d^=v<>~]|[*xX]$)/.test(str);
18-
};
282+
function isRequiredVersion(str) {
283+
return VERSION_PATTERN_REGEXP.test(str);
284+
}
285+
286+
exports.isRequiredVersion = isRequiredVersion;
287+
288+
/**
289+
* @see https://docs.npmjs.com/cli/v7/configuring-npm/package-json#urls-as-dependencies
290+
* @param {string} versionDesc version to be normalized
291+
* @returns {string} normalized version
292+
*/
293+
function normalizeVersion(versionDesc) {
294+
versionDesc = (versionDesc && versionDesc.trim()) || "";
295+
296+
if (isRequiredVersion(versionDesc)) {
297+
return versionDesc;
298+
}
299+
300+
// add handle for URL Dependencies
301+
return getGitUrlVersion(versionDesc.toLowerCase());
302+
}
303+
304+
exports.normalizeVersion = normalizeVersion;
19305

20306
/**
21307
*
@@ -64,27 +350,27 @@ exports.getRequiredVersionFromDescriptionFile = (data, packageName) => {
64350
typeof data.optionalDependencies === "object" &&
65351
packageName in data.optionalDependencies
66352
) {
67-
return data.optionalDependencies[packageName];
353+
return normalizeVersion(data.optionalDependencies[packageName]);
68354
}
69355
if (
70356
data.dependencies &&
71357
typeof data.dependencies === "object" &&
72358
packageName in data.dependencies
73359
) {
74-
return data.dependencies[packageName];
360+
return normalizeVersion(data.dependencies[packageName]);
75361
}
76362
if (
77363
data.peerDependencies &&
78364
typeof data.peerDependencies === "object" &&
79365
packageName in data.peerDependencies
80366
) {
81-
return data.peerDependencies[packageName];
367+
return normalizeVersion(data.peerDependencies[packageName]);
82368
}
83369
if (
84370
data.devDependencies &&
85371
typeof data.devDependencies === "object" &&
86372
packageName in data.devDependencies
87373
) {
88-
return data.devDependencies[packageName];
374+
return normalizeVersion(data.devDependencies[packageName]);
89375
}
90376
};

0 commit comments

Comments
 (0)