diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index b96f6f2..f8cf5e4 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -1,22 +1,13 @@ name: Docs +# The playground runs the checkout's own generator (lib/ wrapper + a wasm build +# of src/), so every push and PR builds the site. Production only updates for a +# release commit or a docs-only change; other pushes upload a preview version. on: workflow_dispatch: push: branches: [main] - paths: - - 'website/**' - - '.github/workflows/docs.yml' - - 'bun.lock' - - 'bunfig.toml' - - 'stackblitz/petstore.openapi.yaml' pull_request: - paths: - - 'website/**' - - '.github/workflows/docs.yml' - - 'bun.lock' - - 'bunfig.toml' - - 'stackblitz/petstore.openapi.yaml' concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -32,6 +23,9 @@ jobs: pull-requests: write steps: - uses: actions/checkout@v7 + with: + # the deploy gate diffs the pushed range + fetch-depth: 0 - name: Setup bun uses: oven-sh/setup-bun@v2 with: @@ -40,11 +34,61 @@ jobs: uses: actions/setup-node@v7 with: node-version: 22 + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + with: + targets: wasm32-wasip1-threads + - name: Cache cargo + uses: actions/cache@v6 + with: + path: | + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + ~/.napi-rs + .cargo-cache + target/ + # same key as the CI matrix entry so both workflows share the cache + key: wasm32-wasip1-threads-cargo-ubuntu-latest - name: Install dependencies run: bun ci + - name: Build wasm engine + run: bun run build --target wasm32-wasip1-threads + # `napi build` regenerates the committed entry files; the site aliases lib/. + - name: Restore committed entry files + run: git checkout -- native.js browser.js index.d.ts + - name: Decide deploy target + id: gate + env: + BEFORE: ${{ github.event.before }} + run: | + set -eu + # wrangler caps version tags at 25 characters + echo "short=$(git rev-parse --short=7 HEAD)" >> "$GITHUB_OUTPUT" + if [ "${{ github.event_name }}" = "pull_request" ]; then + echo "production=false" >> "$GITHUB_OUTPUT"; echo "pull request: preview only"; exit 0 + fi + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + echo "production=true" >> "$GITHUB_OUTPUT"; echo "manual run"; exit 0 + fi + # HEAD only, same as the publish job in CI.yml: a release commit that is + # not the push HEAD reaches neither npm nor production. + if git log -1 --pretty=%B | grep -q '^v\?[0-9]\+\.[0-9]\+\.[0-9]\+$'; then + echo "production=true" >> "$GITHUB_OUTPUT"; echo "release commit"; exit 0 + fi + # a force-push or new branch has no usable `before` + if [ -n "$BEFORE" ] && ! echo "$BEFORE" | grep -q '^0*$' && git cat-file -e "$BEFORE" 2>/dev/null; then + if ! git diff --name-only "$BEFORE" HEAD | grep -qv '^\(website/\|\.github/workflows/docs\.yml\|bun\.lock\|bunfig\.toml\|stackblitz/\)'; then + echo "production=true" >> "$GITHUB_OUTPUT"; echo "docs-only change"; exit 0 + fi + fi + echo "production=false" >> "$GITHUB_OUTPUT"; echo "unreleased generator change: preview only" - name: Build site working-directory: website run: bun run build + env: + # previews label the engine with the commit they run + ENGINE_VERSION_SUFFIX: ${{ steps.gate.outputs.production != 'true' && format('+{0}', steps.gate.outputs.short) || '' }} - name: Type-check working-directory: website run: bun run check @@ -55,7 +99,7 @@ jobs: working-directory: website run: bun run test - name: Deploy to production - if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' + if: steps.gate.outputs.production == 'true' uses: cloudflare/wrangler-action@v3 with: apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} @@ -63,7 +107,16 @@ jobs: gitHubToken: ${{ secrets.GITHUB_TOKEN }} workingDirectory: website command: deploy - - name: Upload preview version + - name: Upload preview version (main) + if: github.event_name == 'push' && steps.gate.outputs.production == 'false' + uses: cloudflare/wrangler-action@v3 + with: + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + gitHubToken: ${{ secrets.GITHUB_TOKEN }} + workingDirectory: website + command: versions upload --tag main-${{ steps.gate.outputs.short }} --message "main@${{ steps.gate.outputs.short }}" + - name: Upload preview version (PR) if: github.event_name == 'pull_request' uses: cloudflare/wrangler-action@v3 with: diff --git a/bun.lock b/bun.lock index 87fe609..4ae8a77 100644 --- a/bun.lock +++ b/bun.lock @@ -57,8 +57,6 @@ "version": "0.0.0", "dependencies": { "@astrojs/starlight": "0.41.7", - "@avsystem/openapi-ng": "0.6.0", - "@avsystem/openapi-ng-wasm32-wasi": "0.6.0", "@codemirror/lang-javascript": "6.2.5", "@codemirror/lang-json": "6.0.2", "@codemirror/lang-yaml": "6.1.3", @@ -149,28 +147,10 @@ "@astrojs/yaml2ts": ["@astrojs/yaml2ts@0.2.4", "", { "dependencies": { "yaml": "^2.8.3" } }, "sha512-8oddpOae35pJsXPQXhTkM0ypfKPskVsh2bCxRtbf7e+/Epw2nReakFYpLKjZMEr75CsoF203PMnCocpfz0s69A=="], - "@avsystem/openapi-ng": ["@avsystem/openapi-ng@0.6.0", "", { "dependencies": { "yaml": "^2.9.0" }, "optionalDependencies": { "@avsystem/openapi-ng-darwin-arm64": "0.6.0", "@avsystem/openapi-ng-darwin-x64": "0.6.0", "@avsystem/openapi-ng-linux-arm64-gnu": "0.6.0", "@avsystem/openapi-ng-linux-arm64-musl": "0.6.0", "@avsystem/openapi-ng-linux-x64-gnu": "0.6.0", "@avsystem/openapi-ng-linux-x64-musl": "0.6.0", "@avsystem/openapi-ng-win32-arm64-msvc": "0.6.0", "@avsystem/openapi-ng-win32-x64-msvc": "0.6.0" }, "peerDependencies": { "@angular/common": ">=20.0.0", "@angular/core": ">=20.0.0", "@angular/forms": ">=21.0.0", "rxjs": ">=7.0.0" }, "optionalPeers": ["@angular/common", "@angular/core", "@angular/forms", "rxjs"], "bin": { "openapi-ng": "bin/openapi-ng.js" } }, "sha512-q43BsT4C0ATysku2087R4EUcfIKewUyb/fHLFp51lKbceSiowCUaBQ8SaE8bTUagpLRnWtKSjjTmubc1LopWfA=="], - "@avsystem/openapi-ng-angular-consumer-proof": ["@avsystem/openapi-ng-angular-consumer-proof@workspace:__test__/angular-consumer"], - "@avsystem/openapi-ng-darwin-arm64": ["@avsystem/openapi-ng-darwin-arm64@0.6.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-dRi2SbyN2cbhxC1ERHBG94K6/PeB17+q3pI/OGB1Suhj89AlOBOJt9IQSd5KIi1ifSILyEdAoHoIBbiawVFnuQ=="], - - "@avsystem/openapi-ng-darwin-x64": ["@avsystem/openapi-ng-darwin-x64@0.6.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-qifdJYNNYUbGU0KAWPrIfnG+1FO5XRopCIw+Q8OIfe7yvDA1M2jyi7zCvHx340ex8dpIlaIdo4rFiiQTyX1KNg=="], - - "@avsystem/openapi-ng-linux-arm64-gnu": ["@avsystem/openapi-ng-linux-arm64-gnu@0.6.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-3JS/FPI77ScqpyT79X9pHgSenoNiZdMRN8GX2K2uCE3hP6bhx9JjqU+S71gkVjBjaYzPvJVeABasuV7K/ClXFQ=="], - - "@avsystem/openapi-ng-linux-x64-gnu": ["@avsystem/openapi-ng-linux-x64-gnu@0.6.0", "", { "os": "linux", "cpu": "x64" }, "sha512-My8WNIrBZjdywduwvyFYgjLUnLYxBJiQzVklhm02DXUY040EFqDygIQDVlRCTJf5HIcMtpFZsSdzBnUVJ65faA=="], - - "@avsystem/openapi-ng-linux-x64-musl": ["@avsystem/openapi-ng-linux-x64-musl@0.6.0", "", { "os": "linux", "cpu": "x64" }, "sha512-AHIukjlGBt8UjC3tvMbLLYfy71itAMoz0HwAhd1ilhzaPOMlbjalEJFsGQiBhiKAkuTIKYzrLwzDJYvyf0e1LQ=="], - - "@avsystem/openapi-ng-wasm32-wasi": ["@avsystem/openapi-ng-wasm32-wasi@0.6.0", "", { "dependencies": { "@emnapi/core": "1.11.3", "@emnapi/runtime": "1.11.3", "@napi-rs/wasm-runtime": "~1.2.3" } }, "sha512-5QKmNXSWSPWKl00Wgz9qP9kZG9b8VN8jHsrQ43hIqeHGfj+TyEXzq3CNEKv4Vq0TRXOOQmwoby4m6kzA5D7TdA=="], - "@avsystem/openapi-ng-website": ["@avsystem/openapi-ng-website@workspace:website"], - "@avsystem/openapi-ng-win32-arm64-msvc": ["@avsystem/openapi-ng-win32-arm64-msvc@0.6.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-OjDlQBqpy3038BhWarysgPlDJRb0/xf34/WVhHe5PZPkH8vWxYKf83FQGJ9S/rLz6TgyiqlviFd+Qqv2PPT3fQ=="], - - "@avsystem/openapi-ng-win32-x64-msvc": ["@avsystem/openapi-ng-win32-x64-msvc@0.6.0", "", { "os": "win32", "cpu": "x64" }, "sha512-FdLSsrJvn6I0Efrzl3r1BgmHp1uuihSaiO6hymlbG0AftNJpj1B3Ai3vox+77cirXqIfgFIEHwa1HdPjgGY9rQ=="], - "@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], diff --git a/website/README.md b/website/README.md index 897eddd..4af8d13 100644 --- a/website/README.md +++ b/website/README.md @@ -10,48 +10,19 @@ bun run --filter @avsystem/openapi-ng-website build # production build bun run --filter @avsystem/openapi-ng-website test # vitest + playwright ``` -## Running the playground against the local build +## The playground engine -The site pins the published `@avsystem/openapi-ng` and `@avsystem/openapi-ng-wasm32-wasi` -packages in `package.json`, and `scripts/bundle-engine.mjs` copies the wasm loader out of -`node_modules` on every `dev`/`build`. To try unreleased generator changes in the playground, -link both packages to the local build. From the repo root: +The playground runs the generator from this checkout, not a published package: +`astro.config.mjs` aliases `@avsystem/openapi-ng/browser` to the root `lib/browser.js`, and +`scripts/bundle-engine.mjs` copies the wasm loader from the repo root on every `dev`/`build`. +Build the engine once before starting the site, and again after any Rust or template change: ```bash -bun run build --target wasm32-wasip1-threads # wasm engine; writes openapi-ng.wasi* and *.wasm at the root -git checkout native.js # the build rewrites the committed entry file -bunx napi create-npm-dirs # npm//package.json for every target (gitignored) -cp openapi-ng.wasm32-wasi.wasm openapi-ng.wasi.cjs openapi-ng.wasi.d.cts \ - openapi-ng.wasi-browser.js wasi-worker.mjs wasi-worker-browser.mjs npm/wasm32-wasi/ +bun run build --target wasm32-wasip1-threads # writes openapi-ng.wasi* and *.wasm at the root +git checkout -- native.js browser.js index.d.ts # the build regenerates the committed entries ``` -Then register both local packages with bun and point the site at them. Bun's `link:` protocol -refers to packages registered via `bun link` (a relative path there is not supported, and -`workspace:*` cannot name the workspace root): - -```bash -bun link # registers @avsystem/openapi-ng (repo root) -(cd npm/wasm32-wasi && bun link) # registers @avsystem/openapi-ng-wasm32-wasi -``` - -In `website/package.json` set - -```json -"@avsystem/openapi-ng": "link:@avsystem/openapi-ng", -"@avsystem/openapi-ng-wasm32-wasi": "link:@avsystem/openapi-ng-wasm32-wasi" -``` - -and run `bun install && bun run --filter @avsystem/openapi-ng-website dev`. Both packages must be -linked: the JS wrapper validates option keys, so a published wrapper rejects options the local -engine adds. The links are symlinks, so after further Rust or template changes just rebuild the -wasm, copy the six files again and restart `dev` so `predev` re-bundles the engine; no reinstall -is needed. Avoid `file:..` for the root package: bun copies `file:` dependencies, and that copies -the whole repo (including `target/`) into `node_modules`. - -Before committing, revert `website/package.json` and the root `bun.lock`: - -```bash -git checkout -- website/package.json bun.lock && bun install -``` - -Bump the pins only once the new version is on npm. +Restart `dev` afterwards so `predev` re-bundles the engine. The deployed site is built the same +way by `.github/workflows/docs.yml`, so a release commit ships the docs and playground for that +exact version; the workflow deploys to production only for a release commit or a docs-only +change and uploads a preview version for everything else. diff --git a/website/astro.config.mjs b/website/astro.config.mjs index 89cf18a..e784ca9 100644 --- a/website/astro.config.mjs +++ b/website/astro.config.mjs @@ -1,5 +1,9 @@ import { defineConfig } from 'astro/config'; import starlight from '@astrojs/starlight'; +import { fileURLToPath } from 'node:url'; + +// Forward slashes: Vite module ids and the picomatch glob below use them on every OS. +const repoRoot = fileURLToPath(new URL('..', import.meta.url)).replace(/\\/g, '/'); // `astro dev` ignores public/_headers; mirror its COOP/COEP scope so the // playground page is cross-origin isolated and its wasm worker inherits COEP. @@ -77,13 +81,23 @@ export default defineConfig({ }), ], vite: { - build: { target: 'es2022' }, + // The playground runs the checkout's own wrapper (CommonJS under lib/), + // not a published package; see scripts/bundle-engine.mjs for the engine. + resolve: { + alias: { '@avsystem/openapi-ng/browser': `${repoRoot}lib/browser.js` }, + }, + build: { + target: 'es2022', + commonjsOptions: { include: [/node_modules/, `${repoRoot}lib/*.js`] }, + }, server: { fs: { allow: ['..'] } }, plugins: [playgroundHeaders], // The playground's imports are only reachable through src/playground/*.ts, so // dev discovers them late and re-optimizes, which 504s already-served modules. optimizeDeps: { include: [ + // Resolved through the alias above; pre-bundling is what turns the + // CommonJS wrapper into ESM in dev, so this entry stays. '@avsystem/openapi-ng/browser', 'codemirror', '@codemirror/state', diff --git a/website/package.json b/website/package.json index 7aef994..b3259bd 100644 --- a/website/package.json +++ b/website/package.json @@ -18,8 +18,6 @@ }, "dependencies": { "@astrojs/starlight": "0.41.7", - "@avsystem/openapi-ng": "0.6.0", - "@avsystem/openapi-ng-wasm32-wasi": "0.6.0", "@codemirror/lang-javascript": "6.2.5", "@codemirror/lang-json": "6.0.2", "@codemirror/lang-yaml": "6.1.3", diff --git a/website/scripts/bundle-engine.mjs b/website/scripts/bundle-engine.mjs index 5de147a..8463eff 100644 --- a/website/scripts/bundle-engine.mjs +++ b/website/scripts/bundle-engine.mjs @@ -1,6 +1,9 @@ #!/usr/bin/env node // Pre-bundles the napi-rs WASI browser loader so the page can import it // from /playground-engine/ without Vite processing node_modules internals. +// The engine comes from the repo checkout: `napi build --target +// wasm32-wasip1-threads` at the repo root writes the loader, worker and wasm +// next to package.json, so the playground always runs the commit it is built from. import { build } from 'esbuild'; import fs from 'node:fs'; import { createRequire } from 'node:module'; @@ -9,10 +12,21 @@ import { fileURLToPath } from 'node:url'; const require = createRequire(import.meta.url); const websiteRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const repoRoot = path.resolve(websiteRoot, '..'); const outDir = path.join(websiteRoot, 'public', 'playground-engine'); -const pkgDir = path.dirname( - require.resolve('@avsystem/openapi-ng-wasm32-wasi/package.json'), -); + +const ENGINE_FILES = [ + 'openapi-ng.wasi-browser.js', + 'wasi-worker-browser.mjs', + 'openapi-ng.wasm32-wasi.wasm', +]; +const missing = ENGINE_FILES.filter(file => !fs.existsSync(path.join(repoRoot, file))); +if (missing.length) { + throw new Error( + `bundle-engine: missing ${missing.join(', ')} at the repo root; run ` + + '`bun run build --target wasm32-wasip1-threads` there first.', + ); +} const PACKAGE_WORKER_URL = "new URL('@avsystem/openapi-ng-wasm32-wasi/wasi-worker-browser.mjs', import.meta.url)"; @@ -22,11 +36,11 @@ fs.rmSync(outDir, { recursive: true, force: true }); fs.mkdirSync(outDir, { recursive: true }); const loaderSource = fs.readFileSync( - path.join(pkgDir, 'openapi-ng.wasi-browser.js'), + path.join(repoRoot, 'openapi-ng.wasi-browser.js'), 'utf8', ); -// `napi artifacts` rewrites the published loader to the bare specifier; a local -// `napi build` already emits the relative one, so the replace below is a no-op. +// `napi build` emits the relative worker URL; `napi artifacts` rewrites it to +// the bare specifier, so accept both. if ( !loaderSource.includes(PACKAGE_WORKER_URL) && !loaderSource.includes(LOCAL_WORKER_URL) @@ -39,7 +53,7 @@ if ( await build({ stdin: { contents: loaderSource.replace(PACKAGE_WORKER_URL, LOCAL_WORKER_URL), - resolveDir: pkgDir, + resolveDir: repoRoot, sourcefile: 'openapi-ng.wasi-browser.js', loader: 'js', }, @@ -52,7 +66,7 @@ await build({ }); await build({ - entryPoints: [path.join(pkgDir, 'wasi-worker-browser.mjs')], + entryPoints: [path.join(repoRoot, 'wasi-worker-browser.mjs')], bundle: true, format: 'esm', platform: 'browser', @@ -62,10 +76,11 @@ await build({ }); fs.copyFileSync( - path.join(pkgDir, 'openapi-ng.wasm32-wasi.wasm'), + path.join(repoRoot, 'openapi-ng.wasm32-wasi.wasm'), path.join(outDir, 'openapi-ng.wasm32-wasi.wasm'), ); -const { version } = require('@avsystem/openapi-ng-wasm32-wasi/package.json'); +const version = + require(path.join(repoRoot, 'package.json')).version + (process.env.ENGINE_VERSION_SUFFIX ?? ''); fs.writeFileSync(path.join(outDir, 'version.json'), JSON.stringify({ version })); console.log(`bundle-engine: wrote ${outDir} (v${version})`); diff --git a/website/tsconfig.json b/website/tsconfig.json index 36d027d..9428fcd 100644 --- a/website/tsconfig.json +++ b/website/tsconfig.json @@ -1,5 +1,8 @@ { "extends": "astro/tsconfigs/strict", "include": [".astro/types.d.ts", "**/*"], - "exclude": ["dist", "public"] + "exclude": ["dist", "public"], + "compilerOptions": { + "paths": { "@avsystem/openapi-ng/browser": ["../browser.d.ts"] } + } }