Cache and UI fixes

main
Elbert Alias 3 years ago
parent 7cdedba035
commit 11db690209

@ -13,6 +13,7 @@
* { * {
box-sizing: border-box; box-sizing: border-box;
scrollbar-width: none;
} }
body { body {
@ -23,10 +24,13 @@ body {
font-size: .9rem; font-size: .9rem;
line-height: 1.5rem; line-height: 1.5rem;
margin: 0; margin: 0;
width: 34rem;
overflow-x: hidden; overflow-x: hidden;
} }
.body__popup {
width: 34rem;
}
a, a:focus, a:hover { a, a:focus, a:hover {
color: var(--color-primary); color: var(--color-primary);
outline: none; outline: none;

@ -11,7 +11,7 @@
<script src="../js/utils.js"></script> <script src="../js/utils.js"></script>
<script src="../js/options.js"></script> <script src="../js/options.js"></script>
</head> </head>
<body> <body class="body__options">
<div class="options"> <div class="options">
<button data-i18n="clearCache" class="options__cache">&nbsp;</button> <button data-i18n="clearCache" class="options__cache">&nbsp;</button>

@ -11,7 +11,7 @@
<script src="../js/utils.js"></script> <script src="../js/utils.js"></script>
<script src="../js/popup.js"></script> <script src="../js/popup.js"></script>
</head> </head>
<body> <body class="body__popup">
<div class="popup"> <div class="popup">
<div class="header"> <div class="header">
<a href="https://www.wappalyzer.com/?utm_source=popup&utm_medium=extension&utm_campaign=wappalyzer" class="header__link"> <a href="https://www.wappalyzer.com/?utm_source=popup&utm_medium=extension&utm_campaign=wappalyzer" class="header__link">
@ -46,8 +46,8 @@
</div> </div>
<div class="tabs"> <div class="tabs">
<div class="tab tab--technologies tab--active" data-i18n="tabTechnologies"></div> <div class="tab tab--technologies tab--active" data-i18n="tabTechnologies">&nbsp;</div>
<div class="tab tab--pro" data-i18n="tabPro"></div> <div class="tab tab--pro" data-i18n="tabPro">&nbsp;</div>
<div class="credits credits--hidden"> <div class="credits credits--hidden">
<span data-i18n="creditBalance">&nbsp;</span> <span data-i18n="creditBalance">&nbsp;</span>
@ -57,7 +57,7 @@
</div> </div>
<div class="tab-item"> <div class="tab-item">
<div class="empty"> <div class="empty empty--hidden">
<div class="empty__text" data-i18n="noAppsDetected">&nbsp;</div> <div class="empty__text" data-i18n="noAppsDetected">&nbsp;</div>
<div class="ttt-game"> <div class="ttt-game">
@ -103,7 +103,7 @@
</div> </div>
</div> </div>
<div class="detections detections--hidden"></div> <div class="detections"></div>
<div class="terms terms--hidden"> <div class="terms terms--hidden">
<div class="terms__content" data-i18n="termsContent"></div> <div class="terms__content" data-i18n="termsContent"></div>

@ -74,12 +74,12 @@ const Driver = {
chrome.tabs.onRemoved.addListener((id) => delete Driver.cache.tabs[id]) chrome.tabs.onRemoved.addListener((id) => delete Driver.cache.tabs[id])
chrome.tabs.onUpdated.addListener(async (id, { status }) => { chrome.tabs.onUpdated.addListener(async (id, { status, url }) => {
delete Driver.cache.tabs[id]
if (status === 'complete') { if (status === 'complete') {
const { url } = await promisify(chrome.tabs, 'get', id) ;({ url } = await promisify(chrome.tabs, 'get', id))
}
if (url) {
const { hostname } = new URL(url) const { hostname } = new URL(url)
const cache = Driver.cache.hostnames[hostname] const cache = Driver.cache.hostnames[hostname]
@ -714,6 +714,8 @@ const Driver = {
({ cached }) => showCached || cached === false ({ cached }) => showCached || cached === false
) )
Driver.log({ id, url, resolved })
await Driver.setIcon(url, resolved) await Driver.setIcon(url, resolved)
return resolved return resolved

@ -42,20 +42,17 @@ const footers = [
] ]
function setDisabledDomain(enabled) { function setDisabledDomain(enabled) {
const el = {
headerSwitchEnabled: document.querySelector('.header__switch--enabled'),
headerSwitchDisabled: document.querySelector('.header__switch--disabled'),
}
if (enabled) { if (enabled) {
document el.headerSwitchEnabled.classList.add('header__switch--hidden')
.querySelector('.header__switch--enabled') el.headerSwitchDisabled.classList.remove('header__switch--hidden')
.classList.add('header__switch--hidden')
document
.querySelector('.header__switch--disabled')
.classList.remove('header__switch--hidden')
} else { } else {
document el.headerSwitchEnabled.classList.remove('header__switch--hidden')
.querySelector('.header__switch--enabled') el.headerSwitchDisabled.classList.add('header__switch--hidden')
.classList.remove('header__switch--hidden')
document
.querySelector('.header__switch--disabled')
.classList.add('header__switch--hidden')
} }
} }
@ -64,10 +61,39 @@ const Popup = {
* Initialise popup * Initialise popup
*/ */
async init() { async init() {
const el = {
body: document.body,
terms: document.querySelector('.terms'),
detections: document.querySelector('.detections'),
empty: document.querySelector('.empty'),
footer: document.querySelector('.footer'),
tabPro: document.querySelector('.tab--pro'),
termsButtonAccept: document.querySelector('.terms__button--accept'),
termsButtonDecline: document.querySelector('.terms__button--decline'),
headerSwitches: document.querySelectorAll('.header__switch'),
headerSwitchEnabled: document.querySelector('.header__switch--enabled'),
headerSwitchDisabled: document.querySelector('.header__switch--disabled'),
proConfigureApiKey: document.querySelector('.pro-configure__apikey'),
proConfigureSave: document.querySelector('.pro-configure__save'),
headerSettings: document.querySelector('.header__settings'),
headerThemes: document.querySelectorAll('.header__theme'),
headerThemeLight: document.querySelector('.header__theme--light'),
headerThemeDark: document.querySelector('.header__theme--dark'),
templates: document.querySelectorAll('[data-template]'),
tabs: document.querySelectorAll('.tab'),
tabItems: document.querySelectorAll('.tab-item'),
credits: document.querySelector('.credits'),
footerHeadingText: document.querySelector('.footer__heading-text'),
footerContentBody: document.querySelector('.footer__content-body'),
footerButtonText: document.querySelector('.footer .button__text'),
footerButtonLink: document.querySelector('.footer .button__link'),
footerToggleClose: document.querySelector('.footer__toggle--close'),
footerToggleOpen: document.querySelector('.footer__toggle--open'),
footerHeading: document.querySelector('.footer__heading'),
}
// Templates // Templates
Popup.templates = Array.from( Popup.templates = Array.from(el.templates).reduce((templates, template) => {
document.querySelectorAll('[data-template]')
).reduce((templates, template) => {
templates[template.dataset.template] = template.cloneNode(true) templates[template.dataset.template] = template.cloneNode(true)
template.remove() template.remove()
@ -79,7 +105,7 @@ const Popup = {
const dynamicIcon = await getOption('dynamicIcon', false) const dynamicIcon = await getOption('dynamicIcon', false)
if (dynamicIcon) { if (dynamicIcon) {
document.querySelector('body').classList.add('dynamic-icon') el.body.classList.add('dynamic-icon')
} }
// Disabled domains // Disabled domains
@ -89,13 +115,9 @@ const Popup = {
const theme = await getOption('theme', 'light') const theme = await getOption('theme', 'light')
if (theme === 'dark') { if (theme === 'dark') {
document.querySelector('body').classList.add('dark') el.body.classList.add('dark')
document el.headerThemeLight.classList.remove('header__icon--hidden')
.querySelector('.header__theme--light') el.headerThemeDark.classList.add('header__icon--hidden')
.classList.remove('header__icon--hidden')
document
.querySelector('.header__theme--dark')
.classList.add('header__icon--hidden')
} }
// Terms // Terms
@ -103,44 +125,39 @@ const Popup = {
agent === 'chrome' || (await getOption('termsAccepted', false)) agent === 'chrome' || (await getOption('termsAccepted', false))
if (termsAccepted) { if (termsAccepted) {
document.querySelector('.terms').classList.add('terms--hidden') el.terms.classList.add('terms--hidden')
document.querySelector('.empty').classList.remove('empty--hidden')
Popup.onGetDetections(await Popup.driver('getDetections')) Popup.driver('getDetections').then(Popup.onGetDetections.bind(this))
} else { } else {
document.querySelector('.terms').classList.remove('terms--hidden') el.terms.classList.remove('terms--hidden')
document.querySelector('.detections').classList.add('detections--hidden') el.detections.classList.add('detections--hidden')
document.querySelector('.empty').classList.add('empty--hidden') el.footer.classList.add('footer--hidden')
document.querySelector('.footer').classList.add('footer--hidden') el.tabPro.classList.add('tab--disabled')
document.querySelector('.tab--pro').classList.add('tab--disabled')
document
.querySelector('.terms__button--accept')
.addEventListener('click', async () => {
await setOption('termsAccepted', true)
await setOption('tracking', true)
document.querySelector('.terms').classList.add('terms--hidden') el.termsButtonAccept.addEventListener('click', async () => {
document.querySelector('.empty').classList.remove('empty--hidden') await setOption('termsAccepted', true)
document.querySelector('.footer').classList.remove('footer--hidden') await setOption('tracking', true)
document.querySelector('.tab--pro').classList.remove('tab--disabled')
Popup.onGetDetections(await Popup.driver('getDetections')) el.terms.classList.add('terms--hidden')
}) el.footer.classList.remove('footer--hidden')
el.tabPro.classList.remove('tab--disabled')
Popup.driver('getDetections').then(Popup.onGetDetections.bind(this))
})
document el.termsButtonDecline('.terms__button--decline').addEventListener(
.querySelector('.terms__button--decline') 'click',
.addEventListener('click', async () => { async () => {
await setOption('termsAccepted', true) await setOption('termsAccepted', true)
await setOption('tracking', false) await setOption('tracking', false)
document.querySelector('.terms').classList.add('terms--hidden') el.terms.classList.add('terms--hidden')
document.querySelector('.empty').classList.remove('empty--hidden') el.footer.classList.remove('footer--hidden')
document.querySelector('.footer').classList.remove('footer--hidden') el.tabPro.classList.remove('tab--disabled')
document.querySelector('.tab--pro').classList.remove('tab--disabled')
Popup.onGetDetections(await Popup.driver('getDetections')) Popup.driver('getDetections').then(Popup.onGetDetections.bind(this))
}) }
)
} }
let url let url
@ -158,76 +175,61 @@ const Popup = {
setDisabledDomain(disabledDomains.includes(hostname)) setDisabledDomain(disabledDomains.includes(hostname))
document el.headerSwitchDisabled.addEventListener('click', async () => {
.querySelector('.header__switch--disabled') disabledDomains = disabledDomains.filter(
.addEventListener('click', async () => { (_hostname) => _hostname !== hostname
disabledDomains = disabledDomains.filter( )
(_hostname) => _hostname !== hostname
)
await setOption('disabledDomains', disabledDomains) await setOption('disabledDomains', disabledDomains)
setDisabledDomain(false) setDisabledDomain(false)
Popup.onGetDetections(await Popup.driver('getDetections')) Popup.driver('getDetections').then(Popup.onGetDetections.bind(this))
}) })
document el.headerSwitchEnabled.addEventListener('click', async () => {
.querySelector('.header__switch--enabled') disabledDomains.push(hostname)
.addEventListener('click', async () => {
disabledDomains.push(hostname)
await setOption('disabledDomains', disabledDomains) await setOption('disabledDomains', disabledDomains)
setDisabledDomain(true) setDisabledDomain(true)
Popup.onGetDetections(await Popup.driver('getDetections')) Popup.driver('getDetections').then(Popup.onGetDetections.bind(this))
}) })
} else { } else {
for (const el of document.querySelectorAll('.header__switch')) { for (const headerSwitch of el.headerSwitches) {
el.classList.add('header__switch--hidden') headerSwitch.classList.add('header__switch--hidden')
} }
document.querySelector('.tab--pro').classList.add('tab--disabled') el.tabPro.classList.add('tab--disabled')
} }
} }
// PRO configuration // PRO configuration
const apiKey = document.querySelector('.pro-configure__apikey') el.proConfigureApiKey.value = await getOption('apiKey', '')
apiKey.value = await getOption('apiKey', '')
document el.proConfigureSave.addEventListener('click', async (event) => {
.querySelector('.pro-configure__save') await setOption('apiKey', el.proConfigureApiKey.value)
.addEventListener('click', async (event) => {
await setOption(
'apiKey',
document.querySelector('.pro-configure__apikey').value
)
await Popup.getPro(url) await Popup.getPro(url)
}) })
// Header // Header
document el.headerSettings.addEventListener('click', () =>
.querySelector('.header__settings') chrome.runtime.openOptionsPage()
.addEventListener('click', () => chrome.runtime.openOptionsPage()) )
// Theme // Theme
const body = document.querySelector('body') el.headerThemes.forEach((headerTheme) =>
const dark = document.querySelector('.header__theme--dark') headerTheme.addEventListener('click', async () => {
const light = document.querySelector('.header__theme--light')
document.querySelectorAll('.header__theme').forEach((el) =>
el.addEventListener('click', async () => {
const theme = await getOption('theme', 'light') const theme = await getOption('theme', 'light')
body.classList[theme === 'dark' ? 'remove' : 'add']('dark') el.body.classList[theme === 'dark' ? 'remove' : 'add']('dark')
body.classList[theme === 'dark' ? 'add' : 'remove']('light') el.body.classList[theme === 'dark' ? 'add' : 'remove']('light')
dark.classList[theme === 'dark' ? 'remove' : 'add']( el.headerThemeDark.classList[theme === 'dark' ? 'remove' : 'add'](
'header__icon--hidden' 'header__icon--hidden'
) )
light.classList[theme === 'dark' ? 'add' : 'remove']( el.headerThemeLight.classList[theme === 'dark' ? 'add' : 'remove'](
'header__icon--hidden' 'header__icon--hidden'
) )
@ -236,19 +238,16 @@ const Popup = {
) )
// Tabs // Tabs
const tabHeadings = Array.from(document.querySelectorAll('.tab')) el.tabs.forEach((tab, index) => {
const tabItems = Array.from(document.querySelectorAll('.tab-item'))
const credits = document.querySelector('.credits')
tabHeadings.forEach((tab, index) => {
tab.addEventListener('click', async () => { tab.addEventListener('click', async () => {
tabHeadings.forEach((tab) => tab.classList.remove('tab--active')) el.tabs.forEach((tab) => tab.classList.remove('tab--active'))
tabItems.forEach((item) => item.classList.add('tab-item--hidden')) el.tabItems.forEach((item) => item.classList.add('tab-item--hidden'))
tab.classList.add('tab--active') tab.classList.add('tab--active')
tabItems[index].classList.remove('tab-item--hidden') el.tabItems[index].classList.remove('tab-item--hidden')
credits.classList.add('credits--hidden') el.credits.classList.add('credits--hidden')
el.footer.classList.remove('footer--hidden')
if (tab.classList.contains('tab--pro')) { if (tab.classList.contains('tab--pro')) {
await Popup.getPro(url) await Popup.getPro(url)
@ -264,39 +263,32 @@ const Popup = {
: Math.round(Math.random() * (footers.length - 1)) : Math.round(Math.random() * (footers.length - 1))
] ]
document.querySelector('.footer__heading-text').textContent = item.heading el.footerHeadingText.textContent = item.heading
document.querySelector('.footer__content-body').textContent = item.body el.footerContentBody.textContent = item.body
document.querySelector('.footer .button__text').textContent = el.footerButtonText.textContent = item.buttonText
item.buttonText el.footerButtonLink.href = item.buttonLink
document.querySelector('.footer .button__link').href = item.buttonLink
const collapseFooter = await getOption('collapseFooter', false) const collapseFooter = await getOption('collapseFooter', false)
const footer = document.querySelector('.footer')
const footerClose = document.querySelector('.footer__toggle--close')
const footerOpen = document.querySelector('.footer__toggle--open')
if (collapseFooter) { if (collapseFooter) {
footer.classList.add('footer--collapsed') el.footer.classList.add('footer--collapsed')
footerClose.classList.add('footer__toggle--hidden') el.footerToggleClose.classList.add('footer__toggle--hidden')
footerOpen.classList.remove('footer__toggle--hidden') el.footerToggleOpen.classList.remove('footer__toggle--hidden')
} }
document el.footerHeading.addEventListener('click', async () => {
.querySelector('.footer__heading') const collapsed = el.footer.classList.contains('footer--collapsed')
.addEventListener('click', async () => {
const collapsed = footer.classList.contains('footer--collapsed')
footer.classList[collapsed ? 'remove' : 'add']('footer--collapsed') el.footer.classList[collapsed ? 'remove' : 'add']('footer--collapsed')
footerClose.classList[collapsed ? 'remove' : 'add']( el.footerToggleClose.classList[collapsed ? 'remove' : 'add'](
'footer__toggle--hidden' 'footer__toggle--hidden'
) )
footerOpen.classList[collapsed ? 'add' : 'remove']( el.footerToggleOpen.classList[collapsed ? 'add' : 'remove'](
'footer__toggle--hidden' 'footer__toggle--hidden'
) )
await setOption('collapseFooter', !collapsed) await setOption('collapseFooter', !collapsed)
}) })
Array.from(document.querySelectorAll('a')).forEach((a) => Array.from(document.querySelectorAll('a')).forEach((a) =>
a.addEventListener('click', (event) => { a.addEventListener('click', (event) => {
@ -353,25 +345,27 @@ const Popup = {
* @param {Array} detections * @param {Array} detections
*/ */
async onGetDetections(detections = []) { async onGetDetections(detections = []) {
const el = {
empty: document.querySelector('.empty'),
detections: document.querySelector('.detections'),
}
detections = (detections || []) detections = (detections || [])
.filter(({ confidence }) => confidence >= 50) .filter(({ confidence }) => confidence >= 50)
.filter(({ slug }) => slug !== 'cart-functionality') .filter(({ slug }) => slug !== 'cart-functionality')
if (!detections || !detections.length) { if (!detections || !detections.length) {
document.querySelector('.empty').classList.remove('empty--hidden') el.empty.classList.remove('empty--hidden')
document.querySelector('.detections').classList.add('detections--hidden') el.detections.classList.add('detections--hidden')
return return
} }
document.querySelector('.empty').classList.add('empty--hidden') el.empty.classList.add('empty--hidden')
el.detections.classList.remove('detections--hidden')
const el = document.querySelector('.detections')
el.classList.remove('detections--hidden')
while (el.firstChild) { while (el.detections.firstChild) {
el.removeChild(detections.lastChild) el.detections.removeChild(detections.firstChild)
} }
const pinnedCategory = await getOption('pinnedCategory') const pinnedCategory = await getOption('pinnedCategory')
@ -381,31 +375,34 @@ const Popup = {
categorised.forEach(({ id, name, slug: categorySlug, technologies }) => { categorised.forEach(({ id, name, slug: categorySlug, technologies }) => {
const categoryNode = Popup.templates.category.cloneNode(true) const categoryNode = Popup.templates.category.cloneNode(true)
const link = categoryNode.querySelector('.category__link') const el = {
detections: document.querySelector('.detections'),
link.href = `https://www.wappalyzer.com/technologies/${categorySlug}/?utm_source=popup&utm_medium=extension&utm_campaign=wappalyzer` link: categoryNode.querySelector('.category__link'),
link.dataset.i18n = `categoryName${id}` pins: categoryNode.querySelectorAll('.category__pin'),
pinsActive: document.querySelectorAll('.category__pin--active'),
}
const pins = categoryNode.querySelectorAll('.category__pin') el.link.href = `https://www.wappalyzer.com/technologies/${categorySlug}/?utm_source=popup&utm_medium=extension&utm_campaign=wappalyzer`
el.link.dataset.i18n = `categoryName${id}`
if (pinnedCategory === id) { if (pinnedCategory === id) {
pins.forEach((pin) => pin.classList.add('category__pin--active')) el.pins.forEach((pin) => pin.classList.add('category__pin--active'))
} }
pins.forEach((pin) => el.pins.forEach((pin) =>
pin.addEventListener('click', async () => { pin.addEventListener('click', async () => {
const pinnedCategory = await getOption('pinnedCategory') const pinnedCategory = await getOption('pinnedCategory')
Array.from( el.pinsActive.forEach((pin) =>
document.querySelectorAll('.category__pin--active') pin.classList.remove('category__pin--active')
).forEach((pin) => pin.classList.remove('category__pin--active')) )
if (pinnedCategory === id) { if (pinnedCategory === id) {
await setOption('pinnedCategory', null) await setOption('pinnedCategory', null)
} else { } else {
await setOption('pinnedCategory', id) await setOption('pinnedCategory', id)
pins.forEach((pin) => pin.classList.add('category__pin--active')) el.pins.forEach((pin) => pin.classList.add('category__pin--active'))
} }
}) })
) )
@ -414,49 +411,41 @@ const Popup = {
({ name, slug, confidence, version, icon, website }) => { ({ name, slug, confidence, version, icon, website }) => {
const technologyNode = Popup.templates.technology.cloneNode(true) const technologyNode = Popup.templates.technology.cloneNode(true)
const image = technologyNode.querySelector('.technology__icon img') const el = {
technologies: categoryNode.querySelector('.technologies'),
image.src = `../images/icons/${icon}` iconImage: technologyNode.querySelector('.technology__icon img'),
link: technologyNode.querySelector('.technology__link'),
const link = technologyNode.querySelector('.technology__link') name: technologyNode.querySelector('.technology__name'),
const linkText = technologyNode.querySelector('.technology__name') version: technologyNode.querySelector('.technology__version'),
confidence: technologyNode.querySelector('.technology__confidence'),
}
link.href = `https://www.wappalyzer.com/technologies/${categorySlug}/${slug}/?utm_source=popup&utm_medium=extension&utm_campaign=wappalyzer` el.iconImage.src = `../images/icons/${icon}`
linkText.textContent = name
const confidenceNode = technologyNode.querySelector( el.link.href = `https://www.wappalyzer.com/technologies/${categorySlug}/${slug}/?utm_source=popup&utm_medium=extension&utm_campaign=wappalyzer`
'.technology__confidence' el.name.textContent = name
)
if (confidence < 100) { if (confidence < 100) {
confidenceNode.textContent = `${confidence}% sure` el.confidence.textContent = `${confidence}% sure`
} else { } else {
confidenceNode.remove() el.confidence.remove()
} }
const versionNode = technologyNode.querySelector(
'.technology__version'
)
if (version) { if (version) {
versionNode.textContent = version el.version.textContent = version
} else { } else {
versionNode.remove() el.version.remove()
} }
categoryNode el.technologies.appendChild(technologyNode)
.querySelector('.technologies')
.appendChild(technologyNode)
} }
) )
document.querySelector('.detections').appendChild(categoryNode) el.detections.appendChild(categoryNode)
}) })
if (categorised.length === 1) { if (categorised.length === 1) {
document el.detections.appendChild(Popup.templates.category.cloneNode(true))
.querySelector('.detections')
.appendChild(Popup.templates.category.cloneNode(true))
} }
Array.from(document.querySelectorAll('a')).forEach((a) => Array.from(document.querySelectorAll('a')).forEach((a) =>
@ -490,6 +479,7 @@ const Popup = {
configure: document.querySelector('.pro-configure'), configure: document.querySelector('.pro-configure'),
credits: document.querySelector('.credits'), credits: document.querySelector('.credits'),
creditsRemaining: document.querySelector('.credits__remaining'), creditsRemaining: document.querySelector('.credits__remaining'),
footer: document.querySelector('.footer'),
} }
el.error.classList.add('pro-error--hidden') el.error.classList.add('pro-error--hidden')
@ -497,9 +487,11 @@ const Popup = {
if (apiKey) { if (apiKey) {
el.loading.classList.remove('loading--hidden') el.loading.classList.remove('loading--hidden')
el.configure.classList.add('pro-configure--hidden') el.configure.classList.add('pro-configure--hidden')
el.footer.classList.remove('footer--hidden')
} else { } else {
el.loading.classList.add('loading--hidden') el.loading.classList.add('loading--hidden')
el.configure.classList.remove('pro-configure--hidden') el.configure.classList.remove('pro-configure--hidden')
el.footer.classList.add('footer--hidden')
return return
} }

@ -49,7 +49,9 @@ const Wappalyzer = {
if (name === technology.name) { if (name === technology.name) {
confidence = Math.min(100, confidence + pattern.confidence) confidence = Math.min(100, confidence + pattern.confidence)
version = version =
_version.length > version.length && _version.length <= 10 _version.length > version.length &&
_version.length <= 15 &&
(parseInt(_version, 10) || 0) < 10000 // Ignore long numeric strings like timestamps
? _version ? _version
: version : version
} }