Skip to content

Commit 41bf138

Browse files
RISCfutureclaude
andcommitted
chore: normalize build, Sentry, and CI configuration
Applies the approved fleet normalization proposals for this site: - preload-recovery-everywhere: install src/utils/preloadRecovery.ts and call it from main.ts before createApp - sentry-release-and-environment: release/environment in Sentry.init, VITE_SENTRY_RELEASE wired through the deploy build - shared-sentry-ignore-errors: the canonical five-entry ignoreErrors array - stop-publishing-sourcemaps: build.sourcemap 'hidden', and the deploy strips .map files after the Sentry upload and before wrangler deploy - ci-lint-and-unit-parity: CI lint job runs the aggregate pnpm run lint (eslint, stylelint, oxlint, knip) and unit tests run with --run - workflow-hygiene-block: contents:read on CI, deploy concurrency never cancels in flight, submodules checkout dropped, dead ci:build removed - vue-devtools-serve-only: devtools gated on command === 'serve' and launchEditor read from VITE_LAUNCH_EDITOR - manifest-installability: theme_color reconciled with index.html, icon entries that misreport apple-touch-icon.png's size removed - tsconfig-node-target-alignment: extend @tsconfig/node26, drop the nightwatch include, give tsconfig.vitest.json a real lib array Also harmonizes registerServiceWorker() with the fleet template by composing the worker URL and scope from import.meta.env.BASE_URL. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 78dec2a commit 41bf138

10 files changed

Lines changed: 129 additions & 60 deletions

File tree

‎.github/workflows/ci.yml‎

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,3 @@
1-
# This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node
2-
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
3-
41
name: CI
52

63
on:
@@ -9,14 +6,19 @@ on:
96
pull_request:
107
branches: [master]
118

9+
permissions:
10+
contents: read
11+
12+
concurrency:
13+
group: ${{ github.workflow }}-${{ github.ref }}
14+
cancel-in-progress: true
15+
1216
jobs:
1317
unit:
1418
name: Unit Tests
1519
runs-on: ubuntu-latest
1620
steps:
1721
- uses: actions/checkout@v6
18-
with:
19-
submodules: true
2022
- name: Set up pnpm
2123
uses: pnpm/action-setup@v6
2224
- name: Set up Node.js
@@ -27,14 +29,13 @@ jobs:
2729
- name: Install dependencies
2830
run: pnpm install --frozen-lockfile
2931
- name: Run unit tests
30-
run: pnpm run test:unit
32+
run: pnpm run test:unit -- --run
33+
3134
e2e:
3235
name: E2E Tests
3336
runs-on: ubuntu-latest
3437
steps:
3538
- uses: actions/checkout@v6
36-
with:
37-
submodules: true
3839
- name: Set up pnpm
3940
uses: pnpm/action-setup@v6
4041
- name: Set up Node.js
@@ -44,17 +45,22 @@ jobs:
4445
node-version-file: .nvmrc
4546
- name: Install dependencies
4647
run: pnpm install --frozen-lockfile
47-
- name: Install Playwright browsers
48-
run: pnpm exec playwright install --with-deps
48+
- name: Install Playwright system dependencies
49+
run: pnpm exec playwright install-deps
50+
timeout-minutes: 3
51+
- name: Install Playwright Browsers
52+
run: pnpm exec playwright install
53+
timeout-minutes: 3
54+
- name: Build project
55+
run: pnpm run build
4956
- name: Run E2E tests
5057
run: pnpm run test:e2e
58+
5159
lint:
5260
name: Linters
5361
runs-on: ubuntu-latest
5462
steps:
5563
- uses: actions/checkout@v6
56-
with:
57-
submodules: true
5864
- name: Set up pnpm
5965
uses: pnpm/action-setup@v6
6066
- name: Set up Node.js
@@ -64,9 +70,7 @@ jobs:
6470
node-version-file: .nvmrc
6571
- name: Install dependencies
6672
run: pnpm install --frozen-lockfile
67-
- name: Run ESLint
68-
run: pnpm run lint:eslint
69-
- name: Run Stylelint
70-
run: pnpm run lint:stylelint
73+
- name: Run linters
74+
run: pnpm run lint
7175
- name: Type check
7276
run: pnpm run type-check

‎.github/workflows/deploy.yml‎

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,14 @@ on:
1313
permissions:
1414
contents: read
1515

16-
# Allow one concurrent deployment
16+
# Allow one concurrent deployment, and never cancel one in flight
1717
concurrency:
1818
group: deploy
19-
cancel-in-progress: true
19+
cancel-in-progress: false
2020

2121
jobs:
2222
deploy:
23-
name: Build and Deploy
24-
if: ${{ github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success' }}
25-
environment:
26-
name: production
27-
url: https://icrashedmyplane.com/
23+
if: github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success'
2824
runs-on: ubuntu-latest
2925
steps:
3026
- name: Checkout
@@ -44,10 +40,8 @@ jobs:
4440
run: pnpm run build
4541
env:
4642
VITE_SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
47-
- name: Deploy to Cloudflare Workers
48-
run: pnpm exec wrangler deploy
49-
env:
50-
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
43+
VITE_SENTRY_RELEASE: ${{ github.sha }}
44+
# Sourcemaps must reach Sentry BEFORE they are deleted. Do not reorder.
5145
- name: Create Sentry release
5246
uses: getsentry/action-release@v3
5347
env:
@@ -58,3 +52,10 @@ jobs:
5852
environment: production
5953
release: ${{ github.sha }}
6054
sourcemaps: './dist'
55+
# Sourcemaps must not reach the public. Do not reorder.
56+
- name: Strip source maps from the published assets
57+
run: find dist -name '*.map' -delete
58+
- name: Deploy to Cloudflare Workers
59+
run: pnpm exec wrangler deploy
60+
env:
61+
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}

‎package.json‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
"scripts": {
1010
"build": "run-p type-check \"build-only {@}\" --",
1111
"build-only": "vite build",
12-
"ci:build": "run-p type-check \"build-only {@}\" --",
1312
"ci:unit": "vitest --run",
1413
"dev": "vite",
1514
"doc": "npx typedoc --out docs src",
@@ -41,7 +40,7 @@
4140
"@pinia/testing": "^1.0.3",
4241
"@playwright/test": "^1.61.1",
4342
"@testing-library/vue": "^8.1.0",
44-
"@tsconfig/node22": "^22.0.5",
43+
"@tsconfig/node26": "^26.0.1",
4544
"@types/cldrjs": "^0.4.29",
4645
"@types/jsdom": "^28.0.3",
4746
"@types/lodash-es": "^4.17.12",

‎pnpm-lock.yaml‎

Lines changed: 6 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎public/site.webmanifest‎

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"scope": "/",
77
"display": "standalone",
88
"orientation": "portrait",
9-
"theme_color": "#0e6ba8",
9+
"theme_color": "#ffffff",
1010
"background_color": "#ffffff",
1111
"lang": "en",
1212
"dir": "ltr",
@@ -28,18 +28,6 @@
2828
"sizes": "any",
2929
"type": "image/svg+xml",
3030
"purpose": "maskable"
31-
},
32-
{
33-
"src": "/apple-touch-icon.png",
34-
"sizes": "192x192",
35-
"type": "image/png",
36-
"purpose": "any"
37-
},
38-
{
39-
"src": "/apple-touch-icon.png",
40-
"sizes": "512x512",
41-
"type": "image/png",
42-
"purpose": "any"
4331
}
4432
]
4533
}

‎src/main.ts‎

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,11 @@ import { createApp } from 'vue'
99
import { createPinia } from 'pinia'
1010
import * as Sentry from '@sentry/vue'
1111
import i18n from '@/i18n'
12+
import { recoverFromPreloadErrors } from '@/utils/preloadRecovery'
1213
import App from './App.vue'
1314

15+
recoverFromPreloadErrors()
16+
1417
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument -- .vue default export is untyped
1518
const app = createApp(App)
1619

@@ -22,6 +25,9 @@ const sentryDSN = import.meta.env.VITE_SENTRY_DSN as string | undefined
2225
Sentry.init({
2326
app,
2427
dsn: sentryDSN,
28+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/prefer-nullish-coalescing -- untyped Vite env; an unset release arrives as an empty string, which `??` would forward
29+
release: import.meta.env.VITE_SENTRY_RELEASE || undefined,
30+
environment: import.meta.env.PROD ? 'production' : 'development',
2531
sendDefaultPii: false,
2632
integrations: [
2733
Sentry.vueIntegration({
@@ -32,6 +38,30 @@ Sentry.init({
3238
],
3339
tracesSampleRate: 1.0,
3440
enableLogs: true,
41+
ignoreErrors: [
42+
// Browser-extension content scripts inject WebExtension messaging into
43+
// the page; their failures are not our code and are unfixable here.
44+
// Sentry TIM-DOT-CODES-6.
45+
/runtime\.sendMessage/u,
46+
// vite-plugin-pwa's injected SW registration throws InvalidStateError
47+
// when Chrome registers during prerender. No elegant in-plugin or
48+
// newer-version fix exists, so we filter the noise. Sentry
49+
// TIM-DOT-CODES-5.
50+
/Failed to register a ServiceWorker/u,
51+
// Native in-app browsers (WKWebView wrappers) inject a bridge script that
52+
// calls `window.webkit.messageHandlers`; it throws when that handler is
53+
// absent. Not our code and unfixable here. Sentry TIM-DOT-CODES-8.
54+
/messageHandlers/u,
55+
// Android WebView tears down its JS bridge mid-post, so a `postMessage`
56+
// from the injected bridge rejects with "Java object is gone". Not our
57+
// code and unfixable here. Sentry RACCOONBETS-FRONTEND-D.
58+
/Java object is gone/u,
59+
// Microsoft's Outlook SafeLinks crawler rejects a promise from its own
60+
// injected instrumentation while previewing a link. It arrives without a
61+
// stacktrace from an Azure address, never from a visitor. Sentry
62+
// TIM-DOT-CODES-C.
63+
/Object Not Found Matching Id/u,
64+
],
3565
})
3666

3767
/**
@@ -40,15 +70,18 @@ Sentry.init({
4070
* A failed registration costs offline caching and nothing else, so the
4171
* rejection is logged rather than left to surface as an unhandled error.
4272
*/
43-
function registerServiceWorker() {
73+
function registerServiceWorker(): void {
4474
if (!('serviceWorker' in navigator)) return
4575

4676
window.addEventListener('load', () => {
47-
navigator.serviceWorker.register('/sw.js', { scope: '/' }).catch((error: unknown) => {
48-
Sentry.logger.warn('Service worker registration failed', {
49-
reason: error instanceof Error ? error.message : String(error),
77+
const swURL = `${import.meta.env.BASE_URL}sw.js`
78+
navigator.serviceWorker
79+
.register(swURL, { scope: import.meta.env.BASE_URL })
80+
.catch((error: unknown) => {
81+
Sentry.logger.warn('Service worker registration failed', {
82+
reason: error instanceof Error ? error.message : String(error),
83+
})
5084
})
51-
})
5285
})
5386
}
5487

‎src/utils/preloadRecovery.ts‎

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// A tab may spend one reload per cooldown recovering a failed chunk. Beyond that, an asset that
2+
// is genuinely unreachable would put the tab in a refresh loop.
3+
const RELOAD_KEY = 'preload-error-reload'
4+
const RELOAD_COOLDOWN_MS = 10 * 60 * 1000
5+
6+
/**
7+
* Reloads the page when a lazily-loaded chunk can't be fetched.
8+
*
9+
* Vite's preload helper rejects when a code-split chunk or its stylesheet fails to load — a
10+
* dropped request, or a tab whose `index.html` predates the current deploy and so asks for asset
11+
* hashes that no longer exist. Nothing downstream recovers on its own, so the reload fetches a
12+
* fresh `index.html` and with it the current hashes.
13+
*
14+
* Reloads are limited to one per cooldown: a deploy that really is broken surfaces the failure to
15+
* error reporting rather than refreshing forever, while a tab left open across several deploys
16+
* still heals each time.
17+
*/
18+
export function recoverFromPreloadErrors(): void {
19+
window.addEventListener('vite:preloadError', reloadOnce)
20+
}
21+
22+
function reloadOnce(event: VitePreloadErrorEvent): void {
23+
if (!claimReload()) return
24+
25+
// Suppresses Vite's rethrow: the reload is the recovery, so the rejection isn't worth
26+
// reporting. A failure that can't claim a reload stays unprevented and reaches Sentry.
27+
event.preventDefault()
28+
window.location.reload()
29+
}
30+
31+
function claimReload(): boolean {
32+
try {
33+
const lastReload = Number(window.sessionStorage.getItem(RELOAD_KEY) ?? 0)
34+
if (Number.isFinite(lastReload) && Date.now() - lastReload < RELOAD_COOLDOWN_MS) return false
35+
36+
window.sessionStorage.setItem(RELOAD_KEY, String(Date.now()))
37+
return true
38+
} catch {
39+
// sessionStorage may be unavailable (private mode, etc.). With nowhere to record the attempt
40+
// a reload could loop, so decline it.
41+
return false
42+
}
43+
}

‎tsconfig.node.json‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
{
2-
"extends": "@tsconfig/node22/tsconfig.json",
3-
"include": ["vite.config.*", "vitest.config.*", "nightwatch.conf.*", "playwright.config.*"],
2+
"extends": "@tsconfig/node26/tsconfig.json",
3+
"include": ["vite.config.*", "vitest.config.*", "playwright.config.*"],
44
"compilerOptions": {
55
"noEmit": true,
66
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
77

8+
"strict": true,
89
"module": "ESNext",
910
"moduleResolution": "Bundler",
1011
"types": ["node"]

‎tsconfig.vitest.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"exclude": [],
55
"compilerOptions": {
66
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.vitest.tsbuildinfo",
7-
"lib": [],
7+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
88
"types": ["node", "jsdom"],
99
"paths": {
1010
"@/*": ["./src/*"],

‎vite.config.ts‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@ import vueDevTools from 'vite-plugin-vue-devtools'
66
import { VitePWA } from 'vite-plugin-pwa'
77

88
// https://vite.dev/config/
9-
export default defineConfig(() => {
9+
export default defineConfig(({ command }) => {
1010
return {
1111
plugins: [
1212
vue(),
13-
vueDevTools({ launchEditor: 'rubymine' }),
13+
command === 'serve' && vueDevTools({ launchEditor: process.env.VITE_LAUNCH_EDITOR }),
1414
VitePWA({
1515
registerType: 'autoUpdate',
1616
manifest: false,
@@ -27,14 +27,14 @@ export default defineConfig(() => {
2727
skipWaiting: true,
2828
},
2929
}),
30-
],
30+
].filter(Boolean),
3131
resolve: {
3232
alias: {
3333
'@': fileURLToPath(new URL('./src', import.meta.url)),
3434
},
3535
},
3636
build: {
37-
sourcemap: true,
37+
sourcemap: 'hidden',
3838
},
3939
}
4040
})

0 commit comments

Comments
 (0)