voidflow 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +156 -0
- package/package.json +31 -0
- package/playwright/index.d.mts +20 -0
- package/playwright/index.mjs +69 -0
- package/scripts/unlighthouse/run.ts +127 -0
- package/scripts/unlighthouse/unlighthouse.config.example.ts +23 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Tara \X/ Void
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# voidflow
|
|
2
|
+
|
|
3
|
+
Semi-opinionated GitHub Actions workflows for my personal sites and consultancy shop [RavenFlight Industries, LLC](https://rvnflt.com/github)
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
Point `uses` in your configuration to a workflow, e.g. in your `.github/workflows/ci.yml`
|
|
8
|
+
|
|
9
|
+
```yaml
|
|
10
|
+
name: CI
|
|
11
|
+
on:
|
|
12
|
+
pull_request:
|
|
13
|
+
branches: [main, next, live]
|
|
14
|
+
types: [opened, synchronize, reopened, ready_for_review]
|
|
15
|
+
workflow_dispatch:
|
|
16
|
+
|
|
17
|
+
jobs:
|
|
18
|
+
validate:
|
|
19
|
+
uses: taraxvoid/voidflow/.github/workflows/site-ci.yml@main
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### Non-GitHub / self-hosted runners
|
|
23
|
+
|
|
24
|
+
Pass `runner` (defaults to GitHub runner `ubuntu-latest`)
|
|
25
|
+
|
|
26
|
+
```yaml
|
|
27
|
+
jobs:
|
|
28
|
+
validate:
|
|
29
|
+
uses: taraxvoid/voidflow/.github/workflows/site-ci.yml@main
|
|
30
|
+
with:
|
|
31
|
+
runner: my-hosted-runner
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Workflow Types
|
|
35
|
+
|
|
36
|
+
### Validation / Checks
|
|
37
|
+
|
|
38
|
+
Runs lint, typecheck, check for GPL licenses, e2e with Playwright
|
|
39
|
+
|
|
40
|
+
### Unlighthouse (performance / SEO budgets)
|
|
41
|
+
|
|
42
|
+
`site-ci.yml` runs `bun run test:e2e:lighthouse` on PRs into `live`. Sites
|
|
43
|
+
implement that script with the shared runner here, which serves the built
|
|
44
|
+
static directory on a free port and runs
|
|
45
|
+
[Unlighthouse](https://unlighthouse.dev/) against it, reusing Playwright's
|
|
46
|
+
Chromium. It exits non-zero if a category budget in the site's config fails.
|
|
47
|
+
|
|
48
|
+
In the site's `package.json` (pin the tag):
|
|
49
|
+
|
|
50
|
+
```json
|
|
51
|
+
"test:e2e:lighthouse": "bun run build && bunx --package @taraxvoid/voidflow@0.6.0 unlighthouse-runner --dir dist"
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Options: `--dir` (default `dist`, use `dist/client` for Cloudflare adapter
|
|
55
|
+
builds), `--config` (default `unlighthouse.config.ts`), `--version` (pinned
|
|
56
|
+
Unlighthouse version). Start from
|
|
57
|
+
[`scripts/unlighthouse/unlighthouse.config.example.ts`](scripts/unlighthouse/unlighthouse.config.example.ts)
|
|
58
|
+
and add `.unlighthouse/` to the site's `.gitignore`.
|
|
59
|
+
|
|
60
|
+
### Playwright preview config
|
|
61
|
+
|
|
62
|
+
Shared `playwright.config` for Astro sites that run e2e against
|
|
63
|
+
`astro preview`. It picks a free port outside CI, sets
|
|
64
|
+
`ASTRO_PREVIEW_BACKGROUND=false` (Astro backgrounds `preview` in agent
|
|
65
|
+
environments, which makes Playwright think the server exited), and drops the
|
|
66
|
+
`webServer` when `PLAYWRIGHT_BASE_URL` is set.
|
|
67
|
+
|
|
68
|
+
```sh
|
|
69
|
+
bun add -d @taraxvoid/voidflow # or: pnpm add -D @taraxvoid/voidflow
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
```js
|
|
73
|
+
// playwright.config.js
|
|
74
|
+
import { definePreviewConfig } from '@taraxvoid/voidflow/playwright'
|
|
75
|
+
|
|
76
|
+
export default definePreviewConfig({ ciPort: 4141 })
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Options: `ciPort` (required in CI), `runner` (default `bun`, e.g. `pnpm`),
|
|
80
|
+
`timeout` (default 30000), `projects` (default Pixel 7 and Desktop Chrome),
|
|
81
|
+
`testDir` (default `./test/e2e`). Requires `@playwright/test` in the site.
|
|
82
|
+
|
|
83
|
+
## Deployments
|
|
84
|
+
|
|
85
|
+
Example deployment job added to your workflow above. `resolve-env` maps the
|
|
86
|
+
branch to dev/staging/prod once, up front, so its output can be used both to
|
|
87
|
+
drive the deploy and to label the job (`Deploy [dev]`, `Deploy [staging]`,
|
|
88
|
+
`Deploy [prod]`) — a job's `name:` can reference `needs.<job>.outputs.*` but
|
|
89
|
+
not a step output from within itself, hence the split:
|
|
90
|
+
|
|
91
|
+
```yaml
|
|
92
|
+
jobs:
|
|
93
|
+
resolve-env:
|
|
94
|
+
needs: validate
|
|
95
|
+
if: github.event_name == 'push'
|
|
96
|
+
uses: taraxvoid/voidflow/.github/workflows/resolve-env.yml@main
|
|
97
|
+
|
|
98
|
+
deploy:
|
|
99
|
+
name: Deploy [${{ needs.resolve-env.outputs.env }}]
|
|
100
|
+
needs: resolve-env
|
|
101
|
+
if: github.event_name == 'push'
|
|
102
|
+
environment: ${{ needs.resolve-env.outputs.env }}
|
|
103
|
+
steps:
|
|
104
|
+
- uses: actions/checkout@v7.0.1
|
|
105
|
+
- uses: taraxvoid/voidflow/actions/cloudflare-deploy@main
|
|
106
|
+
with:
|
|
107
|
+
env: ${{ needs.resolve-env.outputs.env }}
|
|
108
|
+
cloudflare-api-token: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Versioning
|
|
112
|
+
|
|
113
|
+
This project uses semver, released manually in two steps (`main` requires PRs, so nothing
|
|
114
|
+
can push straight to it — not even Actions):
|
|
115
|
+
|
|
116
|
+
1. Dispatch [`release.yml`](.github/workflows/release.yml) (`workflow_dispatch`, with a
|
|
117
|
+
`version` input) — it regenerates [`CHANGELOG.md`](CHANGELOG.md) via git-cliff, validates,
|
|
118
|
+
and opens a `chore(release): prepare for vX.Y.Z` PR.
|
|
119
|
+
2. Merging that PR triggers [`tag-release.yml`](.github/workflows/tag-release.yml), which
|
|
120
|
+
tags the merge commit and publishes the GitHub release. (Tag pushes aren't covered by
|
|
121
|
+
`main`'s branch protection, only branch pushes are.)
|
|
122
|
+
|
|
123
|
+
Commit messages should follow [Conventional Commits](https://www.conventionalcommits.org) —
|
|
124
|
+
enforced loosely, as an advisory `commit-msg` hint (see
|
|
125
|
+
[`scripts/check-commit-msg.sh`](scripts/check-commit-msg.sh)), not a blocking check. They
|
|
126
|
+
drive the generated changelog.
|
|
127
|
+
|
|
128
|
+
Treat `@main` as unstable. Consumers should pin to an exact release tag, e.g. `@v0.3.0` —
|
|
129
|
+
there is no floating major-version tag (`@v1`) to track yet.
|
|
130
|
+
|
|
131
|
+
## Self-validation
|
|
132
|
+
|
|
133
|
+
This repo validates its own workflows and composite actions (`.github/workflows/ci.yml`):
|
|
134
|
+
|
|
135
|
+
- **actionlint** — workflow schema/logic; also shellchecks `run:` steps in `.github/workflows/*.yml` automatically (`shellcheck` ships on `ubuntu-latest`)
|
|
136
|
+
- **check-jsonschema** — schema-checks `actions/**/action.yml` and the workflow files against [SchemaStore](https://www.schemastore.org/)'s `github-action.json` / `github-workflow.json`
|
|
137
|
+
- **shellcheck** (`scripts/shellcheck-actions.sh`) — checks `run:` steps inside `actions/**/action.yml`, which actionlint doesn't reach
|
|
138
|
+
- **zizmor** — security audit (unpinned refs, script injection via `${{ }}` in `run:`, excess permissions, etc.)
|
|
139
|
+
|
|
140
|
+
### Local dev setup
|
|
141
|
+
|
|
142
|
+
```sh
|
|
143
|
+
brew install just lefthook actionlint shellcheck yq act uv
|
|
144
|
+
lefthook install
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
- `just validate` — run everything CI runs, locally
|
|
148
|
+
- `just lint-workflows` / `lint-actions` / `lint-shell` / `security` — run one check at a time
|
|
149
|
+
- `just dry-run` — full local run of `ci.yml` via [`act`](https://github.com/nektos/act) (needs Docker running); pass extra args, e.g. `just dry-run -j validation`
|
|
150
|
+
- lefthook runs the fast checks (`lint-workflows`, `lint-actions`, `lint-shell`) on `pre-commit`, and `zizmor` on `pre-push`
|
|
151
|
+
|
|
152
|
+
**Colima users:** `.actrc` already disables the docker-socket mount (`--container-daemon-socket -`) — `act` binds `/var/run/docker.sock` by default, which doesn't exist under Colima and fails with `operation not supported`. None of these workflow steps need docker-in-docker, so this is safe.
|
|
153
|
+
|
|
154
|
+
**Run `just dry-run` from a normal checkout, not a linked git worktree.** `act` copies the working tree via `docker cp` rather than a real clone, so a worktree's `.git` pointer back to the parent repo doesn't resolve inside the container — `git ls-files`-based steps (schema validation, `scripts/shellcheck-actions.sh`) silently see zero files instead of erroring. Verified clean end-to-end (`🏁 Job succeeded`) from a plain clone.
|
|
155
|
+
|
|
156
|
+
YMMV, caveat emptor, your satisfaction **not** guaranteed
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "voidflow",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"description": "Shared workflows, actions and tooling for @taraxvoid sites",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/taraxvoid/voidflow.git"
|
|
9
|
+
},
|
|
10
|
+
"publishConfig": {
|
|
11
|
+
"access": "public",
|
|
12
|
+
"provenance": true
|
|
13
|
+
},
|
|
14
|
+
"bin": {
|
|
15
|
+
"unlighthouse-runner": "scripts/unlighthouse/run.ts"
|
|
16
|
+
},
|
|
17
|
+
"exports": {
|
|
18
|
+
"./playwright": {
|
|
19
|
+
"types": "./playwright/index.d.mts",
|
|
20
|
+
"default": "./playwright/index.mjs"
|
|
21
|
+
},
|
|
22
|
+
"./package.json": "./package.json"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"playwright",
|
|
26
|
+
"scripts/unlighthouse"
|
|
27
|
+
],
|
|
28
|
+
"peerDependencies": {
|
|
29
|
+
"@playwright/test": ">=1.40"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { PlaywrightTestConfig, Project } from '@playwright/test'
|
|
2
|
+
|
|
3
|
+
export interface PreviewConfigOptions {
|
|
4
|
+
/** Fixed port used when `CI` is set. Required in CI. */
|
|
5
|
+
ciPort?: number
|
|
6
|
+
/** Package manager that runs the `preview` script. Default `bun`. */
|
|
7
|
+
runner?: 'bun' | 'pnpm' | 'npm' | 'yarn' | (string & {})
|
|
8
|
+
/** Test and web server timeout in ms. Default 30000. */
|
|
9
|
+
timeout?: number
|
|
10
|
+
/** Playwright projects. Default: mobile-chrome (Pixel 7) and desktop-chrome. */
|
|
11
|
+
projects?: Project[]
|
|
12
|
+
/** Default `./test/e2e`. */
|
|
13
|
+
testDir?: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const defaultProjects: Project[]
|
|
17
|
+
|
|
18
|
+
export function definePreviewConfig(
|
|
19
|
+
options?: PreviewConfigOptions,
|
|
20
|
+
): PlaywrightTestConfig
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Shared Playwright config for Astro sites that e2e against `astro preview`.
|
|
3
|
+
*
|
|
4
|
+
* import { definePreviewConfig } from '@taraxvoid/voidflow/playwright'
|
|
5
|
+
* export default definePreviewConfig({ ciPort: 4141 })
|
|
6
|
+
*/
|
|
7
|
+
import { createServer } from 'node:net'
|
|
8
|
+
import { devices } from '@playwright/test'
|
|
9
|
+
|
|
10
|
+
function getFreePort() {
|
|
11
|
+
return new Promise((resolve, reject) => {
|
|
12
|
+
const srv = createServer()
|
|
13
|
+
srv.unref()
|
|
14
|
+
srv.on('error', reject)
|
|
15
|
+
srv.listen(0, () => {
|
|
16
|
+
const { port } = srv.address()
|
|
17
|
+
srv.close(() => resolve(port))
|
|
18
|
+
})
|
|
19
|
+
})
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Worktrees run tests concurrently, so a fixed port would collide (or reuse
|
|
23
|
+
// another worktree's server). Outside CI, ask the OS for a free port. The
|
|
24
|
+
// result is cached in PLAYWRIGHT_TEST_PORT because the config is reloaded in
|
|
25
|
+
// the runner and in every worker; each reload must agree on one port.
|
|
26
|
+
if (!process.env.CI && !process.env.PLAYWRIGHT_TEST_PORT) {
|
|
27
|
+
process.env.PLAYWRIGHT_TEST_PORT = String(await getFreePort())
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const defaultProjects = [
|
|
31
|
+
{ name: 'mobile-chrome', use: devices['Pixel 7'] },
|
|
32
|
+
{ name: 'desktop-chrome', use: devices['Desktop Chrome'] },
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
export function definePreviewConfig({
|
|
36
|
+
ciPort,
|
|
37
|
+
runner = 'bun',
|
|
38
|
+
timeout = 30_000,
|
|
39
|
+
projects = defaultProjects,
|
|
40
|
+
testDir = './test/e2e',
|
|
41
|
+
} = {}) {
|
|
42
|
+
if (process.env.CI && ciPort === undefined) {
|
|
43
|
+
throw new Error('definePreviewConfig: ciPort is required in CI')
|
|
44
|
+
}
|
|
45
|
+
const port = process.env.CI ? ciPort : process.env.PLAYWRIGHT_TEST_PORT
|
|
46
|
+
const external = process.env.PLAYWRIGHT_BASE_URL
|
|
47
|
+
const baseURL = external ?? `http://localhost:${port}`
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
testDir,
|
|
51
|
+
timeout,
|
|
52
|
+
fullyParallel: true,
|
|
53
|
+
use: { baseURL, serviceWorkers: 'block' },
|
|
54
|
+
// Set PLAYWRIGHT_BASE_URL to test a deployed site or preview instead.
|
|
55
|
+
webServer: external
|
|
56
|
+
? undefined
|
|
57
|
+
: {
|
|
58
|
+
command: `${runner} run preview --port ${port}`,
|
|
59
|
+
url: baseURL,
|
|
60
|
+
reuseExistingServer: !process.env.CI,
|
|
61
|
+
timeout,
|
|
62
|
+
// Astro backgrounds `astro preview` in agentic environments
|
|
63
|
+
// (CLAUDECODE, AI_AGENT), which makes Playwright think the
|
|
64
|
+
// server exited early. Any non-empty value skips detection.
|
|
65
|
+
env: { ASTRO_PREVIEW_BACKGROUND: 'false' },
|
|
66
|
+
},
|
|
67
|
+
projects,
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/*
|
|
3
|
+
* Shared Unlighthouse CI runner for the Astro/bun site stack.
|
|
4
|
+
*
|
|
5
|
+
* Serves a built static directory on a free local port, then runs
|
|
6
|
+
* `unlighthouse-ci` against it with the caller's config file and exits with
|
|
7
|
+
* its status (non-zero when a category budget fails). Reuses the Chromium that
|
|
8
|
+
* Playwright already installed, so no second Chrome download.
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* bun run.ts [--dir dist] [--config unlighthouse.config.ts] [--version 0.19.0]
|
|
12
|
+
*
|
|
13
|
+
* Paths are relative to the current working directory (the caller's repo root).
|
|
14
|
+
*/
|
|
15
|
+
import { spawn } from 'node:child_process'
|
|
16
|
+
import { existsSync, readdirSync, statSync } from 'node:fs'
|
|
17
|
+
import { homedir } from 'node:os'
|
|
18
|
+
import { join, resolve } from 'node:path'
|
|
19
|
+
import { parseArgs } from 'node:util'
|
|
20
|
+
|
|
21
|
+
const { values } = parseArgs({
|
|
22
|
+
options: {
|
|
23
|
+
dir: { type: 'string', default: 'dist' },
|
|
24
|
+
config: { type: 'string', default: 'unlighthouse.config.ts' },
|
|
25
|
+
version: { type: 'string', default: '0.19.0' },
|
|
26
|
+
},
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
const dir = resolve(values.dir as string)
|
|
30
|
+
const config = resolve(values.config as string)
|
|
31
|
+
|
|
32
|
+
if (!existsSync(dir)) {
|
|
33
|
+
console.error(`dist directory not found: ${dir} (run the build first)`)
|
|
34
|
+
process.exit(2)
|
|
35
|
+
}
|
|
36
|
+
if (!existsSync(config)) {
|
|
37
|
+
console.error(`unlighthouse config not found: ${config}`)
|
|
38
|
+
process.exit(2)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function findPlaywrightChromium(): string | null {
|
|
42
|
+
const base =
|
|
43
|
+
process.platform === 'darwin'
|
|
44
|
+
? join(homedir(), 'Library', 'Caches', 'ms-playwright')
|
|
45
|
+
: process.platform === 'win32'
|
|
46
|
+
? join(homedir(), 'AppData', 'Local', 'ms-playwright')
|
|
47
|
+
: join(homedir(), '.cache', 'ms-playwright')
|
|
48
|
+
if (!existsSync(base)) return null
|
|
49
|
+
|
|
50
|
+
const relPaths = [
|
|
51
|
+
'chrome-linux64/chrome',
|
|
52
|
+
'chrome-linux/chrome',
|
|
53
|
+
'chrome-mac-arm64/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing',
|
|
54
|
+
'chrome-mac/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing',
|
|
55
|
+
'chrome-win64/chrome.exe',
|
|
56
|
+
'chrome-win/chrome.exe',
|
|
57
|
+
]
|
|
58
|
+
for (const entry of readdirSync(base, { withFileTypes: true })) {
|
|
59
|
+
if (!entry.isDirectory()) continue
|
|
60
|
+
for (const rel of relPaths) {
|
|
61
|
+
const candidate = join(base, entry.name, rel)
|
|
62
|
+
if (existsSync(candidate)) return candidate
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return null
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Resolve a request path to a file the way static hosts do: exact file,
|
|
69
|
+
// then <path>/index.html, then <path>.html.
|
|
70
|
+
function resolveFile(pathname: string): string | null {
|
|
71
|
+
const rel = decodeURIComponent(pathname).replace(/^\/+/, '')
|
|
72
|
+
const candidates = [rel, join(rel, 'index.html'), `${rel}.html`]
|
|
73
|
+
for (const c of candidates) {
|
|
74
|
+
const full = resolve(dir, c)
|
|
75
|
+
if (!full.startsWith(`${dir}/`) && full !== dir) continue
|
|
76
|
+
if (existsSync(full) && statSync(full).isFile()) return full
|
|
77
|
+
}
|
|
78
|
+
return null
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const server = Bun.serve({
|
|
82
|
+
port: 0,
|
|
83
|
+
fetch(req) {
|
|
84
|
+
const file = resolveFile(new URL(req.url).pathname)
|
|
85
|
+
if (file) return new Response(Bun.file(file))
|
|
86
|
+
const notFound = resolveFile('/404.html')
|
|
87
|
+
return notFound
|
|
88
|
+
? new Response(Bun.file(notFound), { status: 404 })
|
|
89
|
+
: new Response('Not found', { status: 404 })
|
|
90
|
+
},
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
const site = `http://localhost:${server.port}`
|
|
94
|
+
const chrome = findPlaywrightChromium()
|
|
95
|
+
const env: Record<string, string | undefined> = { ...process.env }
|
|
96
|
+
if (chrome) {
|
|
97
|
+
env.CHROME_PATH = chrome
|
|
98
|
+
env.PUPPETEER_EXECUTABLE_PATH = chrome
|
|
99
|
+
console.log(`Using Chromium: ${chrome}`)
|
|
100
|
+
} else {
|
|
101
|
+
console.log('No Playwright Chromium found; relying on system Chrome lookup.')
|
|
102
|
+
}
|
|
103
|
+
console.log(`Serving ${dir} at ${site}`)
|
|
104
|
+
|
|
105
|
+
const child = spawn(
|
|
106
|
+
'bunx',
|
|
107
|
+
[
|
|
108
|
+
'--package',
|
|
109
|
+
`unlighthouse@${values.version}`,
|
|
110
|
+
'unlighthouse-ci',
|
|
111
|
+
'--site',
|
|
112
|
+
site,
|
|
113
|
+
'--config-file',
|
|
114
|
+
config,
|
|
115
|
+
],
|
|
116
|
+
{ env, stdio: 'inherit' },
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
child.on('error', (err) => {
|
|
120
|
+
console.error('failed to start unlighthouse-ci:', err.message)
|
|
121
|
+
server.stop(true)
|
|
122
|
+
process.exit(1)
|
|
123
|
+
})
|
|
124
|
+
child.on('close', (code) => {
|
|
125
|
+
server.stop(true)
|
|
126
|
+
process.exit(code ?? 1)
|
|
127
|
+
})
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Example per-repo config, copy to the repo root as `unlighthouse.config.ts`.
|
|
2
|
+
// The runner supplies `site`, so it is not set here.
|
|
3
|
+
export default {
|
|
4
|
+
scanner: {
|
|
5
|
+
samples: 1,
|
|
6
|
+
device: 'mobile',
|
|
7
|
+
// Keep the run short: explicit routes, no crawling.
|
|
8
|
+
// urls: ['/', '/friends', '/friends/o4us'],
|
|
9
|
+
dynamicSampling: 3,
|
|
10
|
+
skipJavascript: true,
|
|
11
|
+
},
|
|
12
|
+
lighthouseOptions: {
|
|
13
|
+
onlyCategories: ['seo', 'performance'],
|
|
14
|
+
},
|
|
15
|
+
ci: {
|
|
16
|
+
// Per-category minimum scores (0-100); the run exits non-zero if any fail.
|
|
17
|
+
budget: {
|
|
18
|
+
seo: 80,
|
|
19
|
+
performance: 90,
|
|
20
|
+
},
|
|
21
|
+
buildStatic: false,
|
|
22
|
+
},
|
|
23
|
+
}
|