mirror of
https://github.com/rxliuli/apps.apple.com.git
synced 2025-11-09 20:00:34 +00:00
init commit
This commit is contained in:
83
shared/utils/node_modules/@amp/runtime-detect/dist/extensions/compare.js
generated
vendored
Normal file
83
shared/utils/node_modules/@amp/runtime-detect/dist/extensions/compare.js
generated
vendored
Normal file
@@ -0,0 +1,83 @@
|
||||
import { eq, gt, gte, lt, lte } from '../version.js';
|
||||
|
||||
function _define_property(obj, key, value) {
|
||||
if (key in obj) {
|
||||
Object.defineProperty(obj, key, {
|
||||
value: value,
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true
|
||||
});
|
||||
} else {
|
||||
obj[key] = value;
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
function _object_spread(target) {
|
||||
for(var i = 1; i < arguments.length; i++){
|
||||
var source = arguments[i] != null ? arguments[i] : {};
|
||||
var ownKeys = Object.keys(source);
|
||||
if (typeof Object.getOwnPropertySymbols === "function") {
|
||||
ownKeys = ownKeys.concat(Object.getOwnPropertySymbols(source).filter(function(sym) {
|
||||
return Object.getOwnPropertyDescriptor(source, sym).enumerable;
|
||||
}));
|
||||
}
|
||||
ownKeys.forEach(function(key) {
|
||||
_define_property(target, key, source[key]);
|
||||
});
|
||||
}
|
||||
return target;
|
||||
}
|
||||
function ownKeys(object, enumerableOnly) {
|
||||
var keys = Object.keys(object);
|
||||
if (Object.getOwnPropertySymbols) {
|
||||
var symbols = Object.getOwnPropertySymbols(object);
|
||||
if (enumerableOnly) {
|
||||
symbols = symbols.filter(function(sym) {
|
||||
return Object.getOwnPropertyDescriptor(object, sym).enumerable;
|
||||
});
|
||||
}
|
||||
keys.push.apply(keys, symbols);
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
function _object_spread_props(target, source) {
|
||||
source = source != null ? source : {};
|
||||
if (Object.getOwnPropertyDescriptors) {
|
||||
Object.defineProperties(target, Object.getOwnPropertyDescriptors(source));
|
||||
} else {
|
||||
ownKeys(Object(source)).forEach(function(key) {
|
||||
Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key));
|
||||
});
|
||||
}
|
||||
return target;
|
||||
}
|
||||
function compareExtension(descriptor) {
|
||||
function makeComparable(data) {
|
||||
var _data_major, _data_minor, _data_patch;
|
||||
const version = {
|
||||
major: (_data_major = data.major) !== null && _data_major !== void 0 ? _data_major : 0,
|
||||
minor: (_data_minor = data.minor) !== null && _data_minor !== void 0 ? _data_minor : 0,
|
||||
patch: (_data_patch = data.patch) !== null && _data_patch !== void 0 ? _data_patch : 0
|
||||
};
|
||||
return _object_spread_props(_object_spread({}, data), {
|
||||
eq: (value)=>eq(version, value),
|
||||
gt: (value)=>gt(version, value),
|
||||
gte: (value)=>gte(version, value),
|
||||
lt: (value)=>lt(version, value),
|
||||
lte: (value)=>lte(version, value),
|
||||
is: (value)=>data.name === value || data.variant === value
|
||||
});
|
||||
}
|
||||
return _object_spread_props(_object_spread({}, descriptor), {
|
||||
extensions: [
|
||||
...descriptor.extensions,
|
||||
'compare'
|
||||
],
|
||||
browser: makeComparable(descriptor.browser),
|
||||
engine: makeComparable(descriptor.engine),
|
||||
os: makeComparable(descriptor.os)
|
||||
});
|
||||
}
|
||||
|
||||
export { compareExtension };
|
||||
105
shared/utils/node_modules/@amp/runtime-detect/dist/extensions/flags.js
generated
vendored
Normal file
105
shared/utils/node_modules/@amp/runtime-detect/dist/extensions/flags.js
generated
vendored
Normal file
@@ -0,0 +1,105 @@
|
||||
function _define_property(obj, key, value) {
|
||||
if (key in obj) {
|
||||
Object.defineProperty(obj, key, {
|
||||
value: value,
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true
|
||||
});
|
||||
} else {
|
||||
obj[key] = value;
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
function _object_spread(target) {
|
||||
for(var i = 1; i < arguments.length; i++){
|
||||
var source = arguments[i] != null ? arguments[i] : {};
|
||||
var ownKeys = Object.keys(source);
|
||||
if (typeof Object.getOwnPropertySymbols === "function") {
|
||||
ownKeys = ownKeys.concat(Object.getOwnPropertySymbols(source).filter(function(sym) {
|
||||
return Object.getOwnPropertyDescriptor(source, sym).enumerable;
|
||||
}));
|
||||
}
|
||||
ownKeys.forEach(function(key) {
|
||||
_define_property(target, key, source[key]);
|
||||
});
|
||||
}
|
||||
return target;
|
||||
}
|
||||
function ownKeys(object, enumerableOnly) {
|
||||
var keys = Object.keys(object);
|
||||
if (Object.getOwnPropertySymbols) {
|
||||
var symbols = Object.getOwnPropertySymbols(object);
|
||||
if (enumerableOnly) {
|
||||
symbols = symbols.filter(function(sym) {
|
||||
return Object.getOwnPropertyDescriptor(object, sym).enumerable;
|
||||
});
|
||||
}
|
||||
keys.push.apply(keys, symbols);
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
function _object_spread_props(target, source) {
|
||||
source = source != null ? source : {};
|
||||
if (Object.getOwnPropertyDescriptors) {
|
||||
Object.defineProperties(target, Object.getOwnPropertyDescriptors(source));
|
||||
} else {
|
||||
ownKeys(Object(source)).forEach(function(key) {
|
||||
Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key));
|
||||
});
|
||||
}
|
||||
return target;
|
||||
}
|
||||
function flagsExtension(descriptor) {
|
||||
let osName = descriptor.os.name;
|
||||
const isAndroid = descriptor.os.name === 'android';
|
||||
let isMacOS = descriptor.os.name === 'macos';
|
||||
let isIOS = descriptor.os.name === 'ios';
|
||||
var _descriptor_navigator_maxTouchPoints;
|
||||
/**
|
||||
* Newer iPads will identify as macOS in the UserAgent string but can still be differentiated by
|
||||
* inspecting `maxTouchPoints`. The macOS and iOS values need to be reset when detected.
|
||||
*/ const isIPadOS = osName === 'ipados' || isIOS && /ipad/i.test(descriptor.ua) || isMacOS && ((_descriptor_navigator_maxTouchPoints = descriptor.navigator.maxTouchPoints) !== null && _descriptor_navigator_maxTouchPoints !== void 0 ? _descriptor_navigator_maxTouchPoints : 0) >= 2;
|
||||
if (isIPadOS) {
|
||||
osName = 'ipados';
|
||||
isIOS = false;
|
||||
isMacOS = false;
|
||||
}
|
||||
const browser = _object_spread_props(_object_spread({}, descriptor.browser), {
|
||||
isUnknown: descriptor.browser.name === 'unknown',
|
||||
isSafari: descriptor.browser.name === 'safari',
|
||||
isChrome: descriptor.browser.name === 'chrome',
|
||||
isFirefox: descriptor.browser.name === 'firefox',
|
||||
isEdge: descriptor.browser.name === 'edge',
|
||||
isWebView: descriptor.browser.name === 'webview',
|
||||
isOther: descriptor.browser.name === 'other',
|
||||
isMobile: descriptor.browser.mobile || isIOS || isIPadOS || isAndroid || false
|
||||
});
|
||||
const engine = _object_spread_props(_object_spread({}, descriptor.engine), {
|
||||
isUnknown: descriptor.engine.name === 'unknown',
|
||||
isWebKit: descriptor.engine.name === 'webkit',
|
||||
isBlink: descriptor.engine.name === 'blink',
|
||||
isGecko: descriptor.engine.name === 'gecko'
|
||||
});
|
||||
const os = _object_spread_props(_object_spread({}, descriptor.os), {
|
||||
name: osName,
|
||||
isUnknown: descriptor.os.name === 'unknown',
|
||||
isLinux: descriptor.os.name === 'linux',
|
||||
isWindows: descriptor.os.name === 'windows',
|
||||
isMacOS,
|
||||
isAndroid,
|
||||
isIOS,
|
||||
isIPadOS
|
||||
});
|
||||
return _object_spread_props(_object_spread({}, descriptor), {
|
||||
extensions: [
|
||||
...descriptor.extensions,
|
||||
'flags'
|
||||
],
|
||||
browser,
|
||||
os,
|
||||
engine
|
||||
});
|
||||
}
|
||||
|
||||
export { flagsExtension };
|
||||
22
shared/utils/node_modules/@amp/runtime-detect/dist/rules.js
generated
vendored
Normal file
22
shared/utils/node_modules/@amp/runtime-detect/dist/rules.js
generated
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
function applyRules(rules, navigator, data) {
|
||||
const { userAgent } = navigator !== null && navigator !== void 0 ? navigator : {};
|
||||
if (typeof userAgent !== 'string' || userAgent.trim() === '') {
|
||||
return data;
|
||||
}
|
||||
for (const rule of rules){
|
||||
const patterns = rule.slice(0, -1);
|
||||
const parser = rule[rule.length - 1];
|
||||
let match = null;
|
||||
for (const pattern of patterns){
|
||||
match = userAgent.match(pattern);
|
||||
if (match !== null) {
|
||||
Object.assign(data, parser(match, navigator, data));
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (match !== null) break;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
export { applyRules };
|
||||
392
shared/utils/node_modules/@amp/runtime-detect/dist/user-agent.js
generated
vendored
Normal file
392
shared/utils/node_modules/@amp/runtime-detect/dist/user-agent.js
generated
vendored
Normal file
@@ -0,0 +1,392 @@
|
||||
import { parseVersion } from './version.js';
|
||||
import { applyRules } from './rules.js';
|
||||
|
||||
function _define_property(obj, key, value) {
|
||||
if (key in obj) {
|
||||
Object.defineProperty(obj, key, {
|
||||
value: value,
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true
|
||||
});
|
||||
} else {
|
||||
obj[key] = value;
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
function _object_spread(target) {
|
||||
for(var i = 1; i < arguments.length; i++){
|
||||
var source = arguments[i] != null ? arguments[i] : {};
|
||||
var ownKeys = Object.keys(source);
|
||||
if (typeof Object.getOwnPropertySymbols === "function") {
|
||||
ownKeys = ownKeys.concat(Object.getOwnPropertySymbols(source).filter(function(sym) {
|
||||
return Object.getOwnPropertyDescriptor(source, sym).enumerable;
|
||||
}));
|
||||
}
|
||||
ownKeys.forEach(function(key) {
|
||||
_define_property(target, key, source[key]);
|
||||
});
|
||||
}
|
||||
return target;
|
||||
}
|
||||
function ownKeys(object, enumerableOnly) {
|
||||
var keys = Object.keys(object);
|
||||
if (Object.getOwnPropertySymbols) {
|
||||
var symbols = Object.getOwnPropertySymbols(object);
|
||||
if (enumerableOnly) {
|
||||
symbols = symbols.filter(function(sym) {
|
||||
return Object.getOwnPropertyDescriptor(object, sym).enumerable;
|
||||
});
|
||||
}
|
||||
keys.push.apply(keys, symbols);
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
function _object_spread_props(target, source) {
|
||||
source = source != null ? source : {};
|
||||
if (Object.getOwnPropertyDescriptors) {
|
||||
Object.defineProperties(target, Object.getOwnPropertyDescriptors(source));
|
||||
} else {
|
||||
ownKeys(Object(source)).forEach(function(key) {
|
||||
Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key));
|
||||
});
|
||||
}
|
||||
return target;
|
||||
}
|
||||
var _match_, _match_1;
|
||||
const RULES = {
|
||||
// BROWSER ==========================================================================
|
||||
browser: [
|
||||
// WEBVIEW ------------------------------------------------------------------------
|
||||
// iTunes/Music.app/TV.app
|
||||
[
|
||||
/^(itunes|music|tv)\/([\w.]+)\s/i,
|
||||
(match)=>_object_spread_props(_object_spread({
|
||||
name: 'webview',
|
||||
variant: match[1].trim().toLowerCase().replace(/(music|tv)/i, '$1.app')
|
||||
}, parseVersion(match[2])), {
|
||||
mobile: false
|
||||
})
|
||||
],
|
||||
// Facebook
|
||||
[
|
||||
/(?:(?:fban\/fbios|fb_iab\/fb4a)(?!.+fbav)|;fbav\/([\w.]+);)/i,
|
||||
(match)=>_object_spread({
|
||||
name: 'webview',
|
||||
variant: 'facebook',
|
||||
mobile: true
|
||||
}, parseVersion(match[1]))
|
||||
],
|
||||
// Instagram / SnapChat
|
||||
[
|
||||
/(instagram|snapchat)[/ ]([-\w.]+)/i,
|
||||
(match)=>_object_spread({
|
||||
name: 'webview',
|
||||
variant: match[1].trim().toLowerCase(),
|
||||
mobile: true
|
||||
}, parseVersion(match[2]))
|
||||
],
|
||||
// TikTok
|
||||
[
|
||||
/musical_ly(?:.+app_?version\/|_)([\w.]+)/i,
|
||||
(match)=>_object_spread({
|
||||
name: 'webview',
|
||||
variant: 'tiktok',
|
||||
mobile: true
|
||||
}, parseVersion(match[1]))
|
||||
],
|
||||
// Twitter
|
||||
[
|
||||
/twitter/i,
|
||||
()=>({
|
||||
name: 'webview',
|
||||
variant: 'twitter',
|
||||
mobile: true
|
||||
})
|
||||
],
|
||||
// Chrome WebView
|
||||
[
|
||||
/ wv\).+?(?:version|chrome)\/([\w.]+)/i,
|
||||
(match)=>_object_spread({
|
||||
name: 'webview',
|
||||
mobile: true
|
||||
}, parseVersion(match[1]))
|
||||
],
|
||||
// ELECTRON -----------------------------------------------------------------------
|
||||
[
|
||||
/electron\/([\w.]+) safari/i,
|
||||
(match)=>_object_spread({
|
||||
name: 'electron',
|
||||
mobile: false
|
||||
}, parseVersion(match[1]))
|
||||
],
|
||||
// OTHER --------------------------------------------------------------------------
|
||||
[
|
||||
/tesla\/(.*?(20\d\d\.([-\w.])+))/i,
|
||||
(match)=>_object_spread_props(_object_spread({
|
||||
name: 'other',
|
||||
variant: 'tesla',
|
||||
mobile: false
|
||||
}, parseVersion(match[2])), {
|
||||
version: match[1]
|
||||
})
|
||||
],
|
||||
[
|
||||
/(samsung|huawei)browser\/([-\w.]+)/i,
|
||||
(match)=>_object_spread({
|
||||
name: 'other',
|
||||
variant: match[1].trim().toLowerCase().replace(/browser/i, ''),
|
||||
mobile: true
|
||||
}, parseVersion(match[2]))
|
||||
],
|
||||
[
|
||||
/yabrowser\/([-\w.]+)/i,
|
||||
(match)=>_object_spread({
|
||||
name: 'other',
|
||||
variant: 'yandex',
|
||||
mobile: false
|
||||
}, parseVersion(match[1]))
|
||||
],
|
||||
[
|
||||
/(brave|flock|rockmelt|midori|epiphany|silk|skyfire|ovibrowser|bolt|iron|vivaldi|iridium|phantomjs|bowser|quark|qupzilla|falkon|rekonq|puffin|whale(?!.+naver)|qqbrowserlite|qq|duckduckgo)\/([-\w.]+)/i,
|
||||
(match, { userAgent })=>_object_spread({
|
||||
name: 'other',
|
||||
variant: match[1].trim().toLowerCase(),
|
||||
mobile: /mobile/i.test(userAgent)
|
||||
}, parseVersion(match[2].replace(/-/g, '.')))
|
||||
],
|
||||
// EDGE / IE ----------------------------------------------------------------------
|
||||
[
|
||||
/edg(e|ios|a)?\/([\w.]+)/i,
|
||||
(match)=>_object_spread({
|
||||
name: 'edge',
|
||||
mobile: /(edgios|edga)/i.test((_match_ = match[1]) !== null && _match_ !== void 0 ? _match_ : '')
|
||||
}, parseVersion(match[2]))
|
||||
],
|
||||
[
|
||||
/trident.+rv[: ]([\w.]{1,9})\b.+like gecko/i,
|
||||
(match)=>_object_spread({
|
||||
name: 'ie',
|
||||
mobile: false
|
||||
}, parseVersion(match[1]))
|
||||
],
|
||||
// OPERA --------------------------------------------------------------------------
|
||||
[
|
||||
/opr\/([\w.]+)/i,
|
||||
/opera mini\/([-\w.]+)/i,
|
||||
/opera [mobiletab]{3,6}\b.+version\/([-\w.]+)/i,
|
||||
/opera(?:.+version\/|[/ ]+)([\w.]+)/i,
|
||||
(match)=>_object_spread({
|
||||
name: 'opera',
|
||||
mobile: /mobile/i.test(match[0])
|
||||
}, parseVersion(match[1]))
|
||||
],
|
||||
// CHROME -------------------------------------------------------------------------
|
||||
// Headless
|
||||
[
|
||||
/headlesschrome(?:\/([\w.]+)| )/i,
|
||||
(match)=>_object_spread({
|
||||
name: 'chrome',
|
||||
variant: 'headless',
|
||||
mobile: false
|
||||
}, parseVersion(match[1]))
|
||||
],
|
||||
// Chrome for iOS
|
||||
[
|
||||
/\b(?:crmo|crios)\/([\w.]+)/i,
|
||||
(match)=>_object_spread({
|
||||
name: 'chrome',
|
||||
mobile: true
|
||||
}, parseVersion(match[1]))
|
||||
],
|
||||
// Chrome
|
||||
[
|
||||
/chrome(?: browser)?\/v?([\w.]+)( mobile)?/i,
|
||||
(match)=>_object_spread({
|
||||
name: 'chrome',
|
||||
mobile: /mobile/i.test((_match_1 = match[2]) !== null && _match_1 !== void 0 ? _match_1 : '')
|
||||
}, parseVersion(match[1]))
|
||||
],
|
||||
// FIREFOX ------------------------------------------------------------------------
|
||||
// Focus
|
||||
[
|
||||
/\bfocus\/([\w.]+)/i,
|
||||
(match)=>_object_spread({
|
||||
name: 'firefox',
|
||||
variant: 'focus',
|
||||
mobile: true
|
||||
}, parseVersion(match[1]))
|
||||
],
|
||||
// Firefox for iOS
|
||||
[
|
||||
/fxios\/([\w.-]+)/i,
|
||||
/(?:mobile|tablet);.*(?:firefox)\/([\w.-]+)/i,
|
||||
(match)=>_object_spread({
|
||||
name: 'firefox',
|
||||
mobile: true
|
||||
}, parseVersion(match[1]))
|
||||
],
|
||||
// Firefox OSS versions
|
||||
[
|
||||
/(icedragon|iceweasel|camino|chimera|fennec|maemo browser|minimo|conkeror|klar)[/ ]?([\w.+]+)/i,
|
||||
(match)=>_object_spread({
|
||||
name: 'firefox',
|
||||
variant: match[1].trim().toLowerCase(),
|
||||
mobile: false
|
||||
}, parseVersion(match[2]))
|
||||
],
|
||||
// Firefox
|
||||
[
|
||||
/(?:firefox)\/([\w.]+)/i,
|
||||
/(?:mozilla)\/([\w.]+) .+rv:.+gecko\/\d+/i,
|
||||
(match)=>_object_spread({
|
||||
name: 'firefox',
|
||||
mobile: false
|
||||
}, parseVersion(match[1]))
|
||||
],
|
||||
// SAFARI -------------------------------------------------------------------------
|
||||
[
|
||||
/version\/([\w.,]+) .*mobile(?:\/\w+ | ?)safari/i,
|
||||
/version\/([\w.,]+) .*(safari)/i,
|
||||
/webkit.+?(?:mobile ?safari|safari)(?:\/([\w.]+))/i,
|
||||
(match)=>_object_spread({
|
||||
name: 'safari',
|
||||
mobile: /mobile/i.test(match[0])
|
||||
}, parseVersion(match[1]))
|
||||
]
|
||||
],
|
||||
// ENGINE ---------------------------------------------------------------------------
|
||||
engine: [
|
||||
[
|
||||
/webkit\/(?:537\.36).+chrome\/(?!27)([\w.]+)/i,
|
||||
(match)=>_object_spread({
|
||||
name: 'blink'
|
||||
}, parseVersion(match[1]))
|
||||
],
|
||||
[
|
||||
/windows.+ edge\/([\w.]+)/i,
|
||||
(match)=>_object_spread({
|
||||
name: 'blink'
|
||||
}, parseVersion(match[1]))
|
||||
],
|
||||
[
|
||||
/presto\/([\w.]+)/i,
|
||||
(match)=>_object_spread({
|
||||
name: 'presto'
|
||||
}, parseVersion(match[2]))
|
||||
],
|
||||
[
|
||||
/trident\/([\w.]+)/i,
|
||||
(match)=>_object_spread({
|
||||
name: 'trident'
|
||||
}, parseVersion(match[1]))
|
||||
],
|
||||
[
|
||||
/gecko\/([\w.]+)/i,
|
||||
(match)=>_object_spread({
|
||||
name: 'gecko'
|
||||
}, parseVersion(match[1]))
|
||||
],
|
||||
[
|
||||
/(khtml|netfront|netsurf|amaya|lynx|w3m|goanna)\/([\w.]+)/i,
|
||||
(match)=>_object_spread({
|
||||
name: 'other'
|
||||
}, parseVersion(match[2]))
|
||||
],
|
||||
[
|
||||
/webkit\/([\w.]+)/i,
|
||||
(match)=>_object_spread({
|
||||
name: 'webkit'
|
||||
}, parseVersion(match[1]))
|
||||
]
|
||||
],
|
||||
// OS -------------------------------------------------------------------------------
|
||||
os: [
|
||||
// Windows
|
||||
[
|
||||
/microsoft windows (vista|xp)/i,
|
||||
/windows nt 6\.2; (arm)/i,
|
||||
/windows (?:phone(?: os)?|mobile)[/ ]?([\d.\w ]*)/i,
|
||||
/windows[/ ]?([ntce\d. ]+\w)(?!.+xbox)/i,
|
||||
/(?:win(?=3|9|n)|win 9x )([nt\d.]+)/i,
|
||||
(match)=>_object_spread({
|
||||
name: 'windows'
|
||||
}, parseVersion(match[1]))
|
||||
],
|
||||
// iOS (iPhone/iPad)
|
||||
[
|
||||
/ip[honead]{2,4}\b(?:.*os ([\w]+) like mac|; opera)/i,
|
||||
/(?:ios;fbsv\/|iphone.+ios[/ ])([\d.]+)/i,
|
||||
(match)=>_object_spread({
|
||||
name: 'ios'
|
||||
}, parseVersion(match[1].replace(/_/g, '.')))
|
||||
],
|
||||
// macOS
|
||||
[
|
||||
/mac(?:intosh;?)? os x ?([\d._]+)/i,
|
||||
(match)=>_object_spread({
|
||||
name: 'macos'
|
||||
}, parseVersion(match[1].replace(/_/g, '.')))
|
||||
],
|
||||
// ChromeOS
|
||||
[
|
||||
/cros [\w]+(?:\)| ([\w.]+)\b)/i,
|
||||
(match)=>_object_spread({
|
||||
name: 'chromeos'
|
||||
}, parseVersion(match[1]))
|
||||
],
|
||||
// Android
|
||||
[
|
||||
/(?:android|webos|qnx|bada|rim tablet os|maemo|meego|sailfish)[-/ ]?([\w.]*)/i,
|
||||
/droid ([\w.]+|[\d+])\b.+(android[- ]x86|harmonyos)/i,
|
||||
(match)=>_object_spread({
|
||||
name: 'android'
|
||||
}, parseVersion(match[1]))
|
||||
],
|
||||
// Linux
|
||||
[
|
||||
/linux/i,
|
||||
()=>({
|
||||
name: 'linux'
|
||||
})
|
||||
]
|
||||
]
|
||||
};
|
||||
/**
|
||||
* Extend a data structure by running a list of functions over it.
|
||||
*/ function applyExtensions(data, extensions) {
|
||||
let result = data;
|
||||
for (const extension of extensions){
|
||||
result = extension(result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
/**
|
||||
* Parse the user agent string from the navigator object into a descriptor.
|
||||
*
|
||||
* @param navigator The Navigator object to parse
|
||||
* @param options Parse options
|
||||
* @returns The descriptor with optional extensions applied
|
||||
*/ function parseUserAgent(navigator, options) {
|
||||
var _navigator, _options;
|
||||
var _navigator_userAgent;
|
||||
const descriptor = {
|
||||
navigator: navigator,
|
||||
ua: (_navigator_userAgent = (_navigator = navigator) === null || _navigator === void 0 ? void 0 : _navigator.userAgent) !== null && _navigator_userAgent !== void 0 ? _navigator_userAgent : '',
|
||||
extensions: [],
|
||||
browser: applyRules(RULES.browser, navigator, {
|
||||
name: 'unknown',
|
||||
mobile: false
|
||||
}),
|
||||
engine: applyRules(RULES.engine, navigator, {
|
||||
name: 'unknown'
|
||||
}),
|
||||
os: applyRules(RULES.os, navigator, {
|
||||
name: 'unknown'
|
||||
})
|
||||
};
|
||||
var _options_extensions;
|
||||
return applyExtensions(descriptor, (_options_extensions = (_options = options) === null || _options === void 0 ? void 0 : _options.extensions) !== null && _options_extensions !== void 0 ? _options_extensions : []);
|
||||
}
|
||||
|
||||
export { parseUserAgent };
|
||||
99
shared/utils/node_modules/@amp/runtime-detect/dist/version.js
generated
vendored
Normal file
99
shared/utils/node_modules/@amp/runtime-detect/dist/version.js
generated
vendored
Normal file
@@ -0,0 +1,99 @@
|
||||
function parseVersion(input) {
|
||||
const data = {
|
||||
version: input.toLowerCase()
|
||||
};
|
||||
const parts = input.toLowerCase().split('.').filter((p)=>!!p);
|
||||
// Only parse single parts that are actual numbers.
|
||||
// This check will prevent `parseInt` from pasing the leading chars if they are
|
||||
// valid numbers.
|
||||
if (parts.length <= 1 && !/^\d+$/.test(parts[0])) {
|
||||
return data;
|
||||
}
|
||||
const major = parseInt(parts[0], 10);
|
||||
const minor = parseInt(parts[1], 10);
|
||||
const patch = parseInt(parts[2], 10);
|
||||
// Only add converted versions if `major` part was valid
|
||||
if (!Number.isNaN(major)) {
|
||||
data.major = major;
|
||||
if (!Number.isNaN(minor)) data.minor = minor;
|
||||
if (!Number.isNaN(patch)) data.patch = patch;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
/**
|
||||
* Check if the given value is a complete Version struct.
|
||||
*/ // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
function isVersion(value) {
|
||||
var _value, _value1;
|
||||
return typeof ((_value = value) === null || _value === void 0 ? void 0 : _value.major) === 'number' && typeof ((_value1 = value) === null || _value1 === void 0 ? void 0 : _value1.minor) === 'number';
|
||||
}
|
||||
/**
|
||||
* Compare two version numbers together.
|
||||
*
|
||||
* NOTE: This only supports the first 3 segments (major, minor, patch) and does not
|
||||
* do a full SemVer compare.
|
||||
*
|
||||
* @example
|
||||
* ```javascript
|
||||
* compareVersion('1.2.3', '1.2.4');
|
||||
* // => -1
|
||||
* ```
|
||||
*/ function compareVersion(base, comp) {
|
||||
let baseList = toNumbers(base);
|
||||
let compList = toNumbers(comp);
|
||||
// Right pad versions with zeros to make them equal length
|
||||
const versionLength = Math.max(baseList.length, compList.length);
|
||||
baseList = baseList.concat(Array(versionLength).fill(0)).slice(0, versionLength);
|
||||
compList = compList.concat(Array(versionLength).fill(0)).slice(0, versionLength);
|
||||
/** Constrain the given value to the output range of this function. */ const constrain = (value)=>{
|
||||
if (value <= -1) return -1;
|
||||
else if (value >= 1) return 1;
|
||||
else return 0;
|
||||
};
|
||||
for(let index = 0; index < versionLength; index++){
|
||||
const aValue = baseList[index];
|
||||
const bValue = compList[index];
|
||||
if (aValue !== bValue) {
|
||||
return constrain(aValue - bValue);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
function eq(base, comp) {
|
||||
return compareVersion(base, comp) === 0;
|
||||
}
|
||||
function gt(base, comp) {
|
||||
return compareVersion(base, comp) > 0;
|
||||
}
|
||||
function gte(base, comp) {
|
||||
const result = compareVersion(base, comp);
|
||||
return result > 0 || result === 0;
|
||||
}
|
||||
function lt(base, comp) {
|
||||
return compareVersion(base, comp) < 0;
|
||||
}
|
||||
function lte(base, comp) {
|
||||
const result = compareVersion(base, comp);
|
||||
return result < 0 || result === 0;
|
||||
}
|
||||
function toNumbers(value) {
|
||||
if (Array.isArray(value)) {
|
||||
return value;
|
||||
} else if (typeof value === 'number') {
|
||||
return [
|
||||
value
|
||||
];
|
||||
} else if (typeof value === 'string') {
|
||||
return toNumbers(parseVersion(value));
|
||||
} else {
|
||||
const values = [
|
||||
value.major,
|
||||
value.minor,
|
||||
value.patch
|
||||
];
|
||||
const uidx = values.indexOf(undefined);
|
||||
return uidx === -1 ? values : values.slice(0, uidx);
|
||||
}
|
||||
}
|
||||
|
||||
export { compareVersion, eq, gt, gte, isVersion, lt, lte, parseVersion };
|
||||
39
shared/utils/src/get-pwa-display-mode.ts
Normal file
39
shared/utils/src/get-pwa-display-mode.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
export enum PWADisplayMode {
|
||||
TWA = 'twa',
|
||||
BROWSER = 'browser',
|
||||
STANDALONE = 'standalone',
|
||||
MINIMAL = 'minimal-ui',
|
||||
FULLSCREEN = 'fullscreen',
|
||||
OVERLAY = 'window-controls-overlay',
|
||||
UNKNOWN = 'unknown',
|
||||
}
|
||||
|
||||
/**
|
||||
* For PWA, reads the "display" value from the manifest.json and returns the proper value if it matches.
|
||||
* Inspired by the sample snippet here: https://web.dev/learn/pwa/detection
|
||||
*/
|
||||
export const getPWADisplayMode = (): PWADisplayMode => {
|
||||
switch (true) {
|
||||
case document.referrer.startsWith('android-app://'):
|
||||
return PWADisplayMode.TWA;
|
||||
|
||||
case window.matchMedia('(display-mode: browser)').matches:
|
||||
return PWADisplayMode.BROWSER;
|
||||
|
||||
case window.matchMedia('(display-mode: standalone)').matches:
|
||||
return PWADisplayMode.STANDALONE;
|
||||
|
||||
case window.matchMedia('(display-mode: minimal-ui)').matches:
|
||||
return PWADisplayMode.MINIMAL;
|
||||
|
||||
case window.matchMedia('(display-mode: fullscreen)').matches:
|
||||
return PWADisplayMode.FULLSCREEN;
|
||||
|
||||
case window.matchMedia('(display-mode: window-controls-overlay)')
|
||||
.matches:
|
||||
return PWADisplayMode.OVERLAY;
|
||||
|
||||
default:
|
||||
return PWADisplayMode.UNKNOWN;
|
||||
}
|
||||
};
|
||||
168
shared/utils/src/history.ts
Normal file
168
shared/utils/src/history.ts
Normal file
@@ -0,0 +1,168 @@
|
||||
import type { Logger, LoggerFactory } from '@amp/web-apps-logger';
|
||||
import { LruMap } from './lru-map';
|
||||
import type { ScrollableElement } from './try-scroll';
|
||||
import { tryScroll } from './try-scroll';
|
||||
import { removeHost } from './url';
|
||||
import { generateUuid } from './uuid';
|
||||
|
||||
export interface Options {
|
||||
getScrollablePageElement(): ScrollableElement | null;
|
||||
}
|
||||
|
||||
type Id = string;
|
||||
const HISTORY_SIZE_LIMIT = 10;
|
||||
|
||||
interface WithScrollPosition<State> {
|
||||
scrollY: number;
|
||||
state: State;
|
||||
}
|
||||
/**
|
||||
* We are using a currentStateId on this class to always store the state id instead of saving
|
||||
* it on the window.history.state because there seems to be a bug in Safari where it is mutating
|
||||
* the window.history.state to null after our Sign In flow which includes multiple iframes
|
||||
* and multiple internal state changes inside the iframes. We can move back to window.history.state storing the id
|
||||
* if the Safari Issue is fixed in future.
|
||||
*/
|
||||
export class History<State> {
|
||||
private readonly log: Logger;
|
||||
private readonly states: LruMap<Id, WithScrollPosition<State>>;
|
||||
private readonly getScrollablePageElement: () => ScrollableElement | null;
|
||||
private currentStateId: string | undefined;
|
||||
|
||||
constructor(
|
||||
loggerFactory: LoggerFactory,
|
||||
options: Options,
|
||||
sizeLimit: number = HISTORY_SIZE_LIMIT,
|
||||
) {
|
||||
this.log = loggerFactory.loggerFor('History');
|
||||
this.states = new LruMap(sizeLimit);
|
||||
this.getScrollablePageElement = options.getScrollablePageElement;
|
||||
}
|
||||
|
||||
// Update page data but keep scroll position
|
||||
updateState(update: (state?: State) => State): void {
|
||||
if (!this.currentStateId) {
|
||||
this.log.warn(
|
||||
'failed: encountered a null currentStateId inside updateState',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const currentState = this.states.get(this.currentStateId);
|
||||
const newState = update(currentState?.state);
|
||||
this.log.info('updateState', newState, this.currentStateId);
|
||||
this.states.set(this.currentStateId, {
|
||||
...(currentState as WithScrollPosition<State>),
|
||||
state: newState,
|
||||
});
|
||||
}
|
||||
|
||||
replaceState(state: State, url: string | null): void {
|
||||
const id = generateId();
|
||||
this.log.info('replaceState', state, url, id);
|
||||
window.history.replaceState({ id }, '', this.removeHost(url));
|
||||
this.currentStateId = id;
|
||||
this.states.set(id, { state, scrollY: 0 });
|
||||
this.scrollTop = 0;
|
||||
}
|
||||
|
||||
pushState(state: State, url: string | null): void {
|
||||
const id = generateId();
|
||||
this.log.info('pushState', state, url, id);
|
||||
window.history.pushState({ id }, '', this.removeHost(url));
|
||||
this.currentStateId = id;
|
||||
this.states.set(id, { state, scrollY: 0 });
|
||||
this.scrollTop = 0;
|
||||
}
|
||||
|
||||
beforeTransition(): void {
|
||||
const { state } = window.history;
|
||||
|
||||
if (!state) {
|
||||
return;
|
||||
}
|
||||
|
||||
const oldState = this.states.get(state.id);
|
||||
if (!oldState) {
|
||||
this.log.info(
|
||||
'current history state evicted from LRU, not saving scroll position',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const { scrollTop } = this;
|
||||
|
||||
this.states.set(state.id, {
|
||||
...oldState,
|
||||
scrollY: scrollTop,
|
||||
});
|
||||
|
||||
this.log.info('saving scroll position', scrollTop);
|
||||
}
|
||||
|
||||
private removeHost(url: string | null): string | undefined {
|
||||
if (!url) {
|
||||
this.log.warn('received null URL');
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: rdar://77982655 (Investigate router improvements): host mismatch?
|
||||
return removeHost(url);
|
||||
}
|
||||
|
||||
onPopState(
|
||||
listener: (url: string, state: State | undefined) => void,
|
||||
): void {
|
||||
window.addEventListener('popstate', (event: PopStateEvent): void => {
|
||||
this.currentStateId = event.state?.id;
|
||||
|
||||
if (!this.currentStateId) {
|
||||
this.log.warn(
|
||||
'encountered a null event.state.id in onPopState event: ',
|
||||
window.location.href,
|
||||
);
|
||||
}
|
||||
|
||||
this.log.info('popstate', this.states, this.currentStateId);
|
||||
const state = this.currentStateId
|
||||
? this.states.get(this.currentStateId)
|
||||
: undefined;
|
||||
listener(window.location.href, state?.state);
|
||||
|
||||
if (!state) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { scrollY } = state;
|
||||
|
||||
this.log.info('restoring scroll to', scrollY);
|
||||
|
||||
tryScroll(this.log, () => this.getScrollablePageElement(), scrollY);
|
||||
});
|
||||
}
|
||||
|
||||
private get scrollTop(): number {
|
||||
return this.getScrollablePageElement()?.scrollTop || 0;
|
||||
}
|
||||
|
||||
private set scrollTop(scrollTop: number) {
|
||||
const element = this.getScrollablePageElement();
|
||||
if (element) {
|
||||
element.scrollTop = scrollTop;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: rdar://77982655 (Investigate router improvements): offPopState?
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a (unique) id for storing in window.history.state.
|
||||
*
|
||||
* @return the generated ID
|
||||
*/
|
||||
function generateId(): Id {
|
||||
// The use of something random (and not say, an incrementing counter) is important
|
||||
// here. These states can survive refreshes so the IDs used must be globally unique
|
||||
// (and not just unique to the current page load).
|
||||
return generateUuid();
|
||||
}
|
||||
20
shared/utils/src/is-pojo.ts
Normal file
20
shared/utils/src/is-pojo.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* Determine if {@linkcode arg} is a Plain Old JavaScript Object.
|
||||
*
|
||||
* @see https://masteringjs.io/tutorials/fundamentals/pojo
|
||||
*
|
||||
* @param arg to test
|
||||
* @returns true if {@linkcode arg} is a POJO
|
||||
*/
|
||||
export function isPOJO(arg: unknown): arg is Record<string, unknown> {
|
||||
if (!arg || typeof arg !== 'object') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const proto = Object.getPrototypeOf(arg);
|
||||
if (!proto) {
|
||||
return true; // `Object.create(null)`
|
||||
}
|
||||
|
||||
return proto === Object.prototype;
|
||||
}
|
||||
109
shared/utils/src/launch/launch-client.ts
Normal file
109
shared/utils/src/launch/launch-client.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import { createClientLink } from './scheme';
|
||||
import type { Platform } from '../platform';
|
||||
|
||||
/**
|
||||
* Navigator for older Microsoft (MS) browsers like Internet Explorer.
|
||||
*/
|
||||
type MSNavigator = Navigator & {
|
||||
msLaunchUri: (
|
||||
href: string | URL,
|
||||
successCallback: () => void,
|
||||
failureCallback: () => void,
|
||||
) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if the given value is an MSNavigator.
|
||||
*/
|
||||
function isMSNavigator(value: Partial<MSNavigator>): value is MSNavigator {
|
||||
return typeof value?.msLaunchUri === 'function';
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for client launches.
|
||||
*/
|
||||
export type LaunchCallback = (result: {
|
||||
link: URL;
|
||||
success: boolean;
|
||||
}) => void | Promise<void>;
|
||||
|
||||
/**
|
||||
* Attempt to launch the native client for the given Web URL.
|
||||
*/
|
||||
export function launchClient(
|
||||
url: string | URL,
|
||||
platform: Platform,
|
||||
callback: LaunchCallback = () => {},
|
||||
): void {
|
||||
const { window, browser, os } = platform;
|
||||
|
||||
/** URL for opening the native application */
|
||||
const link = createClientLink(url, { platform });
|
||||
|
||||
// macOS Safari
|
||||
if (os.isMacOS && browser.isSafari) {
|
||||
launchOnMacOS(link, platform, callback);
|
||||
}
|
||||
// Proprietary msLaunchUri method (IE 10+ on Windows 8+)
|
||||
else if (isMSNavigator(platform.navigator)) {
|
||||
platform.navigator.msLaunchUri(
|
||||
String(link),
|
||||
() => callback({ link, success: true }),
|
||||
() => callback({ link, success: false }),
|
||||
);
|
||||
}
|
||||
// Other platforms
|
||||
else {
|
||||
try {
|
||||
// on iOS, Windows and Android simply opening the href works
|
||||
window!.top!.window.location.href = String(link);
|
||||
callback({ link, success: true });
|
||||
} catch (e) {
|
||||
// we know this is NOT installed
|
||||
callback({ link, success: false });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function launchOnMacOS(
|
||||
link: URL,
|
||||
platform: Platform,
|
||||
callback: LaunchCallback,
|
||||
): void {
|
||||
const { window } = platform;
|
||||
|
||||
if (typeof window === 'undefined') {
|
||||
callback({ link, success: false });
|
||||
return;
|
||||
}
|
||||
|
||||
/** Timer for blur fallback */
|
||||
let timer: number;
|
||||
|
||||
/** IFrame reference for opening the client link */
|
||||
let iframe: HTMLIFrameElement | undefined;
|
||||
|
||||
/** Cleanup function run after the client launch has been initiated */
|
||||
function finalize() {
|
||||
clearTimeout(timer);
|
||||
window!.removeEventListener('blur', finalize);
|
||||
if (iframe !== undefined) {
|
||||
window!.document.body.removeChild(iframe);
|
||||
}
|
||||
|
||||
callback({ link, success: true });
|
||||
}
|
||||
|
||||
// Add an iFrame window to the current document to open the URL
|
||||
iframe = window.document.createElement('iframe');
|
||||
iframe.id = 'launch-client-opener';
|
||||
iframe.style.display = 'none';
|
||||
window.document.body.appendChild(iframe);
|
||||
|
||||
// Redirect the iFrame to the client link to trigger it to open
|
||||
iframe.contentWindow!.location.href = String(link);
|
||||
|
||||
// Wait a tiny amount of time for the client launch to appear
|
||||
window.addEventListener('blur', finalize);
|
||||
timer = setTimeout(finalize, 50) as unknown as number;
|
||||
}
|
||||
339
shared/utils/src/launch/scheme.ts
Normal file
339
shared/utils/src/launch/scheme.ts
Normal file
@@ -0,0 +1,339 @@
|
||||
import { removeScheme } from '..';
|
||||
import { Platform } from '../platform';
|
||||
|
||||
/**
|
||||
* Check if the URL hostname matches the given value.
|
||||
*/
|
||||
const matchesHostName = (url: URL, hostName: string) =>
|
||||
url.hostname === hostName;
|
||||
|
||||
/**
|
||||
* Check if the URL `?app=xyz` search param matches the given value.
|
||||
*/
|
||||
const matchesAppName = (url: URL, appName: string) =>
|
||||
url.searchParams.get('app') === appName;
|
||||
|
||||
/**
|
||||
* Check if the URL `?mt=n` search param matches any of the given values.
|
||||
*/
|
||||
const matchesMediaType = (url: URL, mediaTypes: string[]) => {
|
||||
const mt = url.searchParams.get('mt');
|
||||
return mt ? mediaTypes.includes(mt) : false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if the URL pathname matches the given pattern.
|
||||
*/
|
||||
const matchesPathName = (url: URL, pattern: RegExp | string) =>
|
||||
new RegExp(pattern).test(url.pathname);
|
||||
|
||||
/**
|
||||
* Check if the URL is for Audiobooks
|
||||
*/
|
||||
const isAudiobookURL = (url: URL): boolean =>
|
||||
matchesAppName(url, 'audiobook') ||
|
||||
matchesMediaType(url, ['3']) ||
|
||||
matchesPathName(url, /\/(audiobook\/|viewAudiobook)/i);
|
||||
|
||||
/**
|
||||
* Check if the URL is for Books.
|
||||
*/
|
||||
const isBooksURL = (url: URL): boolean =>
|
||||
!isAudiobookURL(url) &&
|
||||
(matchesHostName(url, 'books.apple.com') ||
|
||||
matchesAppName(url, 'books') ||
|
||||
matchesMediaType(url, ['11', '13']) ||
|
||||
matchesPathName(url, '/book/'));
|
||||
|
||||
/**
|
||||
* Check if the URL is for Commerce.
|
||||
*/
|
||||
const isCommerceURL = (url: URL): boolean =>
|
||||
matchesHostName(url, 'finance-app.itunes.apple.com') ||
|
||||
matchesPathName(url, '/account/');
|
||||
|
||||
/**
|
||||
* Check if the URL is for a macOS App.
|
||||
*/
|
||||
const isMacAppURL = (url: URL): boolean =>
|
||||
matchesAppName(url, 'mac-app') ||
|
||||
matchesMediaType(url, ['12']) ||
|
||||
matchesPathName(url, '/mac-app/');
|
||||
|
||||
/**
|
||||
* Check if the URL is an AppStore Story.
|
||||
*/
|
||||
const isStoryURL = (url: URL): boolean =>
|
||||
matchesAppName(url, 'story') || matchesPathName(url, '/story/');
|
||||
|
||||
/**
|
||||
* Check if the URL is for Messages.
|
||||
*/
|
||||
const isMessagesURL = (url: URL): boolean => matchesAppName(url, 'messages');
|
||||
|
||||
/**
|
||||
* Check if the URL is for Music.
|
||||
*/
|
||||
const isMusicURL = (url: URL): boolean =>
|
||||
matchesHostName(url, 'music.apple.com') ||
|
||||
matchesAppName(url, 'music') ||
|
||||
matchesPathName(
|
||||
url,
|
||||
/\/(album|artist|playlist|station|curator|music-video)\//i,
|
||||
);
|
||||
|
||||
/**
|
||||
* Check if the URL is for Podcasts.
|
||||
*/
|
||||
const isPodcastsURL = (url: URL): boolean =>
|
||||
matchesHostName(url, 'podcasts.apple.com') ||
|
||||
matchesAppName(url, 'podcasts') ||
|
||||
matchesMediaType(url, ['2']) ||
|
||||
matchesPathName(url, '/podcast/');
|
||||
|
||||
/**
|
||||
* Check if the URL is for TV.
|
||||
*/
|
||||
const isTVURL = (url: URL): boolean =>
|
||||
matchesHostName(url, 'tv.apple.com') ||
|
||||
matchesPathName(
|
||||
url,
|
||||
/\/(episode|movie|movie-collection|show|season|sporting-event|person)\//i,
|
||||
);
|
||||
|
||||
/**
|
||||
* Check if the URL is for the Watch.
|
||||
*/
|
||||
const isWatchURL = (url: URL): boolean => matchesAppName(url, 'watch');
|
||||
|
||||
/**
|
||||
* Check if the URL is developer.apple.com related.
|
||||
*/
|
||||
const isDeveloperURL = (url: URL): boolean =>
|
||||
matchesAppName(url, 'developer') || matchesPathName(url, '/developer/');
|
||||
|
||||
/**
|
||||
* Check if the URL is for an app.
|
||||
*/
|
||||
const isAppsURL = (url: URL): boolean =>
|
||||
matchesMediaType(url, ['8']) && !isMessagesURL(url) && !isWatchURL(url);
|
||||
|
||||
/**
|
||||
* Function for identifying application schemes from web URLs.
|
||||
*/
|
||||
type SchemeIdentifier = (url: URL, platform: Platform) => boolean;
|
||||
|
||||
/**
|
||||
* List of schemes and functions to identify them based on a URL and Platform details.
|
||||
*
|
||||
* These schemes are derived from [Jingle Properties](https://github.pie.apple.com/amp-dev/Jingle/blob/6392929afb8540ac488315647992c3f46a9cc82f/MZConfig/Properties/apps/MZInit2/common.properties#L993).
|
||||
*
|
||||
* ```java
|
||||
* // <rdar://problem/66551318> iOS Bag: Move mobile-url-handlers to a property defined list
|
||||
* MZInit.iOS.acceptedUrlHandlers=("applenews", "applenewss", "applestore", "applestore-sec", "bridge", "com.apple.tv", "disneymoviesanywhere",\
|
||||
* "http", "https", "itms", "itmss", "itms-apps", "itms-appss", "itms-books", "itms-bookss", "itms-gc", "itms-gcs", "itms-itunesu",\
|
||||
* "itms-itunesus", "itms-podcast", "itms-podcasts", "itms-ui", "its-music", "its-musics", "its-news", "its-newss", "its-videos",\
|
||||
* "its-videoss", "itsradio", "livenation", "mailto", "message", "moviesanywhere", "music", "musics", "prefs", "shoebox")
|
||||
* ```
|
||||
*/
|
||||
const identifiers: [string, SchemeIdentifier, ...SchemeIdentifier[]][] = [
|
||||
[
|
||||
'itms-apps',
|
||||
(url, platform) =>
|
||||
platform.os.isIOS &&
|
||||
(isCommerceURL(url) ||
|
||||
isAppsURL(url) ||
|
||||
isStoryURL(url) ||
|
||||
isDeveloperURL(url)),
|
||||
],
|
||||
|
||||
// Watch app on mobile
|
||||
[
|
||||
'itms-watch',
|
||||
(url, platform) => platform.browser.isMobile && isWatchURL(url),
|
||||
],
|
||||
|
||||
// Messages app on mobile
|
||||
[
|
||||
'itms-messages',
|
||||
function (url: URL, platform: Platform) {
|
||||
return platform.browser.isMobile && isMessagesURL(url);
|
||||
},
|
||||
],
|
||||
|
||||
[
|
||||
'itms-books',
|
||||
(url, platform) =>
|
||||
platform.os.isMacOS &&
|
||||
platform.os.gte('10.15') &&
|
||||
isAudiobookURL(url),
|
||||
(url, _platform) => isBooksURL(url),
|
||||
],
|
||||
|
||||
// Music on Android
|
||||
[
|
||||
'apple-music',
|
||||
(url, platform) => platform.os.isAndroid && isMusicURL(url),
|
||||
],
|
||||
|
||||
// Music on iOS/macOS
|
||||
[
|
||||
'music',
|
||||
(url, platform) => platform.os.isIOS && isMusicURL(url),
|
||||
(url, platform) => {
|
||||
return (
|
||||
platform.os.isMacOS &&
|
||||
platform.os.gte('10.15') &&
|
||||
isMusicURL(url)
|
||||
);
|
||||
},
|
||||
],
|
||||
|
||||
// Podcasts on iOS
|
||||
[
|
||||
'itms-podcasts',
|
||||
(url, platform) => platform.os.isIOS && isPodcastsURL(url),
|
||||
],
|
||||
|
||||
// Podcasts on macOS
|
||||
[
|
||||
'podcasts',
|
||||
(url, platform) =>
|
||||
platform.os.isMacOS &&
|
||||
platform.os.gte('10.15') &&
|
||||
isPodcastsURL(url),
|
||||
],
|
||||
|
||||
// TV on iOS
|
||||
[
|
||||
'com.apple.tv',
|
||||
(url, platform) =>
|
||||
platform.os.isIOS && platform.os.gte('10.2') && isTVURL(url),
|
||||
],
|
||||
|
||||
// TV on macOS
|
||||
[
|
||||
'videos',
|
||||
(url: URL, platform: Platform) =>
|
||||
platform.os.isMacOS && platform.os.gte('10.15') && isTVURL(url),
|
||||
],
|
||||
|
||||
[
|
||||
'macappstore',
|
||||
(url, _platform) => isMacAppURL(url),
|
||||
(url, platform) =>
|
||||
platform.os.isMacOS &&
|
||||
platform.os.gte('10.15') &&
|
||||
isCommerceURL(url),
|
||||
|
||||
// Story and developer pages should launch Mac App Store on Mojave(10.14)+
|
||||
// <rdar://problem/46461633> Story page with ls=1 QP should attempt to open Mac App Store on Mojave +
|
||||
// rdar://81291713 (Star: https://apps.apple.com/developer/id463855590?ls=1 launches Music App)
|
||||
(url, platform) =>
|
||||
platform.os.isMacOS &&
|
||||
platform.os.gte('10.14') &&
|
||||
(isStoryURL(url) || isDeveloperURL(url)),
|
||||
],
|
||||
|
||||
// Catch All
|
||||
['itms', (_url, _platform) => true],
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the Scheme for attempting to open a platform native application.
|
||||
*
|
||||
* @see {@link https://en.wikipedia.org/wiki/Uniform_Resource_Identifier#Syntax}
|
||||
*/
|
||||
export function detectClientScheme(
|
||||
url: string | URL,
|
||||
options?: { platform?: Platform },
|
||||
): string {
|
||||
url = new URL(url);
|
||||
|
||||
// Assume that any URLs that don't have the http(s) scheme already have the
|
||||
// correct scheme assigned.
|
||||
if (/https?/i.test(url.protocol)) {
|
||||
const platform = options?.platform ?? Platform.detect();
|
||||
|
||||
for (const [scheme, ...fns] of identifiers) {
|
||||
for (const fn of fns) {
|
||||
if (fn(url, platform)) {
|
||||
return scheme;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// At this point something should have matched. If not just return the original
|
||||
// scheme and have the browser or system handle it.
|
||||
return normalizeScheme(url.protocol);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the given URL has an Apple specific Scheme.
|
||||
*
|
||||
* @example
|
||||
* ```javascript
|
||||
* hasAppleClientScheme('music://music.apple.com/browse') // => true
|
||||
* hasAppleClientScheme('https://music.apple.com/browse') // => false
|
||||
* ```
|
||||
*/
|
||||
export function hasAppleClientScheme(
|
||||
url: URL | string,
|
||||
_options?: { platform?: Platform },
|
||||
) {
|
||||
const pattern =
|
||||
/^(?:itms(?:-.*)?|macappstore|podcast|video|(?:apple-)?music)s?(:|$)/im;
|
||||
return pattern.test(new URL(url).protocol);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a link for attempting to open a platform native application based on a web URL.
|
||||
*
|
||||
* @example
|
||||
* ```javascript
|
||||
* createClientLink('https://music.apple.com/browse');
|
||||
* // => 'music://music.apple.com/browse'
|
||||
* ```
|
||||
*/
|
||||
export function createClientLink(
|
||||
url: string | URL,
|
||||
options?: { platform?: Platform },
|
||||
): URL {
|
||||
const link = new URL(url);
|
||||
|
||||
// Removes any development prefixes in order to correctly identify the scheme
|
||||
link.host = link.host.replace(
|
||||
/^(?:[^-]+[-.])?([^.]+)\.apple\.com/,
|
||||
'$1.apple.com',
|
||||
);
|
||||
|
||||
// Remove any port designation, this should not be present in application links
|
||||
link.port = '';
|
||||
|
||||
const scheme = detectClientScheme(link, {
|
||||
platform: options?.platform,
|
||||
});
|
||||
|
||||
// If the identified scheme is already assigned we want to leave the URL unmodified
|
||||
if (scheme === normalizeScheme(link.protocol)) {
|
||||
return new URL(url);
|
||||
}
|
||||
|
||||
return new URL(scheme + '://' + removeScheme(link));
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize a scheme value by removing any separators from it.
|
||||
*
|
||||
* @example
|
||||
* ```javascript
|
||||
* normalizeScheme('music') // => 'music'
|
||||
* normalizeScheme('TV') // => 'tv'
|
||||
* normalizeScheme('https:') // => 'https'
|
||||
* normalizeScheme('https://') // => 'https'
|
||||
* ```
|
||||
*/
|
||||
function normalizeScheme(value: string): string {
|
||||
return value.replace(/[:]+$/, '').toLowerCase();
|
||||
}
|
||||
60
shared/utils/src/lru-map.ts
Normal file
60
shared/utils/src/lru-map.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* LRU Map implementation storing key/values up to a provided size limit. Beyond that
|
||||
* size limit, the least recently used entry is evicted.
|
||||
*
|
||||
* @see https://github.pie.apple.com/isao/lru-map
|
||||
*/
|
||||
export class LruMap<K, V> extends Map<K, V> {
|
||||
private sizeLimit: number;
|
||||
|
||||
constructor(sizeLimit: number) {
|
||||
super();
|
||||
this.setSizeLimit(sizeLimit);
|
||||
// Needed to convince TS that this is set (it's actually handled by setSizeLimit)
|
||||
this.sizeLimit = sizeLimit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a value from the map with a given key.
|
||||
* @param key The key for the entry
|
||||
* @return value The entry's value (or undefined if non existent)
|
||||
*/
|
||||
get(key: K): V | undefined {
|
||||
let value: V | undefined;
|
||||
|
||||
if (this.has(key)) {
|
||||
value = super.get(key);
|
||||
|
||||
// Map entries are always in insertion order. So
|
||||
// readding, pushes this entry to the top of the LRU.
|
||||
this.delete(key);
|
||||
super.set(key, value!);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
set(key: K, value: V): this {
|
||||
super.set(key, value);
|
||||
this.prune();
|
||||
return this;
|
||||
}
|
||||
|
||||
setSizeLimit(newSizeLimit: number): void {
|
||||
if (newSizeLimit < 0 || !isFinite(newSizeLimit)) {
|
||||
throw new Error(
|
||||
`setSizeLimit expects finite positive number, got: ${newSizeLimit}`,
|
||||
);
|
||||
}
|
||||
|
||||
this.sizeLimit = newSizeLimit;
|
||||
this.prune();
|
||||
}
|
||||
|
||||
private prune(): void {
|
||||
while (this.size > this.sizeLimit) {
|
||||
const leastRecentlyUsedKey = this.keys().next().value;
|
||||
this.delete(leastRecentlyUsedKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
18
shared/utils/src/object-from-entries.ts
Normal file
18
shared/utils/src/object-from-entries.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
// TODO: rdar://78109780 (Update to Node 16)
|
||||
/**
|
||||
* Create an object from an iterable of key/value pairs.
|
||||
*
|
||||
* @param entries The key value pairs (ex. [['a', 1], ['b', 2]])
|
||||
* @return The created object
|
||||
*/
|
||||
export function fromEntries<V>(entries: Iterable<readonly [PropertyKey, V]>): {
|
||||
[k: string]: V;
|
||||
} {
|
||||
const result: Record<PropertyKey, V> = {};
|
||||
|
||||
for (const [key, value] of entries) {
|
||||
result[key] = value;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
22
shared/utils/src/optional.ts
Normal file
22
shared/utils/src/optional.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
export type Optional<T> = T | None;
|
||||
export type None = null | undefined;
|
||||
|
||||
/**
|
||||
* Determine if an optional value is present.
|
||||
*
|
||||
* @param optional value
|
||||
* @return true if present, false otherwise
|
||||
*/
|
||||
export function isSome<T>(optional: Optional<T>): optional is T {
|
||||
return optional !== null && optional !== undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if an optional value is not present.
|
||||
*
|
||||
* @param optional value
|
||||
* @return true if not present, false otherwise
|
||||
*/
|
||||
export function isNone<T>(optional: Optional<T>): optional is None {
|
||||
return optional === null || optional === undefined;
|
||||
}
|
||||
249
shared/utils/src/platform.ts
Normal file
249
shared/utils/src/platform.ts
Normal file
@@ -0,0 +1,249 @@
|
||||
import {
|
||||
parseUserAgent,
|
||||
flagsExtension,
|
||||
compareExtension,
|
||||
} from '@amp/runtime-detect';
|
||||
import { launchClient, type LaunchCallback } from './launch/launch-client';
|
||||
|
||||
type NavigatorLike = {
|
||||
userAgent: string;
|
||||
maxTouchPoints?: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Detect a Platform descriptor from the browsers user agent.
|
||||
*/
|
||||
function detectDescriptor(options?: {
|
||||
window?: Window;
|
||||
navigator?: NavigatorLike;
|
||||
}) {
|
||||
const defaultNavigator: NavigatorLike =
|
||||
typeof options?.window?.navigator !== 'undefined'
|
||||
? options.window.navigator
|
||||
: {
|
||||
userAgent: '',
|
||||
maxTouchPoints: 0,
|
||||
};
|
||||
|
||||
return parseUserAgent(options?.navigator ?? defaultNavigator, {
|
||||
extensions: [flagsExtension, compareExtension],
|
||||
});
|
||||
}
|
||||
|
||||
export type PlatformDescriptor = ReturnType<typeof detectDescriptor>;
|
||||
|
||||
export class Platform {
|
||||
static detect(
|
||||
this: typeof Platform,
|
||||
options?: { window?: Window; navigator?: NavigatorLike },
|
||||
) {
|
||||
const window = options?.window ?? globalThis?.window;
|
||||
return new this({
|
||||
window: window,
|
||||
descriptor: detectDescriptor({
|
||||
window: window,
|
||||
navigator: options?.navigator,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Descriptor from detecting platform data.
|
||||
*/
|
||||
readonly descriptor: PlatformDescriptor;
|
||||
|
||||
/**
|
||||
* Navigator value used to create the platform descriptor.
|
||||
*/
|
||||
readonly navigator: NavigatorLike;
|
||||
|
||||
/**
|
||||
* Reference to the platform Window object. This might be `undefined` in some
|
||||
* environments.
|
||||
*/
|
||||
readonly window: Window | undefined;
|
||||
|
||||
/**
|
||||
* User Agent string the platform descriptor was parsed from.
|
||||
*/
|
||||
readonly ua: string;
|
||||
|
||||
/**
|
||||
* Browser descriptor for the Platform.
|
||||
*/
|
||||
readonly browser: PlatformDescriptor['browser'];
|
||||
|
||||
/**
|
||||
* Browser Engine descriptor for the Platform.
|
||||
*/
|
||||
readonly engine: PlatformDescriptor['engine'];
|
||||
|
||||
/**
|
||||
* Operating System descriptor for the Platform.
|
||||
*/
|
||||
readonly os: PlatformDescriptor['os'];
|
||||
|
||||
constructor(config: {
|
||||
descriptor: PlatformDescriptor;
|
||||
window?: Window;
|
||||
navigator?: NavigatorLike;
|
||||
}) {
|
||||
const { descriptor } = config;
|
||||
this.descriptor = descriptor;
|
||||
this.navigator = config.navigator ?? descriptor.navigator;
|
||||
this.window = config.window;
|
||||
|
||||
this.ua = descriptor.ua;
|
||||
this.browser = descriptor.browser;
|
||||
this.engine = descriptor.engine;
|
||||
this.os = descriptor.os;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if Apple native applications can be opened on the Platform.
|
||||
*/
|
||||
canOpenNative(): boolean {
|
||||
return this.ismacOS() || this.isiOS();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the Platform is running a mobile browser.
|
||||
*/
|
||||
isMobile(): boolean {
|
||||
return this.browser.isMobile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the Platform registers as running the Android operating system.
|
||||
*/
|
||||
isAndroid(): boolean {
|
||||
return this.os.isAndroid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the Platform registers as running the iOS operating system.
|
||||
*/
|
||||
isiOS(): boolean {
|
||||
return this.os.isIOS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the Platform registers as running the iPadOS operating system.
|
||||
*/
|
||||
isiPadOS(): boolean {
|
||||
return this.os.isIPadOS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the Platform registers as running the macOS operating system.
|
||||
*/
|
||||
ismacOS(): boolean {
|
||||
return this.os.isMacOS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the Platform registers as running the Windows operating system.
|
||||
*/
|
||||
isWindows(): boolean {
|
||||
return this.os.isWindows;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the Platform registers as running a Linux operating system.
|
||||
*/
|
||||
isLinux(): boolean {
|
||||
return this.os.isLinux;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the Platform is running the Apple Safari browser.
|
||||
*/
|
||||
isSafari(): boolean {
|
||||
return this.browser.isSafari;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the Platform is running the Google Chrome browser.
|
||||
*/
|
||||
isChrome(): boolean {
|
||||
return this.browser.isChrome;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the Platform is running the Mozilla Firefox browser.
|
||||
*/
|
||||
isFirefox(): boolean {
|
||||
return this.browser.isFirefox;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the Platform is running the Microsoft Edge browser.
|
||||
*/
|
||||
isEdge(): boolean {
|
||||
return this.browser.isEdge;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get name for the Platform browser.
|
||||
* @deprecated Use `platform.browser.name` directly
|
||||
*/
|
||||
clientName(): string {
|
||||
return this.browser.name[0].toUpperCase() + this.browser.name.slice(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Platform browser major version number.
|
||||
* @deprecated Use `platform.browser.major` directly
|
||||
*/
|
||||
majorVersion(): number {
|
||||
return this.browser.major ?? 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Platform browser minor version number.
|
||||
* @deprecated Use `platform.browser.minor` directly
|
||||
*/
|
||||
minorVersion(): number {
|
||||
return this.browser.minor ?? 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the name for the Platform operating system.
|
||||
* @deprecated Use `platform.os.name` directly
|
||||
*/
|
||||
osName(): string {
|
||||
return this.os.name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to launch a native client for the given web URL.
|
||||
*
|
||||
* The callback is called with a report if the attempt was successful.
|
||||
*
|
||||
* @example
|
||||
* ```javascript
|
||||
* platform.launchClient(
|
||||
* 'https://music.apple.com/browse',
|
||||
* function ({ link, success }) {
|
||||
* if (success) {
|
||||
* console.log(`Opened client with ${link}`);
|
||||
* } else {
|
||||
* console.log(`Failed to open client with ${link}`);
|
||||
* }
|
||||
* }
|
||||
* );
|
||||
* ```
|
||||
*/
|
||||
launchClient(url: string, callback?: LaunchCallback): void {
|
||||
launchClient(url, this, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the platform has full support for playing encrypted HLS content.
|
||||
*/
|
||||
hasEncryptedPlaybackSupport(): boolean {
|
||||
return !this.os.isIOS || this.os.gte('17.5');
|
||||
}
|
||||
}
|
||||
|
||||
export const platform = Platform.detect();
|
||||
65
shared/utils/src/try-scroll.ts
Normal file
65
shared/utils/src/try-scroll.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import type { Logger } from '@amp/web-apps-logger';
|
||||
export interface ScrollableElement {
|
||||
scrollTop: number;
|
||||
scrollHeight: number;
|
||||
offsetHeight: number;
|
||||
}
|
||||
|
||||
// Global is okay here as this only runs in the browser
|
||||
let nextTry: number | null = null;
|
||||
|
||||
export function tryScroll(
|
||||
log: Logger,
|
||||
getScrollablePageElement: Function,
|
||||
scrollY: number,
|
||||
): void {
|
||||
let tries = 0;
|
||||
|
||||
if (nextTry !== null) {
|
||||
window.cancelAnimationFrame(nextTry);
|
||||
}
|
||||
|
||||
nextTry = window.requestAnimationFrame(function doNextTry() {
|
||||
// At 16ms per frame, this is 1600ms
|
||||
// See: https://github.com/DockYard/ember-router-scroll/blob/2f17728f/addon/services/router-scroll.js#L56
|
||||
if (++tries >= 100) {
|
||||
log.warn("wasn't able to restore scroll within 100 frames");
|
||||
nextTry = null;
|
||||
return;
|
||||
}
|
||||
|
||||
let element = getScrollablePageElement();
|
||||
if (!element) {
|
||||
log.warn(
|
||||
'could not restore scroll: the scrollable element is missing',
|
||||
);
|
||||
return;
|
||||
}
|
||||
const { scrollHeight, offsetHeight } = element;
|
||||
|
||||
// Only scroll once we're able to get a full screen of content when
|
||||
// scrollTop is set to scrollY
|
||||
//
|
||||
// +16 is a bit of a fudge factor to count for imperfections in
|
||||
// features like lazy loading. If the scroll position to restore is
|
||||
// the very bottom of the page, then scrollY + offsetHeight must be
|
||||
// exactly scrollHeight. But if lazy loading components (for example)
|
||||
// cause the page to grow by a few pixels, then this will never hold.
|
||||
// Thus, we fudge by a few pixels to be more forgiving in this scenario.
|
||||
const canScroll = scrollY + offsetHeight <= scrollHeight + 16;
|
||||
|
||||
if (!canScroll) {
|
||||
log.info('page is not tall enough for scroll yet', {
|
||||
scrollHeight,
|
||||
offsetHeight,
|
||||
});
|
||||
|
||||
nextTry = window.requestAnimationFrame(doNextTry);
|
||||
return;
|
||||
}
|
||||
|
||||
element.scrollTop = scrollY;
|
||||
log.info('scroll restored to', scrollY);
|
||||
nextTry = null;
|
||||
});
|
||||
}
|
||||
90
shared/utils/src/url.ts
Normal file
90
shared/utils/src/url.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* Remove the scheme and separators from the given URL.
|
||||
*
|
||||
* @example
|
||||
* ```javascript
|
||||
* removeScheme('https://music.apple.com/browse') // => 'music.apple.com/browse'
|
||||
* removeScheme('apple-music://music.apple.com/browse') // => 'music.apple.com/browse'
|
||||
* removeScheme('music.apple.com/browse') // => 'music.apple.com/browse'
|
||||
* ```
|
||||
*/
|
||||
export function removeScheme(
|
||||
url: string | URL | null | undefined,
|
||||
): string | undefined {
|
||||
if (url === null || url === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return String(url).replace(/^((?:[^:]*:[/]{0,2})|(?::?\/\/))/i, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip scheme and host (hostname + port) from a URL, leaving just the path, query
|
||||
* params, and hash.
|
||||
*
|
||||
* @param {string} url The URL possibly containing a host
|
||||
* @returns {string} hostlessUrl The url without its host
|
||||
*/
|
||||
export function removeHost(
|
||||
url: string | URL | null | undefined,
|
||||
): string | undefined {
|
||||
return removeScheme(url)?.replace(/^([^/]*)/i, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip query params and fragment from a URL.
|
||||
*/
|
||||
export function removeQueryParams(
|
||||
url: string | URL | undefined,
|
||||
): string | undefined {
|
||||
if (url === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const value = String(url);
|
||||
const splitIndex = value.indexOf('?');
|
||||
return splitIndex >= 0 ? value.slice(0, splitIndex) : value;
|
||||
}
|
||||
|
||||
export function getBaseUrl(): string {
|
||||
const currentUrl = new URL(window.location.href);
|
||||
return `${currentUrl.protocol}//${currentUrl.host}`;
|
||||
}
|
||||
|
||||
export function buildUrl(props: {
|
||||
protocol?: string;
|
||||
hostname: string;
|
||||
pathname?: string | string[];
|
||||
queryParams?: string | Record<string, string>;
|
||||
hash?: string;
|
||||
}): URL {
|
||||
const {
|
||||
hostname,
|
||||
pathname = '/',
|
||||
queryParams = {},
|
||||
protocol = 'https',
|
||||
hash = '',
|
||||
} = props;
|
||||
|
||||
// Base URL with domain
|
||||
const url = new URL(protocol + '://' + removeScheme(hostname));
|
||||
|
||||
// URL path
|
||||
url.pathname = Array.isArray(pathname)
|
||||
? '/' + pathname.map(encodeURIComponent).join('/').replace(/[/]+/, '/')
|
||||
: pathname;
|
||||
|
||||
// URL search (a.k.a. queryParams)
|
||||
if (typeof queryParams === 'string') {
|
||||
url.search = queryParams;
|
||||
} else {
|
||||
for (const [key, value] of Object.entries(queryParams)) {
|
||||
url.searchParams.set(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
// URL hash
|
||||
url.hash = hash;
|
||||
|
||||
return url;
|
||||
}
|
||||
22
shared/utils/src/uuid.ts
Normal file
22
shared/utils/src/uuid.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* Generate a variant 1 UUIDv4.
|
||||
*
|
||||
* @return the UUID
|
||||
*/
|
||||
export function generateUuid(): string {
|
||||
return 'xxxxxxxx-xxxx-4xxx-Vxxx-xxxxxxxxxxxx'.replace(
|
||||
/[xV]/g,
|
||||
(placeholder) => {
|
||||
let nibble = (Math.random() * 16) | 0;
|
||||
|
||||
if (placeholder === 'V') {
|
||||
// Per RFC, the two MSB of byte 8 must be 0b10 (0x8).
|
||||
// 0x3 (0b11) masks out the bottom two bits.
|
||||
// See: https://tools.ietf.org/html/rfc4122.html#section-4.1.1
|
||||
nibble = (nibble & 0x3) | 0x8;
|
||||
}
|
||||
|
||||
return nibble.toString(16);
|
||||
},
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user