├── .nvmrc ├── .pr-preview.json ├── .gitignore ├── .prettierrc ├── .codespellrc ├── .editorconfig ├── .prettierignore ├── test ├── setup.test.ts ├── validate-input-markup.test.ts ├── validate-links.test.ts ├── validate-pubrules.test.ts ├── validate-markup.test.ts ├── validate-webidl.test.ts ├── build.test.ts ├── deploy-gh-pages.test.ts ├── prepare.test.ts └── index.test.ts ├── docs ├── resources.md ├── README.md ├── index.html ├── examples.md └── options.md ├── tsconfig.json ├── src ├── constants.ts ├── prepare-validate.ts ├── validate-input-markup.ts ├── validate-markup.ts ├── validate-links.ts ├── setup.ts ├── validate-webidl.ts ├── prepare-deploy.ts ├── prepare.ts ├── validate-pubrules.ts ├── deploy-gh-pages.ts ├── deploy-w3c-echidna.ts ├── utils.ts ├── build.ts └── prepare-build.ts ├── .github └── workflows │ ├── pr.yml │ └── docs.yml ├── package.json ├── LICENSE ├── README.md ├── action.yml └── pnpm-lock.yaml /.nvmrc: -------------------------------------------------------------------------------- 1 | 24 2 | -------------------------------------------------------------------------------- /.pr-preview.json: -------------------------------------------------------------------------------- 1 | { 2 | "src_file": "docs/index.html", 3 | "type": "respec" 4 | } 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | python_modules 3 | .vscode 4 | *.js 5 | *.map 6 | package-lock.json 7 | yarn.lock 8 | *.log 9 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "avoid", 3 | "singleQuote": false, 4 | "trailingComma": "all", 5 | "tabWidth": 2, 6 | "useTabs": true 7 | } 8 | -------------------------------------------------------------------------------- /.codespellrc: -------------------------------------------------------------------------------- 1 | # Ref: https://github.com/codespell-project/codespell#using-a-config-file 2 | [codespell] 3 | skip = .git,.codespellrc 4 | check-hidden = true 5 | # ignore-regex = 6 | # ignore-words-list = 7 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | indent_style = tab 4 | indent_size = 2 5 | tab_width = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # copied from .gitignore 2 | # https://github.com/prettier/prettier/issues/8048 3 | node_modules 4 | python_modules 5 | .vscode 6 | *.js 7 | *.map 8 | package-lock.json 9 | yarn.lock 10 | *.log 11 | 12 | # actual .prettierignore 13 | pnpm-lock.yaml 14 | -------------------------------------------------------------------------------- /test/setup.test.ts: -------------------------------------------------------------------------------- 1 | import main from "../src/setup.ts"; 2 | import type { Outputs } from "./index.test.ts"; 3 | 4 | export default async function setup(outputs: Outputs) { 5 | const { toolchain = "respec" } = outputs?.prepare?.build || {}; 6 | return await main(toolchain); 7 | } 8 | -------------------------------------------------------------------------------- /docs/resources.md: -------------------------------------------------------------------------------- 1 | # Other useful resources 2 | 3 | - Denis Ah-Kang (W3C) gave an overview of the automatic publishing system on the 18 March 2021: 4 | [video with transcript](https://www.w3.org/2021/03/18-echidna/video.html) 5 | and [minutes](https://www.w3.org/2021/03/18-pub-minutes.html) are available. 6 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noEmit": true, 4 | "checkJs": false, 5 | "target": "esnext", 6 | "moduleResolution": "nodenext", 7 | "module": "nodenext", 8 | "strict": true, 9 | "rewriteRelativeImportExtensions": true, 10 | "verbatimModuleSyntax": true, 11 | "useUnknownInCatchVariables": false 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | import * as path from "node:path"; 2 | 3 | export const ACTION_DIR = path.join(import.meta.dirname, ".."); 4 | 5 | export const PUPPETEER_ENV = (() => { 6 | const skipChromeDownload = 7 | !!process.env.PUPPETEER_SKIP_DOWNLOAD || !!process.env.GITHUB_ACTIONS; 8 | if (!skipChromeDownload) return {}; 9 | return { 10 | PUPPETEER_SKIP_DOWNLOAD: "1", 11 | PUPPETEER_EXECUTABLE_PATH: 12 | process.env.PUPPETEER_EXECUTABLE_PATH || "/usr/bin/google-chrome", 13 | }; 14 | })(); 15 | -------------------------------------------------------------------------------- /test/validate-input-markup.test.ts: -------------------------------------------------------------------------------- 1 | import main from "../src/validate-input-markup.ts"; 2 | import type { Outputs } from "./index.test.ts"; 3 | 4 | export default async function validateInputMarkup(outputs: Outputs) { 5 | const { input_markup: shouldValidate = false } = 6 | outputs?.prepare?.validate || {}; 7 | if (shouldValidate === false) { 8 | return; 9 | } 10 | 11 | const { source } = outputs?.prepare?.build || { source: "index.html" }; 12 | 13 | return await main({ source }); 14 | } 15 | -------------------------------------------------------------------------------- /test/validate-links.test.ts: -------------------------------------------------------------------------------- 1 | import main from "../src/validate-links.ts"; 2 | import type { Outputs } from "./index.test.ts"; 3 | 4 | export default async function validateLinks(outputs: Outputs) { 5 | const { links: shouldValidate = false } = outputs?.prepare?.validate || {}; 6 | if (shouldValidate === false) { 7 | return; 8 | } 9 | 10 | const { dest = process.cwd() + ".common", file = "index.html" } = 11 | outputs?.build?.w3c || {}; 12 | 13 | return await main({ dest, file }); 14 | } 15 | -------------------------------------------------------------------------------- /test/validate-pubrules.test.ts: -------------------------------------------------------------------------------- 1 | import main from "../src/validate-pubrules.ts"; 2 | import type { Outputs } from "./index.test.ts"; 3 | 4 | export default async function validatePubrules(outputs: Outputs) { 5 | const { pubrules: shouldValidate = false } = outputs?.prepare?.validate || {}; 6 | if (shouldValidate === false) { 7 | return; 8 | } 9 | 10 | const { dest = process.cwd() + ".w3c", file = "index.html" } = 11 | outputs?.build?.w3c || {}; 12 | return await main({ dest, file }); 13 | } 14 | -------------------------------------------------------------------------------- /test/validate-markup.test.ts: -------------------------------------------------------------------------------- 1 | import main from "../src/validate-markup.ts"; 2 | import type { Outputs } from "./index.test.ts"; 3 | 4 | export default async function validateMarkup(outputs: Outputs) { 5 | const { markup: shouldValidate = false } = outputs?.prepare?.validate || {}; 6 | if (shouldValidate === false) { 7 | return; 8 | } 9 | 10 | const { dest = process.cwd() + ".common", file = "index.html" } = 11 | outputs?.build?.w3c || {}; 12 | 13 | return await main({ dest, file }); 14 | } 15 | -------------------------------------------------------------------------------- /test/validate-webidl.test.ts: -------------------------------------------------------------------------------- 1 | import main from "../src/validate-webidl.ts"; 2 | import type { Outputs } from "./index.test.ts"; 3 | 4 | export default async function validateWebIdl(outputs: Outputs) { 5 | const { webidl: shouldValidate = false } = outputs?.prepare?.validate || {}; 6 | if (shouldValidate === false) { 7 | return; 8 | } 9 | 10 | const { dest = process.cwd() + ".common", file = "index.html" } = 11 | outputs?.build?.w3c || {}; 12 | 13 | return await main({ dest, file }); 14 | } 15 | -------------------------------------------------------------------------------- /src/prepare-validate.ts: -------------------------------------------------------------------------------- 1 | import type { Inputs } from "./prepare.ts"; 2 | import { yesOrNo } from "./utils.ts"; 3 | 4 | export function validation(inputs: Inputs) { 5 | const input_markup = yesOrNo(inputs.VALIDATE_INPUT_MARKUP) || false; 6 | const links = yesOrNo(inputs.VALIDATE_LINKS) || false; 7 | const markup = yesOrNo(inputs.VALIDATE_MARKUP) || false; 8 | const pubrules = yesOrNo(inputs.VALIDATE_PUBRULES) || false; 9 | const webidl = yesOrNo(inputs.VALIDATE_WEBIDL) || false; 10 | return { input_markup, links, markup, pubrules, webidl }; 11 | } 12 | -------------------------------------------------------------------------------- /.github/workflows/pr.yml: -------------------------------------------------------------------------------- 1 | name: Node CI (PR) 2 | on: 3 | pull_request: {} 4 | 5 | env: 6 | PUPPETEER_SKIP_DOWNLOAD: 1 7 | PUPPETEER_EXECUTABLE_PATH: /usr/bin/google-chrome 8 | 9 | jobs: 10 | lint: 11 | name: Check linting issues 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v5 15 | - uses: codespell-project/actions-codespell@v2 16 | - uses: pnpm/action-setup@v4 17 | - uses: actions/setup-node@v5 18 | with: 19 | node-version-file: ".nvmrc" 20 | cache: pnpm 21 | - run: pnpm i 22 | - run: pnpm lint 23 | - run: pnpm typecheck 24 | -------------------------------------------------------------------------------- /test/build.test.ts: -------------------------------------------------------------------------------- 1 | import main from "../src/build.ts"; 2 | import type { Outputs } from "./index.test.ts"; 3 | 4 | export default async function build(outputs: Outputs) { 5 | const { 6 | toolchain = "respec", 7 | source = { dir: "", file: "index.html", path: "index.html" }, 8 | destination = { dir: "", file: "index.html", path: "index.html" }, 9 | flags = ["-e"], 10 | artifactName = "spec-prod-result", 11 | configOverride = { w3c: { dir: "", file: "" }, gh: null }, 12 | } = outputs?.prepare?.build || {}; 13 | 14 | return await main({ 15 | toolchain, 16 | source, 17 | destination, 18 | flags, 19 | configOverride, 20 | artifactName, 21 | }); 22 | } 23 | -------------------------------------------------------------------------------- /test/deploy-gh-pages.test.ts: -------------------------------------------------------------------------------- 1 | import main from "../src/deploy-gh-pages.ts"; 2 | import type { Outputs } from "./index.test.ts"; 3 | 4 | export default async function deployGhPages(outputs: Outputs) { 5 | const { ghPages = false } = outputs?.prepare?.deploy || {}; 6 | if (ghPages === false) { 7 | return; 8 | } 9 | if (ghPages === true) { 10 | throw new Error("Unexpected."); 11 | } 12 | const inputs = { 13 | targetBranch: ghPages.targetBranch || "gh-pages", 14 | token: ghPages.token || "TOKEN", 15 | event: ghPages.event || "push", 16 | sha: ghPages.sha || "SHA", 17 | repository: ghPages.repository || "sidvishnoi/w3c-deploy-test", 18 | actor: ghPages.actor || "sidvishnoi", 19 | }; 20 | 21 | const { root: outputDir = process.cwd() + ".common" } = 22 | outputs?.build?.w3c || {}; 23 | 24 | return await main(inputs, outputDir); 25 | } 26 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Docs 2 | on: 3 | pull_request: 4 | paths: ["docs/**"] 5 | push: 6 | branches: ["main"] 7 | paths: ["docs/**"] 8 | workflow_dispatch: {} 9 | 10 | env: 11 | PUPPETEER_SKIP_DOWNLOAD: 1 12 | PUPPETEER_EXECUTABLE_PATH: /usr/bin/google-chrome 13 | 14 | jobs: 15 | lint: 16 | name: Build Documentation 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Get ref to checkout 20 | id: ref 21 | run: | 22 | echo "ref=${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}" >> $GITHUB_OUTPUT 23 | - uses: actions/checkout@v5 24 | with: 25 | ref: ${{ steps.ref.outputs.ref }} 26 | 27 | - name: "Run w3c/spec-prod@${{ steps.ref.outputs.ref }}" 28 | uses: "./" 29 | with: 30 | SOURCE: docs/index.html 31 | DESTINATION: index.html 32 | TOOLCHAIN: respec 33 | GH_PAGES_BRANCH: gh-pages 34 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "@actions/core": "^2.0.1", 4 | "finalhandler": "^2.1.1", 5 | "puppeteer": "^24", 6 | "serve-static": "^2.2.0", 7 | "split2": "^4.2.0", 8 | "subresources": "^2.1.0", 9 | "yaml": "^2.8.2" 10 | }, 11 | "pnpm": { 12 | "onlyBuiltDependencies": [ 13 | "puppeteer" 14 | ], 15 | "overrides": { 16 | "link-checker": "1.4.2", 17 | "reffy": "^15", 18 | "respec": "^35", 19 | "specberus": "^11", 20 | "vnu-jar": "^24", 21 | "webidl2": "^24" 22 | } 23 | }, 24 | "engines": { 25 | "node": "^24" 26 | }, 27 | "type": "module", 28 | "packageManager": "pnpm@10.25.0", 29 | "scripts": { 30 | "dev": "tsc -w", 31 | "typecheck": "tsc", 32 | "lint": "prettier . --check" 33 | }, 34 | "devDependencies": { 35 | "@types/finalhandler": "^1.2.4", 36 | "@types/node": "^24.5.2", 37 | "@types/serve-static": "^2.2.0", 38 | "@types/split2": "^4.2.3", 39 | "prettier": "^3.7.4", 40 | "typescript": "^5.9.3" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/validate-input-markup.ts: -------------------------------------------------------------------------------- 1 | import { createRequire } from "node:module"; 2 | import { env, exit, install, sh, yesOrNo } from "./utils.ts"; 3 | 4 | import type { ProcessedInput } from "./prepare.ts"; 5 | type Input = Pick; 6 | 7 | const require = createRequire(import.meta.url); 8 | 9 | if (import.meta.main) { 10 | if (yesOrNo(env("INPUTS_VALIDATE_INPUT_MARKUP")) === false) { 11 | exit("Skipped", 0); 12 | } 13 | 14 | const input: Input = JSON.parse(env("INPUTS_BUILD")); 15 | main(input).catch(err => exit(err.message || "Failed", err.code)); 16 | } 17 | 18 | export default async function main({ source }: Input) { 19 | console.log(`Validating ${source}...`); 20 | await install("vnu-jar"); 21 | const vnuJar = require("vnu-jar"); 22 | 23 | try { 24 | await sh(`java -jar "${vnuJar}" ${source}`, { 25 | output: "stream", 26 | }); 27 | exit("✅ Looks good! No HTML validation errors!", 0); 28 | } catch { 29 | exit("❌ Not so good... please fix the issues above."); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/validate-markup.ts: -------------------------------------------------------------------------------- 1 | import { createRequire } from "node:module"; 2 | import { env, exit, install, sh, yesOrNo } from "./utils.ts"; 3 | 4 | import type { BuildResult } from "./build.ts"; 5 | type Input = Pick; 6 | 7 | const require = createRequire(import.meta.url); 8 | 9 | if (import.meta.main) { 10 | if (yesOrNo(env("INPUTS_VALIDATE_MARKUP")) === false) { 11 | exit("Skipped", 0); 12 | } 13 | 14 | const input: Input = JSON.parse(env("OUTPUTS_BUILD")); 15 | main(input).catch(err => exit(err.message || "Failed", err.code)); 16 | } 17 | 18 | export default async function main({ dest, file }: Input) { 19 | console.log(`Validating ${file}...`); 20 | await install("vnu-jar"); 21 | const vnuJar = require("vnu-jar"); 22 | 23 | try { 24 | await sh(`java -jar "${vnuJar}" --also-check-css ${file}`, { 25 | output: "stream", 26 | cwd: dest, 27 | }); 28 | exit("✅ Looks good! No HTML validation errors!", 0); 29 | } catch { 30 | exit("❌ Not so good... please fix the issues above."); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/validate-links.ts: -------------------------------------------------------------------------------- 1 | import { env, exit, install, sh } from "./utils.ts"; 2 | import type { BuildResult } from "./build.ts"; 3 | type Input = Pick; 4 | 5 | const URL_IGNORE = [ 6 | // Doesn't like robots 7 | "https://ev.buaa.edu.cn/", 8 | // The to-be published /TR URL. 9 | // Ideally should include shortname, but may be good enough. 10 | `/TR/.+${new Date().toISOString().slice(0, 10).replace(/-/g, "")}/$`, 11 | ]; 12 | 13 | if (import.meta.main) { 14 | const input: Input = JSON.parse(env("OUTPUTS_BUILD")); 15 | main(input).catch(err => exit(err.message || "Failed", err.code)); 16 | } 17 | 18 | export default async function main({ dest: dir }: Input) { 19 | await install("link-checker"); 20 | const opts = getLinkCheckerOptions(URL_IGNORE); 21 | // Note: link-checker checks a directory, not a file. 22 | await sh(`link-checker ${opts} ${dir}`, "stream"); 23 | } 24 | 25 | function getLinkCheckerOptions(ignoreList: string[]) { 26 | return ignoreList 27 | .map(url => `--url-ignore="${url}"`) 28 | .concat(["--http-timeout=50000", "--http-redirects=3", "--http-always-get"]) 29 | .join(" "); 30 | } 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Sid Vishnoi 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. 22 | -------------------------------------------------------------------------------- /src/setup.ts: -------------------------------------------------------------------------------- 1 | import * as path from "node:path"; 2 | import { addPath, exportVariable } from "@actions/core"; 3 | import { ACTION_DIR, PUPPETEER_ENV } from "./constants.ts"; 4 | import { env, exit, install, sh } from "./utils.ts"; 5 | 6 | const PYTHONUSERBASE = path.join(ACTION_DIR, "python_modules"); 7 | 8 | if (import.meta.main) { 9 | const toolchain = env("INPUTS_TOOLCHAIN"); 10 | main(toolchain).catch(err => exit(err.message || "Failed", err.code)); 11 | } 12 | 13 | export default async function main(toolchain: "respec" | "bikeshed" | string) { 14 | addPath(path.join(ACTION_DIR, "node_modules", ".bin")); 15 | addPath(path.join(PYTHONUSERBASE, "bin")); 16 | 17 | switch (toolchain) { 18 | case "respec": { 19 | await install("respec", PUPPETEER_ENV); 20 | await sh("respec --version", "buffer"); 21 | break; 22 | } 23 | case "bikeshed": { 24 | await sh("pipx --version", "buffer"); 25 | await sh(`pipx install 'bikeshed==5.*' --quiet`, { 26 | output: "stream", 27 | env: { 28 | PYTHONUSERBASE, 29 | }, 30 | }); 31 | exportVariable("PYTHONUSERBASE", PYTHONUSERBASE); 32 | await sh("bikeshed update", "stream"); 33 | await sh("pipx list --short | grep -i bikeshed", "buffer"); 34 | break; 35 | } 36 | default: { 37 | const msg = `Environment variable "INPUTS_TOOLCHAIN" must be either one of "respec" or "bikeshed". found "${toolchain}"`; 38 | exit(msg, 1); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | ## Getting started 2 | 3 | To get started, do the following: 4 | 5 | 1. If you want to deploy to W3C /TR: 6 | 1. [Request an Echidna token][request-token] for your spec (W3C Team Members and Chairs only!). 7 | 1. [Save the token][save-token] as a "Secret" named `ECHIDNA_TOKEN` in the spec's repository's settings. 8 | 1. Create a `.github/workflows/auto-publish.yml` file at the root of the spec's repository. 9 | 1. In the `auto-publish.yml`, copy-paste and modify one of the [examples](examples.md) below that suits your needs. Most typical one: 10 | 11 | ```yml 12 | # Inside .github/workflows/auto-publish.yml 13 | name: Automatic Publication 14 | 15 | on: 16 | pull_request: {} 17 | push: 18 | branches: [main] 19 | 20 | jobs: 21 | validate-and-publish: 22 | name: Validate and Publish to TR 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: actions/checkout@v5 26 | - uses: w3c/spec-prod@v2 27 | with: 28 | TOOLCHAIN: respec # or bikeshed 29 | W3C_ECHIDNA_TOKEN: ${{ secrets.ECHIDNA_TOKEN }} 30 | W3C_WG_DECISION_URL: " See Options for URLs! " 31 | # Convert Editor's Draft to Working Draft! 32 | W3C_BUILD_OVERRIDE: | 33 | specStatus: WD 34 | ``` 35 | 36 | [request-token]: https://www.w3.org/Web/publications/register 37 | [save-token]: https://docs.github.com/en/actions/reference/encrypted-secrets#creating-encrypted-secrets-for-a-repository 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Spec Prod | [Documentation 📘](https://w3c.github.io/spec-prod/) 2 | 3 | This GitHub Action lets you: 4 | 5 | - Build [ReSpec](https://github.com/w3c/respec) and [Bikeshed](https://github.com/tabatkins/bikeshed) specs. 6 | - Validate generated document's markup and check for broken hyperlinks. 7 | - Publish generated spec to GitHub Pages and/or w3.org (using Echidna). 8 | 9 | ## Basic Usage 10 | 11 | During a pull request, the action: 12 | 13 | - figures out if you're using ReSpec (`index.html`) or Bikeshed (`index.bs`) 14 | - converts the ReSpec/Bikeshed source document to regular HTML 15 | - runs broken hyperlink checker, and validate markup using W3C nu validator 16 | 17 | Additionally, if a commit is pushed to the "main" branch, the action deploys the built specification to /TR/. 18 | 19 | ```yml 20 | # .github/workflows/auto-publish.yml 21 | name: CI 22 | on: 23 | pull_request: {} 24 | push: 25 | branches: [main] 26 | jobs: 27 | main: 28 | name: Build, Validate and Deploy 29 | runs-on: ubuntu-latest 30 | steps: 31 | - uses: actions/checkout@v5 32 | - uses: w3c/spec-prod@v2 33 | with: 34 | W3C_ECHIDNA_TOKEN: ${{ secrets.ECHIDNA_TOKEN }} 35 | # Replace following with appropriate value. See options.md for details. 36 | W3C_WG_DECISION_URL: https://lists.w3.org/Archives/Public/public-group/2014JulSep/1234.html 37 | # Usually, you want the following set too... 38 | W3C_BUILD_OVERRIDE: | 39 | shortName: your-specs-shortname-here 40 | specStatus: WD 41 | ``` 42 | 43 | ## More examples 44 | 45 | Learn from [usage examples](docs/examples.md), including: 46 | 47 | - [Run as a validator on pull requests](docs/examples.md#run-as-a-validator-on-pull-requests) 48 | - [Deploy to GitHub pages](docs/examples.md#deploy-to-github-pages) 49 | - And more... 50 | 51 | ## Options 52 | 53 | Read more about [the available options](docs/options.md) 54 | -------------------------------------------------------------------------------- /test/prepare.test.ts: -------------------------------------------------------------------------------- 1 | import * as path from "node:path"; 2 | import { readFileSync, readdirSync } from "node:fs"; 3 | import * as yaml from "yaml"; 4 | 5 | import main, { type Inputs, type GitHubContext } from "../src/prepare.ts"; 6 | 7 | type Job = { steps: { uses?: string; with: object }[] }; 8 | type Workflow = { 9 | inputs: { [inputName: string]: { default?: string } }; 10 | jobs: { [jobId: string]: Job }; 11 | }; 12 | 13 | export default async function prepare() { 14 | const INPUTS_USER = { 15 | ...getDefaultInputs(), 16 | ...getInputsFromWorkflow(), 17 | } as Inputs; 18 | const INPUTS_GITHUB: GitHubContext = { 19 | event_name: "push", 20 | repository: "sidvishnoi/w3c-deploy-test", 21 | event: { repository: { default_branch: "main", has_pages: false } }, 22 | token: "GITHUB_TOKEN", 23 | sha: "HEAD^", 24 | actor: "GITHUB_ACTOR", 25 | }; 26 | return await main(INPUTS_USER, INPUTS_GITHUB); 27 | } 28 | 29 | function getInputsFromWorkflow(): Partial { 30 | try { 31 | const workflowDir = path.join(process.cwd(), ".github", "workflows"); 32 | const workflow = readdirSync(workflowDir).find(f => /\.ya?ml$/.test(f)); 33 | if (!workflow) throw new Error("No workflow found."); 34 | const text = readFileSync(path.join(workflowDir, workflow), "utf8"); 35 | const parsed = yaml.parse(text); 36 | for (const job of Object.values(parsed.jobs) as Job[]) { 37 | const step = job.steps.find(step => step.uses?.includes("/spec-prod@")); 38 | if (!step) continue; 39 | return step.with; 40 | } 41 | } catch (error) { 42 | console.error("Failed to read workflow inputs."); 43 | console.error(error); 44 | } 45 | return {}; 46 | } 47 | 48 | function getDefaultInputs(): Partial { 49 | const action = path.join(import.meta.dirname, "..", "action.yml"); 50 | const text = readFileSync(action, "utf8"); 51 | const parsed = yaml.parse(text) as Workflow; 52 | return Object.fromEntries( 53 | Object.entries(parsed.inputs) 54 | .filter(([_inputName, inp]) => inp.default) 55 | .map(([inputName, inp]) => [inputName, inp.default as string]), 56 | ); 57 | } 58 | -------------------------------------------------------------------------------- /src/validate-webidl.ts: -------------------------------------------------------------------------------- 1 | import { rm } from "node:fs/promises"; 2 | import { createRequire } from "node:module"; 3 | import { env, exit, install, yesOrNo } from "./utils.ts"; 4 | import { PUPPETEER_ENV } from "./constants.ts"; 5 | 6 | import type { BuildResult } from "./build.ts"; 7 | type Input = Pick; 8 | 9 | const require = createRequire(import.meta.url); 10 | 11 | if (import.meta.main) { 12 | if (yesOrNo(env("INPUTS_VALIDATE_WEBIDL")) === false) { 13 | exit("Skipped", 0); 14 | } 15 | 16 | const input: Input = JSON.parse(env("OUTPUTS_BUILD")); 17 | main(input).catch(err => exit(err.message || "Failed", err.code)); 18 | } 19 | 20 | export default async function main({ dest, file }: Input) { 21 | console.log(`Validating Web IDL defined in ${file}...`); 22 | Object.assign(process.env, PUPPETEER_ENV); 23 | await install("reffy"); 24 | const { crawlSpecs } = require("reffy"); 25 | 26 | const fileurl = new URL(file, `file://${dest}/`).href; 27 | const results = await crawlSpecs( 28 | [{ url: fileurl, nightly: { url: fileurl } }], 29 | { modules: ["idl"] }, 30 | ); 31 | Object.keys(PUPPETEER_ENV).forEach(key => delete process.env[key]); 32 | await rm(".cache", { recursive: true, force: true }); 33 | 34 | const idl = results[0]?.idl; 35 | if (!idl) { 36 | exit("No Web IDL found in spec, skipped validation", 0); 37 | } 38 | 39 | await install("webidl2"); 40 | // An outdated version might be cached from importing reffy. 41 | delete require.cache[require.resolve("webidl2")]; 42 | 43 | const { parse, validate } = require("webidl2"); 44 | let errors: { message: string }[] = []; 45 | try { 46 | const tree = parse(idl); 47 | errors = validate(tree); 48 | } catch (error) { 49 | errors = [error]; 50 | } 51 | if (!errors.length) { 52 | exit("✅ Looks good! No Web IDL validation errors!", 0); 53 | } else { 54 | console.group("Invalid Web IDL detected:"); 55 | for (const error of errors) { 56 | console.log(error.message); 57 | console.log(""); 58 | } 59 | console.groupEnd(); 60 | exit("❌ Invalid Web IDL detected... please fix the issues above."); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/prepare-deploy.ts: -------------------------------------------------------------------------------- 1 | import { exit, yesOrNo } from "./utils.ts"; 2 | 3 | import type { Inputs, GitHubContext } from "./prepare.ts"; 4 | export type W3CDeployOptions = Awaited>; 5 | export type GithubPagesDeployOptions = ReturnType; 6 | 7 | export function githubPagesDeployment( 8 | inputs: Inputs, 9 | githubContext: GitHubContext, 10 | ) { 11 | const { event_name: event, sha, repository, actor } = githubContext; 12 | const { default_branch: defaultBranch, has_pages: hasGitHubPagesEnabled } = 13 | githubContext.event.repository; 14 | const ghPagesBranch = inputs.GH_PAGES_BRANCH; 15 | 16 | if (!shouldTryDeploy(event)) { 17 | return false; 18 | } 19 | 20 | const askedNotToDeploy = yesOrNo(ghPagesBranch) === false; 21 | if (askedNotToDeploy || !ghPagesBranch) { 22 | return false; 23 | } 24 | 25 | const targetBranch = yesOrNo(ghPagesBranch) ? "gh-pages" : ghPagesBranch; 26 | 27 | if (defaultBranch === targetBranch) { 28 | exit( 29 | `Default branch and "GH_PAGES_BRANCH": "${targetBranch}" cannot be same.`, 30 | ); 31 | } 32 | 33 | if (!hasGitHubPagesEnabled) { 34 | console.log(`📣 Please enable GitHub pages in repository settings.`); 35 | } 36 | 37 | let token = inputs.GH_PAGES_TOKEN; 38 | if (!token) { 39 | token = githubContext.token; 40 | } 41 | 42 | return { targetBranch, token, event, sha, repository, actor }; 43 | } 44 | 45 | export async function w3cEchidnaDeployment( 46 | inputs: Inputs, 47 | githubContext: GitHubContext, 48 | ) { 49 | const { event_name: event, repository } = githubContext; 50 | if (!shouldTryDeploy(event)) { 51 | return false; 52 | } 53 | 54 | const token = inputs.W3C_ECHIDNA_TOKEN; 55 | const wgDecisionURL = inputs.W3C_WG_DECISION_URL; 56 | if (!token || !wgDecisionURL) { 57 | console.log( 58 | "📣 Skipping deploy to W3C as required inputs were not provided.", 59 | ); 60 | return false; 61 | } 62 | 63 | const cc = inputs.W3C_NOTIFICATIONS_CC; 64 | 65 | return { wgDecisionURL, cc, token: token, repository }; 66 | } 67 | 68 | function shouldTryDeploy(githubEvent: GitHubContext["event_name"]) { 69 | return githubEvent === "push" || githubEvent === "workflow_dispatch"; 70 | } 71 | -------------------------------------------------------------------------------- /test/index.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This tries to mimic the behaviour of action.yml running on GitHub. 3 | * Useful during development, until we add proper tests. 4 | * 5 | * Run this file in a local GitHub repo, and change inputs as needed. 6 | */ 7 | import { devNull } from "node:os"; 8 | import { createRequire } from "node:module"; 9 | import { formatAsHeading, pprint } from "../src/utils.ts"; 10 | 11 | let SILENT_CHILD = !true; 12 | 13 | Object.assign(process.env, { 14 | GITHUB_ENV: devNull, 15 | GITHUB_PATH: devNull, 16 | }); 17 | 18 | const console = global.console; 19 | if (SILENT_CHILD) { 20 | global.console = new Proxy(global.console, { 21 | get(target, prop) { 22 | // @ts-expect-error 23 | if (typeof target[prop] === "function") { 24 | return () => {}; 25 | } 26 | throw new Error("Not implemented."); 27 | }, 28 | }); 29 | } 30 | 31 | export interface Outputs { 32 | prepare: Awaited>; 33 | setup: Awaited>; 34 | build: Awaited>; 35 | } 36 | const outputs: Partial = {}; 37 | type AsyncFn = (outputs: Partial) => Promise; 38 | const run = (fn: AsyncFn) => async () => { 39 | await Promise.resolve(); 40 | console.log(formatAsHeading(fn.name, "+")); 41 | const res = await fn(outputs); 42 | console.log(res); 43 | console.log(); 44 | // @ts-ignore 45 | outputs[fn.name as keyof Outputs] = res; 46 | }; 47 | 48 | const require = createRequire(import.meta.url); 49 | Promise.resolve() 50 | .then(run(require("./prepare.test.ts").default)) 51 | .then(run(require("./setup.test.ts").default)) 52 | .then(run(require("./validate-input-markup.test.ts").default)) 53 | .then(run(require("./build.test.ts").default)) 54 | .then(run(require("./validate-links.test.ts").default)) 55 | .then(run(require("./validate-markup.test.ts").default)) 56 | .then(run(require("./validate-webidl.test.ts").default)) 57 | .then(run(require("./validate-pubrules.test.ts").default)) 58 | .then(run(require("./deploy-gh-pages.test.ts").default)) 59 | .then(() => { 60 | console.log(); 61 | console.log(formatAsHeading("OUTPUTS", "#")); 62 | pprint(outputs); 63 | }) 64 | .catch(err => { 65 | console.error(err); 66 | process.exit(1); 67 | }); 68 | -------------------------------------------------------------------------------- /src/prepare.ts: -------------------------------------------------------------------------------- 1 | import { env, exit, formatAsHeading, pprint, setOutput } from "./utils.ts"; 2 | 3 | import { buildOptions } from "./prepare-build.ts"; 4 | import { 5 | githubPagesDeployment, 6 | w3cEchidnaDeployment, 7 | } from "./prepare-deploy.ts"; 8 | import { validation } from "./prepare-validate.ts"; 9 | 10 | export interface Inputs { 11 | TOOLCHAIN: "respec" | "bikeshed" | string; 12 | SOURCE: string; 13 | DESTINATION: string; 14 | BUILD_FAIL_ON: string; 15 | VALIDATE_LINKS: string; 16 | VALIDATE_INPUT_MARKUP: string; 17 | VALIDATE_MARKUP: string; 18 | VALIDATE_PUBRULES: string; 19 | VALIDATE_WEBIDL: string; 20 | GH_PAGES_BRANCH: string; 21 | GH_PAGES_TOKEN: string; 22 | GH_PAGES_BUILD_OVERRIDE: string; 23 | W3C_ECHIDNA_TOKEN: string; 24 | W3C_BUILD_OVERRIDE: string; 25 | W3C_WG_DECISION_URL: string; 26 | W3C_NOTIFICATIONS_CC: string; 27 | ARTIFACT_NAME?: string; 28 | } 29 | 30 | export interface GitHubContext { 31 | token: string; 32 | event_name: string; 33 | repository: `${string}/${string}`; 34 | sha: string; 35 | actor: string; 36 | event: { 37 | repository: { default_branch: string; has_pages: boolean }; 38 | }; 39 | } 40 | 41 | if (import.meta.main) { 42 | const inputs: Inputs = JSON.parse(env("INPUTS_USER")); 43 | const githubContext: GitHubContext = JSON.parse(env("INPUTS_GITHUB")); 44 | main(inputs, githubContext).catch(err => 45 | exit(err.message || "Failed", err.code), 46 | ); 47 | } 48 | 49 | export default async function main( 50 | inputs: Inputs, 51 | githubContext: GitHubContext, 52 | ) { 53 | console.log(formatAsHeading("Provided input")); 54 | pprint(inputs); 55 | console.log(); 56 | 57 | const normalizedInputs = await processInputs(inputs, githubContext); 58 | 59 | console.log(`\n${formatAsHeading("Normalized input")}`); 60 | pprint(normalizedInputs); 61 | 62 | // Make processed inputs available to next steps. 63 | return { 64 | ...setOutput("build", normalizedInputs.build), 65 | ...setOutput("validate", normalizedInputs.validate), 66 | ...setOutput("deploy", normalizedInputs.deploy), 67 | }; 68 | } 69 | 70 | export type ProcessedInput = Awaited>; 71 | async function processInputs(inputs: Inputs, githubContext: GitHubContext) { 72 | return { 73 | build: await buildOptions(inputs, githubContext), 74 | validate: validation(inputs), 75 | deploy: { 76 | ghPages: githubPagesDeployment(inputs, githubContext), 77 | w3c: await w3cEchidnaDeployment(inputs, githubContext), 78 | }, 79 | }; 80 | } 81 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Spec Prod Documentation 6 | 7 | 12 | 72 | 73 | 74 | 75 |
76 |

The w3c/spec-prod GitHub Action lets you:

77 |
    78 |
  • 79 | Build ReSpec and 80 | Bikeshed 81 | specs. 82 |
  • 83 |
  • 84 | Validate input/generated document's markup and check for broken 85 | hyperlinks. 86 |
  • 87 |
  • 88 | Publish generated spec to GitHub Pages and/or w3.org (using Echidna). 89 |
  • 90 |
91 |
92 |
93 |
97 |
98 |
102 | 103 | 104 | -------------------------------------------------------------------------------- /src/validate-pubrules.ts: -------------------------------------------------------------------------------- 1 | import { createRequire } from "node:module"; 2 | import { 3 | env, 4 | exit, 5 | install, 6 | StaticServer, 7 | yesOrNo, 8 | formatAsHeading, 9 | } from "./utils.ts"; 10 | 11 | import type { BuildResult } from "./build.ts"; 12 | type Input = Pick; 13 | 14 | interface SpecberusError { 15 | name: string; 16 | key: string; 17 | detailMessage: string; 18 | extra: any; 19 | } 20 | interface Result { 21 | success: boolean; 22 | errors: SpecberusError[]; 23 | warnings: SpecberusError[]; 24 | info?: any[]; 25 | } 26 | interface ExtractMetadataResult { 27 | success: boolean; 28 | metadata: { 29 | profile: string; 30 | }; 31 | } 32 | interface SpecberusProfile { 33 | config: unknown; 34 | name: string; 35 | rules: { name: string; [key: string]: unknown }[]; 36 | } 37 | 38 | const require = createRequire(import.meta.url); 39 | 40 | const IGNORED_RULES = new Set([ 41 | // Forbidden host (localhost) 42 | "validation.html", 43 | // Uses validator.w3c.org so we can't use localhost there 44 | "links.linkchecker", 45 | ]); 46 | 47 | if (import.meta.main) { 48 | if (yesOrNo(env("INPUTS_VALIDATE_PUBRULES")) === false) { 49 | exit("Skipped", 0); 50 | } 51 | 52 | const input: Input = JSON.parse(env("OUTPUTS_BUILD")); 53 | main(input).catch(err => exit(err.message || "Failed", err.code)); 54 | } 55 | 56 | export default async function main({ dest, file }: Input) { 57 | console.log(`Running specberus on ${file} in ${dest}...`); 58 | await install("specberus"); 59 | 60 | const server = await new StaticServer(dest).start(); 61 | const url = new URL(file, server.url); 62 | let result; 63 | try { 64 | result = await validate(url); 65 | } catch (error) { 66 | console.error(error); 67 | exit("Something went wrong"); 68 | } finally { 69 | await server.stop(); 70 | } 71 | 72 | if (result.errors?.length) { 73 | console.log(formatAsHeading("Errors")); 74 | console.log(result.errors); 75 | } 76 | if (result.warnings?.length) { 77 | console.log(formatAsHeading("Warnings")); 78 | console.log(result.warnings); 79 | } 80 | if (!result.success) { 81 | exit("There were some errors"); 82 | } 83 | } 84 | 85 | function sinkAsync() { 86 | const { Sink } = require("specberus/lib/sink"); 87 | const noop = () => {}; 88 | const sink = new Sink(noop, noop, noop, noop); 89 | return { 90 | sink, 91 | resultPromise: new Promise((res, rej) => { 92 | sink.on("end-all", res); 93 | sink.on("exception", rej); 94 | }), 95 | }; 96 | } 97 | 98 | async function validate(url: URL) { 99 | const { Specberus } = require("specberus"); 100 | const specberus = new Specberus(); 101 | 102 | console.log("getting metadata"); 103 | const { metadata } = await extractMetadata(url); 104 | 105 | const { profiles } = require("specberus/lib/util"); 106 | const importedProfile: SpecberusProfile = await profiles[metadata.profile]; 107 | const profile: SpecberusProfile = { 108 | ...importedProfile, 109 | rules: importedProfile.rules.filter(({ name }) => !IGNORED_RULES.has(name)), 110 | }; 111 | 112 | console.log(`validating using profile: ${profile.name}`); 113 | const { sink, resultPromise } = sinkAsync(); 114 | specberus.validate({ 115 | url: url.href, 116 | profile, 117 | events: sink, 118 | echidnaReady: true, 119 | }); 120 | const result = await resultPromise; 121 | delete result.info; 122 | return result; 123 | } 124 | 125 | async function extractMetadata(url: URL) { 126 | const { Specberus } = require("specberus"); 127 | const specberus = new Specberus(); 128 | 129 | const { sink, resultPromise } = sinkAsync(); 130 | specberus.extractMetadata({ url, events: sink }); 131 | const result = await resultPromise; 132 | return result; 133 | } 134 | -------------------------------------------------------------------------------- /src/deploy-gh-pages.ts: -------------------------------------------------------------------------------- 1 | import * as path from "node:path"; 2 | import * as os from "node:os"; 3 | import * as fs from "node:fs/promises"; 4 | import { setSecret } from "@actions/core"; 5 | import { env, exit, sh } from "./utils.ts"; 6 | 7 | import type { GithubPagesDeployOptions } from "./prepare-deploy.ts"; 8 | type Input = Exclude; 9 | 10 | if (import.meta.main) { 11 | const inputs: GithubPagesDeployOptions = JSON.parse(env("INPUTS_DEPLOY")); 12 | const outputDir = env("OUTPUT_DIR"); 13 | 14 | if (inputs === false) { 15 | exit("Skipped.", 0); 16 | } 17 | main(inputs, outputDir).catch(err => exit(err.message || "Failed", err.code)); 18 | } 19 | 20 | export default async function main(inputs: Input, outputDir: string) { 21 | let error = null; 22 | await fs.copyFile(".git/config", "/tmp/spec-prod-git-config"); 23 | try { 24 | await prepare(inputs, outputDir); 25 | const gitStatus = await sh(`git status`, "stream"); 26 | const hasChanges = !gitStatus.includes("nothing to commit"); 27 | const committed = hasChanges && (await commit(inputs)); 28 | if (!committed) { 29 | await cleanUp(); 30 | exit(`Nothing to commit. Skipping deploy.`, 0); 31 | } 32 | await push(inputs); 33 | } catch (err) { 34 | console.log(err); 35 | error = err; 36 | } finally { 37 | await cleanUp(); 38 | if (error) { 39 | console.log(); 40 | console.log("=".repeat(60)); 41 | exit(error.message); 42 | } 43 | } 44 | } 45 | 46 | type PrepareInputs = Pick; 47 | async function prepare(opts: PrepareInputs, outputDir: string) { 48 | if (!outputDir.endsWith(path.sep)) outputDir += path.sep; 49 | const { targetBranch, repository } = opts; 50 | 51 | // Check if target branch remote exists on remote. 52 | // If it exists, we do a pull, otherwise we create a new orphan branch. 53 | const repoUri = `https://github.com/${repository}.git/`; 54 | if (await sh(`git ls-remote --heads "${repoUri}" "${targetBranch}"`)) { 55 | await sh(`git fetch origin "${targetBranch}"`, "stream"); 56 | await sh(`git checkout "${targetBranch}"`, "stream"); 57 | } else { 58 | await sh(`git checkout --orphan "${targetBranch}"`, "stream"); 59 | await sh(`git reset --hard`, "stream"); 60 | } 61 | 62 | await sh(`rsync -a ${outputDir} .`, "stream"); 63 | await sh(`git add -A`, "stream"); 64 | } 65 | 66 | type CommitInputs = Pick; 67 | async function commit({ sha, event, actor }: CommitInputs) { 68 | const GITHUB_ACTIONS_BOT = `github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>`; 69 | 70 | const author = await sh(`git show -s --format='%an | %ae' ${sha}`); 71 | const [name, email] = author.split(" | "); 72 | setSecret(email); 73 | await sh(`git config user.name "${name}"`); 74 | await sh(`git config user.email "${email}"`); 75 | 76 | const commitHeadline = await sh(`git show -s --format=%s ${sha}`); 77 | const commitMessage = [ 78 | commitHeadline, 79 | "", 80 | `SHA: ${sha}`, 81 | `Reason: ${event}, by ${actor}`, 82 | "", 83 | "", 84 | `Co-authored-by: ${GITHUB_ACTIONS_BOT}`, 85 | ].join("\n"); 86 | const COMMIT_MESSAGE_FILE = path.join(os.tmpdir(), "COMMIT_MSG"); 87 | await fs.writeFile(COMMIT_MESSAGE_FILE, commitMessage, "utf-8"); 88 | 89 | try { 90 | await sh(`git commit --file "${COMMIT_MESSAGE_FILE}"`); 91 | await sh(`git show --stat --format=""`); 92 | return true; 93 | } catch (error) { 94 | console.error(error); 95 | return false; 96 | } 97 | } 98 | 99 | type PushInputs = Pick; 100 | async function push({ repository, targetBranch, token }: PushInputs) { 101 | const repoURI = `https://x-access-token:${token}@github.com/${repository}.git/`; 102 | await sh(`git remote set-url origin "${repoURI}"`); 103 | await sh(`git pull origin "${targetBranch}" --rebase`).catch(() => {}); 104 | await sh(`git push --force-with-lease origin "${targetBranch}"`, "stream"); 105 | } 106 | 107 | async function cleanUp() { 108 | console.group("Cleanup"); 109 | try { 110 | await sh(`git reset`); 111 | await sh(`git clean -fd`); 112 | await sh(`git checkout -`); 113 | await sh(`git checkout -- .`); 114 | } catch (error) { 115 | console.error(error); 116 | } finally { 117 | await fs.copyFile("/tmp/spec-prod-git-config", ".git/config"); 118 | console.groupEnd(); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/deploy-w3c-echidna.ts: -------------------------------------------------------------------------------- 1 | import { env, exit, pprint, sh } from "./utils.ts"; 2 | 3 | import type { W3CDeployOptions } from "./prepare-deploy.ts"; 4 | import type { BuildResult } from "./build.ts"; 5 | type Input = Exclude; 6 | 7 | const MAILING_LIST = `https://lists.w3.org/Archives/Public/public-tr-notifications/`; 8 | const API_URL = "https://labs.w3.org/echidna/api/request"; 9 | 10 | if (import.meta.main) { 11 | const inputs: W3CDeployOptions = JSON.parse(env("INPUTS_DEPLOY")); 12 | const buildOutput: BuildResult = JSON.parse(env("OUTPUTS_BUILD")); 13 | if (inputs === false) { 14 | exit("Skipped.", 0); 15 | } 16 | main(inputs, buildOutput).catch(err => { 17 | exit(err.message || "Failed", err.code); 18 | }); 19 | } 20 | 21 | interface PublishState { 22 | id: string; 23 | status: "success" | "failure" | "pending"; 24 | details: string; 25 | deploymentURL?: string; 26 | response?: string | EchidnaResponse | { message: string; [x: string]: any }; 27 | } 28 | 29 | interface EchidnaJobs extends Record< 30 | | "token-checker" 31 | | "third-party-checker" 32 | | "update-tr-shortlink" 33 | | "tr-install" 34 | | "publish" 35 | | "ip-checker" 36 | | "metadata", 37 | { errors: any[] } 38 | > { 39 | specberus: { 40 | errors: { 41 | key: string; 42 | detailMessage: string; 43 | type: { name: string; section: string; rule: string }; 44 | extra?: { message: string; link: string }; 45 | }[]; 46 | }; 47 | } 48 | 49 | interface EchidnaResponse { 50 | id: string; 51 | decision: string; 52 | results: { 53 | status: PublishState["status"]; 54 | metadata: { 55 | thisVersion: string; 56 | previousVersion: string; 57 | }; 58 | jobs: EchidnaJobs; 59 | }; 60 | } 61 | 62 | export default async function main(inputs: Input, buildOutput: BuildResult) { 63 | console.log(`📣 If it fails, check ${MAILING_LIST}`); 64 | const id = await publish(inputs, buildOutput); 65 | 66 | console.group("Getting publish status..."); 67 | const result = await getPublishStatus(id); 68 | console.groupEnd(); 69 | const { response } = result; 70 | 71 | if (result.status !== "failure") { 72 | delete result.response; // don't need to print it right now. 73 | pprint(result); 74 | } 75 | 76 | if (result.status === "success") { 77 | exit(`🎉 Published at: ${result.deploymentURL}`, 0); 78 | } 79 | 80 | if (result.status !== "failure") { 81 | exit("🚧 Echidna publish job is pending.", 0); 82 | } 83 | 84 | if (response && typeof response !== "string") { 85 | // Assume it is JSON 86 | showErrors(response.results.jobs); 87 | } 88 | console.log(`Details: ${result.details}`); 89 | exit("💥 Echidna publish has failed."); 90 | } 91 | 92 | async function publish(input: Input, buildOutput: BuildResult) { 93 | const { dest: outputDir, file } = buildOutput; 94 | const { wgDecisionURL: decision, token, cc, repository } = input; 95 | const annotation = `triggered by auto-publish spec-prod action on ${repository}`; 96 | const tarFileName = "/tmp/echidna.tar"; 97 | await sh(`mv -n ${file} Overview.html`, { cwd: outputDir }); 98 | await sh(`tar cvf ${tarFileName} *`, { 99 | output: "stream", 100 | cwd: outputDir, 101 | }); 102 | await sh(`mv -n Overview.html ${file}`, { cwd: outputDir }); 103 | 104 | let command = `curl '${API_URL}'`; 105 | // command += ` -F "dry-run=true"`; 106 | command += ` -F "tar=@${tarFileName}"`; 107 | command += ` -F "token=${token}"`; 108 | command += ` -F "annotation=${annotation}"`; 109 | command += ` -F "decision=${decision}"`; 110 | if (cc) command += ` -F "cc=${cc}"`; 111 | 112 | const id = await sh(command); 113 | return id.trim(); 114 | } 115 | 116 | async function getPublishStatus(id: string) { 117 | // How many seconds to wait before retrying job status check? 118 | // The numbers are based on "experience", so are somewhat random. 119 | const RETRY_DURATIONS = [6, 3, 2, 8, 2, 5, 10, 6]; 120 | 121 | let url = new URL("https://labs.w3.org/echidna/api/status"); 122 | url.searchParams.set("id", id); 123 | 124 | const state: PublishState = { 125 | id, 126 | status: "pending", 127 | details: url.href, 128 | deploymentURL: undefined, 129 | response: undefined, 130 | }; 131 | 132 | let response; 133 | do { 134 | const wait = RETRY_DURATIONS.shift(); 135 | if (!wait) break; 136 | console.log(`⏱️ Wait ${wait}s for job to finish...`); 137 | await new Promise(res => setTimeout(res, wait * 1000)); 138 | 139 | console.log(`📡 Request: ${url}`); 140 | const res = await fetch(url); 141 | if (res.headers.get("content-type")?.includes("json")) { 142 | response = (await res.json()) as EchidnaResponse; 143 | } else { 144 | response = await res.text(); 145 | state.response = { message: response }; 146 | continue; 147 | } 148 | 149 | state.status = response.results.status; 150 | state.response = response; 151 | 152 | if (state.status === "success") { 153 | state.deploymentURL = response.results.metadata.thisVersion; 154 | } 155 | } while ( 156 | state.status !== "success" && 157 | state.status !== "failure" && 158 | RETRY_DURATIONS.length > 0 159 | ); 160 | state.response = response; 161 | return state; 162 | } 163 | 164 | function showErrors(jobs: EchidnaJobs) { 165 | type ErrorLoggers = { 166 | [k in keyof EchidnaJobs]?: (errors: EchidnaJobs[k]["errors"]) => void; 167 | }; 168 | const loggers: ErrorLoggers = { 169 | "token-checker"(errors) { 170 | console.group("Token Checker Errors:"); 171 | for (const error of errors) { 172 | console.log(error); 173 | } 174 | console.groupEnd(); 175 | }, 176 | specberus(errors) { 177 | console.group("Specberus Errors:"); 178 | for (const error of errors) { 179 | const { detailMessage } = error; 180 | const { message, link } = error.extra || {}; 181 | const { key, type } = error; 182 | console.group(detailMessage || message || key); 183 | if (type) console.log(type); 184 | if (link) console.log(link); 185 | console.groupEnd(); 186 | } 187 | console.groupEnd(); 188 | }, 189 | }; 190 | 191 | type AvailableErrorLoggers = keyof ErrorLoggers; 192 | for (const type of Object.keys(loggers) as AvailableErrorLoggers[]) { 193 | const logger = loggers[type]!; 194 | if (jobs[type]?.errors.length) { 195 | console.log(); 196 | logger(jobs[type].errors); 197 | console.log(); 198 | } 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | import { deepStrictEqual } from "node:assert"; 3 | import { inspect } from "node:util"; 4 | import { exec, type ExecOptions } from "node:child_process"; 5 | import { readFile, writeFile } from "node:fs/promises"; 6 | import { createServer, type Server } from "node:http"; 7 | 8 | import core from "@actions/core"; 9 | import finalhandler from "finalhandler"; 10 | import serveStatic from "serve-static"; 11 | import split from "split2"; 12 | 13 | import { ACTION_DIR } from "./constants.ts"; 14 | 15 | export function deepEqual(a: unknown, b: unknown) { 16 | try { 17 | deepStrictEqual(a, b); 18 | return true; 19 | } catch { 20 | return false; 21 | } 22 | } 23 | 24 | export function env(name: string) { 25 | const value = process.env[name]; 26 | if (value) return value; 27 | exit(`env variable \`${name}\` is not set.`); 28 | } 29 | 30 | export function exit(message: string, code = 1): never { 31 | if (code === 0) { 32 | console.log(message); 33 | } else { 34 | console.error(message); 35 | core.error(message); 36 | } 37 | process.exit(code); 38 | } 39 | 40 | export function formatAsHeading(text: string, symbol = "=") { 41 | const marker = symbol.repeat(Math.max(50, text.length)); 42 | return `${marker}\n${text}:\n${marker}`; 43 | } 44 | 45 | /** 46 | * Locally install a npm package using pnpm. 47 | */ 48 | export async function install(name: string, env: ExecOptions["env"] = {}) { 49 | const [packageJson, pnpmLock] = await Promise.all([ 50 | readFile(path.join(ACTION_DIR, "package.json")), 51 | readFile(path.join(ACTION_DIR, "pnpm-lock.yaml")), 52 | ]); 53 | const output = await sh(`pnpm add ${name}`, { cwd: ACTION_DIR, env }); 54 | // Restore as if `pnpm add --no-save` was supported. 55 | // https://github.com/pnpm/pnpm/issues/1237 56 | await Promise.all([ 57 | writeFile(path.join(ACTION_DIR, "package.json"), packageJson), 58 | writeFile(path.join(ACTION_DIR, "pnpm-lock.yaml"), pnpmLock), 59 | ]); 60 | const pkgName = name.replace(/@.+/, ""); 61 | const re = new RegExp(String.raw`\+ (${pkgName})\s(.+)$`); 62 | const versionLine = output.split("\n").find(line => re.test(line)); 63 | if (versionLine) { 64 | console.log(versionLine); 65 | } 66 | return output; 67 | } 68 | 69 | /** 70 | * Print print using util.inspect 71 | */ 72 | export function pprint(obj: any) { 73 | console.log(inspect(obj, false, Infinity, true)); 74 | } 75 | 76 | export function setOutput(key: K, value: V) { 77 | core.setOutput(key, value); 78 | return { [key]: value } as { [k in K]: V }; 79 | } 80 | 81 | type ShOutput = "buffer" | "stream" | "silent"; 82 | interface ShOptions extends ExecOptions { 83 | output?: ShOutput; 84 | } 85 | /** 86 | * Asynchronously run a shell command get its result. 87 | * @returns stdout 88 | * @throws {Promise<{ stdout: string, stderr: string, code: number }>} 89 | */ 90 | export async function sh(command: string, options: ShOptions | ShOutput = {}) { 91 | const { output, ...execOptions } = 92 | typeof options === "string" ? { output: options } : options; 93 | 94 | const BOLD = "\x1b[1m"; 95 | const RESET = "\x1b[22m"; 96 | if (output !== "silent") { 97 | console.group(`${BOLD}$ ${command}${RESET}`); 98 | } 99 | 100 | try { 101 | return await new Promise((resolve, reject) => { 102 | let stdout = ""; 103 | let stderr = ""; 104 | const child = exec(command, { 105 | ...execOptions, 106 | env: { ...process.env, ...execOptions.env }, 107 | encoding: "utf-8", 108 | }); 109 | child.stdout!.pipe(split()).on("data", chunk => { 110 | if (output === "stream") console.log(chunk); 111 | stdout += chunk + "\n"; 112 | }); 113 | child.stderr!.pipe(split()).on("data", chunk => { 114 | if (output === "stream") console.log(chunk); 115 | stderr += chunk + "\n"; 116 | }); 117 | child.on("exit", code => { 118 | stdout = stdout.trim(); 119 | stderr = stderr.trim(); 120 | if (output === "buffer") { 121 | if (stdout) console.log(stdout); 122 | if (stderr) console.error(stderr); 123 | } 124 | if (code === 0) { 125 | resolve(stdout); 126 | } else { 127 | const message = `Command \`${command}\` failed with exit code: ${code}.`; 128 | reject({ message, stdout, stderr, code }); 129 | } 130 | }); 131 | }); 132 | } finally { 133 | if (output !== "silent") { 134 | console.groupEnd(); 135 | } 136 | } 137 | } 138 | 139 | export function unique(items: T[], key?: (item: T) => string | number) { 140 | if (!key) { 141 | return [...new Set(items)]; 142 | } 143 | 144 | const alreadyHas = new Set>(); 145 | const uniqueItems: T[] = []; 146 | for (const item of items) { 147 | const k = key(item); 148 | if (!alreadyHas.has(k)) { 149 | uniqueItems.push(item); 150 | alreadyHas.add(k); 151 | } 152 | } 153 | return uniqueItems; 154 | } 155 | 156 | export function yesOrNo(value: string | number | boolean): boolean | undefined { 157 | const str = String(value).trim(); 158 | if (/^(?:y|yes|true|1|on)$/i.test(str)) { 159 | return true; 160 | } 161 | if (/^(?:n|no|false|0|off)$/i.test(str)) { 162 | return false; 163 | } 164 | } 165 | 166 | /** A simple HTTP server without all the fancy things. */ 167 | export class StaticServer { 168 | private _server: Server; 169 | private _port: number = 3000; 170 | 171 | constructor(dir = process.cwd()) { 172 | const serve = serveStatic(dir); 173 | this._server = createServer((req, res) => { 174 | serve(req, res, finalhandler(req, res)); 175 | }); 176 | } 177 | 178 | async start() { 179 | for (this._port = 3000; this._port < 9000; this._port++) { 180 | try { 181 | await this._tryStart(this._port); 182 | return this; 183 | } catch (error) { 184 | await this.stop(); 185 | if (error.code !== "EADDRINUSE") { 186 | throw error; 187 | } 188 | } 189 | } 190 | throw new Error("Failed to start static server."); 191 | } 192 | 193 | private _tryStart(port: number) { 194 | return new Promise((resolve, reject) => { 195 | this._server.listen(port).on("error", reject).on("listening", resolve); 196 | }); 197 | } 198 | 199 | stop() { 200 | return new Promise(resolve => this._server.close(err => resolve())); 201 | } 202 | 203 | get url() { 204 | return new URL(`http://localhost:${this._port}`); 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: Spec Prod 2 | author: "Sid Vishnoi" 3 | description: "Build ReSpec/Bikeshed specs, validate output and publish to w3.org or GitHub pages" 4 | 5 | branding: 6 | icon: archive 7 | color: blue 8 | 9 | inputs: 10 | TOOLCHAIN: 11 | description: Possible values - 'respec', 'bikeshed' 12 | SOURCE: 13 | description: Source file path. 14 | DESTINATION: 15 | description: Destination path, relative to repository root. 16 | BUILD_FAIL_ON: 17 | description: Exit behaviour on errors. 18 | default: fatal 19 | VALIDATE_INPUT_MARKUP: 20 | description: Validate input markup 21 | default: "false" 22 | VALIDATE_LINKS: 23 | description: Validate hyperlinks 24 | default: "false" 25 | required: false 26 | VALIDATE_MARKUP: 27 | description: Validate markup 28 | default: "true" 29 | VALIDATE_WEBIDL: 30 | description: Validate Web IDL 31 | default: "true" 32 | VALIDATE_PUBRULES: 33 | description: Validate against W3C Publication Rules 34 | default: "false" 35 | GH_PAGES_BRANCH: 36 | description: Provide a branch name to deploy to GitHub pages. 37 | GH_PAGES_BUILD_OVERRIDE: 38 | description: Override Bikeshed's metadata or ReSpec's respecConfig for GitHub Pages deployment. 39 | GH_PAGES_TOKEN: 40 | description: GitHub Personal access token. Required only if the default GitHub actions token doesn't have enough permissions. 41 | W3C_ECHIDNA_TOKEN: 42 | description: Echidna token 43 | W3C_WG_DECISION_URL: 44 | description: A URL to the working group decision to use auto-publish (usually from a w3c mailing list). 45 | W3C_BUILD_OVERRIDE: 46 | description: Override Bikeshed's metadata or ReSpec's respecConfig for W3C deployment and validations. 47 | W3C_NOTIFICATIONS_CC: 48 | description: Comma separated list of email addresses to CC 49 | ARTIFACT_NAME: 50 | description: Name for build artifact 51 | 52 | runs: 53 | using: composite 54 | steps: 55 | # Both pnpm and setup-node expect paths relative to checked-out repo. But we 56 | # want to use package.json and .nvmrc from action repo. So, we'll provide 57 | # them a path relative to GITHUB_WORKSPACE 58 | # See https://github.com/actions/setup-node/issues/852 59 | - id: action_relative_path 60 | run: | 61 | action_path=$(node -p 'require("path").relative(process.env.GITHUB_WORKSPACE, "${{ github.action_path }}")') 62 | echo "action_path=${action_path}" >> "$GITHUB_OUTPUT" 63 | shell: bash 64 | 65 | - uses: pnpm/action-setup@v4 66 | with: 67 | package_json_file: ${{ steps.action_relative_path.outputs.action_path }}/package.json 68 | - uses: actions/setup-node@v5 69 | with: 70 | node-version-file: ${{ steps.action_relative_path.outputs.action_path }}/.nvmrc 71 | package-manager-cache: false 72 | - name: Set up action 73 | run: | 74 | echo "::group::Set up action" 75 | pnpm -C ${{ github.action_path }} install --prod 76 | echo "::endgroup::" 77 | shell: bash 78 | env: 79 | FORCE_COLOR: "true" 80 | PUPPETEER_SKIP_DOWNLOAD: "1" 81 | 82 | - name: Prepare 83 | id: prepare 84 | run: | 85 | echo "::group::Prepare" 86 | node ${{ github.action_path }}/src/prepare.ts 87 | echo "::endgroup::" 88 | shell: bash 89 | env: 90 | INPUTS_USER: ${{ toJSON(inputs) }} 91 | INPUTS_GITHUB: ${{ toJSON(github) }} 92 | 93 | - name: Setup toolchain 94 | run: | 95 | echo "::group::Setup toolchain" 96 | node ${{ github.action_path }}/src/setup.ts 97 | echo "::endgroup::" 98 | shell: bash 99 | env: 100 | INPUTS_TOOLCHAIN: ${{ steps.prepare.outputs.build && fromJson(steps.prepare.outputs.build).toolchain }} 101 | 102 | - name: Validate input markup 103 | run: | 104 | echo "::group::Validate input markup" 105 | node ${{ github.action_path }}/src/validate-input-markup.ts 106 | echo "::endgroup::" 107 | shell: bash 108 | env: 109 | INPUTS_VALIDATE_INPUT_MARKUP: ${{ steps.prepare.outputs.validate && fromJson(steps.prepare.outputs.validate).input_markup }} 110 | INPUTS_BUILD: ${{ steps.prepare.outputs.build && toJSON(fromJson(steps.prepare.outputs.build)) }} 111 | 112 | - name: Generate Static HTML 113 | id: build 114 | run: | 115 | echo "::group::Generate Static HTML" 116 | node ${{ github.action_path }}/src/build.ts 117 | echo "::endgroup::" 118 | shell: bash 119 | env: 120 | INPUTS_BUILD: ${{ steps.prepare.outputs.build && toJSON(fromJson(steps.prepare.outputs.build)) }} 121 | 122 | - name: Upload Build Artifacts 123 | id: upload-build-artifacts 124 | uses: actions/upload-artifact@v4 125 | with: 126 | path: |- 127 | ${{ steps.build.outputs.gh && fromJson(steps.build.outputs.gh).dest }} 128 | ${{ steps.build.outputs.w3c && fromJson(steps.build.outputs.w3c).dest }} 129 | name: ${{ steps.prepare.outputs.build && fromJson(steps.prepare.outputs.build).artifactName }} 130 | retention-days: 5 131 | 132 | - name: Validate hyperlinks 133 | if: ${{ success() && steps.prepare.outputs.validate && fromJson(steps.prepare.outputs.validate).links }} 134 | run: | 135 | echo "::group::Validate hyperlinks" 136 | node ${{ github.action_path }}/src/validate-links.ts 137 | echo "::endgroup::" 138 | shell: bash 139 | env: 140 | OUTPUTS_BUILD: ${{ steps.build.outputs.w3c && toJson(fromJson(steps.build.outputs.w3c)) }} 141 | 142 | - name: Validate markup 143 | run: | 144 | echo "::group::Validate markup" 145 | node ${{ github.action_path }}/src/validate-markup.ts 146 | echo "::endgroup::" 147 | shell: bash 148 | env: 149 | INPUTS_VALIDATE_MARKUP: ${{ steps.prepare.outputs.validate && fromJson(steps.prepare.outputs.validate).markup }} 150 | OUTPUTS_BUILD: ${{ steps.build.outputs.w3c && toJson(fromJson(steps.build.outputs.w3c)) }} 151 | 152 | - name: Validate pubrules (Specberus) 153 | run: | 154 | echo "::group::Validate pubrules (Specberus)" 155 | node ${{ github.action_path }}/src/validate-pubrules.ts 156 | echo "::endgroup::" 157 | shell: bash 158 | env: 159 | INPUTS_VALIDATE_PUBRULES: ${{ steps.prepare.outputs.validate && fromJson(steps.prepare.outputs.validate).pubrules }} 160 | OUTPUTS_BUILD: ${{ steps.build.outputs.w3c && toJson(fromJson(steps.build.outputs.w3c)) }} 161 | 162 | - name: Validate Web IDL 163 | run: | 164 | echo "::group::Validate Web IDL" 165 | node ${{ github.action_path }}/src/validate-webidl.ts 166 | echo "::endgroup::" 167 | shell: bash 168 | env: 169 | INPUTS_VALIDATE_WEBIDL: ${{ steps.prepare.outputs.validate && fromJson(steps.prepare.outputs.validate).webidl }} 170 | OUTPUTS_BUILD: ${{ steps.build.outputs.w3c && toJson(fromJson(steps.build.outputs.w3c)) }} 171 | 172 | - name: Deploy to GitHub pages 173 | run: | 174 | echo "::group::Deploy to GitHub pages" 175 | node ${{ github.action_path }}/src/deploy-gh-pages.ts 176 | echo "::endgroup::" 177 | shell: bash 178 | env: 179 | INPUTS_DEPLOY: ${{ steps.prepare.outputs.deploy && toJson(fromJson(steps.prepare.outputs.deploy).ghPages) }} 180 | OUTPUT_DIR: ${{ steps.build.outputs.gh && fromJson(steps.build.outputs.gh).root }} 181 | 182 | - name: Deploy to W3C 183 | run: | 184 | echo "::group::Deploy to W3C" 185 | node ${{ github.action_path }}/src/deploy-w3c-echidna.ts 186 | echo "::endgroup::" 187 | shell: bash 188 | env: 189 | INPUTS_DEPLOY: ${{ steps.prepare.outputs.deploy && toJson(fromJson(steps.prepare.outputs.deploy).w3c) }} 190 | OUTPUTS_BUILD: ${{ steps.build.outputs.w3c && toJson(fromJson(steps.build.outputs.w3c)) }} 191 | 192 | - name: End 193 | if: ${{ failure() }} 194 | shell: bash 195 | run: | 196 | # Failed 197 | node -e 'require("@actions/core").setFailed("Failed. See details above.")' 198 | working-directory: ${{ github.action_path }} 199 | -------------------------------------------------------------------------------- /docs/examples.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | ## Run as a validator on pull requests 4 | 5 | If you do not pass any inputs, it by default builds a ReSpec or Bikeshed document (`index.html` or `index.bs`) and validates the output. It does not deploy the built document anywhere. 6 | 7 | ```yaml 8 | # Create a file called .github/workflows/auto-publish.yml 9 | name: CI 10 | on: 11 | pull_request: {} 12 | jobs: 13 | main: 14 | name: Build and Validate 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v5 18 | - uses: w3c/spec-prod@v2 19 | ``` 20 | 21 | ### Selectively enable/disable validators 22 | 23 | By default, both markup and Web IDL validators are enabled. 24 | 25 | ```yaml 26 | # Create a file called .github/workflows/auto-publish.yml 27 | name: CI 28 | on: 29 | pull_request: {} 30 | jobs: 31 | main: 32 | name: Build and Validate 33 | runs-on: ubuntu-latest 34 | steps: 35 | - uses: actions/checkout@v5 36 | - uses: w3c/spec-prod@v2 37 | with: 38 | VALIDATE_WEBIDL: false 39 | VALIDATE_MARKUP: true 40 | ``` 41 | 42 | ## Specify toolchain: Bikeshed or ReSpec 43 | 44 | Specify `TOOLCHAIN` if the action cannot figure out the toolchain itself, or if you like to be explicit. 45 | 46 | ```yaml 47 | # Create a file called .github/workflows/auto-publish.yml 48 | name: CI 49 | on: 50 | pull_request: {} 51 | jobs: 52 | main: 53 | name: Build and Validate 54 | runs-on: ubuntu-latest 55 | steps: 56 | - uses: actions/checkout@v5 57 | - uses: w3c/spec-prod@v2 58 | with: 59 | TOOLCHAIN: respec # or bikeshed 60 | ``` 61 | 62 | ## Deploy to GitHub pages 63 | 64 | Deployment is only done on `push` events. In this example: 65 | 66 | - the document is built and validated as a check in the pull request. 67 | - the document is built and validated, and then deployed to `gh-pages` branch, when a commit is pushed to the `main` branch. 68 | 69 | ```yaml 70 | # Create a file called .github/workflows/auto-publish.yml 71 | name: CI 72 | on: 73 | pull_request: {} 74 | push: 75 | branches: [main] 76 | jobs: 77 | main: 78 | name: Build, Validate and Deploy 79 | runs-on: ubuntu-latest 80 | permissions: 81 | contents: write 82 | steps: 83 | - uses: actions/checkout@v5 84 | - uses: w3c/spec-prod@v2 85 | with: 86 | GH_PAGES_BRANCH: gh-pages 87 | ``` 88 | 89 | ### Change output location for built files 90 | 91 | By default, output location is mapped to the `SOURCE`. You can change that by providing a [`DESTINATION`](options.md#destination). 92 | 93 | ```yaml 94 | # Create a file called .github/workflows/auto-publish.yml 95 | name: CI 96 | on: 97 | push: 98 | branches: [main] 99 | jobs: 100 | main: 101 | name: Deploy to GitHub pages 102 | runs-on: ubuntu-latest 103 | permissions: 104 | contents: write 105 | steps: 106 | - uses: actions/checkout@v5 107 | - uses: w3c/spec-prod@v2 108 | with: 109 | GH_PAGES_BRANCH: gh-pages 110 | TOOLCHAIN: bikeshed 111 | SOURCE: src/spec.bs 112 | DESTINATION: specification/index.html # `src/spec.html` if not provided. 113 | # Deployment will be available at: https://.github.io//specification/ 114 | ``` 115 | 116 | ## Deploy to W3C using Echidna 117 | 118 | ```yaml 119 | # Create a file called .github/workflows/auto-publish.yml 120 | name: CI 121 | on: 122 | pull_request: {} 123 | push: 124 | branches: [main] 125 | jobs: 126 | main: 127 | name: Build, Validate and Deploy 128 | runs-on: ubuntu-latest 129 | steps: 130 | - uses: actions/checkout@v5 131 | - uses: w3c/spec-prod@v2 132 | with: 133 | W3C_ECHIDNA_TOKEN: ${{ secrets.ECHIDNA_TOKEN }} 134 | # Replace following with appropriate value. See options.md for details. 135 | W3C_WG_DECISION_URL: https://lists.w3.org/Archives/Public/public-group/2014JulSep/1234.html 136 | ``` 137 | 138 | ### Use different `respecConfig` when deploying to W3C 139 | 140 | ```yaml 141 | # Example: Override respecConfig for W3C deployment and validators. 142 | name: CI 143 | on: 144 | pull_request: {} 145 | push: 146 | branches: [main] 147 | jobs: 148 | main: 149 | name: Build, Validate and Deploy 150 | runs-on: ubuntu-latest 151 | steps: 152 | - uses: actions/checkout@v5 153 | - uses: w3c/spec-prod@v2 154 | with: 155 | TOOLCHAIN: respec 156 | W3C_ECHIDNA_TOKEN: ${{ secrets.ECHIDNA_TOKEN }} 157 | W3C_WG_DECISION_URL: https://WG_DECISION_URL_FOR_MY_SPEC.com 158 | # Publish to w3.org/TR as a Working Draft (WD) under a different shortName. 159 | W3C_BUILD_OVERRIDE: | 160 | specStatus: WD 161 | shortName: my-custom-shortname 162 | ``` 163 | 164 | See [`W3C_BUILD_OVERRIDE`](options.md#w3c_build_override) and [`GH_PAGES_BUILD_OVERRIDE`](options.md#gh_pages_build_override) for details. 165 | 166 | ## Multiple specs in same repository 167 | 168 | If you maintain multiple documents in the same repository, you can provide source-destination pairs to build, validate and deploy each one separately. 169 | 170 | ```yaml 171 | # Create a file called .github/workflows/auto-publish.yml 172 | name: CI 173 | on: 174 | pull_request: {} 175 | push: 176 | branches: [main] 177 | jobs: 178 | main: 179 | name: Build, Validate and Deploy 180 | runs-on: ubuntu-latest 181 | permissions: 182 | contents: write 183 | strategy: 184 | max-parallel: 1 185 | matrix: 186 | include: 187 | - source: spec.html 188 | destination: index.html 189 | echidna_token: ECHIDNA_TOKEN_SPEC 190 | - source: spec-1 191 | destination: the-spec 192 | echidna_token: ECHIDNA_TOKEN_SPEC1 193 | - source: spec-2 194 | # destination defaults to spec-2/index.html 195 | # echidna_token defaults to no publication to w3.org/TR 196 | steps: 197 | - uses: actions/checkout@v5 198 | - uses: w3c/spec-prod@v2 199 | with: 200 | SOURCE: ${{ matrix.source }} 201 | DESTINATION: ${{ matrix.destination }} 202 | GH_PAGES_BRANCH: gh-pages 203 | W3C_ECHIDNA_TOKEN: ${{ secrets[matrix.echidna_token] }} 204 | W3C_WG_DECISION_URL: "https://lists.w3.org/Archives/Public/xyz.html" 205 | ``` 206 | 207 | **Note:** Echidna tokens need to be specified per document but secrets cannot be directly evaluated at the `matrix` level, meaning that `${{\ secrets.ECHIDNA_TOKEN_SPEC }}` cannot be evaluated at that level. As in the above example, the idea is rather to name the token secret at the `matrix` level (through `echidna_token: ECHIDNA_TOKEN_SPEC`) and to evaluate the secret in the job's `steps` (through `${{\ secrets[matrix.echidna_token] }}`). 208 | 209 | **Note:** Add the `max-parallel: 1` setting as in the example if you run into situations where jobs attempt to push updates to the repository at the same time and fail (see [#58](https://github.com/w3c/spec-prod/issues/58)). This setting makes GitHub run jobs sequentially. 210 | 211 | **Note:** At present, each source might create its own commit in `GH_PAGES_BRANCH` even when content of other sources hasn't changed. This is because the build output for each source contains build date. Though, if you deploy multiple times in the same day, the noise will reduce effectively as the build date (hence the diff) hasn't changed. The situation will improve when [#8](https://github.com/w3c/spec-prod/issues/8) and [#14](https://github.com/w3c/spec-prod/issues/14) are fixed. 212 | 213 | As a workaround, you can create separate workflows for each document and use GitHub Actions' [`on..paths`](https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#onpushpull_requestpaths) as: 214 | 215 | ```yaml 216 | # Create a file called .github/workflows/auto-publish-spec-1.yml 217 | name: CI (spec-1) 218 | on: 219 | pull_request: 220 | paths: ["spec-1/**"] 221 | push: 222 | branches: [main] 223 | paths: ["spec-1/**"] 224 | 225 | jobs: 226 | main: 227 | name: Build, Validate and Deploy 228 | runs-on: ubuntu-latest 229 | permissions: 230 | contents: write 231 | steps: 232 | - uses: actions/checkout@v5 233 | - uses: w3c/spec-prod@v2 234 | with: 235 | SOURCE: spec-1 236 | DESTINATION: the-spec 237 | GH_PAGES_BRANCH: gh-pages 238 | W3C_ECHIDNA_TOKEN: ${{ secrets.ECHIDNA_TOKEN }} 239 | W3C_WG_DECISION_URL: "https://lists.w3.org/Archives/Public/xyz.html" 240 | 241 | # Create another file called .github/workflows/auto-publish-spec-2.yml 242 | name: CI (spec-2) 243 | on: 244 | pull_request: 245 | paths: ["spec-2/**"] 246 | push: 247 | branches: [main] 248 | paths: ["spec-2/**"] 249 | 250 | jobs: 251 | main: 252 | name: Build, Validate and Deploy 253 | runs-on: ubuntu-latest 254 | permissions: 255 | contents: write 256 | steps: 257 | - uses: actions/checkout@v5 258 | - uses: w3c/spec-prod@v2 259 | with: 260 | SOURCE: spec-2/spec.bs 261 | DESTINATION: spec-2/index.html 262 | GH_PAGES_BRANCH: gh-pages 263 | W3C_ECHIDNA_TOKEN: ${{ secrets.ECHIDNA_TOKEN }} 264 | W3C_WG_DECISION_URL: "https://lists.w3.org/Archives/Public/xyz.html" 265 | ``` 266 | -------------------------------------------------------------------------------- /docs/options.md: -------------------------------------------------------------------------------- 1 | # Options 2 | 3 | ## `TOOLCHAIN` 4 | 5 | Toolchain to use. 6 | 7 | **Possible values:** `respec`, `bikeshed`. 8 | 9 | **Default:** None. Inferred from `SOURCE`: `respec` if an `index.html` exists, or `bikeshed` if an `index.bs` exists. 10 | 11 | ## `SOURCE` 12 | 13 | Source file path. 14 | 15 | **Possible values:** Any valid POSIX file path relative to repository root. 16 | 17 | **Default:** None. Inferred from `TOOLCHAIN`: `index.html`/`index.bs` if exists. 18 | 19 | ## `DESTINATION` 20 | 21 | Location of generated HTML document and other assets. This is useful when you've multiple specs in same repository. 22 | 23 | **Possible values:** Any valid POSIX file path relative to repository root. 24 | 25 | **Default:** `SOURCE`, with file extension set to `.html`. 26 | 27 | | `SOURCE` | `DESTINATION` | Location of generated spec | Assets copied to directory | 28 | | ----------------- | ------------- | -------------------------- | -------------------------- | 29 | | `index.bs` | None | `./index.html` | `./` | 30 | | `my-spec/` | None | `./my-spec/index.html` | `./my-spec/` | 31 | | `path/to/spec.bs` | None | `./path/to/spec.html` | `./path/to/` | 32 | | `my-spec-src` | `my-spec-out` | `./my-spec-out/index.html` | `./my-spec-out/` | 33 | | `index.html` | `index.html` | `./index.html` | `./` | 34 | 35 | ## `BUILD_FAIL_ON` 36 | 37 | Define exit behaviour on build errors or warnings. 38 | 39 | **Possible values:** `"nothing"`, `"fatal"`, `"link-error"`, `"warning"`, `"everything"`. 40 | 41 | | `BUILD_FAIL_ON` | Bikeshed | ReSpec | 42 | | --------------- | ---------------------- | ---------------------- | 43 | | nothing | `--die-on=nothing` | Absent. | 44 | | fatal | `--die-on=fatal ` | `--haltonerror` (`-e`) | 45 | | link-error | `--die-on=link-error` | `--haltonerror` (`-e`) | 46 | | warning | `--die-on=warning ` | `--haltonwarn` (`-w`) | 47 | | everything | `--die-on=everything ` | `-e -w` | 48 | 49 | **Default:** `"fatal"`. 50 | 51 | ## `GH_PAGES_BUILD_OVERRIDE` 52 | 53 | Override Bikeshed metadata or ReSpec config for the GitHub Pages deployment. 54 | 55 | Note that, you need to use [Bikeshed-specific metadata](https://tabatkins.github.io/bikeshed/) (e.g. `status`) when using Bikeshed, and [ReSpec-specific config](https://respec.org/docs/#configuration-options) (e.g. `specStatus`) when using ReSpec. 56 | 57 | **Possible values:** A string or [YAML Literal Block Scalar](https://stackoverflow.com/a/15365296) (multiline string) representing the override config/metadata as key-value pairs. That's mouthful, lets clarify using an example: 58 | 59 | ```yaml 60 | # Example: Override Bikeshed metadata for GitHub Pages deployment 61 | - uses: w3c/spec-prod@v2 62 | with: 63 | TOOLCHAIN: bikeshed 64 | GH_PAGES_BUILD_OVERRIDE: | 65 | status: w3c/WD 66 | TR: https://www.w3.org/TR/my-cool-spec/ 67 | # Warning: The content in GH_PAGES_BUILD_OVERRIDE might look like YAML key-value pairs, but it's actually a string. 68 | # GitHub Actions allow only strings as input. 69 | # 70 | # Info: Above is same as running Bikeshed CLI like: 71 | # bikeshed spec INPUT OUTPUT --md-status="w3c/WD" --md-TR="https://www.w3.org/TR/my-cool-spec/" 72 | ``` 73 | 74 | **Default:** None. 75 | 76 | ## `W3C_BUILD_OVERRIDE` 77 | 78 | Override Bikeshed metadata or ReSpec config for the W3C Deployment and validators. 79 | 80 | The Action will try to make use of metadata/config from previously published version, if available. For example, you do not need to manually provide `respecConfig.previousPublishDate` (or, `Previous Version` in case of Bikeshed) when publishing to W3C. 81 | 82 | **Possible values:** Same as [`GH_PAGES_BUILD_OVERRIDE`](#gh_pages_build_override). 83 | 84 | **Default:** None. 85 | 86 | ```yaml 87 | # Example: Override respecConfig for W3C deployment and validators. 88 | - uses: w3c/spec-prod@v2 89 | with: 90 | TOOLCHAIN: respec 91 | W3C_BUILD_OVERRIDE: | 92 | specStatus: WD 93 | shortName: my-custom-shortname 94 | # Warning: The content in W3C_BUILD_OVERRIDE might look like YAML key-value pairs, but it's actually a string. 95 | # GitHub Actions allow only strings as input. 96 | # 97 | # Info: Above is equivalent to running ReSpec CLI like: 98 | # respec -s index.html?specStatus=WD&shortName=my-custom-shortname… -o OUTPUT 99 | ``` 100 | 101 | ## `VALIDATE_INPUT_MARKUP` 102 | 103 | Whether or not to validate the markup of the input document using the [Nu Html Checker](https://github.com/validator/validator). This option is unlikely to be useful for Bikeshed documents, or for ReSpec documents based on markdown. 104 | 105 | **Possible values:** true, false 106 | 107 | **Default:** false 108 | 109 | ## `VALIDATE_WEBIDL` 110 | 111 | Whether or not to validate the Web IDL that the spec may define. 112 | 113 | Spec authoring tools may already include some level of Web IDL validation but that validation may be restricted to detecting syntax errors. The action also checks additional constraints defined in [Web IDL](https://heycam.github.io/webidl/) such as usage of dictionaries as function parameters or attributes. The action will automatically skip validation if the spec does not define any Web IDL. 114 | 115 | Note that the Web IDL validation is restricted to the spec at hand and cannot validate that references to IDL constructs defined in other specs are valid. As such, there may remain IDL errors that can only be detected by tools that look at all specs in combination such as [Webref](https://github.com/w3c/webref)). 116 | 117 | **Possible values:** true, false 118 | 119 | **Default:** true 120 | 121 | ## `VALIDATE_LINKS` 122 | 123 | Whether or not to check for broken hyperlinks. 124 | 125 | **Warning:** This feature is experimental. 126 | 127 | **Possible values:** true, false 128 | 129 | **Default:** false 130 | 131 | ## `VALIDATE_MARKUP` 132 | 133 | Whether or not to validate markup of the generated document using the [Nu Html Checker](https://github.com/validator/validator). 134 | 135 | **Possible values:** true, false 136 | 137 | **Default:** true 138 | 139 | ## `VALIDATE_PUBRULES` 140 | 141 | Whether or not to validate compliance with [W3C Publication Rules](https://www.w3.org/pubrules/) using [Specberus](https://github.com/w3c/specberus). 142 | 143 | **Possible values:** true, false 144 | 145 | **Default:** false 146 | 147 | ## `GH_PAGES_BRANCH` 148 | 149 | Whether or not to deploy to GitHub pages. Set to a Falsy value to not deploy, or provide a Git branch name to push to. You would need to enable GitHub pages publish source in repository settings manually. 150 | When this option is set, you need to ensure that the `GITHUB_TOKEN` for the job running spec-prod has [`write` access to the `contents` scope](https://docs.github.com/en/actions/security-guides/automatic-token-authentication#permissions-for-the-github_token). 151 | 152 | **Possible values:**: None, or a git branch name. 153 | 154 | **Default:** None 155 | 156 | ## `GH_PAGES_TOKEN` 157 | 158 | GitHub Personal access token. This field is required only if the default GitHub actions token doesn't have enough permissions, or you want to have more control. Make sure to [pass it as a secret](https://docs.github.com/en/actions/configuring-and-managing-workflows/creating-and-storing-encrypted-secrets). 159 | 160 | **Possible values:**: A valid personal GitHub token. 161 | 162 | **Default:** [`GITHUB_TOKEN`](https://docs.github.com/en/actions/configuring-and-managing-workflows/authenticating-with-the-github_token) 163 | 164 | ## `W3C_ECHIDNA_TOKEN` 165 | 166 | The automated publication workflow requires a [token](https://github.com/w3c/echidna/wiki/Token-creation) associated with the specification you want to publish. Working Group Chairs and W3C Team members can [request a token](https://www.w3.org/Web/publications/register) directly from the W3C. This can then be saved as `ECHIDNA_TOKEN` in your repository settings under ["Secrets"](https://user-images.githubusercontent.com/870154/81380287-f9579f80-914d-11ea-84bc-5707bff75dba.png). 167 | 168 | **Possible values:** A valid Echidna token. 169 | 170 | **Default:** None. 171 | 172 | ## `W3C_WG_DECISION_URL` 173 | 174 | A URL to the working group decision to use auto-publish, usually from a w3c mailing list. 175 | 176 | **Possible values:** A non-exhaustive list of possible values: 177 | 178 | - WebApps WG: https://lists.w3.org/Archives/Public/public-webapps/2014JulSep/0627.html 179 | - Media Capture WG: https://lists.w3.org/Archives/Public/public-media-capture/2015Dec/0031.html 180 | - Second Screen WG: https://lists.w3.org/Archives/Public/public-secondscreen/2015Jun/0096.html 181 | - Web RTC: https://lists.w3.org/Archives/Public/public-webrtc/2016Mar/0031.html 182 | - Aria: https://lists.w3.org/Archives/Public/public-html-admin/2015May/0021.html 183 | - Device APIs: https://lists.w3.org/Archives/Public/public-device-apis/2021May/0008.html 184 | - Web Performance: https://lists.w3.org/Archives/Public/public-web-perf/2021Apr/0005.html 185 | - WebAppSec: https://lists.w3.org/Archives/Public/public-webappsec/2015Mar/0170.html 186 | - Web Payments WG: https://www.w3.org/2016/04/14-wpwg-minutes.html#item02 187 | 188 | **Default:** None. 189 | 190 | ## `W3C_NOTIFICATIONS_CC` 191 | 192 | Comma separated list of email addresses to CC. This field is optional. 193 | 194 | **Default:** None. 195 | 196 | ## `ARTIFACT_NAME` 197 | 198 | Name for artifact which will be uploaded to workflow run. Required to be unique when building multiple documents in same job. 199 | 200 | **Possible values:** Any valid artifact name. 201 | 202 | **Default:** `"spec-prod-result"` or inferred from `SOURCE`. 203 | -------------------------------------------------------------------------------- /src/build.ts: -------------------------------------------------------------------------------- 1 | import * as path from "node:path"; 2 | import { copyFile, mkdir, readFile, writeFile, unlink } from "node:fs/promises"; 3 | import { Readable } from "node:stream"; 4 | import type { ReadableStream } from "node:stream/web"; 5 | import { getAllSubResources, type ResourceType } from "subresources"; 6 | import { env, exit, setOutput, sh, unique } from "./utils.ts"; 7 | import { deepEqual, StaticServer } from "./utils.ts"; 8 | import { PUPPETEER_ENV } from "./constants.ts"; 9 | 10 | import type { BasicBuildOptions as BasicBuildOptions_ } from "./prepare-build.ts"; 11 | import type { ProcessedInput } from "./prepare.ts"; 12 | type BasicBuildOptions = Omit; 13 | type Input = ProcessedInput["build"]; 14 | type ConfigOverride = Input["configOverride"]["gh" | "w3c"]; 15 | type BuildSuffix = "common" | "gh" | "w3c"; 16 | 17 | /** 18 | * @example 19 | * ```js 20 | * let destination = "my-spec/index.html"; // then: 21 | * root = process.cwd() + BuildSuffix 22 | * dir = "my-spec" 23 | * file = "index.html" 24 | * dest = path.join(process.cwd() + BuildSuffix, "my-spec") 25 | * ``` 26 | */ 27 | export interface BuildResult { 28 | root: string; 29 | dir: string; 30 | file: string; 31 | dest: string; 32 | } 33 | 34 | const rel = (p: string) => path.relative(process.cwd(), p); 35 | const tmpOutputFile = (source: Input["source"]) => source.path + ".built.html"; 36 | 37 | if (import.meta.main) { 38 | const input: Input = JSON.parse(env("INPUTS_BUILD")); 39 | main(input).catch(err => exit(err.message || "Failed", err.code)); 40 | } 41 | 42 | export default async function main({ 43 | toolchain, 44 | source, 45 | destination, 46 | flags, 47 | configOverride, 48 | }: Input) { 49 | const input: BasicBuildOptions = { toolchain, source, destination }; 50 | if (deepEqual(configOverride.gh, configOverride.w3c)) { 51 | const out = await build(input, flags, null, "common"); 52 | return { ...setOutput("gh", out), ...setOutput("w3c", out) }; 53 | } 54 | 55 | const confGh = configOverride.gh; 56 | const outGh = await build(input, flags, confGh, "gh"); 57 | 58 | const confW3C = configOverride.w3c; 59 | const outW3C = await build(input, flags, confW3C, "w3c"); 60 | 61 | return { ...setOutput("gh", outGh), ...setOutput("w3c", outW3C) }; 62 | } 63 | 64 | async function build( 65 | input: BasicBuildOptions, 66 | additionalFlags: Input["flags"], 67 | conf: ConfigOverride, 68 | suffix: BuildSuffix, 69 | ): Promise { 70 | const { toolchain, source, destination } = input; 71 | console.group( 72 | `[INFO] Build ${toolchain} document "${source.path}" (${suffix})…`, 73 | ); 74 | switch (toolchain) { 75 | case "respec": 76 | await buildReSpec(source, additionalFlags, conf); 77 | break; 78 | case "bikeshed": 79 | await buildBikeshed(source, additionalFlags, conf); 80 | break; 81 | default: 82 | throw new Error(`Unknown "TOOLCHAIN": "${toolchain}"`); 83 | } 84 | 85 | const res = await copyRelevantAssets(source, destination, suffix); 86 | console.groupEnd(); 87 | return res; 88 | } 89 | 90 | async function buildReSpec( 91 | source: Input["source"], 92 | additionalFlags: Input["flags"], 93 | conf: ConfigOverride, 94 | ) { 95 | const flags = additionalFlags.join(" "); 96 | const server = await new StaticServer().start(); 97 | const src = new URL(source.path, server.url); 98 | for (const [key, val] of Object.entries(conf || {})) { 99 | src.searchParams.set(key, val); 100 | } 101 | const outFile = tmpOutputFile(source); 102 | try { 103 | await sh(`respec -s "${src}" -o "${outFile}" --verbose -t 20 ${flags}`, { 104 | output: "stream", 105 | env: PUPPETEER_ENV, 106 | }); 107 | } finally { 108 | await server.stop(); 109 | } 110 | } 111 | 112 | async function buildBikeshed( 113 | source: Input["source"], 114 | additionalFlags: Input["flags"], 115 | conf: ConfigOverride, 116 | ) { 117 | const metadataFlags = Object.entries(conf || {}) 118 | .map(([key, val]) => `--md-${key.replace(/\s+/g, "-")}="${val}"`) 119 | .join(" "); 120 | const flags = additionalFlags.join(" "); 121 | const outFile = tmpOutputFile(source); 122 | await sh( 123 | `bikeshed ${flags} spec "${source.path}" "${outFile}" ${metadataFlags}`, 124 | "stream", 125 | ); 126 | } 127 | 128 | async function copyRelevantAssets( 129 | source: Input["source"], 130 | destination: Input["destination"], 131 | suffix: BuildSuffix, 132 | ): Promise { 133 | const rootDir = path.join(process.cwd() + `.${suffix}`); 134 | const destinationDir = path.join(rootDir, destination.dir); 135 | const destinationFile = path.join(destinationDir, destination.file); 136 | const tmpOutFile = tmpOutputFile(source); 137 | 138 | const assets = await findAssetsToCopy(source); 139 | await copyLocalAssets(assets.local, rootDir); 140 | const newRemoteURLs = await downloadRemoteAssets( 141 | assets.remote, 142 | destinationDir, 143 | ); 144 | 145 | // Copy output file to the publish directory 146 | if (!newRemoteURLs.length) { 147 | await copy(tmpOutFile, destinationFile); 148 | } else { 149 | console.log(`Replacing instances of remote URLs with download asset URLs…`); 150 | let text = await readFile(tmpOutFile, "utf8"); 151 | for (const urls of newRemoteURLs) { 152 | text = replaceAll(urls.old, urls.new, text); 153 | } 154 | await mkdir(path.dirname(destinationFile), { recursive: true }); 155 | await writeFile(destinationFile, text, "utf8"); 156 | } 157 | 158 | await unlink(tmpOutFile); 159 | 160 | // List all files in output directory 161 | await sh(`ls -R`, { output: "buffer", cwd: destinationDir }); 162 | 163 | return { 164 | root: rootDir, 165 | dest: destinationDir, 166 | dir: destination.dir, 167 | file: destination.file, 168 | }; 169 | } 170 | 171 | async function findAssetsToCopy(source: Input["source"]) { 172 | console.groupCollapsed(`[INFO] Finding relevant assets…`); 173 | let localAssets: string[] = []; 174 | let remoteAssets: URL[] = []; 175 | 176 | Object.assign(process.env, PUPPETEER_ENV); 177 | 178 | const server = await new StaticServer().start(); 179 | 180 | const isLocalAsset = (url: URL) => url.origin === server.url.origin; 181 | const remoteAssetRules: ((url: URL, type: ResourceType) => boolean)[] = [ 182 | (url: URL) => url.origin === "https://user-images.githubusercontent.com", 183 | ]; 184 | 185 | const mainPage = urlToPage(new URL(tmpOutputFile(source), server.url)); 186 | const pages = new Set([mainPage]); 187 | for (const page of pages) { 188 | const rootUrl = new URL(page); 189 | console.log(`[INFO] From ${rootUrl.pathname}…`); 190 | const { ok, headers } = await fetch(rootUrl); 191 | if (!ok || !headers.get("content-type")?.includes("text/html")) { 192 | console.log( 193 | `[WARNING] Failed to fetch ${rootUrl.pathname}. Some assets might be missing.`, 194 | ); 195 | continue; 196 | } 197 | 198 | const allSubResources = getAllSubResources(rootUrl, { 199 | links: true, 200 | puppeteerOptions: { 201 | executablePath: PUPPETEER_ENV.PUPPETEER_EXECUTABLE_PATH, 202 | args: ["--no-sandbox"], 203 | }, 204 | }); 205 | for await (const res of allSubResources) { 206 | const url = new URL(res.url); 207 | if (isLocalAsset(url) && res.type === "link") { 208 | const nextPage = urlToPage(url); 209 | if (!pages.has(nextPage)) { 210 | pages.add(nextPage); 211 | localAssets.push(url.pathname); 212 | } 213 | } else if (isLocalAsset(url)) { 214 | localAssets.push(url.pathname); 215 | } else if (remoteAssetRules.some(matcher => matcher(url, res.type))) { 216 | remoteAssets.push(url); 217 | } 218 | } 219 | } 220 | 221 | localAssets = unique(localAssets); 222 | remoteAssets = unique(remoteAssets, url => url.href); 223 | 224 | console.log("Local assets to be copied:", trimList(localAssets)); 225 | console.log( 226 | "Remote assets to be downloaded:", 227 | trimList(remoteAssets.map(u => u.href)), 228 | ); 229 | console.groupEnd(); 230 | 231 | await server.stop(); 232 | return { local: localAssets.sort(), remote: remoteAssets.sort() }; 233 | } 234 | 235 | async function copyLocalAssets(assets: string[], destinationDir: string) { 236 | console.groupCollapsed(`Copying ${assets.length} local files…`); 237 | await Promise.all( 238 | assets.map(asset => copy(asset, path.join(destinationDir, asset))), 239 | ); 240 | console.groupEnd(); 241 | } 242 | 243 | async function downloadRemoteAssets(urls: URL[], destinationDir: string) { 244 | console.groupCollapsed(`Downloading ${urls.length} remote files…`); 245 | const result = await Promise.all( 246 | urls.map(url => download(url, destinationDir)), 247 | ); 248 | console.groupEnd(); 249 | return result; 250 | } 251 | 252 | // /////////////////////// 253 | // Utils 254 | // /////////////////////// 255 | 256 | async function copy(src: string, dst: string) { 257 | src = path.join(process.cwd(), src); 258 | try { 259 | await mkdir(path.dirname(dst), { recursive: true }); 260 | await copyFile(src, dst); 261 | } catch (error) { 262 | const msg = `Copy: ${rel(src)} ➡ ${rel(dst)}`; 263 | console.log("[WARNING]", msg, error.message); 264 | } 265 | } 266 | 267 | async function download(url: URL, destinationDir: string) { 268 | const href = `downloaded-assets/${url.host}${url.pathname}`; 269 | const destination = path.join(destinationDir, href.replace(/\//g, path.sep)); 270 | 271 | try { 272 | const res = await fetch(url); 273 | if (!res.ok) { 274 | throw new Error(`Status: ${res.status}`); 275 | } 276 | await mkdir(path.dirname(destination), { recursive: true }); 277 | await writeFile( 278 | destination, 279 | Readable.fromWeb(res.body as ReadableStream), 280 | ); 281 | } catch (error) { 282 | const msg = `Download: ${url.href} ➡ ${rel(destination)}`; 283 | console.log("[WARNING]", msg, error.message); 284 | } 285 | return { old: url.href, new: href }; 286 | } 287 | 288 | // https://www.npmjs.com/package/replaceall 289 | function replaceAll(replaceThis: string, withThis: string, inThis: string) { 290 | withThis = withThis.replace(/\$/g, "$$$$"); 291 | return inThis.replace( 292 | new RegExp( 293 | replaceThis.replace( 294 | /([\/\,\!\\\^\$\{\}\[\]\(\)\.\*\+\?\|<>\-\&])/g, 295 | "\\$&", 296 | ), 297 | "g", 298 | ), 299 | withThis, 300 | ); 301 | } 302 | 303 | function trimList(list: string[], len = 8) { 304 | if (list.length < len) return list; 305 | return list.slice(0, len).concat([`${list.length - len} more..`]); 306 | } 307 | 308 | function urlToPage(url: URL) { 309 | return new URL(url.pathname, url.origin).href; 310 | } 311 | -------------------------------------------------------------------------------- /src/prepare-build.ts: -------------------------------------------------------------------------------- 1 | import * as path from "node:path"; 2 | import { existsSync } from "node:fs"; 3 | import { readFile } from "node:fs/promises"; 4 | import * as puppeteer from "puppeteer"; 5 | import { PUPPETEER_ENV } from "./constants.ts"; 6 | import { exit } from "./utils.ts"; 7 | 8 | import type { Inputs, GitHubContext } from "./prepare.ts"; 9 | export type BuildOptions = Awaited>; 10 | 11 | export async function buildOptions( 12 | inputs: Inputs, 13 | githubContext: GitHubContext, 14 | ) { 15 | const { toolchain, source, destination, artifactName } = 16 | getBasicBuildOptions(inputs); 17 | 18 | const configOverride = { 19 | gh: getConfigOverride(inputs.GH_PAGES_BUILD_OVERRIDE), 20 | w3c: getConfigOverride(inputs.W3C_BUILD_OVERRIDE), 21 | }; 22 | if (toolchain === "respec") { 23 | configOverride.gh ??= {}; 24 | configOverride.w3c ??= {}; 25 | configOverride.gh.gitRevision = configOverride.w3c.gitRevision = 26 | githubContext.sha; 27 | } 28 | if (inputs.W3C_ECHIDNA_TOKEN || inputs.W3C_WG_DECISION_URL) { 29 | configOverride.w3c = await extendW3CBuildConfig( 30 | configOverride.w3c || {}, 31 | toolchain, 32 | source, 33 | ); 34 | } 35 | 36 | const flags = []; 37 | flags.push(...getFailOnFlags(toolchain, inputs.BUILD_FAIL_ON)); 38 | 39 | return { 40 | toolchain, 41 | source, 42 | destination, 43 | artifactName, 44 | flags, 45 | configOverride, 46 | }; 47 | } 48 | 49 | type NormalizedPath = { dir: string; file: string; path: string }; 50 | export type BasicBuildOptions = { 51 | toolchain: "respec" | "bikeshed"; 52 | source: NormalizedPath; 53 | destination: NormalizedPath; 54 | artifactName: string; 55 | }; 56 | function getBasicBuildOptions(inputs: Inputs): BasicBuildOptions { 57 | let toolchain = inputs.TOOLCHAIN; 58 | let source = inputs.SOURCE; 59 | let destination = inputs.DESTINATION; 60 | 61 | if (toolchain) { 62 | switch (toolchain) { 63 | case "respec": 64 | source ||= "index.html"; 65 | break; 66 | case "bikeshed": 67 | source ||= "index.bs"; 68 | break; 69 | default: 70 | exit(`Invalid input "TOOLCHAIN": ${toolchain}`); 71 | } 72 | } 73 | 74 | if (!source) { 75 | if (existsSync("index.bs")) { 76 | source = "index.bs"; 77 | } else if (existsSync("index.html")) { 78 | source = "index.html"; 79 | } 80 | } 81 | 82 | if (!toolchain && !source) { 83 | exit(`Either of "TOOLCHAIN" or "SOURCE" must be provided.`); 84 | } 85 | 86 | if (!existsSync(source)) { 87 | exit(`"SOURCE" file "${source}" not found.`); 88 | } 89 | 90 | if (!toolchain) { 91 | if (source.endsWith(".html")) { 92 | toolchain = "respec"; 93 | } else if (source.endsWith(".bs")) { 94 | toolchain = "bikeshed"; 95 | } else { 96 | exit( 97 | `Failed to figure out "TOOLCHAIN" from "SOURCE". Please specify the "TOOLCHAIN".`, 98 | ); 99 | } 100 | } 101 | 102 | const getNormalizedPath = (p: string): NormalizedPath => { 103 | const cwd = process.cwd(); 104 | const parsed = path.parse(path.join(cwd, p)); 105 | if (!parsed.base) { 106 | parsed.base = "index.html"; 107 | } else if (!parsed.ext) { 108 | parsed.dir = path.join(parsed.dir, parsed.base); 109 | parsed.base = "index.html"; 110 | } 111 | parsed.dir = path.relative(cwd, parsed.dir); 112 | const { dir, base: file } = parsed; 113 | return { dir, file, path: path.join(dir, file) }; 114 | }; 115 | 116 | const getArtifactNameFromSource = (source: string): string => { 117 | source = source.toLowerCase().trim(); 118 | const sourceSlug = source 119 | .replace(/\//g, "-") 120 | .replace(/\s+/g, "-") 121 | .replace(/index\.(html|bs)$/, "") 122 | .replace(/[^\w-]+/g, "") 123 | .replace(/--+/g, "-") 124 | .replace(/-$/g, ""); 125 | if (sourceSlug) { 126 | return "spec-prod-result" + "-" + sourceSlug; 127 | } 128 | return "spec-prod-result"; 129 | }; 130 | 131 | destination = (() => { 132 | const dest = path.parse(destination || source); 133 | dest.ext = ".html"; 134 | dest.base = dest.base.replace(/\.bs$/, ".html"); 135 | return path.format(dest); 136 | })(); 137 | 138 | return { 139 | toolchain, 140 | artifactName: inputs.ARTIFACT_NAME || getArtifactNameFromSource(source), 141 | source: getNormalizedPath(source), 142 | destination: getNormalizedPath(destination), 143 | } as BasicBuildOptions; 144 | } 145 | 146 | function getConfigOverride(confStr: string) { 147 | if (!confStr) { 148 | return null; 149 | } 150 | 151 | const config: Record = {}; 152 | for (const line of confStr.trim().split("\n")) { 153 | const idx = line.indexOf(":"); 154 | const key = line.slice(0, idx).trim(); 155 | const value = line.slice(idx + 1).trim(); 156 | config[key] = value; 157 | } 158 | return config; 159 | } 160 | 161 | async function extendW3CBuildConfig( 162 | conf: NonNullable>, 163 | toolchain: BasicBuildOptions["toolchain"], 164 | source: BasicBuildOptions["source"], 165 | ) { 166 | /** Get present date in YYYY-MM-DD format */ 167 | const getShortIsoDate = () => new Date().toISOString().slice(0, 10); 168 | 169 | let publishDate = getShortIsoDate(); 170 | if (toolchain === "respec") { 171 | conf.publishDate = publishDate = conf.publishDate || publishDate; 172 | } else if (toolchain === "bikeshed") { 173 | conf.date = publishDate = conf.date || publishDate; 174 | } 175 | 176 | if (toolchain === "bikeshed") { 177 | conf["Prepare For TR"] = "yes"; 178 | } 179 | 180 | let shortName: string | undefined = conf.shortName || conf.shortname; 181 | if (!shortName) { 182 | if (toolchain === "respec") { 183 | shortName = await getShortnameForRespec(source); 184 | } else if (toolchain === "bikeshed") { 185 | shortName = await getShortnameForBikeshed(source); 186 | } 187 | } 188 | if (shortName) { 189 | try { 190 | const prev = await getPreviousVersionInfo(shortName, publishDate); 191 | if (toolchain === "respec") { 192 | conf.previousMaturity = prev.maturity; 193 | conf.previousPublishDate = prev.publishDate; 194 | } else if (toolchain === "bikeshed") { 195 | conf["previous version"] = prev.URI; 196 | } 197 | } catch (error) { 198 | console.error(error.message); 199 | } 200 | } 201 | 202 | return conf; 203 | } 204 | 205 | async function getShortnameForRespec(source: BasicBuildOptions["source"]) { 206 | console.group(`[INFO] Finding shortName for ReSpec document: ${source.path}`); 207 | const browser = await puppeteer.launch({ 208 | executablePath: PUPPETEER_ENV.PUPPETEER_EXECUTABLE_PATH, 209 | headless: true, 210 | }); 211 | 212 | try { 213 | const page = await browser.newPage(); 214 | const url = new URL(source.path, `file://${process.cwd()}/`).href; 215 | console.log("[INFO] Navigating to", url); 216 | await page.goto(url); 217 | await page.waitForFunction(() => window.hasOwnProperty("respecConfig"), { 218 | timeout: 10000, 219 | }); 220 | const shortName: string = await page.evaluate( 221 | // @ts-ignore 222 | () => window.respecConfig.shortName as string, 223 | ); 224 | console.log("[INFO] shortName:", shortName); 225 | return shortName; 226 | } catch (error) { 227 | console.warn(`[WARN] ${error.message}`); 228 | } finally { 229 | console.groupEnd(); 230 | await browser.close(); 231 | } 232 | } 233 | 234 | // Parses `pre.metadata` in `source` and gets shortname from there. 235 | async function getShortnameForBikeshed(source: BasicBuildOptions["source"]) { 236 | console.group( 237 | `[INFO] Finding shortName for Bikeshed document: ${source.path}`, 238 | ); 239 | const browser = await puppeteer.launch({ 240 | executablePath: PUPPETEER_ENV.PUPPETEER_EXECUTABLE_PATH, 241 | headless: true, 242 | }); 243 | 244 | try { 245 | console.log("[INFO] Parsing metadata from", source.path); 246 | const page = await browser.newPage(); 247 | // Navigating to `source.path` then finding and parsing `pre.metadata` is 248 | // not possible, as the content of .bs file gets escaped by the browser. 249 | // Alternative is to not use puppeteer and use something like linkeddom, but 250 | // then why add another dependency? So, we use DOMParser inside puppeteer. 251 | const text = await readFile(source.path, "utf8"); 252 | const metadata = await page.evaluate((text: string) => { 253 | const doc = new DOMParser().parseFromString(text, "text/html"); 254 | return doc.querySelector("pre.metadata")?.textContent || null; 255 | }, text); 256 | if (!metadata) { 257 | throw new Error("Failed to read metadata"); 258 | } 259 | 260 | const config = getConfigOverride(metadata)!; 261 | // Bikeshed allows metadata keys to be in any case: Shortname/shortName etc. 262 | const shortnameKey = Object.keys(config).find( 263 | key => key.toLowerCase() === "shortname", 264 | ); 265 | if (!shortnameKey || !config[shortnameKey]) { 266 | throw new Error("No `shortname` found in metadata"); 267 | } 268 | const shortName = config[shortnameKey]; 269 | console.log("[INFO] shortName:", shortName); 270 | return shortName; 271 | } catch (error) { 272 | console.warn(`[WARN] ${error.message}`); 273 | } finally { 274 | console.groupEnd(); 275 | await browser.close(); 276 | } 277 | } 278 | 279 | async function getPreviousVersionInfo(shortName: string, publishDate: string) { 280 | console.group(`[INFO] Finding previous version details...`); 281 | const url = "https://www.w3.org/TR/" + shortName + "/"; 282 | 283 | const browser = await puppeteer.launch({ 284 | executablePath: PUPPETEER_ENV.PUPPETEER_EXECUTABLE_PATH, 285 | headless: true, 286 | }); 287 | 288 | try { 289 | const page = await browser.newPage(); 290 | console.log("[INFO] Navigating to", url); 291 | const res = await page.goto(url); 292 | console.log("[INFO] Navigation complete with statusCode:", res!.status()); 293 | if (!res!.ok) { 294 | throw new Error(`Failed to fetch ${url}`); 295 | } 296 | 297 | const thisURI = await page.$$eval("body div.head dl dt", elems => { 298 | const thisVersion = (elems as HTMLElement[]).find(el => 299 | /this (?:published )?version/i.test(el.textContent!.trim()), 300 | ); 301 | if (thisVersion) { 302 | const dd = thisVersion.nextElementSibling as HTMLElement | null; 303 | if (dd?.localName === "dd") { 304 | const link = dd.querySelector("a"); 305 | if (link) return link.href; 306 | } 307 | } 308 | return null; 309 | }); 310 | console.log("[INFO] thisURI:", thisURI); 311 | 312 | const previousURI = await page.$$eval("body div.head dl dt", elems => { 313 | const thisVersion = (elems as HTMLElement[]).find(el => 314 | /previous (?:published )?version/i.test(el.textContent!.trim()), 315 | ); 316 | if (thisVersion) { 317 | const dd = thisVersion.nextElementSibling as HTMLElement | null; 318 | if (dd?.localName === "dd") { 319 | const link = dd.querySelector("a"); 320 | if (link) return link.href; 321 | } 322 | } 323 | return null; 324 | }); 325 | console.log("[INFO] prevURI:", previousURI); 326 | 327 | if (!thisURI) { 328 | throw new Error( 329 | "Couldn't find a 'This version' uri in the previous version.", 330 | ); 331 | } 332 | 333 | const thisDate = thisURI.match(/[1-2][0-9]{7}/)![0]; 334 | const targetPublishDate = publishDate.replace(/\-/g, ""); 335 | const currentURI = 336 | thisDate === targetPublishDate && previousURI ? previousURI : thisURI; 337 | 338 | const previousMaturity = currentURI.match(/\/TR\/[0-9]{4}\/([A-Z]+)/)![1]; 339 | 340 | const previousPublishDate = currentURI 341 | .match(/[1-2][0-9]{7}/)![0] 342 | .replace(/(\d{4})(\d{2})(\d{2})/, "$1-$2-$3"); 343 | 344 | return { 345 | maturity: previousMaturity, 346 | publishDate: previousPublishDate, 347 | URI: currentURI, 348 | }; 349 | } finally { 350 | console.groupEnd(); 351 | await browser.close(); 352 | } 353 | } 354 | 355 | function getFailOnFlags( 356 | toolchain: BasicBuildOptions["toolchain"], 357 | failOn: string, 358 | ) { 359 | const FAIL_ON_OPTIONS = [ 360 | "nothing", 361 | "fatal", 362 | "link-error", 363 | "warning", 364 | "everything", 365 | ]; 366 | 367 | if (failOn && !FAIL_ON_OPTIONS.includes(failOn)) { 368 | exit( 369 | `BUILD_FAIL_ON must be one of [${FAIL_ON_OPTIONS.join(", ")}]. ` + 370 | `Found "${failOn}".`, 371 | ); 372 | } 373 | switch (toolchain) { 374 | case "respec": { 375 | switch (failOn) { 376 | case "fatal": 377 | case "link-error": 378 | return ["-e"]; 379 | case "warning": 380 | return ["-w"]; 381 | case "everything": 382 | return ["-e", "-w"]; 383 | case "nothing": 384 | default: 385 | return []; 386 | } 387 | } 388 | case "bikeshed": { 389 | return [`--die-on=${failOn}`]; 390 | } 391 | default: 392 | throw new Error("Unreachable"); 393 | } 394 | } 395 | -------------------------------------------------------------------------------- /pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: '9.0' 2 | 3 | settings: 4 | autoInstallPeers: true 5 | excludeLinksFromLockfile: false 6 | 7 | overrides: 8 | link-checker: 1.4.2 9 | reffy: ^15 10 | respec: ^35 11 | specberus: ^11 12 | vnu-jar: ^24 13 | webidl2: ^24 14 | 15 | importers: 16 | 17 | .: 18 | dependencies: 19 | '@actions/core': 20 | specifier: ^2.0.1 21 | version: 2.0.1 22 | finalhandler: 23 | specifier: ^2.1.1 24 | version: 2.1.1 25 | puppeteer: 26 | specifier: ^24 27 | version: 24.22.0(typescript@5.9.3) 28 | serve-static: 29 | specifier: ^2.2.0 30 | version: 2.2.0 31 | split2: 32 | specifier: ^4.2.0 33 | version: 4.2.0 34 | subresources: 35 | specifier: ^2.1.0 36 | version: 2.1.0(typescript@5.9.3) 37 | yaml: 38 | specifier: ^2.8.2 39 | version: 2.8.2 40 | devDependencies: 41 | '@types/finalhandler': 42 | specifier: ^1.2.4 43 | version: 1.2.4 44 | '@types/node': 45 | specifier: ^24.5.2 46 | version: 24.5.2 47 | '@types/serve-static': 48 | specifier: ^2.2.0 49 | version: 2.2.0 50 | '@types/split2': 51 | specifier: ^4.2.3 52 | version: 4.2.3 53 | prettier: 54 | specifier: ^3.7.4 55 | version: 3.7.4 56 | typescript: 57 | specifier: ^5.9.3 58 | version: 5.9.3 59 | 60 | packages: 61 | 62 | '@actions/core@2.0.1': 63 | resolution: {integrity: sha512-oBfqT3GwkvLlo1fjvhQLQxuwZCGTarTE5OuZ2Wg10hvhBj7LRIlF611WT4aZS6fDhO5ZKlY7lCAZTlpmyaHaeg==} 64 | 65 | '@actions/exec@2.0.0': 66 | resolution: {integrity: sha512-k8ngrX2voJ/RIN6r9xB82NVqKpnMRtxDoiO+g3olkIUpQNqjArXrCQceduQZCQj3P3xm32pChRLqRrtXTlqhIw==} 67 | 68 | '@actions/http-client@3.0.0': 69 | resolution: {integrity: sha512-1s3tXAfVMSz9a4ZEBkXXRQD4QhY3+GAsWSbaYpeknPOKEeyRiU3lH+bHiLMZdo2x/fIeQ/hscL1wCkDLVM2DZQ==} 70 | 71 | '@actions/io@2.0.0': 72 | resolution: {integrity: sha512-Jv33IN09XLO+0HS79aaODsvIRyduiF7NY/F6LYeK5oeUmrsz7aFdRphQjFoESF4jS7lMauDOttKALcpapVDIAg==} 73 | 74 | '@babel/code-frame@7.18.6': 75 | resolution: {integrity: sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==} 76 | engines: {node: '>=6.9.0'} 77 | 78 | '@babel/helper-validator-identifier@7.19.1': 79 | resolution: {integrity: sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==} 80 | engines: {node: '>=6.9.0'} 81 | 82 | '@babel/highlight@7.18.6': 83 | resolution: {integrity: sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==} 84 | engines: {node: '>=6.9.0'} 85 | 86 | '@fastify/busboy@2.1.1': 87 | resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==} 88 | engines: {node: '>=14'} 89 | 90 | '@puppeteer/browsers@2.10.10': 91 | resolution: {integrity: sha512-3ZG500+ZeLql8rE0hjfhkycJjDj0pI/btEh3L9IkWUYcOrgP0xCNRq3HbtbqOPbvDhFaAWD88pDFtlLv8ns8gA==} 92 | engines: {node: '>=18'} 93 | hasBin: true 94 | 95 | '@tootallnate/quickjs-emscripten@0.23.0': 96 | resolution: {integrity: sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==} 97 | 98 | '@types/finalhandler@1.2.4': 99 | resolution: {integrity: sha512-ojpQ5ywnKZko/+tw8lR4xvUN5Uvfnar4ZtfpoLG1TdxlmiIqcQGUbXmc9iV5ud7J6rRtacNbwTkCE98NmsBPYw==} 100 | 101 | '@types/http-errors@2.0.1': 102 | resolution: {integrity: sha512-/K3ds8TRAfBvi5vfjuz8y6+GiAYBZ0x4tXv1Av6CWBWn0IlADc+ZX9pMq7oU0fNQPnBwIZl3rmeLp6SBApbxSQ==} 103 | 104 | '@types/node@24.5.2': 105 | resolution: {integrity: sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ==} 106 | 107 | '@types/serve-static@2.2.0': 108 | resolution: {integrity: sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==} 109 | 110 | '@types/split2@4.2.3': 111 | resolution: {integrity: sha512-59OXIlfUsi2k++H6CHgUQKEb2HKRokUA39HY1i1dS8/AIcqVjtAAFdf8u+HxTWK/4FUHMJQlKSZ4I6irCBJ1Zw==} 112 | 113 | '@types/yauzl@2.9.1': 114 | resolution: {integrity: sha512-A1b8SU4D10uoPjwb0lnHmmu8wZhR9d+9o2PKBQT2jU5YPTKsxac6M2qGAdY7VcL+dHHhARVUDmeg0rOrcd9EjA==} 115 | 116 | agent-base@7.1.4: 117 | resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} 118 | engines: {node: '>= 14'} 119 | 120 | ansi-regex@5.0.1: 121 | resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} 122 | engines: {node: '>=8'} 123 | 124 | ansi-styles@3.2.1: 125 | resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} 126 | engines: {node: '>=4'} 127 | 128 | ansi-styles@4.3.0: 129 | resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} 130 | engines: {node: '>=8'} 131 | 132 | argparse@2.0.1: 133 | resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} 134 | 135 | ast-types@0.13.4: 136 | resolution: {integrity: sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==} 137 | engines: {node: '>=4'} 138 | 139 | b4a@1.6.4: 140 | resolution: {integrity: sha512-fpWrvyVHEKyeEvbKZTVOeZF3VSKKWtJxFIxX/jaVPf+cLbGUSitjb49pHLqPV2BUNNZ0LcoeEGfE/YCpyDYHIw==} 141 | 142 | bare-events@2.7.0: 143 | resolution: {integrity: sha512-b3N5eTW1g7vXkw+0CXh/HazGTcO5KYuu/RCNaJbDMPI6LHDi+7qe8EmxKUVe1sUbY2KZOVZFyj62x0OEz9qyAA==} 144 | 145 | bare-fs@4.4.4: 146 | resolution: {integrity: sha512-Q8yxM1eLhJfuM7KXVP3zjhBvtMJCYRByoTT+wHXjpdMELv0xICFJX+1w4c7csa+WZEOsq4ItJ4RGwvzid6m/dw==} 147 | engines: {bare: '>=1.16.0'} 148 | peerDependencies: 149 | bare-buffer: '*' 150 | peerDependenciesMeta: 151 | bare-buffer: 152 | optional: true 153 | 154 | bare-os@3.6.2: 155 | resolution: {integrity: sha512-T+V1+1srU2qYNBmJCXZkUY5vQ0B4FSlL3QDROnKQYOqeiQR8UbjNHlPa+TIbM4cuidiN9GaTaOZgSEgsvPbh5A==} 156 | engines: {bare: '>=1.14.0'} 157 | 158 | bare-path@3.0.0: 159 | resolution: {integrity: sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==} 160 | 161 | bare-stream@2.7.0: 162 | resolution: {integrity: sha512-oyXQNicV1y8nc2aKffH+BUHFRXmx6VrPzlnaEvMhram0nPBrKcEdcyBg5r08D0i8VxngHFAiVyn1QKXpSG0B8A==} 163 | peerDependencies: 164 | bare-buffer: '*' 165 | bare-events: '*' 166 | peerDependenciesMeta: 167 | bare-buffer: 168 | optional: true 169 | bare-events: 170 | optional: true 171 | 172 | bare-url@2.2.2: 173 | resolution: {integrity: sha512-g+ueNGKkrjMazDG3elZO1pNs3HY5+mMmOet1jtKyhOaCnkLzitxf26z7hoAEkDNgdNmnc1KIlt/dw6Po6xZMpA==} 174 | 175 | basic-ftp@5.0.3: 176 | resolution: {integrity: sha512-QHX8HLlncOLpy54mh+k/sWIFd0ThmRqwe9ZjELybGZK+tZ8rUb9VO0saKJUROTbE+KhzDUT7xziGpGrW8Kmd+g==} 177 | engines: {node: '>=10.0.0'} 178 | 179 | buffer-crc32@0.2.13: 180 | resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} 181 | 182 | callsites@3.1.0: 183 | resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} 184 | engines: {node: '>=6'} 185 | 186 | chalk@2.4.2: 187 | resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} 188 | engines: {node: '>=4'} 189 | 190 | chromium-bidi@8.0.0: 191 | resolution: {integrity: sha512-d1VmE0FD7lxZQHzcDUCKZSNRtRwISXDsdg4HjdTR5+Ll5nQ/vzU12JeNmupD6VWffrPSlrnGhEWlLESKH3VO+g==} 192 | peerDependencies: 193 | devtools-protocol: '*' 194 | 195 | cliui@8.0.1: 196 | resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} 197 | engines: {node: '>=12'} 198 | 199 | color-convert@1.9.3: 200 | resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} 201 | 202 | color-convert@2.0.1: 203 | resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} 204 | engines: {node: '>=7.0.0'} 205 | 206 | color-name@1.1.3: 207 | resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} 208 | 209 | color-name@1.1.4: 210 | resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} 211 | 212 | cosmiconfig@9.0.0: 213 | resolution: {integrity: sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==} 214 | engines: {node: '>=14'} 215 | peerDependencies: 216 | typescript: '>=4.9.5' 217 | peerDependenciesMeta: 218 | typescript: 219 | optional: true 220 | 221 | data-uri-to-buffer@5.0.1: 222 | resolution: {integrity: sha512-a9l6T1qqDogvvnw0nKlfZzqsyikEBZBClF39V3TFoKhDtGBqHu2HkuomJc02j5zft8zrUaXEuoicLeW54RkzPg==} 223 | engines: {node: '>= 14'} 224 | 225 | debug@4.4.3: 226 | resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} 227 | engines: {node: '>=6.0'} 228 | peerDependencies: 229 | supports-color: '*' 230 | peerDependenciesMeta: 231 | supports-color: 232 | optional: true 233 | 234 | degenerator@5.0.1: 235 | resolution: {integrity: sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==} 236 | engines: {node: '>= 14'} 237 | 238 | depd@2.0.0: 239 | resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} 240 | engines: {node: '>= 0.8'} 241 | 242 | devtools-protocol@0.0.1495869: 243 | resolution: {integrity: sha512-i+bkd9UYFis40RcnkW7XrOprCujXRAHg62IVh/Ah3G8MmNXpCGt1m0dTFhSdx/AVs8XEMbdOGRwdkR1Bcta8AA==} 244 | 245 | ee-first@1.1.1: 246 | resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} 247 | 248 | emoji-regex@8.0.0: 249 | resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} 250 | 251 | encodeurl@2.0.0: 252 | resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} 253 | engines: {node: '>= 0.8'} 254 | 255 | end-of-stream@1.4.4: 256 | resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} 257 | 258 | env-paths@2.2.1: 259 | resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} 260 | engines: {node: '>=6'} 261 | 262 | error-ex@1.3.2: 263 | resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} 264 | 265 | escalade@3.1.1: 266 | resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} 267 | engines: {node: '>=6'} 268 | 269 | escape-html@1.0.3: 270 | resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} 271 | 272 | escape-string-regexp@1.0.5: 273 | resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} 274 | engines: {node: '>=0.8.0'} 275 | 276 | escodegen@2.1.0: 277 | resolution: {integrity: sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==} 278 | engines: {node: '>=6.0'} 279 | hasBin: true 280 | 281 | esprima@4.0.1: 282 | resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} 283 | engines: {node: '>=4'} 284 | hasBin: true 285 | 286 | estraverse@5.3.0: 287 | resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} 288 | engines: {node: '>=4.0'} 289 | 290 | esutils@2.0.3: 291 | resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} 292 | engines: {node: '>=0.10.0'} 293 | 294 | etag@1.8.1: 295 | resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} 296 | engines: {node: '>= 0.6'} 297 | 298 | extract-zip@2.0.1: 299 | resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==} 300 | engines: {node: '>= 10.17.0'} 301 | hasBin: true 302 | 303 | fast-fifo@1.3.2: 304 | resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==} 305 | 306 | fd-slicer@1.1.0: 307 | resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} 308 | 309 | finalhandler@2.1.1: 310 | resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} 311 | engines: {node: '>= 18.0.0'} 312 | 313 | fresh@2.0.0: 314 | resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} 315 | engines: {node: '>= 0.8'} 316 | 317 | fs-extra@8.1.0: 318 | resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==} 319 | engines: {node: '>=6 <7 || >=8'} 320 | 321 | get-caller-file@2.0.5: 322 | resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} 323 | engines: {node: 6.* || 8.* || >= 10.*} 324 | 325 | get-stream@5.2.0: 326 | resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} 327 | engines: {node: '>=8'} 328 | 329 | get-uri@6.0.1: 330 | resolution: {integrity: sha512-7ZqONUVqaabogsYNWlYj0t3YZaL6dhuEueZXGF+/YVmf6dHmaFg8/6psJKqhx9QykIDKzpGcy2cn4oV4YC7V/Q==} 331 | engines: {node: '>= 14'} 332 | 333 | graceful-fs@4.2.11: 334 | resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} 335 | 336 | has-flag@3.0.0: 337 | resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} 338 | engines: {node: '>=4'} 339 | 340 | http-errors@2.0.0: 341 | resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} 342 | engines: {node: '>= 0.8'} 343 | 344 | http-proxy-agent@7.0.2: 345 | resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} 346 | engines: {node: '>= 14'} 347 | 348 | https-proxy-agent@7.0.6: 349 | resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} 350 | engines: {node: '>= 14'} 351 | 352 | import-fresh@3.3.0: 353 | resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} 354 | engines: {node: '>=6'} 355 | 356 | inherits@2.0.4: 357 | resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} 358 | 359 | ip-address@10.0.1: 360 | resolution: {integrity: sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==} 361 | engines: {node: '>= 12'} 362 | 363 | is-arrayish@0.2.1: 364 | resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} 365 | 366 | is-fullwidth-code-point@3.0.0: 367 | resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} 368 | engines: {node: '>=8'} 369 | 370 | js-tokens@4.0.0: 371 | resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} 372 | 373 | js-yaml@4.1.0: 374 | resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} 375 | hasBin: true 376 | 377 | json-parse-even-better-errors@2.3.1: 378 | resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} 379 | 380 | jsonfile@4.0.0: 381 | resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} 382 | 383 | lines-and-columns@1.2.4: 384 | resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} 385 | 386 | lru-cache@7.18.3: 387 | resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} 388 | engines: {node: '>=12'} 389 | 390 | mime-db@1.54.0: 391 | resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} 392 | engines: {node: '>= 0.6'} 393 | 394 | mime-types@3.0.1: 395 | resolution: {integrity: sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==} 396 | engines: {node: '>= 0.6'} 397 | 398 | mitt@3.0.1: 399 | resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} 400 | 401 | ms@2.1.3: 402 | resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} 403 | 404 | netmask@2.0.2: 405 | resolution: {integrity: sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==} 406 | engines: {node: '>= 0.4.0'} 407 | 408 | on-finished@2.4.1: 409 | resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} 410 | engines: {node: '>= 0.8'} 411 | 412 | once@1.4.0: 413 | resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} 414 | 415 | pac-proxy-agent@7.2.0: 416 | resolution: {integrity: sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==} 417 | engines: {node: '>= 14'} 418 | 419 | pac-resolver@7.0.1: 420 | resolution: {integrity: sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==} 421 | engines: {node: '>= 14'} 422 | 423 | parent-module@1.0.1: 424 | resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} 425 | engines: {node: '>=6'} 426 | 427 | parse-json@5.2.0: 428 | resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} 429 | engines: {node: '>=8'} 430 | 431 | parseurl@1.3.3: 432 | resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} 433 | engines: {node: '>= 0.8'} 434 | 435 | pend@1.2.0: 436 | resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} 437 | 438 | prettier@3.7.4: 439 | resolution: {integrity: sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==} 440 | engines: {node: '>=14'} 441 | hasBin: true 442 | 443 | progress@2.0.3: 444 | resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} 445 | engines: {node: '>=0.4.0'} 446 | 447 | proxy-agent@6.5.0: 448 | resolution: {integrity: sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==} 449 | engines: {node: '>= 14'} 450 | 451 | proxy-from-env@1.1.0: 452 | resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} 453 | 454 | pump@3.0.0: 455 | resolution: {integrity: sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==} 456 | 457 | puppeteer-core@24.22.0: 458 | resolution: {integrity: sha512-oUeWlIg0pMz8YM5pu0uqakM+cCyYyXkHBxx9di9OUELu9X9+AYrNGGRLK9tNME3WfN3JGGqQIH3b4/E9LGek/w==} 459 | engines: {node: '>=18'} 460 | 461 | puppeteer@24.22.0: 462 | resolution: {integrity: sha512-QabGIvu7F0hAMiKGHZCIRHMb6UoH0QAJA2OaqxEU2tL5noXPrxUcotg2l3ttOA4p1PFnVIGkr6PXRAWlM2evVQ==} 463 | engines: {node: '>=18'} 464 | hasBin: true 465 | 466 | range-parser@1.2.1: 467 | resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} 468 | engines: {node: '>= 0.6'} 469 | 470 | require-directory@2.1.1: 471 | resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} 472 | engines: {node: '>=0.10.0'} 473 | 474 | resolve-from@4.0.0: 475 | resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} 476 | engines: {node: '>=4'} 477 | 478 | semver@7.7.2: 479 | resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} 480 | engines: {node: '>=10'} 481 | hasBin: true 482 | 483 | send@1.2.0: 484 | resolution: {integrity: sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==} 485 | engines: {node: '>= 18'} 486 | 487 | serve-static@2.2.0: 488 | resolution: {integrity: sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==} 489 | engines: {node: '>= 18'} 490 | 491 | setprototypeof@1.2.0: 492 | resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} 493 | 494 | smart-buffer@4.2.0: 495 | resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} 496 | engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} 497 | 498 | socks-proxy-agent@8.0.5: 499 | resolution: {integrity: sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==} 500 | engines: {node: '>= 14'} 501 | 502 | socks@2.8.7: 503 | resolution: {integrity: sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==} 504 | engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} 505 | 506 | source-map@0.6.1: 507 | resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} 508 | engines: {node: '>=0.10.0'} 509 | 510 | split2@4.2.0: 511 | resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} 512 | engines: {node: '>= 10.x'} 513 | 514 | statuses@2.0.1: 515 | resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} 516 | engines: {node: '>= 0.8'} 517 | 518 | streamx@2.22.1: 519 | resolution: {integrity: sha512-znKXEBxfatz2GBNK02kRnCXjV+AA4kjZIUxeWSr3UGirZMJfTE9uiwKHobnbgxWyL/JWro8tTq+vOqAK1/qbSA==} 520 | 521 | string-width@4.2.3: 522 | resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} 523 | engines: {node: '>=8'} 524 | 525 | strip-ansi@6.0.1: 526 | resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} 527 | engines: {node: '>=8'} 528 | 529 | subresources@2.1.0: 530 | resolution: {integrity: sha512-fHYy8gVI0MV6acfpWSJMFCk/C9jjWZG5eBf49wUvd61EJ/dxUI6aH26AIHg1rnIN5rJBYMmOSrlVba3KTt/5UA==} 531 | 532 | supports-color@5.5.0: 533 | resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} 534 | engines: {node: '>=4'} 535 | 536 | tar-fs@3.1.1: 537 | resolution: {integrity: sha512-LZA0oaPOc2fVo82Txf3gw+AkEd38szODlptMYejQUhndHMLQ9M059uXR+AfS7DNo0NpINvSqDsvyaCrBVkptWg==} 538 | 539 | tar-stream@3.1.6: 540 | resolution: {integrity: sha512-B/UyjYwPpMBv+PaFSWAmtYjwdrlEaZQEhMIBFNC5oEG8lpiW8XjcSdmEaClj28ArfKScKHs2nshz3k2le6crsg==} 541 | 542 | text-decoder@1.2.3: 543 | resolution: {integrity: sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==} 544 | 545 | toidentifier@1.0.1: 546 | resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} 547 | engines: {node: '>=0.6'} 548 | 549 | tslib@2.6.2: 550 | resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} 551 | 552 | tunnel@0.0.6: 553 | resolution: {integrity: sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==} 554 | engines: {node: '>=0.6.11 <=0.7.0 || >=0.7.3'} 555 | 556 | typed-query-selector@2.12.0: 557 | resolution: {integrity: sha512-SbklCd1F0EiZOyPiW192rrHZzZ5sBijB6xM+cpmrwDqObvdtunOHHIk9fCGsoK5JVIYXoyEp4iEdE3upFH3PAg==} 558 | 559 | typescript@5.9.3: 560 | resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} 561 | engines: {node: '>=14.17'} 562 | hasBin: true 563 | 564 | undici-types@7.12.0: 565 | resolution: {integrity: sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ==} 566 | 567 | undici@5.29.0: 568 | resolution: {integrity: sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==} 569 | engines: {node: '>=14.0'} 570 | 571 | universalify@0.1.2: 572 | resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} 573 | engines: {node: '>= 4.0.0'} 574 | 575 | webdriver-bidi-protocol@0.2.11: 576 | resolution: {integrity: sha512-Y9E1/oi4XMxcR8AT0ZC4OvYntl34SPgwjmELH+owjBr0korAX4jKgZULBWILGCVGdVCQ0dodTToIETozhG8zvA==} 577 | 578 | wrap-ansi@7.0.0: 579 | resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} 580 | engines: {node: '>=10'} 581 | 582 | wrappy@1.0.2: 583 | resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} 584 | 585 | ws@8.18.3: 586 | resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==} 587 | engines: {node: '>=10.0.0'} 588 | peerDependencies: 589 | bufferutil: ^4.0.1 590 | utf-8-validate: '>=5.0.2' 591 | peerDependenciesMeta: 592 | bufferutil: 593 | optional: true 594 | utf-8-validate: 595 | optional: true 596 | 597 | y18n@5.0.8: 598 | resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} 599 | engines: {node: '>=10'} 600 | 601 | yaml@2.8.2: 602 | resolution: {integrity: sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==} 603 | engines: {node: '>= 14.6'} 604 | hasBin: true 605 | 606 | yargs-parser@21.1.1: 607 | resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} 608 | engines: {node: '>=12'} 609 | 610 | yargs@17.7.2: 611 | resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} 612 | engines: {node: '>=12'} 613 | 614 | yauzl@2.10.0: 615 | resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==} 616 | 617 | zod@3.25.76: 618 | resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} 619 | 620 | snapshots: 621 | 622 | '@actions/core@2.0.1': 623 | dependencies: 624 | '@actions/exec': 2.0.0 625 | '@actions/http-client': 3.0.0 626 | 627 | '@actions/exec@2.0.0': 628 | dependencies: 629 | '@actions/io': 2.0.0 630 | 631 | '@actions/http-client@3.0.0': 632 | dependencies: 633 | tunnel: 0.0.6 634 | undici: 5.29.0 635 | 636 | '@actions/io@2.0.0': {} 637 | 638 | '@babel/code-frame@7.18.6': 639 | dependencies: 640 | '@babel/highlight': 7.18.6 641 | 642 | '@babel/helper-validator-identifier@7.19.1': {} 643 | 644 | '@babel/highlight@7.18.6': 645 | dependencies: 646 | '@babel/helper-validator-identifier': 7.19.1 647 | chalk: 2.4.2 648 | js-tokens: 4.0.0 649 | 650 | '@fastify/busboy@2.1.1': {} 651 | 652 | '@puppeteer/browsers@2.10.10': 653 | dependencies: 654 | debug: 4.4.3 655 | extract-zip: 2.0.1 656 | progress: 2.0.3 657 | proxy-agent: 6.5.0 658 | semver: 7.7.2 659 | tar-fs: 3.1.1 660 | yargs: 17.7.2 661 | transitivePeerDependencies: 662 | - bare-buffer 663 | - supports-color 664 | 665 | '@tootallnate/quickjs-emscripten@0.23.0': {} 666 | 667 | '@types/finalhandler@1.2.4': 668 | dependencies: 669 | '@types/node': 24.5.2 670 | 671 | '@types/http-errors@2.0.1': {} 672 | 673 | '@types/node@24.5.2': 674 | dependencies: 675 | undici-types: 7.12.0 676 | 677 | '@types/serve-static@2.2.0': 678 | dependencies: 679 | '@types/http-errors': 2.0.1 680 | '@types/node': 24.5.2 681 | 682 | '@types/split2@4.2.3': 683 | dependencies: 684 | '@types/node': 24.5.2 685 | 686 | '@types/yauzl@2.9.1': 687 | dependencies: 688 | '@types/node': 24.5.2 689 | optional: true 690 | 691 | agent-base@7.1.4: {} 692 | 693 | ansi-regex@5.0.1: {} 694 | 695 | ansi-styles@3.2.1: 696 | dependencies: 697 | color-convert: 1.9.3 698 | 699 | ansi-styles@4.3.0: 700 | dependencies: 701 | color-convert: 2.0.1 702 | 703 | argparse@2.0.1: {} 704 | 705 | ast-types@0.13.4: 706 | dependencies: 707 | tslib: 2.6.2 708 | 709 | b4a@1.6.4: {} 710 | 711 | bare-events@2.7.0: 712 | optional: true 713 | 714 | bare-fs@4.4.4: 715 | dependencies: 716 | bare-events: 2.7.0 717 | bare-path: 3.0.0 718 | bare-stream: 2.7.0(bare-events@2.7.0) 719 | bare-url: 2.2.2 720 | fast-fifo: 1.3.2 721 | optional: true 722 | 723 | bare-os@3.6.2: 724 | optional: true 725 | 726 | bare-path@3.0.0: 727 | dependencies: 728 | bare-os: 3.6.2 729 | optional: true 730 | 731 | bare-stream@2.7.0(bare-events@2.7.0): 732 | dependencies: 733 | streamx: 2.22.1 734 | optionalDependencies: 735 | bare-events: 2.7.0 736 | optional: true 737 | 738 | bare-url@2.2.2: 739 | dependencies: 740 | bare-path: 3.0.0 741 | optional: true 742 | 743 | basic-ftp@5.0.3: {} 744 | 745 | buffer-crc32@0.2.13: {} 746 | 747 | callsites@3.1.0: {} 748 | 749 | chalk@2.4.2: 750 | dependencies: 751 | ansi-styles: 3.2.1 752 | escape-string-regexp: 1.0.5 753 | supports-color: 5.5.0 754 | 755 | chromium-bidi@8.0.0(devtools-protocol@0.0.1495869): 756 | dependencies: 757 | devtools-protocol: 0.0.1495869 758 | mitt: 3.0.1 759 | zod: 3.25.76 760 | 761 | cliui@8.0.1: 762 | dependencies: 763 | string-width: 4.2.3 764 | strip-ansi: 6.0.1 765 | wrap-ansi: 7.0.0 766 | 767 | color-convert@1.9.3: 768 | dependencies: 769 | color-name: 1.1.3 770 | 771 | color-convert@2.0.1: 772 | dependencies: 773 | color-name: 1.1.4 774 | 775 | color-name@1.1.3: {} 776 | 777 | color-name@1.1.4: {} 778 | 779 | cosmiconfig@9.0.0(typescript@5.9.3): 780 | dependencies: 781 | env-paths: 2.2.1 782 | import-fresh: 3.3.0 783 | js-yaml: 4.1.0 784 | parse-json: 5.2.0 785 | optionalDependencies: 786 | typescript: 5.9.3 787 | 788 | data-uri-to-buffer@5.0.1: {} 789 | 790 | debug@4.4.3: 791 | dependencies: 792 | ms: 2.1.3 793 | 794 | degenerator@5.0.1: 795 | dependencies: 796 | ast-types: 0.13.4 797 | escodegen: 2.1.0 798 | esprima: 4.0.1 799 | 800 | depd@2.0.0: {} 801 | 802 | devtools-protocol@0.0.1495869: {} 803 | 804 | ee-first@1.1.1: {} 805 | 806 | emoji-regex@8.0.0: {} 807 | 808 | encodeurl@2.0.0: {} 809 | 810 | end-of-stream@1.4.4: 811 | dependencies: 812 | once: 1.4.0 813 | 814 | env-paths@2.2.1: {} 815 | 816 | error-ex@1.3.2: 817 | dependencies: 818 | is-arrayish: 0.2.1 819 | 820 | escalade@3.1.1: {} 821 | 822 | escape-html@1.0.3: {} 823 | 824 | escape-string-regexp@1.0.5: {} 825 | 826 | escodegen@2.1.0: 827 | dependencies: 828 | esprima: 4.0.1 829 | estraverse: 5.3.0 830 | esutils: 2.0.3 831 | optionalDependencies: 832 | source-map: 0.6.1 833 | 834 | esprima@4.0.1: {} 835 | 836 | estraverse@5.3.0: {} 837 | 838 | esutils@2.0.3: {} 839 | 840 | etag@1.8.1: {} 841 | 842 | extract-zip@2.0.1: 843 | dependencies: 844 | debug: 4.4.3 845 | get-stream: 5.2.0 846 | yauzl: 2.10.0 847 | optionalDependencies: 848 | '@types/yauzl': 2.9.1 849 | transitivePeerDependencies: 850 | - supports-color 851 | 852 | fast-fifo@1.3.2: {} 853 | 854 | fd-slicer@1.1.0: 855 | dependencies: 856 | pend: 1.2.0 857 | 858 | finalhandler@2.1.1: 859 | dependencies: 860 | debug: 4.4.3 861 | encodeurl: 2.0.0 862 | escape-html: 1.0.3 863 | on-finished: 2.4.1 864 | parseurl: 1.3.3 865 | statuses: 2.0.1 866 | transitivePeerDependencies: 867 | - supports-color 868 | 869 | fresh@2.0.0: {} 870 | 871 | fs-extra@8.1.0: 872 | dependencies: 873 | graceful-fs: 4.2.11 874 | jsonfile: 4.0.0 875 | universalify: 0.1.2 876 | 877 | get-caller-file@2.0.5: {} 878 | 879 | get-stream@5.2.0: 880 | dependencies: 881 | pump: 3.0.0 882 | 883 | get-uri@6.0.1: 884 | dependencies: 885 | basic-ftp: 5.0.3 886 | data-uri-to-buffer: 5.0.1 887 | debug: 4.4.3 888 | fs-extra: 8.1.0 889 | transitivePeerDependencies: 890 | - supports-color 891 | 892 | graceful-fs@4.2.11: {} 893 | 894 | has-flag@3.0.0: {} 895 | 896 | http-errors@2.0.0: 897 | dependencies: 898 | depd: 2.0.0 899 | inherits: 2.0.4 900 | setprototypeof: 1.2.0 901 | statuses: 2.0.1 902 | toidentifier: 1.0.1 903 | 904 | http-proxy-agent@7.0.2: 905 | dependencies: 906 | agent-base: 7.1.4 907 | debug: 4.4.3 908 | transitivePeerDependencies: 909 | - supports-color 910 | 911 | https-proxy-agent@7.0.6: 912 | dependencies: 913 | agent-base: 7.1.4 914 | debug: 4.4.3 915 | transitivePeerDependencies: 916 | - supports-color 917 | 918 | import-fresh@3.3.0: 919 | dependencies: 920 | parent-module: 1.0.1 921 | resolve-from: 4.0.0 922 | 923 | inherits@2.0.4: {} 924 | 925 | ip-address@10.0.1: {} 926 | 927 | is-arrayish@0.2.1: {} 928 | 929 | is-fullwidth-code-point@3.0.0: {} 930 | 931 | js-tokens@4.0.0: {} 932 | 933 | js-yaml@4.1.0: 934 | dependencies: 935 | argparse: 2.0.1 936 | 937 | json-parse-even-better-errors@2.3.1: {} 938 | 939 | jsonfile@4.0.0: 940 | optionalDependencies: 941 | graceful-fs: 4.2.11 942 | 943 | lines-and-columns@1.2.4: {} 944 | 945 | lru-cache@7.18.3: {} 946 | 947 | mime-db@1.54.0: {} 948 | 949 | mime-types@3.0.1: 950 | dependencies: 951 | mime-db: 1.54.0 952 | 953 | mitt@3.0.1: {} 954 | 955 | ms@2.1.3: {} 956 | 957 | netmask@2.0.2: {} 958 | 959 | on-finished@2.4.1: 960 | dependencies: 961 | ee-first: 1.1.1 962 | 963 | once@1.4.0: 964 | dependencies: 965 | wrappy: 1.0.2 966 | 967 | pac-proxy-agent@7.2.0: 968 | dependencies: 969 | '@tootallnate/quickjs-emscripten': 0.23.0 970 | agent-base: 7.1.4 971 | debug: 4.4.3 972 | get-uri: 6.0.1 973 | http-proxy-agent: 7.0.2 974 | https-proxy-agent: 7.0.6 975 | pac-resolver: 7.0.1 976 | socks-proxy-agent: 8.0.5 977 | transitivePeerDependencies: 978 | - supports-color 979 | 980 | pac-resolver@7.0.1: 981 | dependencies: 982 | degenerator: 5.0.1 983 | netmask: 2.0.2 984 | 985 | parent-module@1.0.1: 986 | dependencies: 987 | callsites: 3.1.0 988 | 989 | parse-json@5.2.0: 990 | dependencies: 991 | '@babel/code-frame': 7.18.6 992 | error-ex: 1.3.2 993 | json-parse-even-better-errors: 2.3.1 994 | lines-and-columns: 1.2.4 995 | 996 | parseurl@1.3.3: {} 997 | 998 | pend@1.2.0: {} 999 | 1000 | prettier@3.7.4: {} 1001 | 1002 | progress@2.0.3: {} 1003 | 1004 | proxy-agent@6.5.0: 1005 | dependencies: 1006 | agent-base: 7.1.4 1007 | debug: 4.4.3 1008 | http-proxy-agent: 7.0.2 1009 | https-proxy-agent: 7.0.6 1010 | lru-cache: 7.18.3 1011 | pac-proxy-agent: 7.2.0 1012 | proxy-from-env: 1.1.0 1013 | socks-proxy-agent: 8.0.5 1014 | transitivePeerDependencies: 1015 | - supports-color 1016 | 1017 | proxy-from-env@1.1.0: {} 1018 | 1019 | pump@3.0.0: 1020 | dependencies: 1021 | end-of-stream: 1.4.4 1022 | once: 1.4.0 1023 | 1024 | puppeteer-core@24.22.0: 1025 | dependencies: 1026 | '@puppeteer/browsers': 2.10.10 1027 | chromium-bidi: 8.0.0(devtools-protocol@0.0.1495869) 1028 | debug: 4.4.3 1029 | devtools-protocol: 0.0.1495869 1030 | typed-query-selector: 2.12.0 1031 | webdriver-bidi-protocol: 0.2.11 1032 | ws: 8.18.3 1033 | transitivePeerDependencies: 1034 | - bare-buffer 1035 | - bufferutil 1036 | - supports-color 1037 | - utf-8-validate 1038 | 1039 | puppeteer@24.22.0(typescript@5.9.3): 1040 | dependencies: 1041 | '@puppeteer/browsers': 2.10.10 1042 | chromium-bidi: 8.0.0(devtools-protocol@0.0.1495869) 1043 | cosmiconfig: 9.0.0(typescript@5.9.3) 1044 | devtools-protocol: 0.0.1495869 1045 | puppeteer-core: 24.22.0 1046 | typed-query-selector: 2.12.0 1047 | transitivePeerDependencies: 1048 | - bare-buffer 1049 | - bufferutil 1050 | - supports-color 1051 | - typescript 1052 | - utf-8-validate 1053 | 1054 | range-parser@1.2.1: {} 1055 | 1056 | require-directory@2.1.1: {} 1057 | 1058 | resolve-from@4.0.0: {} 1059 | 1060 | semver@7.7.2: {} 1061 | 1062 | send@1.2.0: 1063 | dependencies: 1064 | debug: 4.4.3 1065 | encodeurl: 2.0.0 1066 | escape-html: 1.0.3 1067 | etag: 1.8.1 1068 | fresh: 2.0.0 1069 | http-errors: 2.0.0 1070 | mime-types: 3.0.1 1071 | ms: 2.1.3 1072 | on-finished: 2.4.1 1073 | range-parser: 1.2.1 1074 | statuses: 2.0.1 1075 | transitivePeerDependencies: 1076 | - supports-color 1077 | 1078 | serve-static@2.2.0: 1079 | dependencies: 1080 | encodeurl: 2.0.0 1081 | escape-html: 1.0.3 1082 | parseurl: 1.3.3 1083 | send: 1.2.0 1084 | transitivePeerDependencies: 1085 | - supports-color 1086 | 1087 | setprototypeof@1.2.0: {} 1088 | 1089 | smart-buffer@4.2.0: {} 1090 | 1091 | socks-proxy-agent@8.0.5: 1092 | dependencies: 1093 | agent-base: 7.1.4 1094 | debug: 4.4.3 1095 | socks: 2.8.7 1096 | transitivePeerDependencies: 1097 | - supports-color 1098 | 1099 | socks@2.8.7: 1100 | dependencies: 1101 | ip-address: 10.0.1 1102 | smart-buffer: 4.2.0 1103 | 1104 | source-map@0.6.1: 1105 | optional: true 1106 | 1107 | split2@4.2.0: {} 1108 | 1109 | statuses@2.0.1: {} 1110 | 1111 | streamx@2.22.1: 1112 | dependencies: 1113 | fast-fifo: 1.3.2 1114 | text-decoder: 1.2.3 1115 | optionalDependencies: 1116 | bare-events: 2.7.0 1117 | 1118 | string-width@4.2.3: 1119 | dependencies: 1120 | emoji-regex: 8.0.0 1121 | is-fullwidth-code-point: 3.0.0 1122 | strip-ansi: 6.0.1 1123 | 1124 | strip-ansi@6.0.1: 1125 | dependencies: 1126 | ansi-regex: 5.0.1 1127 | 1128 | subresources@2.1.0(typescript@5.9.3): 1129 | dependencies: 1130 | puppeteer: 24.22.0(typescript@5.9.3) 1131 | transitivePeerDependencies: 1132 | - bare-buffer 1133 | - bufferutil 1134 | - supports-color 1135 | - typescript 1136 | - utf-8-validate 1137 | 1138 | supports-color@5.5.0: 1139 | dependencies: 1140 | has-flag: 3.0.0 1141 | 1142 | tar-fs@3.1.1: 1143 | dependencies: 1144 | pump: 3.0.0 1145 | tar-stream: 3.1.6 1146 | optionalDependencies: 1147 | bare-fs: 4.4.4 1148 | bare-path: 3.0.0 1149 | transitivePeerDependencies: 1150 | - bare-buffer 1151 | 1152 | tar-stream@3.1.6: 1153 | dependencies: 1154 | b4a: 1.6.4 1155 | fast-fifo: 1.3.2 1156 | streamx: 2.22.1 1157 | 1158 | text-decoder@1.2.3: 1159 | dependencies: 1160 | b4a: 1.6.4 1161 | 1162 | toidentifier@1.0.1: {} 1163 | 1164 | tslib@2.6.2: {} 1165 | 1166 | tunnel@0.0.6: {} 1167 | 1168 | typed-query-selector@2.12.0: {} 1169 | 1170 | typescript@5.9.3: {} 1171 | 1172 | undici-types@7.12.0: {} 1173 | 1174 | undici@5.29.0: 1175 | dependencies: 1176 | '@fastify/busboy': 2.1.1 1177 | 1178 | universalify@0.1.2: {} 1179 | 1180 | webdriver-bidi-protocol@0.2.11: {} 1181 | 1182 | wrap-ansi@7.0.0: 1183 | dependencies: 1184 | ansi-styles: 4.3.0 1185 | string-width: 4.2.3 1186 | strip-ansi: 6.0.1 1187 | 1188 | wrappy@1.0.2: {} 1189 | 1190 | ws@8.18.3: {} 1191 | 1192 | y18n@5.0.8: {} 1193 | 1194 | yaml@2.8.2: {} 1195 | 1196 | yargs-parser@21.1.1: {} 1197 | 1198 | yargs@17.7.2: 1199 | dependencies: 1200 | cliui: 8.0.1 1201 | escalade: 3.1.1 1202 | get-caller-file: 2.0.5 1203 | require-directory: 2.1.1 1204 | string-width: 4.2.3 1205 | y18n: 5.0.8 1206 | yargs-parser: 21.1.1 1207 | 1208 | yauzl@2.10.0: 1209 | dependencies: 1210 | buffer-crc32: 0.2.13 1211 | fd-slicer: 1.1.0 1212 | 1213 | zod@3.25.76: {} 1214 | --------------------------------------------------------------------------------