mirror of
https://github.com/ThatGuySam/doesitarm.git
synced 2026-05-18 06:44:46 -07:00
Compare commits
No commits in common. "c5ec942de048afcefa5ac6598bdd41c8f9f2cd6b" and "e701c48fa8b4f3c1622c75995258c75391cc8699" have entirely different histories.
c5ec942de0
...
e701c48fa8
43 changed files with 3806 additions and 8456 deletions
11
.github/workflows/deploy-cloudflare-workers.yml
vendored
11
.github/workflows/deploy-cloudflare-workers.yml
vendored
|
|
@ -14,19 +14,12 @@ jobs:
|
|||
name: Deploy
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@master
|
||||
|
||||
- name: Setup PNPM
|
||||
uses: pnpm/action-setup@v4
|
||||
uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 10.12.1
|
||||
run_install: false
|
||||
|
||||
- name: Use Node.js 24
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
cache: pnpm
|
||||
|
||||
- name: Write Wrangler configs
|
||||
run: |
|
||||
|
|
|
|||
30
.github/workflows/run-ava-tests.js.yml
vendored
30
.github/workflows/run-ava-tests.js.yml
vendored
|
|
@ -1,8 +1,9 @@
|
|||
# This workflow validates the Node 24 toolchain on GitHub-hosted runners.
|
||||
name: Run Node 24 Checks
|
||||
# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
|
||||
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
|
||||
|
||||
name: Run Ava Tests
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
|
|
@ -12,26 +13,21 @@ jobs:
|
|||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
PUBLIC_URL: https://doesitarm.com
|
||||
PUBLIC_API_DOMAIN: https://api.doesitarm.com
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [24.x]
|
||||
node-version: [14.x, 15.x]
|
||||
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Setup PNPM
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10.12.1
|
||||
run_install: false
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v6
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: pnpm
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: pnpm netlify-build
|
||||
- run: |
|
||||
touch .env
|
||||
echo ${{ secrets.GH_ENV }} >> .env
|
||||
- run: npm ci
|
||||
- run: npm run generate
|
||||
- run: npm test
|
||||
|
|
|
|||
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -82,8 +82,6 @@ typings/
|
|||
|
||||
# Local Netlify folder
|
||||
.netlify
|
||||
.astro/
|
||||
vitest.config.mjs.timestamp-*
|
||||
|
||||
# Build Output
|
||||
dist
|
||||
|
|
@ -92,7 +90,6 @@ dist
|
|||
/static/**/*.json
|
||||
/static/**/*.toml
|
||||
/static/**/*.st
|
||||
/static/pagefind/
|
||||
/commits-data.json
|
||||
/static/tailwind.css
|
||||
|
||||
|
|
|
|||
2
.nvmrc
2
.nvmrc
|
|
@ -1 +1 @@
|
|||
v24
|
||||
v22
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import vue from '@astrojs/vue'
|
|||
import tailwind from '@astrojs/tailwind'
|
||||
// Astro Netlify Reference
|
||||
// https://github.com/withastro/astro/tree/main/packages/integrations/netlify
|
||||
import netlify from '@astrojs/netlify'
|
||||
import netlify from '@astrojs/netlify/functions'
|
||||
// import sitemap from '@astrojs/sitemap'
|
||||
import partytown from '@astrojs/partytown'
|
||||
|
||||
|
|
|
|||
24
ava.config.mjs
Normal file
24
ava.config.mjs
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
/**
|
||||
* @type {import('ava').AVAConfig}
|
||||
*/
|
||||
export default () => {
|
||||
return {
|
||||
require: [
|
||||
'dotenv/config',
|
||||
'esm',
|
||||
'tsconfig-paths/register'
|
||||
],
|
||||
// https://github.com/avajs/ava/blob/main/docs/recipes/watch-mode.md
|
||||
watchMode: {
|
||||
ignoreChanges: [
|
||||
'!**/*.{js,vue}',
|
||||
'./build',
|
||||
'./dist',
|
||||
'./.output',
|
||||
],
|
||||
},
|
||||
// tap: true,
|
||||
// verbose: true,
|
||||
color: true
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { dirname, basename, extname, join } from 'path'
|
||||
import { dirname, basename } from 'path'
|
||||
|
||||
import os from 'os'
|
||||
import fs from 'fs-extra'
|
||||
|
|
@ -41,12 +41,6 @@ import { makeSearchableList } from '~/helpers/searchable-list.js'
|
|||
import {
|
||||
writeStorkToml
|
||||
} from '~/helpers/stork/toml.js'
|
||||
import {
|
||||
writePagefindIndex
|
||||
} from '~/helpers/pagefind/index.js'
|
||||
import {
|
||||
getSearchProvider
|
||||
} from '~/helpers/search/config.js'
|
||||
import {
|
||||
KindListMemoized as KindList
|
||||
} from '~/helpers/api/kind.js'
|
||||
|
|
@ -454,14 +448,6 @@ class BuildLists {
|
|||
|
||||
const apiListDirectory = `${ apiDirectory }/${ listOptions.endpointPrefix }`
|
||||
|
||||
await fs.ensureDir( apiListDirectory )
|
||||
|
||||
for ( const existingFile of await fs.readdir( apiListDirectory ) ) {
|
||||
if ( extname( existingFile ) !== '.json' ) continue
|
||||
|
||||
await fs.remove( join( apiListDirectory, existingFile ) )
|
||||
}
|
||||
|
||||
// const poolSize = 1000
|
||||
|
||||
// Store app bundles to memory
|
||||
|
|
@ -530,18 +516,14 @@ class BuildLists {
|
|||
}
|
||||
|
||||
// Count saved files
|
||||
const fileCount = fs.readdirSync( apiListDirectory )
|
||||
.filter( fileName => extname( fileName ) === '.json' )
|
||||
.length
|
||||
const fileCount = fs.readdirSync( apiListDirectory ).length
|
||||
|
||||
console.log( fileCount, 'Files saved in', apiListDirectory )
|
||||
console.log( this.lists[listOptions.name].size, 'Entries' )
|
||||
|
||||
if ( fileCount !== this.lists[listOptions.name].size ) {
|
||||
const listSlugs = Array.from( this.lists[listOptions.name] ).map( listEntry => listEntry.slug )
|
||||
const fileNames = fs.readdirSync( apiListDirectory )
|
||||
.filter( fileName => extname( fileName ) === '.json' )
|
||||
.map( fileName => basename(fileName).split('.')[0] )
|
||||
const fileNames = fs.readdirSync( apiListDirectory ).map( fileName => basename(fileName).split('.')[0] )
|
||||
|
||||
logArraysDifference( listSlugs, fileNames )
|
||||
|
||||
|
|
@ -741,15 +723,9 @@ class BuildLists {
|
|||
console.log('Building XML Sitemap')
|
||||
await saveSitemap( sitemapEndpoints.map( ({ route }) => route ) )
|
||||
|
||||
const searchProvider = getSearchProvider( process.env.PUBLIC_SEARCH_PROVIDER )
|
||||
|
||||
if ( searchProvider === 'stork' ) {
|
||||
console.log('Building Stork toml index')
|
||||
await writeStorkToml( sitemapEndpoints )
|
||||
} else {
|
||||
console.log('Building Pagefind index')
|
||||
await writePagefindIndex( sitemapEndpoints )
|
||||
}
|
||||
// Save stork toml index
|
||||
console.log('Building Stork toml index')
|
||||
await writeStorkToml( sitemapEndpoints )
|
||||
|
||||
console.log('Total Nuxt Endpoints', this.endpointMaps.nuxt.size )
|
||||
console.log('Total Eleventy Endpoints', this.endpointMaps.eleventy.size )
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@
|
|||
hasActiveFilter( button.query ) ? 'border-opacity-50 bg-darkest' : 'border-opacity-0 neumorphic-shadow-inner'
|
||||
]"
|
||||
:aria-label="`Filter list by ${button.label}`"
|
||||
@click="toggleFilter(button.query); queryResults()"
|
||||
@click="toggleFilter(button.query); queryResults(query)"
|
||||
>{{ button.label }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -151,15 +151,21 @@
|
|||
</div>
|
||||
|
||||
<div
|
||||
v-if="listing.resultExcerptsMarkup?.length"
|
||||
v-if="listing.storkResult"
|
||||
class="text-xs leading-5 font-light"
|
||||
>
|
||||
<div
|
||||
v-for="(excerptMarkup, excerptIndex) in listing.resultExcerptsMarkup"
|
||||
v-for="(excerpt, excerptIndex) in listing.storkResult.excerpts"
|
||||
:key="`excerpt-${ excerptIndex }`"
|
||||
class="result-excerpt space-y-3"
|
||||
v-html="excerptMarkup"
|
||||
/>
|
||||
>
|
||||
<div
|
||||
v-for="(range, rangeIndex) in makeHighlightedMarkup( excerpt )"
|
||||
:key="`range-${ rangeIndex }`"
|
||||
|
||||
v-html="range"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- listing.lastUpdated: {{ listing.lastUpdated }} -->
|
||||
<template v-if="listing.lastUpdated">
|
||||
|
|
@ -285,18 +291,9 @@ import {
|
|||
import {
|
||||
getIconForListing
|
||||
} from '~/helpers/app-derived.js'
|
||||
import {
|
||||
PagefindClient,
|
||||
mapPagefindDataToListing
|
||||
} from '~/helpers/pagefind/browser.js'
|
||||
import {
|
||||
getSearchProvider
|
||||
} from '~/helpers/search/config.js'
|
||||
import {
|
||||
SearchFilters
|
||||
} from '~/helpers/search/filters.js'
|
||||
import {
|
||||
StorkClient,
|
||||
StorkFilters,
|
||||
makeHighlightedMarkup,
|
||||
makeHighlightedResultTitle
|
||||
} from '~/helpers/stork/browser.js'
|
||||
|
|
@ -307,7 +304,7 @@ import RelativeTime from '~/components/relative-time.vue'
|
|||
import ListSummary from '~/components/list-summary.vue'
|
||||
import ListEndButtons from '~/components/list-end-buttons.vue'
|
||||
|
||||
const searchProvider = getSearchProvider( import.meta.env.PUBLIC_SEARCH_PROVIDER )
|
||||
let storkClient = null
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
|
@ -350,30 +347,15 @@ export default {
|
|||
hasStartedAnyQuery: false,
|
||||
listingsResults: [],
|
||||
waitingForQuery: false,
|
||||
isSSR: import.meta.env.SSR,
|
||||
searchClient: null,
|
||||
searchFilters: null,
|
||||
lastQueryId: 0
|
||||
isSSR: import.meta.env.SSR
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
activeSearchProvider () {
|
||||
return searchProvider
|
||||
},
|
||||
activeQuery () {
|
||||
storkQuery () {
|
||||
return [
|
||||
this.userTextQuery.trim(),
|
||||
...this.filterQueryList
|
||||
].filter( Boolean ).join(' ')
|
||||
},
|
||||
pagefindResultLimit () {
|
||||
return Math.max( this.initialList.length, 25 )
|
||||
},
|
||||
pagefindFilters () {
|
||||
const filters = new SearchFilters()
|
||||
filters.setFromStringArray( this.filterQueryList )
|
||||
|
||||
return filters.asPagefindFilters
|
||||
].join(' ')
|
||||
},
|
||||
appList () {
|
||||
return this.kindPage.items
|
||||
|
|
@ -408,7 +390,7 @@ export default {
|
|||
return this.baseFilters.length > 0
|
||||
},
|
||||
hasSearchInputText () {
|
||||
return this.userTextQuery.trim().length > 0
|
||||
return this.userTextQuery.length > 0
|
||||
},
|
||||
hasAnyUserFilters () {
|
||||
return this.userFilters.length > 0
|
||||
|
|
@ -420,7 +402,7 @@ export default {
|
|||
return !this.hasAnyUserTerms
|
||||
},
|
||||
inputTerms () {
|
||||
return this.userTextQuery.trim().split(' ').filter( Boolean )
|
||||
return this.userTextQuery.trim().split(' ')
|
||||
},
|
||||
userFilters () {
|
||||
// console.log('filterQueryList', )
|
||||
|
|
@ -460,23 +442,22 @@ export default {
|
|||
}
|
||||
},
|
||||
mounted () {
|
||||
this.searchClient = this.makeSearchClient()
|
||||
// Setup stork client
|
||||
storkClient = new StorkClient()
|
||||
|
||||
this.searchFilters = new SearchFilters()
|
||||
// Store filter instance
|
||||
this.storkFilters = new StorkFilters()
|
||||
|
||||
// Add initial filters
|
||||
this.storkFilters.setFromStringArray( this.baseFilters )
|
||||
|
||||
this.searchFilters.setFromStringArray( this.baseFilters )
|
||||
this.filterQueryList = this.searchFilters.list
|
||||
},
|
||||
methods: {
|
||||
makeHighlightedMarkup,
|
||||
makeHighlightedResultTitle,
|
||||
|
||||
getIconForListing,
|
||||
|
||||
makeSearchClient () {
|
||||
if ( this.activeSearchProvider === 'stork' ) {
|
||||
return new StorkClient()
|
||||
}
|
||||
|
||||
return new PagefindClient()
|
||||
},
|
||||
getSearchLinks (app) {
|
||||
return app?.searchLinks || []
|
||||
},
|
||||
|
|
@ -485,8 +466,10 @@ export default {
|
|||
return this.filterQueryList.includes( filter )
|
||||
},
|
||||
toggleFilter ( newFilterQuery ) {
|
||||
this.searchFilters.toggleFilter( newFilterQuery )
|
||||
this.filterQueryList = this.searchFilters.list
|
||||
|
||||
this.storkFilters.toggleFilter( newFilterQuery )
|
||||
|
||||
this.filterQueryList = this.storkFilters.list
|
||||
},
|
||||
scrollInputToTop () {
|
||||
scrollIntoView(this.$refs['search-container'], {
|
||||
|
|
@ -494,73 +477,15 @@ export default {
|
|||
behavior: 'smooth'
|
||||
})
|
||||
},
|
||||
mapStorkResultToListing ( result ) {
|
||||
return {
|
||||
name: makeHighlightedResultTitle( result ),
|
||||
text: '',
|
||||
endpoint: result.entry.url,
|
||||
slug: result.entry.url,
|
||||
category: {
|
||||
slug: 'uncategorized'
|
||||
},
|
||||
lastUpdated: null,
|
||||
resultExcerptsMarkup: ( result.excerpts || [] ).flatMap( excerpt => {
|
||||
return makeHighlightedMarkup( excerpt )
|
||||
} )
|
||||
}
|
||||
},
|
||||
async runPagefindQuery () {
|
||||
const pagefindQuery = await this.searchClient.lazyQuery( this.userTextQuery, {
|
||||
filters: this.pagefindFilters,
|
||||
sort: this.hasSearchInputText ? {} : {
|
||||
updated: 'desc'
|
||||
}
|
||||
} )
|
||||
|
||||
if ( pagefindQuery === null ) {
|
||||
return null
|
||||
}
|
||||
|
||||
const limitedResults = ( pagefindQuery.results || [] ).slice( 0, this.pagefindResultLimit )
|
||||
const settledResultData = await Promise.allSettled( limitedResults.map( async result => {
|
||||
return await result.data()
|
||||
} ) )
|
||||
|
||||
const resultData = settledResultData.flatMap( settledResult => {
|
||||
if ( settledResult.status === 'fulfilled' ) {
|
||||
return [ settledResult.value ]
|
||||
}
|
||||
|
||||
console.warn('Failed to load Pagefind result data', settledResult.reason)
|
||||
return []
|
||||
} )
|
||||
|
||||
return resultData.map( data => {
|
||||
return mapPagefindDataToListing( data, {
|
||||
highlightTerms: this.inputTerms
|
||||
} )
|
||||
} )
|
||||
},
|
||||
async runStorkQuery () {
|
||||
const storkQuery = await this.searchClient.lazyQuery( this.activeQuery, this.activeQuery.split(' ') )
|
||||
|
||||
if ( storkQuery === null ) {
|
||||
return null
|
||||
}
|
||||
|
||||
return ( storkQuery.results || [] ).map( result => {
|
||||
return this.mapStorkResultToListing( result )
|
||||
} )
|
||||
},
|
||||
|
||||
// Called on input and when a filter is toggled
|
||||
async queryResults ( rawQuery = this.userTextQuery ) {
|
||||
const queryId = ++this.lastQueryId
|
||||
async queryResults ( rawQuery ) {
|
||||
|
||||
if ( this.activeQuery.trim().length === 0 ) {
|
||||
this.waitingForQuery = false
|
||||
return
|
||||
}
|
||||
console.log( 'query', this.storkQuery )
|
||||
|
||||
// If our query is empty
|
||||
// then bail
|
||||
if ( this.storkQuery.trim().length === 0 ) return
|
||||
|
||||
this.waitingForQuery = true
|
||||
|
||||
|
|
@ -569,22 +494,36 @@ export default {
|
|||
// Declare that at least one query has been made
|
||||
this.hasStartedAnyQuery = true
|
||||
|
||||
const results = this.activeSearchProvider === 'stork'
|
||||
? await this.runStorkQuery()
|
||||
: await this.runPagefindQuery()
|
||||
// console.log('rawQuery', rawQuery)
|
||||
|
||||
if ( queryId !== this.lastQueryId ) {
|
||||
const requiredTerms = this.storkQuery.split(' ')
|
||||
|
||||
const storkQuery = await storkClient.lazyQuery( this.storkQuery, requiredTerms )
|
||||
|
||||
// If the query response is empty
|
||||
// then return
|
||||
if ( storkQuery === null ) {
|
||||
return
|
||||
}
|
||||
|
||||
if ( results === null ) {
|
||||
this.waitingForQuery = false
|
||||
return
|
||||
}
|
||||
// console.log( 'storkQuery', storkQuery )
|
||||
|
||||
this.listingsResults = results
|
||||
this.listingsResults = storkQuery.results.map( result => {
|
||||
return {
|
||||
name: makeHighlightedResultTitle( result ),
|
||||
endpoint: result.entry.url,
|
||||
slug: '',
|
||||
category: {
|
||||
slug: 'uncategorized'
|
||||
},
|
||||
storkResult: result
|
||||
}
|
||||
})
|
||||
|
||||
// Switch from loading state and reveal the results
|
||||
this.waitingForQuery = false
|
||||
|
||||
// console.log('this.listingsResults', this.listingsResults)
|
||||
},
|
||||
|
||||
handleSearchInput ( event ) {
|
||||
|
|
|
|||
|
|
@ -1,77 +0,0 @@
|
|||
# Netlify Ubuntu 24 Stork Migration
|
||||
|
||||
Date: 2026-03-15
|
||||
|
||||
## Scope
|
||||
|
||||
Investigate why `doesitarm` fails on Netlify's Noble/Ubuntu 24 image and
|
||||
identify whether the fix belongs in this repo or `../doesitarm-functions/`.
|
||||
|
||||
## Short Answer
|
||||
|
||||
The failing production build is blocked in the Stork setup stage, not in Astro
|
||||
or the companion functions repo.
|
||||
|
||||
The immediate break is that `doesitarm` downloads Stork's
|
||||
`stork-ubuntu-20-04` binary, which is linked against `libssl.so.1.1`. Netlify's
|
||||
Noble/Ubuntu 24 image no longer ships that library, so the binary exits before
|
||||
Astro starts building.
|
||||
|
||||
There is also a separate Node 22 regression in this repo: `isBrowserContext()`
|
||||
uses `navigator` to detect browsers, but Node 22 exposes a global
|
||||
`navigator`. That causes Node processes on macOS to be misdetected as browser
|
||||
contexts and pushes local Stork downloads onto the Linux binary path.
|
||||
|
||||
## What The Evidence Says
|
||||
|
||||
- Confirmed from the user-provided Netlify build log:
|
||||
`./stork-executable: error while loading shared libraries: libssl.so.1.1`
|
||||
- Confirmed from the latest Stork release metadata:
|
||||
`v1.6.0` ships both `stork-ubuntu-20-04` and `stork-ubuntu-22-04`.
|
||||
- Confirmed from local binary inspection:
|
||||
`stork-ubuntu-20-04` references `libssl.so.1.1` and `libcrypto.so.1.1`
|
||||
while `stork-ubuntu-22-04` references `libssl.so.3` and `libcrypto.so.3`.
|
||||
- Confirmed from Node 22 docs and local runtime checks:
|
||||
Node 22 exposes a global `navigator`, so `typeof navigator !== 'undefined'`
|
||||
is no longer a safe browser-only check.
|
||||
- Confirmed locally on 2026-03-15:
|
||||
`CI=1 mise exec node@22 -- pnpm run netlify-build` succeeds after switching
|
||||
the Stork target and fixing runtime detection.
|
||||
|
||||
## What Works
|
||||
|
||||
- Use Stork's `stork-ubuntu-22-04` binary on Linux/Netlify.
|
||||
- Use `stork-macos-13-arm` on Apple Silicon Macs.
|
||||
- Detect browser context with `window` and `document`, not `navigator`.
|
||||
- Keep the fix in `doesitarm`; `../doesitarm-functions/` is an external API
|
||||
dependency referenced via `VFUNCTIONS_URL` and `PUBLIC_API_DOMAIN`, not part
|
||||
of the failing Netlify build path.
|
||||
|
||||
## What To Avoid
|
||||
|
||||
- Do not keep using `stork-ubuntu-20-04` on Noble/Ubuntu 24.
|
||||
- Do not use the `stork-amazon-linux` artifact as a Netlify fallback; its
|
||||
binary references `libssl.so.10`, which is also not a fit for Ubuntu 24.
|
||||
- Do not use `navigator` as the only browser-runtime signal in Node 22+ code.
|
||||
|
||||
## Recommendation
|
||||
|
||||
Keep the Stork fix minimal and repo-local:
|
||||
|
||||
1. Detect the Stork binary target from `process.platform` and `process.arch`.
|
||||
2. Use `stork-ubuntu-22-04` for Linux builds.
|
||||
3. Use `stork-macos-13-arm` for Apple Silicon.
|
||||
4. Add tests for runtime detection and Stork target selection.
|
||||
5. Leave `../doesitarm-functions/` unchanged unless its own deployment starts
|
||||
failing independently.
|
||||
|
||||
## Source Links
|
||||
|
||||
- Stork latest release metadata:
|
||||
https://api.github.com/repos/jameslittle230/stork/releases/latest
|
||||
- Stork install docs:
|
||||
https://stork-search.net/docs/install
|
||||
- Stork CI/Netlify guide:
|
||||
https://stork-search.net/docs/stork-and-netlify
|
||||
- Node 22 globals docs:
|
||||
https://nodejs.org/dist/latest-v22.x/docs/api/globals.html
|
||||
|
|
@ -1,306 +0,0 @@
|
|||
# Pagefind Feature Parity For doesitarm
|
||||
|
||||
Date: 2026-03-15
|
||||
|
||||
## Scope
|
||||
|
||||
Read alongside `docs/research/pagefind-viability-2026-03-15.md`.
|
||||
|
||||
Investigate how a Pagefind migration could preserve the current Stork-backed
|
||||
search UX in `doesitarm`, focusing on user-visible behavior rather than on
|
||||
whether Pagefind is viable in the abstract.
|
||||
|
||||
## Short Answer
|
||||
|
||||
Yes, most of the current search experience can be carried over without the user
|
||||
feeling a major regression, but only if Pagefind is treated as the search core
|
||||
under a custom Vue adapter.
|
||||
|
||||
Recommended parity path:
|
||||
|
||||
1. Keep the current server-rendered initial lists, pagination links, summary
|
||||
block, and page chrome exactly as they are.
|
||||
2. Replace only the "user has started searching/filtering" path with Pagefind's
|
||||
JavaScript API.
|
||||
3. Build the Pagefind index from the existing sitemap/listing data, not from an
|
||||
HTML crawl.
|
||||
4. Use Pagefind `filters` for status/category/type scoping.
|
||||
5. Use Pagefind `meta` only for simple scalar fields needed in result rendering.
|
||||
6. Reattach richer card UI state such as `searchLinks` from a local
|
||||
URL-or-slug keyed map instead of trying to force arrays into Pagefind
|
||||
metadata.
|
||||
|
||||
The one place where a prototype may still change the implementation choice is
|
||||
search quality. If `addCustomRecord()` does not rank app-name and alias matches
|
||||
well enough, the next-best parity option is to generate virtual HTML records via
|
||||
`addHTMLFile()` so Pagefind can use `h1` weighting and `data-pagefind-*`
|
||||
attributes.
|
||||
|
||||
## Current UX Contract In The Repo
|
||||
|
||||
From `components/search-stork.vue`, `helpers/stork/toml.js`, and the scoped
|
||||
Astro pages:
|
||||
|
||||
- The page initially shows the existing paginated list from the API when the
|
||||
user has not typed anything yet.
|
||||
- Search is search-as-you-type, with loading placeholders while results are
|
||||
pending.
|
||||
- The UI exposes quick status chips.
|
||||
- Scoped pages such as `/kind/...` and `/games` inject base filters so the same
|
||||
component behaves like "search within this slice".
|
||||
- Empty results on a scoped page show a "Search Everything" escape hatch.
|
||||
- Query results show highlighted snippets and a detail link.
|
||||
- Non-query cards can also show timestamps and auxiliary action buttons such as
|
||||
benchmark/performance links.
|
||||
- The current Stork index injects synthetic searchable tokens for `status_*`,
|
||||
category, and route type, in addition to title/content/description/aliases and
|
||||
tags.
|
||||
- Stork also post-filters query results so every typed term must be present
|
||||
somewhere in the returned title/URL/excerpts.
|
||||
|
||||
That means parity is not just "can users search", but:
|
||||
|
||||
- can they search globally and within a scoped page
|
||||
- can they click status chips
|
||||
- can they still get good snippets and stable detail URLs
|
||||
- can the initial browse mode remain unchanged
|
||||
|
||||
## What The Evidence Says
|
||||
|
||||
Confirmed from Pagefind docs and repo activity:
|
||||
|
||||
- The Node API supports `addCustomRecord()` with `url`, `content`, `language`,
|
||||
optional flat `meta`, optional flat `filters`, and optional flat `sort`.
|
||||
- The Node API also supports `addHTMLFile()` for virtual HTML pages and
|
||||
`writeFiles()` / `getFiles()` for writing the bundle to `/pagefind/`.
|
||||
- The browser API is intended for custom search interfaces, not just the stock
|
||||
widget.
|
||||
- `pagefind.init()` can be called on focus, and `pagefind.preload()` /
|
||||
`pagefind.debouncedSearch()` exist specifically to reduce first-search
|
||||
latency.
|
||||
- `result.data()` returns `url`, `excerpt`, `meta`, and related result data.
|
||||
The docs explicitly say `excerpt` is safe to use as `innerHTML`, while
|
||||
`content` and `meta` are raw.
|
||||
- The JS API supports filter-only browsing by calling
|
||||
`pagefind.search(null, { filters: ... })`.
|
||||
- The JS API can also return filter counts via `pagefind.filters()`, plus
|
||||
remaining-result counts on subsequent searches.
|
||||
- Filtering defaults to AND semantics, and compound `any` / `all` / `none` /
|
||||
`not` logic is available.
|
||||
- Sorting can be applied at search time, but records missing a sort value are
|
||||
omitted when that sort is active.
|
||||
- Highlighting on destination pages is supported via `highlightParam` and
|
||||
`pagefind-highlight.js`.
|
||||
- Historical GitHub issues `#198` and `#277` asked for direct non-HTML input;
|
||||
both are now closed, and the current docs document that capability.
|
||||
- The latest stable release is `v1.4.0`, published on 2025-09-01.
|
||||
- Issue `#574` about the `npx` wrapper on `ubuntu-latest` is still open as of
|
||||
2026-03-15, so a pinned dependency or direct binary path is safer than a
|
||||
casual CLI swap.
|
||||
|
||||
Community signal:
|
||||
|
||||
- In the main HN discussion for Pagefind's launch, the maintainer explicitly
|
||||
said multi-word query merging is built in.
|
||||
- Another HN commenter reported that deploying Pagefind was "pleasingly easy"
|
||||
and the result was "reasonably nippy".
|
||||
- Zach Leatherman's `pagefind-search` component is a concrete GitHub example of
|
||||
treating Pagefind as a customizable UI layer with explicit fallback content
|
||||
and controlled asset loading.
|
||||
|
||||
## Feature Mapping
|
||||
|
||||
| Current user-visible feature | Carry-over path with Pagefind | Confidence | Notes |
|
||||
| --- | --- | --- | --- |
|
||||
| Search-as-you-type | `pagefind.debouncedSearch()` or manual debounce + `pagefind.preload()` | High | This is native to the JS API. |
|
||||
| Lazy first-load behavior | `pagefind.init()` on focus, or rely on first search | High | This matches the current deferred Stork load pattern. |
|
||||
| Scoped search pages | Keep current initial page data, then call `pagefind.search(term-or-null, { filters })` | High | Better fit than the current synthetic token approach. |
|
||||
| Quick status chips | Map chips to `filters.status` values | High | Pagefind filters are cleaner than indexing `status_native` into content. |
|
||||
| Empty-state "Search Everything" | Clear base filters and rerun, or keep current link to `/` | High | User-visible behavior is easy to preserve. |
|
||||
| Highlighted excerpts | Render `result.data().excerpt` | High | Officially documented and safe as `innerHTML`. |
|
||||
| Highlighted title text | No first-class JS API equivalent was found in the docs | Medium | Likely plain title unless we add client-side emphasis ourselves. |
|
||||
| Detail links | Use `result.data().url` | High | Direct match. |
|
||||
| Relative timestamp text | Put timestamp in `meta`, or join from local listing data | High | `meta` is string-only, so store ISO strings if using metadata. |
|
||||
| Benchmark / Performance buttons | Join from local listing data keyed by URL or slug | High | Inference: better than encoding arrays as metadata strings. |
|
||||
| Status / category / type scoping | Use Pagefind `filters`, not fake searchable tokens | High | Cleaner and more explicit than the current Stork trick. |
|
||||
| "Every typed term must match somewhere" behavior | Likely client-side post-filter using returned raw `content` if needed | Medium | Current Stork behavior is explicit; Pagefind query semantics need a parity check in a prototype. |
|
||||
| Result ordering that favors app names and aliases | Start with `addCustomRecord()` content shaping; fall back to `addHTMLFile()` if needed | Medium | Custom-record metadata appears display-oriented, not ranking-oriented. |
|
||||
|
||||
## Options
|
||||
|
||||
### 1. Custom Vue adapter over `addCustomRecord()` output
|
||||
|
||||
This is the lowest-risk parity path.
|
||||
|
||||
Why it works:
|
||||
|
||||
- It matches the repo's existing data-first indexing model.
|
||||
- It preserves the current page shell and only swaps the query engine.
|
||||
- It uses Pagefind features the way they are documented today:
|
||||
`meta` for display fields, `filters` for scoping, `sort` for explicit sort
|
||||
options.
|
||||
|
||||
Tradeoffs:
|
||||
|
||||
- `meta` is for returned metadata, not clearly for ranking.
|
||||
- Complex card state such as `searchLinks` does not fit naturally into flat
|
||||
string metadata.
|
||||
- The docs do not show title-highlight ranges in the JS API, so exact title
|
||||
highlighting may need custom client logic.
|
||||
|
||||
### 2. Custom Vue adapter over generated virtual HTML via `addHTMLFile()`
|
||||
|
||||
This is the "higher parity if ranking is off" option.
|
||||
|
||||
Why it might be worth it:
|
||||
|
||||
- Pagefind documents default weighting for HTML headings.
|
||||
- Pagefind documents `data-pagefind-weight`, `data-pagefind-meta`,
|
||||
`data-pagefind-filter`, and `data-pagefind-sort`.
|
||||
- If app-name, alias, and status text need finer relevance tuning than a plain
|
||||
custom record gives, virtual HTML gives more levers.
|
||||
|
||||
Tradeoffs:
|
||||
|
||||
- More adapter code.
|
||||
- Harder to justify unless a real query corpus shows ranking problems.
|
||||
|
||||
### 3. Replace the current component with the stock Pagefind UI
|
||||
|
||||
Not recommended.
|
||||
|
||||
Why:
|
||||
|
||||
- It discards the current browse-first behavior and scoped-page behavior.
|
||||
- It loses the current empty-state copy and action-button treatment.
|
||||
- It would make parity depend on overriding Pagefind's UI instead of preserving
|
||||
the repo's existing search component contract.
|
||||
|
||||
## What Works
|
||||
|
||||
- Keep "no query" mode exactly as it is today and switch to Pagefind only after
|
||||
the user types or toggles a filter.
|
||||
- Build one Pagefind record per listing/detail route, using the same sitemap
|
||||
payloads already feeding the Stork pipeline.
|
||||
- Put searchable text in `content`, starting with the fields users most expect
|
||||
to match: app name, aliases, support text, description, tags, and any status
|
||||
phrasing users already see.
|
||||
- Put render-only scalar fields in `meta`, such as title, slug, status label,
|
||||
last-updated ISO timestamp, and short display text.
|
||||
- Use `filters` for `status`, `category`, `kind`, and other scoped-page
|
||||
constraints.
|
||||
- Build a parallel local result-decoration map keyed by URL or slug so Pagefind
|
||||
results can be decorated with the same `searchLinks`, timestamps, or other
|
||||
card chrome without turning the index into a transport format for the whole
|
||||
listing object.
|
||||
- Call `pagefind.filters()` once if you want the chip row to expose counts or
|
||||
disabled states.
|
||||
|
||||
Inference:
|
||||
For `doesitarm`, this "Pagefind for retrieval + local map for decoration"
|
||||
split is probably the cleanest way to preserve the current UI without bloating
|
||||
Pagefind metadata or weakening search semantics.
|
||||
|
||||
## What To Avoid
|
||||
|
||||
- Do not replace the entire search component with the stock Pagefind UI if the
|
||||
goal is parity.
|
||||
- Do not assume `meta` alone is enough for search quality. Metadata is clearly
|
||||
documented as returned result data; searchable content still needs to live in
|
||||
`content`.
|
||||
- Do not try to stuff arrays or nested structures like `searchLinks` into flat
|
||||
Pagefind metadata if the same information already exists in local page data.
|
||||
- Do not apply Pagefind sort options to sparse fields unless every record has a
|
||||
value, because missing sort keys are omitted from sorted results.
|
||||
- Do not assume the `npx pagefind` wrapper is production-safe on Ubuntu CI
|
||||
without pinning and testing.
|
||||
|
||||
## Recommendation
|
||||
|
||||
Recommended implementation order:
|
||||
|
||||
1. Keep the current Astro pages and initial list rendering exactly as-is.
|
||||
2. Build a Pagefind prototype with `addCustomRecord()` from the existing sitemap
|
||||
payloads.
|
||||
3. Map the current scoped-page `baseFilters` to real Pagefind filters.
|
||||
4. Add a thin Pagefind adapter inside the current Vue component rather than
|
||||
replacing the component.
|
||||
5. Use a local `listingByUrl` or `listingBySlug` map to reattach rich card UI
|
||||
fields.
|
||||
6. Compare a real query set against Stork, especially app-name, alias, and
|
||||
multi-term searches.
|
||||
7. Only if ranking quality is weaker than expected, move the prototype from
|
||||
`addCustomRecord()` to generated `addHTMLFile()` records with weighted
|
||||
markup.
|
||||
|
||||
Why this is the best default:
|
||||
|
||||
- It preserves the current user-visible experience in the cheapest way.
|
||||
- It uses Pagefind features where Pagefind is strongest: retrieval, snippets,
|
||||
filtering, counts, and static bundle delivery.
|
||||
- It avoids forcing Pagefind to become the canonical source of every UI field.
|
||||
|
||||
## Missing Information
|
||||
|
||||
- I did not find a documented JS API for title highlight ranges equivalent to
|
||||
the Stork title-range behavior.
|
||||
- I did not find clear documentation on exact multi-term query semantics beyond
|
||||
Pagefind supporting multi-word queries in practice.
|
||||
- I did not find a high-signal Stack Overflow thread that added more than the
|
||||
official docs for this migration.
|
||||
- The Lobsters URL surfaced during search no longer resolved, so I did not use
|
||||
it as evidence.
|
||||
|
||||
## Recommended Next Inspection Steps
|
||||
|
||||
1. Build a small Pagefind prototype against 100-200 representative listings.
|
||||
2. Test 25-50 real queries from the current site vocabulary:
|
||||
app names, aliases, status words, category words, and mixed multi-term
|
||||
queries.
|
||||
3. Decide whether status chips should stay effectively single-select, matching
|
||||
current behavior, or become explicit OR filters within the same filter key.
|
||||
4. Verify whether plain title rendering is acceptable, or whether custom
|
||||
client-side title emphasis is needed.
|
||||
5. Measure first-search latency on mobile before removing Stork.
|
||||
|
||||
## Source Links
|
||||
|
||||
- Pagefind Node API docs:
|
||||
https://pagefind.app/docs/node-api/
|
||||
- Pagefind browser API docs:
|
||||
https://pagefind.app/docs/api/
|
||||
- Pagefind filtering docs:
|
||||
https://pagefind.app/docs/filtering/
|
||||
- Pagefind JS API filtering docs:
|
||||
https://pagefind.app/docs/js-api-filtering/
|
||||
- Pagefind sorting docs:
|
||||
https://pagefind.app/docs/sorts/
|
||||
- Pagefind JS API sorting docs:
|
||||
https://pagefind.app/docs/js-api-sorting/
|
||||
- Pagefind metadata docs:
|
||||
https://pagefind.app/docs/metadata/
|
||||
- Pagefind JS API metadata docs:
|
||||
https://pagefind.app/docs/js-api-metadata/
|
||||
- Pagefind weighting docs:
|
||||
https://pagefind.app/docs/weighting/
|
||||
- Pagefind ranking docs:
|
||||
https://pagefind.app/docs/ranking/
|
||||
- Pagefind highlighting docs:
|
||||
https://pagefind.app/docs/highlighting/
|
||||
- Pagefind sub-results docs:
|
||||
https://pagefind.app/docs/sub-results/
|
||||
- Pagefind latest release `v1.4.0` (published 2025-09-01):
|
||||
https://github.com/Pagefind/pagefind/releases/tag/v1.4.0
|
||||
- Pagefind issue `#198` ("Manually defining content, without passing HTML"):
|
||||
https://github.com/Pagefind/pagefind/issues/198
|
||||
- Pagefind issue `#277` ("Can pagefind pull its data from a json index file?"):
|
||||
https://github.com/Pagefind/pagefind/issues/277
|
||||
- Pagefind issue `#574` (`ubuntu-latest` / `npx` wrapper failure, still open on
|
||||
2026-03-15):
|
||||
https://github.com/Pagefind/pagefind/issues/574
|
||||
- HN discussion for Pagefind launch:
|
||||
https://news.ycombinator.com/item?id=32290634
|
||||
- HN item API for the same discussion:
|
||||
https://hn.algolia.com/api/v1/items/32290634
|
||||
- Zach Leatherman's `pagefind-search` web component:
|
||||
https://github.com/zachleat/pagefind-search
|
||||
|
|
@ -1,118 +0,0 @@
|
|||
# Pagefind Viability For doesitarm
|
||||
|
||||
Date: 2026-03-15
|
||||
|
||||
## Scope
|
||||
|
||||
Investigate whether Pagefind is a good replacement for Stork in `doesitarm`,
|
||||
given the current Astro 2 + Netlify server build and the existing custom search
|
||||
pipeline.
|
||||
|
||||
## Short Answer
|
||||
|
||||
Pagefind is viable for this repo, but not as a drop-in replacement.
|
||||
|
||||
The lowest-risk production path today is to keep the Stork fix and ship it.
|
||||
If `doesitarm` later moves to Pagefind, the right migration path is a
|
||||
behind-feature-flag prototype using Pagefind's Node API with
|
||||
`addCustomRecord()`, not a simple `pagefind --site dist` crawl.
|
||||
|
||||
## What The Evidence Says
|
||||
|
||||
- Current repo shape:
|
||||
`doesitarm` builds with `output: "server"` in Astro and only prerenders a
|
||||
small subset of routes (`/`, `/categories`, `/games`). Most searchable
|
||||
listing pages are SSR routes, not static HTML files in `dist/`.
|
||||
- Current Stork shape:
|
||||
[helpers/stork/toml.js](/Users/athena/Code/doesitarm/helpers/stork/toml.js)
|
||||
generates a structured index from sitemap payloads, and
|
||||
[components/search-stork.vue](/Users/athena/Code/doesitarm/components/search-stork.vue)
|
||||
renders a custom search UI over those records.
|
||||
- Pagefind official docs:
|
||||
the Node API supports `addDirectory()`, `addHTMLFile()`, and
|
||||
`addCustomRecord()`. The docs explicitly describe `addCustomRecord()` as a
|
||||
way to build an index from non-HTML content.
|
||||
- Pagefind browser API:
|
||||
supports custom JS search UIs, per-result lazy loading with `result.data()`,
|
||||
excerpts, filters, and sorting.
|
||||
- Pagefind latest release:
|
||||
`v1.4.0`, published 2025-09-01.
|
||||
- Relevant GitHub issues:
|
||||
`#163` shows Astro + Netlify usage is workable but can need selector/root
|
||||
troubleshooting.
|
||||
`#198` and `#277` show demand for indexing non-HTML/custom data, which is now
|
||||
covered by the Node API docs.
|
||||
`#574` remains open for an `npx` wrapper failure on `ubuntu-latest`, which is
|
||||
a reason to prefer a pinned dependency and explicit integration over a casual
|
||||
CLI-only swap.
|
||||
|
||||
## What Works
|
||||
|
||||
- Pagefind can support this repo's filters and sorts.
|
||||
- Pagefind can support a custom UI instead of the stock widget.
|
||||
- Pagefind can index structured records directly with `addCustomRecord()`,
|
||||
which matches `doesitarm` better than crawling built HTML.
|
||||
- A feature-flagged migration is feasible:
|
||||
1. build Pagefind assets from the existing sitemap payload data
|
||||
2. expose them under `/pagefind/`
|
||||
3. add a Pagefind-backed client alongside the existing Stork component
|
||||
4. switch between them with a runtime/build flag
|
||||
|
||||
## What To Avoid
|
||||
|
||||
- Do not treat Pagefind as a trivial `postbuild` swap in this repo.
|
||||
A plain HTML crawl would miss most of the real searchable surface because the
|
||||
site is primarily SSR on Netlify.
|
||||
- Do not attempt the production migration by replacing Stork first and figuring
|
||||
out the UI later.
|
||||
- Do not rely on `npx pagefind` alone in CI without pinning and testing the
|
||||
binary/package path on the target image.
|
||||
|
||||
## Recommendation
|
||||
|
||||
For production now:
|
||||
|
||||
1. Ship the Stork Ubuntu 24 fix.
|
||||
2. Merge that branch to `master`.
|
||||
3. verify the Netlify deploy is green.
|
||||
|
||||
For a Pagefind migration later:
|
||||
|
||||
1. Add `pagefind` as a pinned dependency.
|
||||
2. Create a build script that maps the same sitemap payloads into
|
||||
`addCustomRecord()` calls.
|
||||
3. Write Pagefind output to `static/pagefind/` or `dist/pagefind/`.
|
||||
4. Add a feature flag that swaps the current Stork client for a Pagefind
|
||||
adapter in the search UI.
|
||||
5. Only remove Stork after the Pagefind path has parity on excerpts, filters,
|
||||
and result URLs.
|
||||
|
||||
Inference:
|
||||
Pagefind is probably the cleaner long-term search engine here, but because this
|
||||
repo already has a data-first indexing pipeline, the migration cost is more
|
||||
about adapter work than about search quality.
|
||||
|
||||
## Source Links
|
||||
|
||||
- Pagefind repo:
|
||||
https://github.com/Pagefind/pagefind
|
||||
- Pagefind latest release:
|
||||
https://github.com/Pagefind/pagefind/releases/tag/v1.4.0
|
||||
- Pagefind Node API docs:
|
||||
https://pagefind.app/docs/node-api/
|
||||
- Pagefind browser API docs:
|
||||
https://pagefind.app/docs/api/
|
||||
- Pagefind filtering docs:
|
||||
https://pagefind.app/docs/filtering/
|
||||
- Pagefind sorts docs:
|
||||
https://pagefind.app/docs/sorts/
|
||||
- Pagefind issue `#163`:
|
||||
https://github.com/Pagefind/pagefind/issues/163
|
||||
- Pagefind issue `#198`:
|
||||
https://github.com/Pagefind/pagefind/issues/198
|
||||
- Pagefind issue `#277`:
|
||||
https://github.com/Pagefind/pagefind/issues/277
|
||||
- Pagefind issue `#574`:
|
||||
https://github.com/Pagefind/pagefind/issues/574
|
||||
- HN result set for Pagefind launches:
|
||||
https://hn.algolia.com/?q=Pagefind
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import fs from 'fs-extra'
|
||||
import axios from 'axios'
|
||||
import 'dotenv/config.js'
|
||||
import 'dotenv/config'
|
||||
|
||||
import {
|
||||
// storkVersion,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import TOML from '@iarna/toml'
|
|||
import fs from 'fs-extra'
|
||||
|
||||
import pkg from '~/package.json'
|
||||
import { publicRuntimeConfig } from '~/helpers/public-runtime-config.mjs'
|
||||
import { getSiteUrl } from '~/helpers/url.js'
|
||||
import { getRouteType } from '~/helpers/app-derived.js'
|
||||
|
||||
|
|
@ -156,7 +155,7 @@ export function makeTitle ( listing ) {
|
|||
}
|
||||
|
||||
export function makeDescription ( listing ) {
|
||||
return `Latest reported support status of ${ listing.name } on Apple Silicon and ${ publicRuntimeConfig.processorsVerbiage } Processors.`
|
||||
return `Latest reported support status of ${ listing.name } on Apple Silicon and ${ this.$config.processorsVerbiage } Processors.`
|
||||
}
|
||||
|
||||
function makeTag ( tag, tagName = 'meta' ) {
|
||||
|
|
|
|||
|
|
@ -6,10 +6,7 @@ export function isNuxt( VueThis ) {
|
|||
}
|
||||
|
||||
export function isBrowserContext () {
|
||||
// Node 22 exposes a global navigator, so use window/document instead.
|
||||
if ( typeof window === 'undefined' ) return false
|
||||
|
||||
if ( typeof document === 'undefined' ) return false
|
||||
if ( typeof navigator === 'undefined' ) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
|
@ -17,8 +14,6 @@ export function isBrowserContext () {
|
|||
export function hasProcesGlobal () {
|
||||
if ( typeof process === 'undefined' ) return false
|
||||
|
||||
if ( !process.versions?.node ) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,238 +0,0 @@
|
|||
import {
|
||||
isNonEmptyString
|
||||
} from '~/helpers/check-types.js'
|
||||
import {
|
||||
pagefindBundleRelativeURL,
|
||||
pagefindScriptURL
|
||||
} from '~/helpers/pagefind/config.js'
|
||||
|
||||
const pagefindGlobalKey = '__doesItArmPagefind'
|
||||
const pagefindLoaderPromiseKey = '__doesItArmPagefindLoaderPromise'
|
||||
const pagefindLoaderTimeoutMs = 10 * 1000
|
||||
|
||||
function escapeHtml ( text = '' ) {
|
||||
return text
|
||||
.replaceAll('&', '&')
|
||||
.replaceAll('<', '<')
|
||||
.replaceAll('>', '>')
|
||||
.replaceAll('"', '"')
|
||||
.replaceAll("'", ''')
|
||||
}
|
||||
|
||||
function escapeRegExp ( value ) {
|
||||
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||
}
|
||||
|
||||
export function makeHighlightedPagefindTitle ( title, terms = [] ) {
|
||||
const highlightedTerms = terms
|
||||
.filter( term => isNonEmptyString( term ) )
|
||||
.map( term => term.trim() )
|
||||
.filter( term => term.length > 0 )
|
||||
.sort( ( a, b ) => b.length - a.length )
|
||||
|
||||
const titleMarkup = escapeHtml( title || '' )
|
||||
|
||||
if ( highlightedTerms.length === 0 ) {
|
||||
return titleMarkup
|
||||
}
|
||||
|
||||
const pattern = highlightedTerms.map( escapeRegExp ).join('|')
|
||||
|
||||
return titleMarkup.replace(
|
||||
new RegExp(`(${ pattern })`, 'gi'),
|
||||
'<span class="stork-highlighted-text font-bold text-white bg-green-800 rounded px-1">$1</span>'
|
||||
)
|
||||
}
|
||||
|
||||
export function normalizePagefindExcerptMarkup ( excerptMarkup = '' ) {
|
||||
if ( !isNonEmptyString( excerptMarkup ) ) return ''
|
||||
|
||||
return excerptMarkup
|
||||
.replace(/<mark[^>]*>/g, '<span class="stork-highlighted-text font-bold text-white bg-green-800 rounded px-1">')
|
||||
.replace(/<\/mark>/g, '</span>')
|
||||
}
|
||||
|
||||
export function mapPagefindDataToListing ( resultData, {
|
||||
highlightTerms = []
|
||||
} = {} ) {
|
||||
const lastUpdatedTimestamp = Number( resultData.meta?.lastUpdatedTimestamp || 0 )
|
||||
|
||||
return {
|
||||
name: makeHighlightedPagefindTitle( resultData.meta?.title || resultData.url, highlightTerms ),
|
||||
text: resultData.meta?.text || resultData.url,
|
||||
endpoint: resultData.url,
|
||||
slug: resultData.meta?.slug || resultData.url,
|
||||
category: {
|
||||
slug: resultData.meta?.categorySlug || 'uncategorized'
|
||||
},
|
||||
lastUpdated: lastUpdatedTimestamp > 0 ? {
|
||||
timestamp: lastUpdatedTimestamp
|
||||
} : null,
|
||||
resultExcerptsMarkup: isNonEmptyString( resultData.excerpt ) ? [
|
||||
normalizePagefindExcerptMarkup( resultData.excerpt )
|
||||
] : []
|
||||
}
|
||||
}
|
||||
|
||||
function getPagefindModuleSource () {
|
||||
return [
|
||||
`import * as pagefindModule from ${ JSON.stringify( pagefindScriptURL ) }`,
|
||||
`globalThis.${ pagefindGlobalKey } = pagefindModule.default || pagefindModule`
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
function waitForPagefindGlobal () {
|
||||
return new Promise( ( resolve, reject ) => {
|
||||
if ( globalThis[ pagefindGlobalKey ] ) {
|
||||
resolve( globalThis[ pagefindGlobalKey ] )
|
||||
return
|
||||
}
|
||||
|
||||
const startedAt = Date.now()
|
||||
const timer = setInterval( () => {
|
||||
if ( globalThis[ pagefindGlobalKey ] ) {
|
||||
clearInterval( timer )
|
||||
resolve( globalThis[ pagefindGlobalKey ] )
|
||||
return
|
||||
}
|
||||
|
||||
if ( Date.now() - startedAt >= pagefindLoaderTimeoutMs ) {
|
||||
clearInterval( timer )
|
||||
reject( new Error(`Timed out waiting for Pagefind browser module at ${ pagefindScriptURL }`) )
|
||||
}
|
||||
}, 20 )
|
||||
} )
|
||||
}
|
||||
|
||||
async function loadPagefindBrowserModule () {
|
||||
if ( typeof document === 'undefined' ) {
|
||||
throw new Error('PagefindClient can only load in a browser document')
|
||||
}
|
||||
|
||||
if ( globalThis[ pagefindGlobalKey ] ) {
|
||||
return globalThis[ pagefindGlobalKey ]
|
||||
}
|
||||
|
||||
if ( !globalThis[ pagefindLoaderPromiseKey ] ) {
|
||||
globalThis[ pagefindLoaderPromiseKey ] = new Promise( async ( resolve, reject ) => {
|
||||
const script = document.createElement('script')
|
||||
|
||||
script.async = true
|
||||
script.type = 'module'
|
||||
script.textContent = getPagefindModuleSource()
|
||||
|
||||
script.onerror = () => {
|
||||
delete globalThis[ pagefindLoaderPromiseKey ]
|
||||
reject( new Error(`Failed to load Pagefind browser module from ${ pagefindScriptURL }`) )
|
||||
}
|
||||
|
||||
document.head.append( script )
|
||||
|
||||
try {
|
||||
resolve( await waitForPagefindGlobal() )
|
||||
} catch ( err ) {
|
||||
delete globalThis[ pagefindLoaderPromiseKey ]
|
||||
reject( err )
|
||||
}
|
||||
} )
|
||||
}
|
||||
|
||||
return await globalThis[ pagefindLoaderPromiseKey ]
|
||||
}
|
||||
|
||||
export class PagefindClient {
|
||||
constructor ( options = {} ) {
|
||||
this.bundlePath = options.bundlePath || pagefindBundleRelativeURL
|
||||
this.pagefind = options.pagefind || null
|
||||
this.debounceMs = options.debounceMs || 100
|
||||
this.cancelCurrentQuery = null
|
||||
}
|
||||
|
||||
setupState = 'not-setup'
|
||||
|
||||
get isSetup () {
|
||||
return this.setupState === 'complete'
|
||||
}
|
||||
|
||||
waitForSetup () {
|
||||
return new Promise( resolve => {
|
||||
if ( this.isSetup ) resolve()
|
||||
|
||||
const timer = setInterval( () => {
|
||||
if ( this.isSetup ) {
|
||||
clearInterval( timer )
|
||||
resolve()
|
||||
}
|
||||
}, 50 )
|
||||
})
|
||||
}
|
||||
|
||||
async loadPagefindScript () {
|
||||
if ( this.pagefind ) return
|
||||
|
||||
const pagefindModule = await loadPagefindBrowserModule()
|
||||
this.pagefind = pagefindModule.default || pagefindModule
|
||||
|
||||
this.pagefind.options({
|
||||
bundlePath: this.bundlePath
|
||||
})
|
||||
}
|
||||
|
||||
async setup () {
|
||||
if ( this.setupState !== 'not-setup' ) {
|
||||
await this.waitForSetup()
|
||||
return
|
||||
}
|
||||
|
||||
this.setupState = 'pending'
|
||||
|
||||
await this.loadPagefindScript()
|
||||
|
||||
if ( typeof this.pagefind.init === 'function' ) {
|
||||
await this.pagefind.init()
|
||||
}
|
||||
|
||||
this.setupState = 'complete'
|
||||
}
|
||||
|
||||
async lazyQuery ( query, {
|
||||
filters = {},
|
||||
sort = {}
|
||||
} = {} ) {
|
||||
const searchOptions = {}
|
||||
|
||||
if ( Object.keys( filters ).length > 0 ) {
|
||||
searchOptions.filters = filters
|
||||
}
|
||||
|
||||
if ( Object.keys( sort ).length > 0 ) {
|
||||
searchOptions.sort = sort
|
||||
}
|
||||
|
||||
const trimmedQuery = query.trim()
|
||||
|
||||
const result = await new Promise( async ( resolve, reject ) => {
|
||||
if ( this.cancelCurrentQuery !== null ) {
|
||||
this.cancelCurrentQuery()
|
||||
}
|
||||
|
||||
this.cancelCurrentQuery = () => { reject({ message: `Cancelled previous query for ${ trimmedQuery }`, canceled: true }) }
|
||||
|
||||
if ( !this.isSetup ) await this.setup()
|
||||
|
||||
if ( trimmedQuery.length === 0 ) {
|
||||
resolve( await this.pagefind.search( null, searchOptions ) )
|
||||
return
|
||||
}
|
||||
|
||||
resolve( await this.pagefind.debouncedSearch( trimmedQuery, searchOptions, this.debounceMs ) )
|
||||
}).catch( err => {
|
||||
console.log('Query rejected', err)
|
||||
return null
|
||||
})
|
||||
|
||||
this.cancelCurrentQuery = null
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
export const pagefindLanguage = 'en'
|
||||
export const pagefindOutputPath = './static/pagefind'
|
||||
export const pagefindBundleRelativeURL = '/pagefind/'
|
||||
export const pagefindScriptURL = `${ pagefindBundleRelativeURL }pagefind.js`
|
||||
export const sitemapEndpointsPath = './static/sitemap-endpoints.json'
|
||||
|
|
@ -1,181 +0,0 @@
|
|||
import fs from 'fs-extra'
|
||||
import * as pagefind from 'pagefind'
|
||||
|
||||
import {
|
||||
isNonEmptyArray,
|
||||
isNonEmptyString
|
||||
} from '~/helpers/check-types.js'
|
||||
import {
|
||||
getRouteType
|
||||
} from '~/helpers/app-derived.js'
|
||||
import {
|
||||
getAppCategory
|
||||
} from '~/helpers/categories.js'
|
||||
import {
|
||||
pagefindLanguage,
|
||||
pagefindOutputPath
|
||||
} from '~/helpers/pagefind/config.js'
|
||||
|
||||
function getSearchListing ( sitemapEntry ) {
|
||||
return sitemapEntry.payload.app || sitemapEntry.payload.listing || sitemapEntry.payload.video || null
|
||||
}
|
||||
|
||||
function pushContentPart ( parts, value ) {
|
||||
if ( !isNonEmptyString( value ) ) return
|
||||
|
||||
parts.push( value.trim() )
|
||||
}
|
||||
|
||||
function pushListContentPart ( parts, values ) {
|
||||
if ( !isNonEmptyArray( values ) ) return
|
||||
|
||||
pushContentPart( parts, values.join(', ') )
|
||||
}
|
||||
|
||||
function normalizeFilterValue ( value ) {
|
||||
if ( !isNonEmptyString( value ) ) return null
|
||||
|
||||
return value.replaceAll('-', '_').trim()
|
||||
}
|
||||
|
||||
export function shouldIndexSitemapEntry ( sitemapEntry ) {
|
||||
return getSearchListing( sitemapEntry ) !== null
|
||||
}
|
||||
|
||||
export function makePagefindTitle ( sitemapEntry ) {
|
||||
const listing = getSearchListing( sitemapEntry )
|
||||
const routeType = getRouteType( sitemapEntry.route )
|
||||
|
||||
let title = listing?.name || sitemapEntry.route
|
||||
|
||||
if ( routeType === 'benchmarks' ) {
|
||||
title = `${ title } Benchmarks`
|
||||
}
|
||||
|
||||
return title
|
||||
}
|
||||
|
||||
export function makePagefindContent ( sitemapEntry ) {
|
||||
const listing = getSearchListing( sitemapEntry )
|
||||
const routeType = getRouteType( sitemapEntry.route )
|
||||
const parts = []
|
||||
|
||||
pushContentPart( parts, makePagefindTitle( sitemapEntry ) )
|
||||
pushContentPart( parts, listing?.text )
|
||||
pushContentPart( parts, listing?.content )
|
||||
pushContentPart( parts, listing?.description )
|
||||
pushListContentPart( parts, listing?.aliases )
|
||||
pushListContentPart( parts, listing?.tags )
|
||||
pushListContentPart( parts, listing?.timestamps?.map( timestamp => timestamp.fullText ) )
|
||||
pushListContentPart( parts, listing?.appLinks?.map( appLink => appLink.name ) )
|
||||
pushContentPart( parts, listing?.category?.label )
|
||||
pushContentPart( parts, listing?.status )
|
||||
|
||||
if ( routeType === 'benchmarks' ) {
|
||||
pushContentPart( parts, 'Benchmarks')
|
||||
pushContentPart( parts, 'Apple Silicon App Tested')
|
||||
}
|
||||
|
||||
return parts.join('\n\n')
|
||||
}
|
||||
|
||||
export function makePagefindFilters ( sitemapEntry ) {
|
||||
const listing = getSearchListing( sitemapEntry )
|
||||
const routeType = getRouteType( sitemapEntry.route )
|
||||
const filters = {
|
||||
type: [ normalizeFilterValue( routeType ) ]
|
||||
}
|
||||
|
||||
const status = normalizeFilterValue( listing?.status )
|
||||
|
||||
if ( status !== null ) {
|
||||
filters.status = [ status ]
|
||||
}
|
||||
|
||||
if ( listing?.category?.slug ) {
|
||||
filters.category = [ getAppCategory( listing ).snakeSlug ]
|
||||
}
|
||||
|
||||
return filters
|
||||
}
|
||||
|
||||
export function mapSitemapEntryToPagefindRecord ( sitemapEntry ) {
|
||||
if ( !shouldIndexSitemapEntry( sitemapEntry ) ) return null
|
||||
|
||||
const listing = getSearchListing( sitemapEntry )
|
||||
const routeType = getRouteType( sitemapEntry.route )
|
||||
const lastUpdatedTimestamp = String( listing?.lastUpdated?.timestamp || 0 )
|
||||
|
||||
return {
|
||||
url: sitemapEntry.route,
|
||||
content: makePagefindContent( sitemapEntry ),
|
||||
language: pagefindLanguage,
|
||||
meta: {
|
||||
title: makePagefindTitle( sitemapEntry ),
|
||||
text: listing?.text || '',
|
||||
slug: listing?.slug || sitemapEntry.route,
|
||||
categorySlug: listing?.category?.slug || 'uncategorized',
|
||||
routeType,
|
||||
lastUpdatedTimestamp
|
||||
},
|
||||
filters: makePagefindFilters( sitemapEntry ),
|
||||
sort: {
|
||||
updated: lastUpdatedTimestamp
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function writePagefindIndex ( sitemapEndpoints, {
|
||||
outputPath = pagefindOutputPath
|
||||
} = {} ) {
|
||||
await fs.remove( outputPath )
|
||||
|
||||
const {
|
||||
errors,
|
||||
index
|
||||
} = await pagefind.createIndex({
|
||||
forceLanguage: pagefindLanguage
|
||||
})
|
||||
|
||||
if ( errors.length > 0 ) {
|
||||
throw new Error(`Pagefind createIndex errors: ${ errors.join(', ') }`)
|
||||
}
|
||||
|
||||
if ( !index ) {
|
||||
throw new Error('Pagefind index was not created')
|
||||
}
|
||||
|
||||
let recordCount = 0
|
||||
|
||||
try {
|
||||
for ( const sitemapEntry of sitemapEndpoints ) {
|
||||
const record = mapSitemapEntryToPagefindRecord( sitemapEntry )
|
||||
|
||||
if ( record === null ) continue
|
||||
|
||||
const response = await index.addCustomRecord( record )
|
||||
|
||||
if ( response.errors.length > 0 ) {
|
||||
throw new Error(`Pagefind addCustomRecord errors for ${ sitemapEntry.route }: ${ response.errors.join(', ') }`)
|
||||
}
|
||||
|
||||
recordCount += 1
|
||||
}
|
||||
|
||||
const writeResponse = await index.writeFiles({
|
||||
outputPath
|
||||
})
|
||||
|
||||
if ( writeResponse.errors.length > 0 ) {
|
||||
throw new Error(`Pagefind writeFiles errors: ${ writeResponse.errors.join(', ') }`)
|
||||
}
|
||||
|
||||
return {
|
||||
outputPath,
|
||||
recordCount
|
||||
}
|
||||
} finally {
|
||||
await index.deleteIndex().catch( () => null )
|
||||
await pagefind.close().catch( () => null )
|
||||
}
|
||||
}
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
export const defaultSearchProvider = 'pagefind'
|
||||
|
||||
export const supportedSearchProviders = new Set([
|
||||
'pagefind',
|
||||
'stork'
|
||||
])
|
||||
|
||||
export function getSearchProvider ( rawProvider = defaultSearchProvider ) {
|
||||
const provider = ( rawProvider || defaultSearchProvider ).toLowerCase()
|
||||
|
||||
if ( supportedSearchProviders.has( provider ) ) {
|
||||
return provider
|
||||
}
|
||||
|
||||
console.warn(`Unknown search provider "${ provider }", falling back to "${ defaultSearchProvider }"`)
|
||||
|
||||
return defaultSearchProvider
|
||||
}
|
||||
|
|
@ -1,119 +0,0 @@
|
|||
import { filterSeparator } from '~/helpers/constants.js'
|
||||
import { isString } from '~/helpers/check-types.js'
|
||||
|
||||
export class SearchFilters {
|
||||
constructor({
|
||||
initialFilters = {}
|
||||
} = {}) {
|
||||
this.initialFilters = initialFilters
|
||||
|
||||
this.filters = {
|
||||
...initialFilters
|
||||
}
|
||||
}
|
||||
|
||||
get list () {
|
||||
return Object.entries( this.filters ).map( ([ filterKey, filterValue ]) => {
|
||||
return `${ filterKey }${ filterSeparator }${ filterValue }`
|
||||
} )
|
||||
}
|
||||
|
||||
get asQuery () {
|
||||
return this.list.join(' ')
|
||||
}
|
||||
|
||||
get asPagefindFilters () {
|
||||
return Object.fromEntries( Object.entries( this.filters ).map( ([ filterKey, filterValue ]) => {
|
||||
return [ filterKey, [ filterValue ] ]
|
||||
}) )
|
||||
}
|
||||
|
||||
getByKey ( key ) {
|
||||
return `${ key }${ filterSeparator }${ this.filters[ key ] }`
|
||||
}
|
||||
|
||||
isQueryValue ( filterNameOrQueryValue ) {
|
||||
return filterNameOrQueryValue.includes( filterSeparator )
|
||||
}
|
||||
|
||||
getKeyAndValue ( filterQueryValue ) {
|
||||
const key = filterQueryValue.substring(0, filterQueryValue.indexOf( filterSeparator ))
|
||||
const value = filterQueryValue.substring(filterQueryValue.indexOf( filterSeparator )+1)
|
||||
|
||||
return { key, value }
|
||||
}
|
||||
|
||||
getFilterNameAndValueFromString ( filterNameOrQueryValue ) {
|
||||
if ( this.isQueryValue( filterNameOrQueryValue ) ) {
|
||||
return this.getKeyAndValue( filterNameOrQueryValue )
|
||||
}
|
||||
|
||||
return {
|
||||
key: filterNameOrQueryValue,
|
||||
value: null
|
||||
}
|
||||
}
|
||||
|
||||
remove ( filterName ) {
|
||||
if ( this.isQueryValue( filterName ) ) throw new Error(`${ filterName } is not a valid filter name`)
|
||||
|
||||
delete this.filters[ filterName ]
|
||||
}
|
||||
|
||||
setFromStringArray ( filterStringArray ) {
|
||||
filterStringArray.forEach( filterString => {
|
||||
const { key, value } = this.getFilterNameAndValueFromString( filterString )
|
||||
|
||||
this.filters[ key ] = value
|
||||
})
|
||||
}
|
||||
|
||||
setFromString ( filterNameOrQueryValue ) {
|
||||
const {
|
||||
key,
|
||||
value = ''
|
||||
} = this.getFilterNameAndValueFromString( filterNameOrQueryValue )
|
||||
|
||||
if ( value.trim().length === 0 ) throw new Error(`${ filterNameOrQueryValue } is not a valid filter value`)
|
||||
|
||||
this.set( key, value )
|
||||
}
|
||||
|
||||
set ( filterName, filterValue ) {
|
||||
if ( this.isQueryValue( filterName ) ) throw new Error(`${ filterName } is not a valid filter name`)
|
||||
|
||||
this.filters[ filterName ] = filterValue
|
||||
}
|
||||
|
||||
toggleFilter ( filterNameOrQueryValue, filterValue = null ) {
|
||||
const fromString = this.getFilterNameAndValueFromString( filterNameOrQueryValue )
|
||||
|
||||
const filterName = fromString.key
|
||||
filterValue = filterValue || fromString.value
|
||||
|
||||
if ( this.has( filterNameOrQueryValue ) ) {
|
||||
this.remove( filterName )
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if ( typeof filterValue !== 'string' ) {
|
||||
throw new Error(`Filter value must be a string. Got ${ typeof filterValue }`)
|
||||
}
|
||||
|
||||
this.set( filterName, filterValue )
|
||||
}
|
||||
|
||||
has ( filterNameOrQueryValue ) {
|
||||
const {
|
||||
key : filterName,
|
||||
value : filterValue = null
|
||||
} = this.getFilterNameAndValueFromString( filterNameOrQueryValue )
|
||||
|
||||
if ( isString( filterValue ) ) {
|
||||
return !!this.filters[ filterName ] && this.filters[ filterName ] === filterValue
|
||||
}
|
||||
|
||||
return !!this.filters[ filterName ]
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import { filterSeparator } from '~/helpers/constants.js'
|
||||
|
||||
import { isString } from '~/helpers/check-types.js'
|
||||
import { SearchFilters } from '~/helpers/search/filters.js'
|
||||
|
||||
import {
|
||||
storkIndexRelativeURL,
|
||||
|
|
@ -275,4 +276,121 @@ export class StorkClient {
|
|||
}
|
||||
}
|
||||
|
||||
export class StorkFilters extends SearchFilters {}
|
||||
export class StorkFilters {
|
||||
constructor({
|
||||
initialFilters = {}
|
||||
} = {}) {
|
||||
this.initialFilters = initialFilters
|
||||
|
||||
this.filters = {
|
||||
...initialFilters
|
||||
}
|
||||
}
|
||||
|
||||
get list () {
|
||||
return Object.entries( this.filters ).map( ([ filterKey, filterValue ]) => {
|
||||
return `${ filterKey }${ filterSeparator }${ filterValue }`
|
||||
} )
|
||||
}
|
||||
|
||||
get asQuery () {
|
||||
return this.list.join(' ')
|
||||
}
|
||||
|
||||
getByKey ( key ) {
|
||||
return `${ key }${ filterSeparator }${ this.filters[ key ] }`
|
||||
}
|
||||
|
||||
isQueryValue ( filterNameOrQueryValue ) {
|
||||
return filterNameOrQueryValue.includes( filterSeparator )
|
||||
}
|
||||
|
||||
getKeyAndValue ( filterQueryValue ) {
|
||||
const key = filterQueryValue.substring(0, filterQueryValue.indexOf( filterSeparator ))
|
||||
const value = filterQueryValue.substring(filterQueryValue.indexOf( filterSeparator )+1)
|
||||
|
||||
return { key, value }
|
||||
}
|
||||
|
||||
getFilterNameAndValueFromString ( filterNameOrQueryValue ) {
|
||||
if ( this.isQueryValue( filterNameOrQueryValue ) ) {
|
||||
return this.getKeyAndValue( filterNameOrQueryValue )
|
||||
}
|
||||
|
||||
return {
|
||||
key: filterNameOrQueryValue,
|
||||
value: null
|
||||
}
|
||||
}
|
||||
|
||||
remove ( filterName ) {
|
||||
// Throw error if it's not a valid filter name
|
||||
if ( this.isQueryValue( filterName ) ) throw new Error(`${ filterName } is not a valid filter name`)
|
||||
|
||||
delete this.filters[ filterName ]
|
||||
}
|
||||
|
||||
setFromStringArray ( filterStringArray ) {
|
||||
filterStringArray.forEach( filterString => {
|
||||
const { key, value } = this.getFilterNameAndValueFromString( filterString )
|
||||
|
||||
this.filters[ key ] = value
|
||||
})
|
||||
}
|
||||
|
||||
setFromString ( filterNameOrQueryValue ) {
|
||||
const {
|
||||
key,
|
||||
value = ''
|
||||
} = this.getFilterNameAndValueFromString( filterNameOrQueryValue )
|
||||
|
||||
// Throw for empty values
|
||||
if ( value.trim().length === 0 ) throw new Error(`${ filterNameOrQueryValue } is not a valid filter value`)
|
||||
|
||||
this.set( key, value )
|
||||
}
|
||||
|
||||
set ( filterName, filterValue ) {
|
||||
// Throw error if it's not a valid filter name
|
||||
if ( this.isQueryValue( filterName ) ) throw new Error(`${ filterName } is not a valid filter name`)
|
||||
|
||||
this.filters[ filterName ] = filterValue
|
||||
}
|
||||
|
||||
toggleFilter ( filterNameOrQueryValue, filterValue = null ) {
|
||||
|
||||
const fromString = this.getFilterNameAndValueFromString( filterNameOrQueryValue )
|
||||
|
||||
const filterName = fromString.key
|
||||
filterValue = filterValue || fromString.value
|
||||
|
||||
// If the filter is already set, remove it
|
||||
if ( this.has( filterNameOrQueryValue ) ) {
|
||||
this.remove( filterName )
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Throw error if filter value is not a string
|
||||
if ( typeof filterValue !== 'string' ) {
|
||||
throw new Error(`Filter value must be a string. Got ${ typeof filterValue }`)
|
||||
}
|
||||
|
||||
this.set( filterName, filterValue )
|
||||
}
|
||||
|
||||
has ( filterNameOrQueryValue ) {
|
||||
|
||||
const {
|
||||
key : filterName,
|
||||
value : filterValue = null
|
||||
} = this.getFilterNameAndValueFromString( filterNameOrQueryValue )
|
||||
|
||||
// If this filter is a name and value, check if it's set
|
||||
if ( isString( filterValue ) ) {
|
||||
return !!this.filters[ filterName ] && this.filters[ filterName ] === filterValue
|
||||
}
|
||||
|
||||
return !!this.filters[ filterName ]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
import fs from 'fs-extra'
|
||||
import execa from 'execa'
|
||||
|
||||
import { isDarwin } from '~/helpers/environment.js'
|
||||
import {
|
||||
storkVersion,
|
||||
storkExecutableName,
|
||||
|
|
@ -10,25 +11,15 @@ import {
|
|||
storkIndexPath
|
||||
} from '~/helpers/stork/config.js'
|
||||
|
||||
// Netlify's Ubuntu 24 (Noble) image needs the OpenSSL 3 compatible binary.
|
||||
export function getStorkExecutableTarget ( {
|
||||
platform = process.platform,
|
||||
arch = process.arch
|
||||
} = {} ) {
|
||||
if ( platform === 'darwin' ) {
|
||||
if ( arch === 'arm64' ) return 'stork-macos-13-arm'
|
||||
|
||||
return 'stork-macos-10-15'
|
||||
}
|
||||
|
||||
return 'stork-ubuntu-22-04'
|
||||
}
|
||||
|
||||
// https://stork-search.net/docs/install
|
||||
export function getStorkExecutableDownloadUrl ( options = {} ) {
|
||||
const target = getStorkExecutableTarget( options )
|
||||
const execDownloadUrls = {
|
||||
darwin: `https://files.stork-search.net/releases/v${ storkVersion }/stork-macos-10-15`,
|
||||
default: `https://files.stork-search.net/releases/v${ storkVersion }/stork-ubuntu-20-04`
|
||||
|
||||
return `https://files.stork-search.net/releases/v${ storkVersion }/${ target }`
|
||||
// Stork 2.0
|
||||
// darwin: `https://files.stork-search.net/releases/v${ storkVersion }/stork-macos-12`,
|
||||
|
||||
// default: `https://files.stork-search.net/releases/v${ storkVersion }/stork-amazon-linux`
|
||||
}
|
||||
|
||||
// Check if a file is executable
|
||||
|
|
@ -42,7 +33,9 @@ async function isExecutable ( path ) {
|
|||
|
||||
// 👩💻 Bash Download example - https://github.com/jmooring/hugo-stork/blob/main/build.sh
|
||||
export async function downloadStorkExecutable () {
|
||||
const execDownloadUrl = getStorkExecutableDownloadUrl()
|
||||
const envKey = isDarwin() ? 'darwin' : 'default'
|
||||
|
||||
const execDownloadUrl = execDownloadUrls[ envKey ]
|
||||
|
||||
// console.log( { execDownloadUrl } )
|
||||
|
||||
|
|
@ -53,7 +46,6 @@ export async function downloadStorkExecutable () {
|
|||
|
||||
// Download the binary
|
||||
await execa( `curl`, [
|
||||
'-fsSL',
|
||||
execDownloadUrl,
|
||||
|
||||
// Set filename
|
||||
|
|
@ -68,7 +60,7 @@ export async function downloadStorkExecutable () {
|
|||
|
||||
|
||||
// console.log( 'isExecutable', isExecutable )
|
||||
if ( !(await isExecutable( storkExecutablePath )) ) throw new Error( `Downloaded binary at ${ storkExecutablePath } is not executable.` )
|
||||
if ( !isExecutable( storkExecutablePath ) ) throw new Error( `Downloaded binary at ${ storkExecutablePath } is not executable.` )
|
||||
|
||||
|
||||
// Check Stork version
|
||||
|
|
@ -86,7 +78,7 @@ export async function downloadStorkExecutable () {
|
|||
|
||||
export async function buildIndex () {
|
||||
|
||||
if ( !(await isExecutable( storkExecutablePath )) ) throw new Error( `Binary at ${ storkExecutablePath } is not executable.` )
|
||||
if ( !isExecutable( storkExecutablePath ) ) throw new Error( `Binary at ${ storkExecutablePath } is not executable.` )
|
||||
|
||||
// Check Stork version
|
||||
// so we know our binary is working
|
||||
|
|
|
|||
55
package.json
55
package.json
|
|
@ -7,7 +7,7 @@
|
|||
"packageManager": "pnpm@10.12.1",
|
||||
"engines": {
|
||||
"pnpm": "^10.*",
|
||||
"node": ">=24",
|
||||
"node": ">=22",
|
||||
"yarn": "forbidden, this project uses pnpm",
|
||||
"npm": "forbidden, this project uses pnpm"
|
||||
},
|
||||
|
|
@ -29,22 +29,17 @@
|
|||
"test-listings": "pnpm run with-env vitest ./test/listings",
|
||||
"test-postbuild-api": "pnpm test-listings",
|
||||
"test-vitest": "vitest",
|
||||
"test": "vitest run",
|
||||
"test:browser": "vitest run --config vitest.playwright.config.mjs",
|
||||
"test:browser:pagefind": "vitest run --config vitest.playwright.config.mjs test/playwright/pagefind-native-filter.playwright.js",
|
||||
"test:browser:pagefind:live": "PLAYWRIGHT_BASE_URL=https://doesitarm.com vitest run --config vitest.playwright.config.mjs test/playwright/pagefind-native-filter.playwright.js",
|
||||
"test": "ava --timeout=1m --verbose",
|
||||
"dev": "pnpm run dev-astro",
|
||||
"build": "pnpm run generate-astro",
|
||||
"build-api": "pnpm run clone-readme && pnpm exec vite-node build-lists.js -- --with-api --no-lists",
|
||||
"build-lists-and-api": "pnpm run clone-readme && pnpm exec vite-node build-lists.js -- --with-api",
|
||||
"generate-dev": "pnpm build && pnpm test",
|
||||
"download-stork:toml": "pnpm exec vite-node scripts/download-stork-toml.js",
|
||||
"download-stork:executable": "pnpm exec vite-node scripts/download-stork-executable.js",
|
||||
"build-api": "pnpm run clone-readme && npx vite-node build-lists.js -- --with-api --no-lists",
|
||||
"build-lists-and-api": "pnpm run clone-readme && npx vite-node build-lists.js -- --with-api",
|
||||
"generate-dev": "pnpm run generate && npm test",
|
||||
"download-stork:toml": "npx vite-node scripts/download-stork-toml.js",
|
||||
"download-stork:executable": "npx vite-node scripts/download-stork-executable.js",
|
||||
"setup-stork": "pnpm run \"/^download-stork:.*/\" --parallel",
|
||||
"build-stork-index": "./$npm_package_config_stork_executable build --input $npm_package_config_stork_toml --output $npm_package_config_stork_index",
|
||||
"build-stork-index-js": "pnpm exec vite-node scripts/build-stork-index.js",
|
||||
"build-pagefind-index": "pnpm exec vite-node scripts/build-pagefind-index.js",
|
||||
"build-search-index": "pnpm exec vite-node scripts/build-search-index.js",
|
||||
"build-stork-index-js": "npx vite-node scripts/build-stork-index.js",
|
||||
"stork-search": "./$npm_package_config_stork_executable search --index $npm_package_config_stork_index --query $1",
|
||||
"stork-index": "pnpm setup-stork && pnpm build-stork-index",
|
||||
"stork-netlify": "chmod +x scripts/stork-netlify.sh && ./scripts/stork-netlify.sh",
|
||||
|
|
@ -59,10 +54,10 @@
|
|||
"scan-new-apps": "pnpm exec vite-node scripts/scan-new-apps.js",
|
||||
"with-env": "dotenv -e .env -- ",
|
||||
"cloudflare-deploy": "pnpm run build-api",
|
||||
"vercel-build": "pnpm exec vite-node scripts/vercel-build.js",
|
||||
"netlify-prebuild:download-sitemaps": "pnpm exec vite-node scripts/download-sitemaps.js",
|
||||
"vercel-build": "npx vite-node scripts/vercel-build.js",
|
||||
"netlify-prebuild:download-sitemaps": "npx vite-node scripts/download-sitemaps.js",
|
||||
"netlify-prebuild:test-prebuild-functions": "pnpm test-prebuild && pnpm test-api-client && pnpm test-listings",
|
||||
"netlify-prebuild": "pnpm run \"/^netlify-prebuild:.*/\" && pnpm build-search-index",
|
||||
"netlify-prebuild": "pnpm run \"/^netlify-prebuild:.*/\" && pnpm stork-index",
|
||||
"netlify-postbuild:test-postbuild-functions": "vitest test/main.test.ts",
|
||||
"netlify-postbuild:test-circular-deps": "madge --circular --extensions js,mjs,ts,vue,astro ./*",
|
||||
"netlify-build": "pnpm run netlify-prebuild && pnpm generate-astro && pnpm run \"/^netlify-postbuild:.*/\"",
|
||||
|
|
@ -70,9 +65,9 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"7z-wasm": "^1.0.0-beta.5",
|
||||
"@astrojs/netlify": "^7.0.2",
|
||||
"@astrojs/partytown": "^2.1.5",
|
||||
"@astrojs/vue": "^6.0.0",
|
||||
"@astrojs/netlify": "^2.6.0",
|
||||
"@astrojs/partytown": "^1.2.3",
|
||||
"@astrojs/vue": "^2.2.1",
|
||||
"@esbuild-plugins/node-globals-polyfill": "^0.1.1",
|
||||
"@esbuild-plugins/node-modules-polyfill": "^0.1.4",
|
||||
"@fontsource/inter": "^4.0.1",
|
||||
|
|
@ -81,7 +76,7 @@
|
|||
"@originjs/vite-plugin-commonjs": "^1.0.3",
|
||||
"@supercharge/promise-pool": "^2.1.0",
|
||||
"@zip.js/zip.js": "^2.5.25",
|
||||
"astro": "^6.0.4",
|
||||
"astro": "^2.10.7",
|
||||
"axios": "^0.21.0",
|
||||
"buffer": "^6.0.3",
|
||||
"can-autoplay": "^3.0.0",
|
||||
|
|
@ -112,7 +107,6 @@
|
|||
"node-html-parser": "^2.0.0",
|
||||
"observe-element-in-viewport": "0.0.15",
|
||||
"ofetch": "^1.0.0",
|
||||
"pagefind": "1.4.0",
|
||||
"plist": "^3.0.1",
|
||||
"pretty-bytes": "^5.5.0",
|
||||
"rollup-plugin-node-polyfills": "^0.2.1",
|
||||
|
|
@ -121,31 +115,32 @@
|
|||
"sitemap": "^7.1.1",
|
||||
"slugify": "^1.4.6",
|
||||
"std-env": "^3.3.2",
|
||||
"terser": "^5.46.0",
|
||||
"terser": "^4.8.0",
|
||||
"uuid": "^8.3.2",
|
||||
"vite-node": "^5.3.0",
|
||||
"vue": "^3.5.30",
|
||||
"vite-node": "^0.34.1",
|
||||
"vue": "^3.2.30",
|
||||
"workerpool": "^6.2.1",
|
||||
"zip-lib": "^0.7.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@astrojs/sitemap": "^3.7.1",
|
||||
"@astrojs/tailwind": "^6.0.2",
|
||||
"@vitest/web-worker": "^4.1.0",
|
||||
"@astrojs/sitemap": "^2.0.1",
|
||||
"@astrojs/tailwind": "^4.0.0",
|
||||
"@vitest/web-worker": "^0.20.3",
|
||||
"autoprefixer": "^10.0.2",
|
||||
"ava": "^6.2.0",
|
||||
"babel-eslint": "^8.2.1",
|
||||
"cssnano": "^4.1.10",
|
||||
"dotenv": "^8.2.0",
|
||||
"dotenv-cli": "^8.0.0",
|
||||
"eslint": "^8.14.0",
|
||||
"eslint-plugin-vue": "^8.7.1",
|
||||
"esm": "^3.2.25",
|
||||
"fast-xml-parser": "^3.19.0",
|
||||
"madge": "^5.0.1",
|
||||
"msw": "^1.2.3",
|
||||
"node-fetch": "^2.6.1",
|
||||
"nodemon": "^1.11.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"playwright-core": "^1.58.2",
|
||||
"postcss": "^8.2.4",
|
||||
"postcss-cli": "^8.3.1",
|
||||
"replace-css-url": "^1.2.6",
|
||||
|
|
@ -153,7 +148,7 @@
|
|||
"tailwindcss": "^3.2.6",
|
||||
"tsconfig-paths": "^3.14.1",
|
||||
"typescript": "^5.7.3",
|
||||
"vite-tsconfig-paths": "^6.1.1",
|
||||
"vitest": "^4.1.0"
|
||||
"vite-tsconfig-paths": "^3.5.0",
|
||||
"vitest": "^2.1.8"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
10149
pnpm-lock.yaml
generated
10149
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
|
|
@ -1,42 +0,0 @@
|
|||
import fs from 'fs-extra'
|
||||
import axios from 'axios'
|
||||
import 'dotenv/config.js'
|
||||
|
||||
import {
|
||||
sitemapEndpointsPath
|
||||
} from '~/helpers/pagefind/config.js'
|
||||
import {
|
||||
writePagefindIndex
|
||||
} from '~/helpers/pagefind/index.js'
|
||||
|
||||
async function loadSitemapEndpoints () {
|
||||
if ( await fs.pathExists( sitemapEndpointsPath ) ) {
|
||||
return await fs.readJson( sitemapEndpointsPath )
|
||||
}
|
||||
|
||||
if ( !process.env.PUBLIC_API_DOMAIN ) {
|
||||
throw new Error(`Missing ${ sitemapEndpointsPath } and PUBLIC_API_DOMAIN is not set`)
|
||||
}
|
||||
|
||||
const apiUrl = new URL( process.env.PUBLIC_API_DOMAIN )
|
||||
apiUrl.pathname = sitemapEndpointsPath.replace(/^\.?\/?static\//, '/')
|
||||
|
||||
const response = await axios.get( apiUrl.toString() )
|
||||
|
||||
return response.data
|
||||
}
|
||||
|
||||
;(async () => {
|
||||
const sitemapEndpoints = await loadSitemapEndpoints()
|
||||
const {
|
||||
outputPath,
|
||||
recordCount
|
||||
} = await writePagefindIndex( sitemapEndpoints )
|
||||
|
||||
console.log(`Built Pagefind index with ${ recordCount } records at ${ outputPath }`)
|
||||
|
||||
process.exit()
|
||||
})().catch( error => {
|
||||
console.error( error )
|
||||
process.exit(1)
|
||||
})
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
import { execSync } from 'child_process'
|
||||
import 'dotenv/config.js'
|
||||
|
||||
import {
|
||||
getSearchProvider
|
||||
} from '~/helpers/search/config.js'
|
||||
|
||||
const searchProvider = getSearchProvider( process.env.PUBLIC_SEARCH_PROVIDER )
|
||||
|
||||
console.log(`Building search index for provider: ${ searchProvider }`)
|
||||
|
||||
if ( searchProvider === 'stork' ) {
|
||||
execSync( 'pnpm stork-index', { stdio: 'inherit' } )
|
||||
process.exit()
|
||||
}
|
||||
|
||||
execSync( 'pnpm build-pagefind-index', { stdio: 'inherit' } )
|
||||
process.exit()
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import fs from 'fs-extra'
|
||||
import 'dotenv/config.js'
|
||||
import 'dotenv/config'
|
||||
import axios from 'axios'
|
||||
|
||||
import {
|
||||
|
|
|
|||
|
|
@ -2,11 +2,11 @@
|
|||
|
||||
# Hugo Bash Example https://github.com/jmooring/hugo-stork/blob/main/build.sh
|
||||
|
||||
# Netlify's Noble/Ubuntu 24 image needs the Ubuntu 22.04 Stork build.
|
||||
# curl -fsSL https://files.stork-search.net/releases/v1.6.0/stork-macos-13-arm -o stork-executable
|
||||
# curl https://files.stork-search.net/releases/latest/stork-amazon-linux -o stork-executable
|
||||
# curl https://files.stork-search.net/releases/v1.4.3/stork-macos-latest -o stork-executable
|
||||
|
||||
curl -fsSL https://files.stork-search.net/releases/v1.6.0/stork-ubuntu-22-04 -o stork-executable
|
||||
# curl -fsSL https://files.stork-search.net/releases/v1.6.0/stork-macos-10-15 -o stork-executable
|
||||
curl https://files.stork-search.net/releases/v1.4.2/stork-amazon-linux -o stork-executable
|
||||
# curl https://files.stork-search.net/releases/v1.4.2/stork-macos-10-15 -o stork-executable
|
||||
|
||||
chmod +x stork-executable
|
||||
./stork-executable build --input static/stork.toml --output static/search-index.st
|
||||
|
|
|
|||
1
src/env.d.ts
vendored
1
src/env.d.ts
vendored
|
|
@ -1,2 +1 @@
|
|||
/// <reference path="../.astro/types.d.ts" />
|
||||
/// <reference types="astro/client" />
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
---
|
||||
// import { getPathPartsFromAstroRequest } from '~/helpers/url.js'
|
||||
import { publicRuntimeConfig } from '~/helpers/public-runtime-config.mjs'
|
||||
import {
|
||||
catchRedirectResponse,
|
||||
applyResponseDefaults
|
||||
|
|
@ -32,7 +31,7 @@ Astro.response.statusText = 'Not found'
|
|||
<Layout
|
||||
headOptions={ {
|
||||
title: `Page is not compatible with Apple Silicon - Does It ARM`,
|
||||
description: `Check for Apple Silicon compatibility for any of your apps instantly before you buy an ${ publicRuntimeConfig.processorsVerbiage } Mac. `,
|
||||
description: `Check for Apple Silicon compatibility for any of your apps instantly before you buy an ${ global.$config.processorsVerbiage } Mac. `,
|
||||
// meta,
|
||||
// link,
|
||||
// structuredData: this.structuredData,
|
||||
|
|
@ -76,3 +75,4 @@ Astro.response.statusText = 'Not found'
|
|||
-->
|
||||
|
||||
</Layout>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
---
|
||||
import { publicRuntimeConfig } from '~/helpers/public-runtime-config.mjs'
|
||||
import {
|
||||
applyResponseDefaults
|
||||
} from '~/helpers/astro/request.js'
|
||||
|
|
@ -23,7 +22,7 @@ applyResponseDefaults( Astro )
|
|||
<Layout
|
||||
headOptions={ {
|
||||
title: `Apple Silicon Compatibility Test Online - Does It ARM`,
|
||||
description: `Check for Apple Silicon compatibility for any of your apps instantly before you buy an ${ publicRuntimeConfig.processorsVerbiage } Mac. `,
|
||||
description: `Check for Apple Silicon compatibility for any of your apps instantly before you buy an ${ global.$config.processorsVerbiage } Mac. `,
|
||||
// meta,
|
||||
// link,
|
||||
// structuredData: this.structuredData,
|
||||
|
|
@ -34,7 +33,7 @@ applyResponseDefaults( Astro )
|
|||
>
|
||||
|
||||
<AppTestPage
|
||||
config={ publicRuntimeConfig }
|
||||
config={ global.$config }
|
||||
client:load
|
||||
/>
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@
|
|||
// https://docs.astro.build/core-concepts/astro-components/
|
||||
|
||||
import { DoesItAPI } from '~/helpers/api/client.js'
|
||||
import { publicRuntimeConfig } from '~/helpers/public-runtime-config.mjs'
|
||||
import {
|
||||
applyResponseDefaults
|
||||
} from '~/helpers/astro/request.js'
|
||||
|
|
@ -118,8 +117,8 @@ for (const video of allVideos) {
|
|||
---
|
||||
<Layout
|
||||
headOptions={ {
|
||||
title: `Benchmarks for ${ publicRuntimeConfig.processorsVerbiage } Processors and Apple Silicon - Does It ARM`,
|
||||
description: `Apple Silicon benchmark, performance, and compatibility videos for Macs using the ${ publicRuntimeConfig.processorsVerbiage } processors.`,
|
||||
title: `Benchmarks for ${ global.$config.processorsVerbiage } Processors and Apple Silicon - Does It ARM`,
|
||||
description: `Apple Silicon benchmark, performance, and compatibility videos for Macs using the ${ global.$config.processorsVerbiage } processors.`,
|
||||
// meta,
|
||||
// link,
|
||||
// structuredData: this.structuredData,
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@
|
|||
// https://docs.astro.build/core-concepts/astro-components/
|
||||
|
||||
import { DoesItAPI } from '~/helpers/api/client.js'
|
||||
import { publicRuntimeConfig } from '~/helpers/public-runtime-config.mjs'
|
||||
import {
|
||||
applyResponseDefaults
|
||||
} from '~/helpers/astro/request.js'
|
||||
|
|
@ -35,7 +34,7 @@ const kinds = Object.values( kindIndex ).map( category => {
|
|||
<Layout
|
||||
headOptions={ {
|
||||
title: 'Categories of App Support lists for Apple Silicon',
|
||||
description: `List of compatibility apps and games for Apple Silicon and the ${ publicRuntimeConfig.processorsVerbiage } Processors including performance reports and benchmarks`,
|
||||
description: `List of compatibility apps and games for Apple Silicon and the ${ global.$config.processorsVerbiage } Processors including performance reports and benchmarks`,
|
||||
// meta,
|
||||
// link,
|
||||
// structuredData: this.structuredData,
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@
|
|||
// https://docs.astro.build/core-concepts/astro-components/
|
||||
|
||||
import { DoesItAPI } from '~/helpers/api/client.js'
|
||||
import { publicRuntimeConfig } from '~/helpers/public-runtime-config.mjs'
|
||||
import {
|
||||
applyResponseDefaults
|
||||
} from '~/helpers/astro/request.js'
|
||||
|
|
@ -32,7 +31,7 @@ const kinds = deviceIndex.items.map( device => {
|
|||
<Layout
|
||||
headOptions={ {
|
||||
title: 'List of Apple Devices for Apple Silicon App Support',
|
||||
description: `List of devices for Apple Silicon and the ${ publicRuntimeConfig.processorsVerbiage } Processors`,
|
||||
description: `List of devices for Apple Silicon and the ${ global.$config.processorsVerbiage } Processors`,
|
||||
// meta,
|
||||
// link,
|
||||
// structuredData: this.structuredData,
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@
|
|||
|
||||
|
||||
import { DoesItAPI } from '~/helpers/api/client.js'
|
||||
import { publicRuntimeConfig } from '~/helpers/public-runtime-config.mjs'
|
||||
import { getPathPartsFromAstroRequest } from '~/helpers/url.js'
|
||||
import {
|
||||
catchRedirectResponse,
|
||||
|
|
@ -66,7 +65,7 @@ const pageLabel = category?.pluralLabel || category.label
|
|||
<Layout
|
||||
headOptions={{
|
||||
title: `List of ${ pageLabel } that work on Apple Silicon?`,
|
||||
description: `Check the latest reported support status of ${ pageLabel } on Apple Silicon and ${ publicRuntimeConfig.processorsVerbiage } Processors. `,
|
||||
description: `Check the latest reported support status of ${ pageLabel } on Apple Silicon and ${ global.$config.processorsVerbiage } Processors. `,
|
||||
// meta,
|
||||
// link,
|
||||
// structuredData: this.structuredData,
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@
|
|||
// https://docs.astro.build/core-concepts/astro-components/
|
||||
|
||||
import { DoesItAPI } from '~/helpers/api/client.js'
|
||||
import { publicRuntimeConfig } from '~/helpers/public-runtime-config.mjs'
|
||||
import {
|
||||
applyResponseDefaults
|
||||
} from '~/helpers/astro/request.js'
|
||||
|
|
@ -30,8 +29,8 @@ const allAppsSummary = await DoesItAPI('all-apps-summary').get()
|
|||
---
|
||||
<Layout
|
||||
headOptions={ {
|
||||
title: `Apple Silicon and ${ publicRuntimeConfig.processorsVerbiage } app and game compatibility list`,
|
||||
description: `List of compatibility apps and games for Apple Silicon and the ${ publicRuntimeConfig.processorsVerbiage } Processors including performance reports and benchmarks`,
|
||||
title: `Apple Silicon and ${ global.$config.processorsVerbiage } app and game compatibility list`,
|
||||
description: `List of compatibility apps and games for Apple Silicon and the ${ global.$config.processorsVerbiage } Processors including performance reports and benchmarks`,
|
||||
// meta,
|
||||
// link,
|
||||
// structuredData: this.structuredData,
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@
|
|||
|
||||
|
||||
import { DoesItAPI } from '~/helpers/api/client.js'
|
||||
import { publicRuntimeConfig } from '~/helpers/public-runtime-config.mjs'
|
||||
import { getPathPartsFromAstroRequest } from '~/helpers/url.js'
|
||||
import {
|
||||
catchRedirectResponse,
|
||||
|
|
@ -91,7 +90,7 @@ const adName = (() => {
|
|||
<Layout
|
||||
headOptions={{
|
||||
title: `List of ${ pageLabel } that work on Apple Silicon?`,
|
||||
description: `Check the latest reported support status of ${ pageLabel } on Apple Silicon and ${ publicRuntimeConfig.processorsVerbiage } Processors. `,
|
||||
description: `Check the latest reported support status of ${ pageLabel } on Apple Silicon and ${ global.$config.processorsVerbiage } Processors. `,
|
||||
// meta,
|
||||
// link,
|
||||
// structuredData: this.structuredData,
|
||||
|
|
|
|||
|
|
@ -1,273 +0,0 @@
|
|||
import { accessSync, constants } from 'node:fs'
|
||||
import { spawn } from 'node:child_process'
|
||||
import net from 'node:net'
|
||||
|
||||
import { chromium } from 'playwright-core'
|
||||
import {
|
||||
afterAll,
|
||||
beforeAll,
|
||||
describe,
|
||||
expect,
|
||||
it
|
||||
} from 'vitest'
|
||||
|
||||
|
||||
const command = process.platform === 'win32' ? 'pnpm.cmd' : 'pnpm'
|
||||
const host = '127.0.0.1'
|
||||
const configuredBaseUrl = process.env.PLAYWRIGHT_BASE_URL || ''
|
||||
|
||||
function canAccessPath ( filePath ) {
|
||||
try {
|
||||
accessSync( filePath, constants.X_OK )
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function getBrowserExecutablePath () {
|
||||
const candidatePaths = [
|
||||
process.env.PLAYWRIGHT_BROWSER_PATH,
|
||||
process.env.CHROME_BIN,
|
||||
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
||||
'/Applications/Chromium.app/Contents/MacOS/Chromium',
|
||||
'/opt/homebrew/bin/chromium',
|
||||
].filter( Boolean )
|
||||
|
||||
const executablePath = candidatePaths.find( canAccessPath )
|
||||
|
||||
if ( !executablePath ) {
|
||||
throw new Error(`No browser executable found. Set PLAYWRIGHT_BROWSER_PATH or CHROME_BIN.`)
|
||||
}
|
||||
|
||||
return executablePath
|
||||
}
|
||||
|
||||
function getAvailablePort () {
|
||||
return new Promise( ( resolve, reject ) => {
|
||||
const server = net.createServer()
|
||||
|
||||
server.unref()
|
||||
server.on( 'error', reject )
|
||||
server.listen( 0, host, () => {
|
||||
const { port } = server.address()
|
||||
server.close( err => {
|
||||
if ( err ) {
|
||||
reject( err )
|
||||
return
|
||||
}
|
||||
|
||||
resolve( port )
|
||||
} )
|
||||
} )
|
||||
} )
|
||||
}
|
||||
|
||||
async function waitForServer ( url, {
|
||||
intervalMs = 250,
|
||||
timeoutMs = 60 * 1000
|
||||
} = {} ) {
|
||||
const startedAt = Date.now()
|
||||
|
||||
while ( Date.now() - startedAt < timeoutMs ) {
|
||||
try {
|
||||
const response = await fetch( url )
|
||||
|
||||
if ( response.ok ) {
|
||||
return
|
||||
}
|
||||
} catch {}
|
||||
|
||||
await new Promise( resolve => setTimeout( resolve, intervalMs ) )
|
||||
}
|
||||
|
||||
throw new Error(`Timed out waiting for dev server at ${ url }`)
|
||||
}
|
||||
|
||||
function stopProcess ( childProcess ) {
|
||||
return new Promise( resolve => {
|
||||
if ( !childProcess ) {
|
||||
resolve()
|
||||
return
|
||||
}
|
||||
|
||||
if ( childProcess.killed || childProcess.exitCode !== null ) {
|
||||
resolve()
|
||||
return
|
||||
}
|
||||
|
||||
childProcess.once( 'exit', () => resolve() )
|
||||
childProcess.kill( 'SIGTERM' )
|
||||
|
||||
setTimeout( () => {
|
||||
if ( childProcess.exitCode === null ) {
|
||||
childProcess.kill( 'SIGKILL' )
|
||||
}
|
||||
}, 5 * 1000 ).unref()
|
||||
} )
|
||||
}
|
||||
|
||||
describe('Pagefind dev search', () => {
|
||||
let browser
|
||||
let devServer
|
||||
let devServerOutput = ''
|
||||
let baseUrl = ''
|
||||
|
||||
beforeAll( async () => {
|
||||
const executablePath = getBrowserExecutablePath()
|
||||
if ( configuredBaseUrl.length > 0 ) {
|
||||
baseUrl = configuredBaseUrl
|
||||
} else {
|
||||
const port = await getAvailablePort()
|
||||
|
||||
baseUrl = `http://${ host }:${ port }`
|
||||
|
||||
devServer = spawn( command, [
|
||||
'exec',
|
||||
'astro',
|
||||
'dev',
|
||||
'--host',
|
||||
host,
|
||||
'--port',
|
||||
String( port )
|
||||
], {
|
||||
cwd: process.cwd(),
|
||||
env: {
|
||||
...process.env,
|
||||
PUBLIC_SEARCH_PROVIDER: 'pagefind'
|
||||
},
|
||||
stdio: [ 'ignore', 'pipe', 'pipe' ]
|
||||
} )
|
||||
|
||||
devServer.stdout.on( 'data', chunk => {
|
||||
devServerOutput += chunk.toString()
|
||||
} )
|
||||
devServer.stderr.on( 'data', chunk => {
|
||||
devServerOutput += chunk.toString()
|
||||
} )
|
||||
}
|
||||
|
||||
await waitForServer( baseUrl )
|
||||
|
||||
browser = await chromium.launch({
|
||||
executablePath,
|
||||
headless: true
|
||||
} )
|
||||
} )
|
||||
|
||||
afterAll( async () => {
|
||||
await browser?.close()
|
||||
await stopProcess( devServer )
|
||||
} )
|
||||
|
||||
it('renders visible Pagefind results when Native Support is clicked', async () => {
|
||||
const page = await browser.newPage()
|
||||
const consoleErrors = []
|
||||
const pageErrors = []
|
||||
const pagefindResponses = []
|
||||
const failedRequests = []
|
||||
let fragmentRequests = 0
|
||||
let failedFragmentRequests = 0
|
||||
|
||||
page.on( 'console', message => {
|
||||
if ( message.type() === 'error' ) {
|
||||
consoleErrors.push( message.text() )
|
||||
}
|
||||
} )
|
||||
|
||||
page.on( 'pageerror', error => {
|
||||
pageErrors.push( error.message )
|
||||
} )
|
||||
|
||||
page.on( 'response', response => {
|
||||
if ( response.url().includes( '/pagefind/pagefind.js' ) ) {
|
||||
pagefindResponses.push({
|
||||
status: response.status(),
|
||||
url: response.url()
|
||||
})
|
||||
}
|
||||
} )
|
||||
|
||||
page.on( 'request', request => {
|
||||
if ( request.url().includes( '/pagefind/' ) && request.url().includes( 'pf_fragment' ) ) {
|
||||
fragmentRequests++
|
||||
}
|
||||
} )
|
||||
|
||||
page.on( 'requestfailed', request => {
|
||||
if ( request.url().includes( '/pagefind/pagefind.js' ) ) {
|
||||
failedRequests.push({
|
||||
errorText: request.failure()?.errorText || 'unknown',
|
||||
url: request.url()
|
||||
})
|
||||
}
|
||||
|
||||
if ( request.url().includes( '/pagefind/' ) && request.url().includes( 'pf_fragment' ) ) {
|
||||
failedFragmentRequests++
|
||||
}
|
||||
} )
|
||||
|
||||
await page.goto( baseUrl, {
|
||||
waitUntil: 'domcontentloaded'
|
||||
} )
|
||||
|
||||
await page.waitForTimeout( 3000 )
|
||||
|
||||
await Promise.all([
|
||||
page.waitForResponse( response => {
|
||||
return response.url().includes( '/pagefind/pagefind.js' )
|
||||
}, {
|
||||
timeout: 10 * 1000
|
||||
} ),
|
||||
page.getByRole( 'button', {
|
||||
name: /native support/i
|
||||
} ).click()
|
||||
])
|
||||
|
||||
await page.waitForFunction( () => {
|
||||
return [ ...document.querySelectorAll( 'li[data-app-slug] h3' ) ].some( node => {
|
||||
const text = node.textContent || ''
|
||||
return text.trim().length > 0 && !/loading/i.test( text )
|
||||
} )
|
||||
}, {
|
||||
timeout: 15 * 1000
|
||||
} )
|
||||
|
||||
const bodyText = await page.locator( 'body' ).textContent()
|
||||
const renderedResults = await page.evaluate( () => {
|
||||
const headings = [ ...document.querySelectorAll( 'li[data-app-slug] h3' ) ].map( node => {
|
||||
return ( node.textContent || '' ).trim()
|
||||
} )
|
||||
|
||||
return {
|
||||
loadingRows: headings.filter( text => /loading/i.test( text ) ).length,
|
||||
rows: document.querySelectorAll( 'li[data-app-slug]' ).length,
|
||||
visibleHeadings: headings.slice( 0, 5 )
|
||||
}
|
||||
} )
|
||||
|
||||
expect( pagefindResponses.some( response => response.status === 200 ), devServerOutput ).toBe( true )
|
||||
expect(
|
||||
pagefindResponses.some( response => response.status >= 400 ),
|
||||
[
|
||||
pagefindResponses.map( response => `${ response.status } ${ response.url }` ).join( '\n' ),
|
||||
failedRequests.map( request => `${ request.errorText } ${ request.url }` ).join( '\n' ),
|
||||
pageErrors.join( '\n' ),
|
||||
consoleErrors.join( '\n' )
|
||||
].join( '\n\n' )
|
||||
).toBe( false )
|
||||
expect( fragmentRequests, JSON.stringify( renderedResults ) ).toBeGreaterThan( 0 )
|
||||
expect( fragmentRequests, JSON.stringify( renderedResults ) ).toBeLessThan( 100 )
|
||||
expect( failedFragmentRequests, JSON.stringify( renderedResults ) ).toBe( 0 )
|
||||
expect( renderedResults.rows, JSON.stringify( renderedResults ) ).toBeGreaterThan( 0 )
|
||||
expect( renderedResults.loadingRows, JSON.stringify( renderedResults ) ).toBe( 0 )
|
||||
expect( bodyText ).not.toContain( 'Failed to load url /pagefind/pagefind.js' )
|
||||
expect( bodyText ).not.toContain( 'No apps found for' )
|
||||
expect( pageErrors.join( '\n' ) ).not.toMatch( /pagefind\/pagefind\.js/ )
|
||||
expect( pageErrors.join( '\n' ) ).not.toMatch( /Failed to fetch/ )
|
||||
expect( consoleErrors.join( '\n' ) ).not.toMatch( /pagefind\/pagefind\.js/ )
|
||||
expect( consoleErrors.join( '\n' ) ).not.toMatch( /ERR_INSUFFICIENT_RESOURCES/ )
|
||||
|
||||
await page.close()
|
||||
} )
|
||||
} )
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import {
|
||||
isBrowserContext,
|
||||
isDarwin,
|
||||
isLinux
|
||||
} from '~/helpers/environment.js'
|
||||
|
||||
describe( 'environment helpers', () => {
|
||||
it( 'does not treat Node 22 navigator as a browser runtime', () => {
|
||||
expect( isBrowserContext() ).toBe( false )
|
||||
})
|
||||
|
||||
it( 'detects darwin directly from process.platform', () => {
|
||||
expect( isDarwin() ).toBe( process.platform === 'darwin' )
|
||||
})
|
||||
|
||||
it( 'detects linux-like runtimes directly from process.platform', () => {
|
||||
expect( isLinux() ).toBe([
|
||||
'linux',
|
||||
'openbsd'
|
||||
].includes( process.platform ) )
|
||||
})
|
||||
})
|
||||
|
|
@ -84,19 +84,5 @@ describe('StorkFilters', () => {
|
|||
filters.setFromString('test_works_yes')
|
||||
expect(filters.asQuery).toBe('test_works_yes')
|
||||
})
|
||||
|
||||
it('should map filters for Pagefind', () => {
|
||||
const filters = new StorkFilters()
|
||||
|
||||
filters.setFromStringArray([
|
||||
'status_native',
|
||||
'category_system_tools'
|
||||
])
|
||||
|
||||
expect(filters.asPagefindFilters).toEqual({
|
||||
status: [ 'native' ],
|
||||
category: [ 'system_tools' ]
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,64 +0,0 @@
|
|||
import { describe, it, expect } from 'vitest'
|
||||
|
||||
import {
|
||||
mapSitemapEntryToPagefindRecord,
|
||||
shouldIndexSitemapEntry
|
||||
} from '~/helpers/pagefind/index.js'
|
||||
|
||||
describe('Pagefind records', () => {
|
||||
it('should skip sitemap entries without searchable payloads', () => {
|
||||
expect( shouldIndexSitemapEntry({
|
||||
route: '/games',
|
||||
payload: {}
|
||||
}) ).toBe( false )
|
||||
})
|
||||
|
||||
it('should map benchmark entries to Pagefind records', () => {
|
||||
const record = mapSitemapEntryToPagefindRecord({
|
||||
route: '/app/example/benchmarks',
|
||||
payload: {
|
||||
app: {
|
||||
name: 'Example App',
|
||||
text: '✅ Native support',
|
||||
content: 'Runs fast on Apple Silicon',
|
||||
aliases: [ 'Example' ],
|
||||
tags: [ 'Utilities' ],
|
||||
status: 'no-in-progress',
|
||||
slug: 'example',
|
||||
category: {
|
||||
slug: 'system-tools',
|
||||
label: 'System Tools'
|
||||
},
|
||||
lastUpdated: {
|
||||
timestamp: 1234567890
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
expect( record ).toMatchObject({
|
||||
url: '/app/example/benchmarks',
|
||||
language: 'en',
|
||||
meta: {
|
||||
title: 'Example App Benchmarks',
|
||||
text: '✅ Native support',
|
||||
slug: 'example',
|
||||
categorySlug: 'system-tools',
|
||||
routeType: 'benchmarks',
|
||||
lastUpdatedTimestamp: '1234567890'
|
||||
},
|
||||
filters: {
|
||||
status: [ 'no_in_progress' ],
|
||||
category: [ 'system_tools' ],
|
||||
type: [ 'benchmarks' ]
|
||||
},
|
||||
sort: {
|
||||
updated: '1234567890'
|
||||
}
|
||||
})
|
||||
|
||||
expect( record.content ).toContain('Example App Benchmarks')
|
||||
expect( record.content ).toContain('Runs fast on Apple Silicon')
|
||||
expect( record.content ).toContain('Apple Silicon App Tested')
|
||||
})
|
||||
})
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import {
|
||||
getStorkExecutableTarget,
|
||||
getStorkExecutableDownloadUrl
|
||||
} from '~/helpers/stork/executable.js'
|
||||
|
||||
describe( 'stork executable selection', () => {
|
||||
it( 'uses the Ubuntu 22.04 binary for Linux builds', () => {
|
||||
expect( getStorkExecutableTarget({
|
||||
platform: 'linux',
|
||||
arch: 'x64'
|
||||
}) ).toBe( 'stork-ubuntu-22-04' )
|
||||
})
|
||||
|
||||
it( 'uses the Apple Silicon macOS binary on arm64 Macs', () => {
|
||||
expect( getStorkExecutableTarget({
|
||||
platform: 'darwin',
|
||||
arch: 'arm64'
|
||||
}) ).toBe( 'stork-macos-13-arm' )
|
||||
})
|
||||
|
||||
it( 'builds the download URL from the selected target', () => {
|
||||
expect( getStorkExecutableDownloadUrl({
|
||||
platform: 'linux',
|
||||
arch: 'x64'
|
||||
}) ).toContain( '/stork-ubuntu-22-04' )
|
||||
})
|
||||
})
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { configDefaults, defineConfig } from 'vitest/config'
|
||||
import { defineConfig } from 'vitest/config'
|
||||
|
||||
import astroConfig from './astro.config.mjs'
|
||||
|
||||
|
|
@ -12,11 +12,7 @@ const vitestConfig = {
|
|||
|
||||
test: {
|
||||
// testTimeout: 60 * 1000,
|
||||
setupFiles: 'tsconfig-paths/register',
|
||||
exclude: [
|
||||
...configDefaults.exclude,
|
||||
'test/_disabled/**'
|
||||
]
|
||||
setupFiles: 'tsconfig-paths/register'
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,23 +0,0 @@
|
|||
import { defineConfig } from 'vitest/config'
|
||||
|
||||
import astroConfig from './astro.config.mjs'
|
||||
|
||||
|
||||
const vitestConfig = {
|
||||
...astroConfig,
|
||||
...astroConfig.vite,
|
||||
test: {
|
||||
setupFiles: 'tsconfig-paths/register',
|
||||
include: [
|
||||
'test/playwright/**/*.playwright.js'
|
||||
],
|
||||
exclude: [
|
||||
'test/_disabled/**'
|
||||
],
|
||||
fileParallelism: false,
|
||||
hookTimeout: 120 * 1000,
|
||||
testTimeout: 120 * 1000
|
||||
}
|
||||
}
|
||||
|
||||
export default defineConfig( vitestConfig )
|
||||
Loading…
Add table
Add a link
Reference in a new issue