diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c06ef861..1a37ae4a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,22 +14,20 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v7 with: submodules: false # Currently not necessary - name: Set up Node - uses: actions/setup-node@v3 + uses: actions/setup-node@v7 with: - node-version: 18 + node-version: 24 - - name: Install JSON validator - run: npm install -g ajv-cli@5 ajv-formats@2 + - name: Install dependencies + run: npm ci - name: Validate features table - run: - ajv -c ajv-formats -s features.schema.json -d features.json - --strict=true --errors=text + run: npm run validate-features build: name: Build & Deploy @@ -39,7 +37,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v7 with: submodules: recursive diff --git a/.github/workflows/update-features.yml b/.github/workflows/update-features.yml new file mode 100644 index 00000000..268be049 --- /dev/null +++ b/.github/workflows/update-features.yml @@ -0,0 +1,78 @@ +name: Update features.json + +on: + schedule: + - cron: '8 6 * * *' # Runs daily as 06:08 UTC (randomly chosen to avoid GitHub Actions' peaky demand) + workflow_dispatch: + pull_request: + paths: + - .github/workflows/update-features.yml + - package.json + - package-lock.json + - scripts/update-features.js + +# PRs push nothing, so give each its own group; schedule/dispatch share one since they push. +concurrency: + group: update-features-${{ github.event_name == 'pull_request' && github.run_id || 'scheduled' }} + cancel-in-progress: false + +jobs: + update-features: + runs-on: ubuntu-slim + permissions: + contents: write + pull-requests: write + defaults: + run: + working-directory: ${{ github.workspace }}/website + + steps: + - name: Checkout website + uses: actions/checkout@v7 + with: + path: ${{ github.workspace }}/website + submodules: false + + - name: Checkout WebAssembly/proposals + uses: actions/checkout@v7 + with: + repository: WebAssembly/proposals + path: ${{ github.workspace }}/proposals + + - name: Set up Node + uses: actions/setup-node@v7 + with: + node-version: 24 + + - name: Install dependencies + run: npm ci + + - name: Update features table + run: npm run update-features + env: + PROPOSALS_DIR: ${{ github.workspace }}/proposals + + - name: Validate features table + run: npm run validate-features + + - name: Check features.json is up to date + if: github.event_name == 'pull_request' + run: | + git diff -- features.json + git diff --quiet -- features.json || (echo "::error::features.json is out of date - run 'npm run update-features' and commit the result" && exit 1) + + - name: Create Pull Request + if: > + github.repository == 'WebAssembly/website' && + (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') + uses: peter-evans/create-pull-request@v6 + with: + path: ${{ github.workspace }}/website + commit-message: 'Update features.json from WebAssembly/proposals' + title: 'Update features.json from WebAssembly/proposals' + body: > + Automated update of proposal phases/entries based on the current + state of WebAssembly/proposals. + branch: automated/update-features + delete-branch: true + add-paths: features.json diff --git a/.github/workflows/update-submodules.yml b/.github/workflows/update-submodules.yml index 9c1e281c..6d4809f8 100644 --- a/.github/workflows/update-submodules.yml +++ b/.github/workflows/update-submodules.yml @@ -14,7 +14,7 @@ jobs: steps: - name: Checkout repository (including submodules) - uses: actions/checkout@v4 + uses: actions/checkout@v7 with: submodules: true fetch-depth: 0 # Ensure full history for submodules diff --git a/features.json b/features.json index 391c4e6e..9e696db0 100644 --- a/features.json +++ b/features.json @@ -21,6 +21,11 @@ "url": "https://github.com/WebAssembly/bulk-memory-operations/blob/master/proposals/bulk-memory-operations/Overview.md", "phase": 5 }, + "callTags": { + "description": "Call Tags", + "url": "https://github.com/WebAssembly/call-tags", + "phase": "inactive" + }, "compactImportSection": { "description": "Compact Import Section", "url": "https://github.com/WebAssembly/compact-import-section/blob/main/proposals/compact-import-section/Overview.md", @@ -36,6 +41,16 @@ "url": "https://github.com/WebAssembly/component-model", "phase": 1 }, + "conditionalSections": { + "description": "Conditional sections", + "url": "https://github.com/WebAssembly/conditional-sections", + "phase": "inactive" + }, + "constantTime": { + "description": "Constant Time", + "url": "https://github.com/WebAssembly/constant-time", + "phase": "inactive" + }, "customAnnotationSyntaxInTheTextFormat": { "description": "Custom Text Format Annotations", "url": "https://github.com/WebAssembly/annotations/blob/main/proposals/annotations/Overview.md", @@ -76,6 +91,11 @@ "url": "https://github.com/WebAssembly/extended-name-section/blob/main/proposals/extended-name-section/Overview.md", "phase": 2 }, + "featureDetection": { + "description": "Feature Detection", + "url": "https://github.com/WebAssembly/feature-detection", + "phase": "inactive" + }, "flexibleVectors": { "description": "Flexible Vectors", "url": "https://github.com/WebAssembly/flexible-vectors/blob/main/proposals/flexible-vectors/Overview.md", @@ -86,6 +106,11 @@ "url": "https://github.com/WebAssembly/frozen-values/blob/main/proposals/frozen-values/Overview.md", "phase": 1 }, + "funcletsFlexibleIntraproceduralControlFlow": { + "description": "Funclets: Flexible Intraprocedural Control Flow", + "url": "https://github.com/WebAssembly/funclets", + "phase": "inactive" + }, "gc": { "description": "Garbage Collection", "url": "https://github.com/WebAssembly/gc", @@ -94,18 +119,28 @@ "halfPrecision": { "description": "Half Precision", "url": "https://github.com/WebAssembly/half-precision/blob/main/proposals/half-precision/Overview.md", - "phase": 1 + "phase": 2 }, "instrumentAndTracingTechnology": { "description": "Instrument and Tracing Technology", "url": "https://github.com/WebAssembly/instrument-tracing/blob/main/proposals/instrument-tracing/Overview.md", "phase": "inactive" }, + "interfaceTypes": { + "description": "Interface Types", + "url": "https://github.com/WebAssembly/interface-types", + "phase": "inactive" + }, "jitInterface": { "description": "JIT Interface", "url": "https://github.com/WebAssembly/jit-interface/blob/main/proposals/jit-interface/Explainer.md", "phase": 1 }, + "jsCustomizationForGcObjects": { + "description": "JS Customization for GC Objects", + "url": "https://github.com/WebAssembly/gc-js-customization", + "phase": "inactive" + }, "jsPrimitiveBuiltins": { "description": "JS Primitive Builtins", "url": "https://github.com/WebAssembly/js-primitive-builtins/blob/main/proposals/js-primitive-builtins/Overview.md", @@ -116,6 +151,11 @@ "url": "https://github.com/WebAssembly/js-string-builtins/blob/main/proposals/js-string-builtins/Overview.md", "phase": 5 }, + "jsTextEncodingBuiltins": { + "description": "JS Text Encoding Builtins", + "url": "https://github.com/WebAssembly/js-text-encoding-builtins", + "phase": 1 + }, "jspi": { "description": "JS Promise Integration", "url": "https://github.com/WebAssembly/js-promise-integration", @@ -131,16 +171,16 @@ "url": "https://github.com/WebAssembly/memory-control/blob/main/proposals/memory-control/Overview.md", "phase": 1 }, + "moduleLinking": { + "description": "Module Linking", + "url": "https://github.com/WebAssembly/module-linking", + "phase": "inactive" + }, "moreArrayConstructors": { "description": "More Array Constructors", "url": "https://github.com/WebAssembly/more-array-constructors/blob/main/proposals/more-array-constructors/Overview.md", "phase": 1 }, - "multibyteArrayAccess": { - "description": "Multibyte Array Access", - "url": "https://github.com/WebAssembly/multibyte-array-access", - "phase": 2 - }, "multiMemory": { "description": "Multiple Memories", "url": "https://github.com/WebAssembly/multi-memory/blob/master/proposals/multi-memory/Overview.md", @@ -151,6 +191,11 @@ "url": "https://github.com/WebAssembly/spec/blob/master/proposals/multi-value/Overview.md", "phase": 5 }, + "multibyteArrayAccess": { + "description": "Multibyte Array Access", + "url": "https://github.com/WebAssembly/multibyte-array-access", + "phase": 2 + }, "mutableGlobals": { "description": "Import/Export of Mutable Globals", "url": "https://github.com/WebAssembly/mutable-global/blob/master/proposals/mutable-global/Overview.md", @@ -241,6 +286,11 @@ "url": "https://github.com/WebAssembly/function-references/blob/main/proposals/function-references/Overview.md", "phase": 5 }, + "unmanagedClosures": { + "description": "Unmanaged closures", + "url": "https://github.com/WebAssembly/proposals/issues/6", + "phase": "inactive" + }, "wasmCApi": { "description": "WebAssembly C and C++ API", "url": "https://github.com/WebAssembly/wasm-c-api", @@ -259,7 +309,11 @@ }, "categories": [ { "name": "Web Browsers", "queryKey": "browsers", "default": true }, - { "name": "Standalone Runtimes", "queryKey": "standalones", "default": true }, + { + "name": "Standalone Runtimes", + "queryKey": "standalones", + "default": true + }, { "name": "Embedded Runtimes", "queryKey": "embeddeds" }, { "name": "Tools", "queryKey": "tools" } ], @@ -384,15 +438,9 @@ "exceptions": "15.2", "extendedConst": "17.4", "gc": "18.2", - "jspi": [ - "flag", - "Supported in Safari Technical Preview 238" - ], + "jspi": ["flag", "Supported in Safari Technical Preview 238"], "jsStringBuiltins": "26.2", - "memory64": [ - "flag", - "Supported in Safari Technical Preview 251" - ], + "memory64": ["flag", "Supported in Safari Technical Preview 251"], "multiValue": "13.1", "mutableGlobals": "13.1", "profiles": null, @@ -448,7 +496,7 @@ "flag", "Requires flag `--experimental-wasm-instruction-tracing`" ], - "jspi":"26.0", + "jspi": "26.0", "jsStringBuiltins": [ "flag", "Requires flag `--experimental-wasm-imported-strings`" @@ -470,10 +518,7 @@ "Requires flag `--experimental-wasm-type-reflection`" ], "webContentSecurityPolicy": null, - "wideArithmetic": [ - "flag", - "Requires flag `--wasm-wide-arithmetic`" - ] + "wideArithmetic": ["flag", "Requires flag `--wasm-wide-arithmetic`"] } }, "Deno": { @@ -531,10 +576,7 @@ "bigInt": "21.3", "bulkMemory": "23.0", "customAnnotationSyntaxInTheTextFormat": null, - "exceptions": [ - "flag", - "Requires flag `--wasm.LegacyExceptions=true`" - ], + "exceptions": ["flag", "Requires flag `--wasm.LegacyExceptions=true`"], "exceptionsFinal": "25.1", "extendedConst": "25.1", "gc": "25.1", @@ -677,7 +719,10 @@ "stackSwitching": false, "threads": false, "tailCall": "0.9.0", - "typedFunctionReferences": ["flag", "Compile with `d_m3HasTypedRefs` set to 1"], + "typedFunctionReferences": [ + "flag", + "Compile with `d_m3HasTypedRefs` set to 1" + ], "typeReflection": null, "webContentSecurityPolicy": null, "wideArithmetic": false @@ -791,7 +836,10 @@ "features": { "bigInt": null, "bulkMemory": true, - "componentModel": [true, "WASI 0.2 command components (wasi:cli/run) execute end-to-end on both engines"], + "componentModel": [ + true, + "WASI 0.2 command components (wasi:cli/run) execute end-to-end on both engines" + ], "customAnnotationSyntaxInTheTextFormat": null, "customPageSizes": true, "esmIntegration": null, @@ -802,17 +850,29 @@ "jspi": null, "jsStringBuiltins": null, "memory64": true, - "multiMemory": [true, "Interpreter engine only; the JIT rejects multi-memory modules and the default auto mode falls back to the interpreter"], + "multiMemory": [ + true, + "Interpreter engine only; the JIT rejects multi-memory modules and the default auto mode falls back to the interpreter" + ], "multiValue": true, "mutableGlobals": true, "referenceTypes": true, - "relaxedSimd": [true, "JIT engine only; the interpreter accepts SIMD modules but traps when a vector instruction executes (the default auto mode runs them on the JIT)"], + "relaxedSimd": [ + true, + "JIT engine only; the interpreter accepts SIMD modules but traps when a vector instruction executes (the default auto mode runs them on the JIT)" + ], "saturatedFloatToInt": true, "signExtensions": true, - "simd": [true, "JIT engine only; the interpreter accepts SIMD modules but traps when a vector instruction executes (the default auto mode runs them on the JIT)"], + "simd": [ + true, + "JIT engine only; the interpreter accepts SIMD modules but traps when a vector instruction executes (the default auto mode runs them on the JIT)" + ], "stackSwitching": false, "tailCall": true, - "threads": [true, "Single-threaded host: atomics, shared memories, and wait/notify are implemented and validated; wait times out and notify returns 0"], + "threads": [ + true, + "Single-threaded host: atomics, shared memories, and wait/notify are implemented and validated; wait times out and notify returns 0" + ], "typedFunctionReferences": true, "typeReflection": null, "webContentSecurityPolicy": null, @@ -1008,17 +1068,32 @@ "signExtensions": true, "simd": ["flag", "Compile with `SOUP_WASM_SIMD` set to `true`"], "memory64": true, - "multiMemory": ["flag", "Compile with `SOUP_WASM_MULTI_MEMORY` set to `true`"], - "extendedConst": ["flag", "Compile with `SOUP_WASM_EXTENDED_CONST` set to `true`"], - "tailCall": ["flag", "Compile with `SOUP_WASM_TAIL_CALL` set to `true`"], - "exceptionsFinal": ["flag", "Compile with `SOUP_WASM_EXCEPTIONS` set to `true`"], + "multiMemory": [ + "flag", + "Compile with `SOUP_WASM_MULTI_MEMORY` set to `true`" + ], + "extendedConst": [ + "flag", + "Compile with `SOUP_WASM_EXTENDED_CONST` set to `true`" + ], + "tailCall": [ + "flag", + "Compile with `SOUP_WASM_TAIL_CALL` set to `true`" + ], + "exceptionsFinal": [ + "flag", + "Compile with `SOUP_WASM_EXCEPTIONS` set to `true`" + ], "bigInt": null, "branchHinting": null, "customAnnotationSyntaxInTheTextFormat": null, "webContentSecurityPolicy": null, "esmIntegration": null, "typeReflection": null, - "customPageSizes": ["flag", "Compile with `SOUP_WASM_CUSTOM_PAGE_SIZES` set to `true`"], + "customPageSizes": [ + "flag", + "Compile with `SOUP_WASM_CUSTOM_PAGE_SIZES` set to `true`" + ], "compilationHints": null, "numericValuesInWat": null, "relaxedDeadCodeValidation": null @@ -1108,15 +1183,24 @@ "branchHinting": false, "bulkMemory": true, "compactImportSection": false, - "componentModel": [true, "Preamble and 10/13 top-level sections decoded; canon, value, and component-level start are stubs; embedded component-type custom section not yet extracted."], + "componentModel": [ + true, + "Preamble and 10/13 top-level sections decoded; canon, value, and component-level start are stubs; embedded component-type custom section not yet extracted." + ], "customPageSizes": false, "exceptions": true, "exceptionsFinal": true, "extendedConst": true, "extendedNameSection": true, "gc": true, - "memory64": [true, "64-bit limit flags and memidx-carrying memory ops decoded; not separately validated against the rest of the proposal scope."], - "multiValue": [true, "Type-index multi-value blocks decoded; empty block-type 0x40 is misdecoded as a reference type (known bug)."], + "memory64": [ + true, + "64-bit limit flags and memidx-carrying memory ops decoded; not separately validated against the rest of the proposal scope." + ], + "multiValue": [ + true, + "Type-index multi-value blocks decoded; empty block-type 0x40 is misdecoded as a reference type (known bug)." + ], "mutableGlobals": true, "referenceTypes": true, "relaxedSimd": true, @@ -1125,7 +1209,10 @@ "simd": true, "stackSwitching": false, "tailCall": true, - "threads": [true, "Full 0xFE atomic opcode family; shared-memory limit flag 0x03 parsed; open-shared flag 0x02 not yet parsed."], + "threads": [ + true, + "Full 0xFE atomic opcode family; shared-memory limit flag 0x03 parsed; open-shared flag 0x02 not yet parsed." + ], "typedFunctionReferences": true, "wideArithmetic": false } diff --git a/package-lock.json b/package-lock.json index 36c7b02a..500cd1da 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,10 @@ "dark-mode-toggle": "^0.18.0" }, "devDependencies": { + "ajv-cli": "^5.0.0", + "ajv-formats": "^2.1.1", + "cheerio": "^1.2.0", + "markdown-it": "^15.0.1", "prettier": "^3.7.4", "shx": "^0.4.0", "svgo": "^4.0.1" @@ -55,6 +59,85 @@ "node": ">= 8" } }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-cli": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ajv-cli/-/ajv-cli-5.0.0.tgz", + "integrity": "sha512-LY4m6dUv44HTyhV+u2z5uX4EhPYTM38Iv1jdgDJJJCyOOuqB8KtZEGjPZ2T+sh5ZIJrXUfgErYx/j3gLd3+PlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0", + "fast-json-patch": "^2.0.0", + "glob": "^7.1.0", + "js-yaml": "^3.14.0", + "json-schema-migrate": "^2.0.0", + "json5": "^2.1.3", + "minimist": "^1.2.0" + }, + "bin": { + "ajv": "dist/index.js" + }, + "peerDependencies": { + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "ts-node": { + "optional": true + } + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, "node_modules/boolbase": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", @@ -62,6 +145,17 @@ "dev": true, "license": "ISC" }, + "node_modules/brace-expansion": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "node_modules/braces": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", @@ -75,6 +169,50 @@ "node": ">=8" } }, + "node_modules/cheerio": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.2.0.tgz", + "integrity": "sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "encoding-sniffer": "^0.2.1", + "htmlparser2": "^10.1.0", + "parse5": "^7.3.0", + "parse5-htmlparser2-tree-adapter": "^7.1.0", + "parse5-parser-stream": "^7.1.2", + "undici": "^7.19.0", + "whatwg-mimetype": "^4.0.0" + }, + "engines": { + "node": ">=20.18.1" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, "node_modules/commander": { "version": "11.1.0", "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", @@ -85,6 +223,13 @@ "node": ">=16" } }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, "node_modules/cross-spawn": { "version": "6.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.6.tgz", @@ -247,6 +392,20 @@ "url": "https://github.com/fb55/domutils?sponsor=1" } }, + "node_modules/encoding-sniffer": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz", + "integrity": "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "^0.6.3", + "whatwg-encoding": "^3.1.1" + }, + "funding": { + "url": "https://github.com/fb55/encoding-sniffer?sponsor=1" + } + }, "node_modules/end-of-stream": { "version": "1.4.5", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", @@ -270,6 +429,20 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/execa": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", @@ -289,6 +462,13 @@ "node": ">=6" } }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, "node_modules/fast-glob": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", @@ -306,6 +486,43 @@ "node": ">=8.6.0" } }, + "node_modules/fast-json-patch": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/fast-json-patch/-/fast-json-patch-2.2.1.tgz", + "integrity": "sha512-4j5uBaTnsYAV5ebkidvxiLUYOwjQ+JSFljeqfTxCrH9bDmlCQaOJFS84oDJ2rAXZq2yskmk3ORfoP9DCwqFNig==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^2.0.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/fast-json-patch/node_modules/fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz", + "integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/fastq": { "version": "1.19.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", @@ -329,6 +546,13 @@ "node": ">=8" } }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -352,6 +576,28 @@ "node": ">=6" } }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/glob-parent": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", @@ -378,6 +624,71 @@ "node": ">= 0.4" } }, + "node_modules/htmlparser2": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", + "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "entities": "^7.0.1" + } + }, + "node_modules/htmlparser2/node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, "node_modules/interpret": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", @@ -454,6 +765,128 @@ "dev": true, "license": "ISC" }, + "node_modules/js-yaml": { + "version": "3.15.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.2.tgz", + "integrity": "sha512-6EuL879VkRA+1Cz578mKMiKvjPNEuk6+r1JaFzoSWejZmtf7xWbIyw1e3KkxlkzTIt9Taw6JBhEppG7utc1P+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-schema-migrate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/json-schema-migrate/-/json-schema-migrate-2.0.0.tgz", + "integrity": "sha512-r38SVTtojDRp4eD6WsCqiE0eNDt4v1WalBXb9cyZYw9ai5cGtBwzRNWjHzJl38w6TxFkXAIA7h+fyX3tnrAFhQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/linkify-it": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-6.1.0.tgz", + "integrity": "sha512-wJ/TwpSDTLepCrQoYWYIExIKg5Zchex2Nn5yk2mFnB+6PtdkHtyLx742md9csRjjOnGkKIS/RrbY7l8D6gT9Vw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/markdown-it" + } + ], + "license": "MIT", + "dependencies": { + "uc.micro": "^3.0.0" + } + }, + "node_modules/markdown-it": { + "version": "15.0.2", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-15.0.2.tgz", + "integrity": "sha512-q4IGxMv56jCqT4OCRCADBoDP3LO4MhmTXjFbphHPXs4g3j9Xg5RDnxqN8IF/3vIWEU+VCnUq+7JUg/cfy2E6Qw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/markdown-it" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^3.0.0", + "entities": "^8.0.0", + "linkify-it": "^6.0.0", + "mdurl": "^2.1.0", + "punycode.js": "^2.3.1", + "uc.micro": "^3.0.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, + "node_modules/markdown-it/node_modules/argparse": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-3.0.2.tgz", + "integrity": "sha512-mFdDM6WqWKraGLsVb+C9CahPnzTXOefAOLq3jYcca2YZ8bEWpr++Tzj+zSaKW9+X9L5uSxcm1AZ3Y6aZJ09OhQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "PSF-2.0" + }, + "node_modules/markdown-it/node_modules/entities": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.1.0.tgz", + "integrity": "sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/mdn-data": { "version": "2.12.2", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz", @@ -461,6 +894,13 @@ "dev": true, "license": "CC0-1.0" }, + "node_modules/mdurl": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.1.0.tgz", + "integrity": "sha512-1+HBaOx0zi/dQWht8rNv9MYf9qqpqL/kxI0hXImU6Y547zM6Sni8BQibt7ifgMcYtQg41ao3Ivd6cnSM86inpg==", + "dev": true, + "license": "MIT" + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -485,6 +925,19 @@ "node": ">=8.6" } }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/minimist": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", @@ -548,6 +1001,69 @@ "node": ">=4" } }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", + "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "domhandler": "^5.0.3", + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-parser-stream": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", + "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/path-key": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", @@ -612,6 +1128,16 @@ "once": "^1.3.1" } }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -645,6 +1171,16 @@ "node": ">= 0.10" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve": { "version": "1.22.10", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", @@ -701,6 +1237,13 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, "node_modules/sax": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/sax/-/sax-1.5.0.tgz", @@ -797,6 +1340,13 @@ "node": ">=0.10.0" } }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, "node_modules/strip-eof": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", @@ -859,6 +1409,47 @@ "node": ">=8.0" } }, + "node_modules/uc.micro": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-3.0.0.tgz", + "integrity": "sha512-U3PppEkleoTnIfi8BozMx3yju3qc/L6SwqWo2Sw+54PX+PX0q9I+r1Um5HCmqD7n9VDX5/v3vQH/AjA6deDdtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.1.tgz", + "integrity": "sha512-RYONW2MeafgYlkVOKYKkA/Ag7BmXqgIWCa8t1m0JcxrQg9pI9lEqRhAOruOBCbAohOa/gkCF+iPi9hrgvTzu6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/which": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", diff --git a/package.json b/package.json index 93adb9c9..a2e81191 100644 --- a/package.json +++ b/package.json @@ -8,8 +8,10 @@ }, "scripts": { "start": "bundle exec jekyll serve", - "prettier": "npx prettier . --write", + "prettier": "prettier . --write", "optimize-svgs": "node scripts/optimize-svgs.js", + "update-features": "node scripts/update-features.js", + "validate-features": "ajv -c ajv-formats -s features.schema.json -d features.json --strict=true --errors=text", "copy-js": "shx cp ./node_modules/dark-mode-toggle/dist/* ./js/" }, "keywords": [ @@ -19,9 +21,13 @@ "author": "The WebAssembly community", "license": "Apache-2.0", "devDependencies": { + "ajv-cli": "^5.0.0", + "ajv-formats": "^2.1.1", + "cheerio": "^1.2.0", + "markdown-it": "^15.0.1", "prettier": "^3.7.4", - "svgo": "^4.0.1", - "shx": "^0.4.0" + "shx": "^0.4.0", + "svgo": "^4.0.1" }, "type": "module", "dependencies": { diff --git a/scripts/update-features.js b/scripts/update-features.js new file mode 100644 index 00000000..156c4683 --- /dev/null +++ b/scripts/update-features.js @@ -0,0 +1,303 @@ +import fs from 'fs'; +import path from 'path'; +import MarkdownIt from 'markdown-it'; +import * as cheerio from 'cheerio'; +import * as prettier from 'prettier'; + +const PROPOSALS_DIR = process.env.PROPOSALS_DIR ?? '../proposals'; +const FEATURES_JSON = 'features.json'; + +// Matches a repo's bare root URL. +const REPO_ROOT_RE = /^https:\/\/github\.com\/WebAssembly\/([^/?#]+)\/?$/i; + +// Matches a blob/tree link into a proposal's own repo, under proposals/. +const PROPOSAL_LINK_RE = + /^https:\/\/github\.com\/WebAssembly\/([^/?#]+)\/(blob|tree)\/[^/?#]+\/proposals\/([^?#]+?)\/?$/i; + +// Splits a proposal name into word tokens for camelCaseSlug. Proposal names +// mix plain words, standalone acronyms, and PascalCase compounds with no +// delimiter between their words (e.g. "WebAssembly"), so each alternative +// below handles one shape: +// - [A-Z]+(?![a-z]) an acronym not immediately followed by a lowercase +// letter, e.g. "JSPI", or the "GC" in "GCFeature" +// - [A-Z]?[a-z]+ a capitalized or lowercase word, e.g. "Web" and +// "Assembly" out of "WebAssembly" +// - \d+ a run of digits, e.g. "64" out of "Memory64" +const CAMEL_CASE_WORD_RE = /[A-Z]+(?![a-z])|[A-Z]?[a-z]+|\d+/g; + +// nontrapping-float-to-int-conversions' repo name doesn't match its +// features.json key. +const LEGACY_KEY_OVERRIDES = new Map([ + ['nontrapping-float-to-int-conversions', 'saturatedFloatToInt'], +]); + +// MVP is the 1.0 baseline, not a feature engines can lack. +const EXCLUDED_NAMES = new Set(['MVP']); + +const md = new MarkdownIt(); + +const problems = []; + +function reportProblem(message) { + console.error(`::error::${message}`); + problems.push(message); +} + +// Canonicalizes a URL to the repo it identifies, so a repo root and a link +// into that repo's proposals/ doc match. Links into WebAssembly/spec are +// keyed by their proposals/ directory instead of "spec". Anything +// else falls back to the URL's exact text. +function identityKey(url) { + const trimmed = url.trim(); + + const rootMatch = trimmed.match(REPO_ROOT_RE); + if (rootMatch) return rootMatch[1].toLowerCase(); + + const linkMatch = trimmed.match(PROPOSAL_LINK_RE); + if (linkMatch) { + const [, repo, linkType, tail] = linkMatch; + const segments = tail.split('/'); + const isProposalDoc = + segments.length <= 2 && + (linkType === 'tree' || segments.at(-1).endsWith('.md')); + if (isProposalDoc) { + const dir = segments[0]; + return (repo.toLowerCase() === 'spec' ? dir : repo).toLowerCase(); + } + } + + return trimmed.replace(/\/$/, '').toLowerCase(); +} + +function camelCaseSlug(name) { + const words = name.match(CAMEL_CASE_WORD_RE) ?? []; + return words + .map((word, i) => + i === 0 + ? word.toLowerCase() + : word[0].toUpperCase() + word.slice(1).toLowerCase() + ) + .join(''); +} + +function extractRows($, table) { + const rows = []; + for (const tr of $(table).find('tbody tr').toArray()) { + const firstCell = $(tr).find('td').first(); + if (firstCell.length === 0) continue; + + const name = firstCell.text().trim(); + const href = firstCell.find('a').first().attr('href'); + if (!href) { + console.warn(`Skipping row with no link: ${JSON.stringify(name)}`); + continue; + } + + rows.push({ name, href }); + } + return rows; +} + +// Phase 0 has no table (just prose), so it's skipped once currentPhase is null. +function parseReadme(text) { + const $ = cheerio.load(md.render(text)); + const proposals = []; + let currentPhase = null; + + for (const el of $('h3, table').toArray()) { + if (el.tagName === 'h3') { + const match = $(el) + .text() + .trim() + .match(/^Phase ([1-5])\b/); + currentPhase = match ? Number(match[1]) : null; + } else if (currentPhase !== null) { + for (const row of extractRows($, el)) { + proposals.push({ ...row, phase: currentPhase }); + } + } + } + + return proposals; +} + +// Each row in finished-proposals.md / inactive-proposals.md shares one implicit phase. +function parseFixedPhaseFile(text, phase) { + const $ = cheerio.load(md.render(text)); + const proposals = []; + + for (const table of $('table').toArray()) { + for (const row of extractRows($, table)) { + proposals.push({ ...row, phase }); + } + } + + return proposals; +} + +function readProposalsFile(name) { + return fs.readFileSync(path.join(PROPOSALS_DIR, name), 'utf8'); +} + +function loadProposals() { + const readmeProposals = parseReadme(readProposalsFile('README.md')); + if (readmeProposals.length === 0) { + reportProblem( + 'Parsed zero proposals from README.md - the "Phase N" heading format may have changed upstream' + ); + } + + return [ + ...readmeProposals, + ...parseFixedPhaseFile(readProposalsFile('finished-proposals.md'), 5), + ...parseFixedPhaseFile( + readProposalsFile('inactive-proposals.md'), + 'inactive' + ), + ]; +} + +function loadFeaturesData() { + const data = JSON.parse(fs.readFileSync(FEATURES_JSON, 'utf8')); + data.features = Object.assign(Object.create(null), data.features); + const originalKeys = new Set(Object.keys(data.features)); + return { data, originalKeys }; +} + +function buildExistingByKeyMap(features) { + const existingByKey = new Map(); + for (const [key, entry] of Object.entries(features)) { + const k = identityKey(entry.url); + if (existingByKey.has(k)) { + reportProblem( + `features.json entries "${existingByKey.get(k)}" and "${key}" both canonicalize to the same key - one is probably stale` + ); + continue; + } + existingByKey.set(k, key); + } + return existingByKey; +} + +function syncExistingFeature(existingKey, proposal, features) { + const entry = features[existingKey]; + if (entry.phase !== proposal.phase) { + console.log( + `Updating ${existingKey}: phase ${entry.phase} -> ${proposal.phase}` + ); + entry.phase = proposal.phase; + } +} + +// Catches proposals whose identity didn't resolve (e.g. a repo rename not yet +// in LEGACY_KEY_OVERRIDES) but that are already tracked under a different key. +function findByDescription(proposalName, features) { + for (const [key, entry] of Object.entries(features)) { + if (entry.description === proposalName) return key; + } + return null; +} + +function addNewFeature(proposal, features) { + const possibleMatch = findByDescription(proposal.name, features); + if (possibleMatch) { + reportProblem( + `"${proposal.name}" (${proposal.href}) isn't linked to existing entry "${possibleMatch}" - it may have moved repos; add a LEGACY_KEY_OVERRIDES entry if so` + ); + return null; + } + + const newKey = camelCaseSlug(proposal.name); + if (features[newKey]) { + reportProblem( + `Generated key "${newKey}" for "${proposal.name}" (${proposal.href}) already exists for a different repo - pick a different key or add an override` + ); + return null; + } + + console.log(`Adding ${newKey}: ${proposal.name} (phase ${proposal.phase})`); + features[newKey] = { + description: proposal.name, + url: proposal.href, + phase: proposal.phase, + }; + return newKey; +} + +function reconcileProposal(proposal, { features, existingByKey, seen }) { + const key = identityKey(proposal.href); + const existingKey = LEGACY_KEY_OVERRIDES.get(key) ?? existingByKey.get(key); + + if (existingKey) { + if (!features[existingKey]) { + reportProblem( + `LEGACY_KEY_OVERRIDES maps to "${existingKey}", which no longer exists in features.json` + ); + return; + } + syncExistingFeature(existingKey, proposal, features); + seen.add(existingKey); + return; + } + + if (EXCLUDED_NAMES.has(proposal.name)) { + console.warn( + `Skipping "${proposal.name}" (${proposal.href}): excluded from tracking` + ); + return; + } + + const newKey = addNewFeature(proposal, features); + if (newKey) seen.add(newKey); +} + +function reportUnseenFeatures(originalKeys, seen) { + for (const key of originalKeys) { + if (!seen.has(key)) { + console.warn( + `features.json entry "${key}" not found in WebAssembly/proposals; leaving untouched` + ); + } + } +} + +async function writeFeaturesJson(data) { + const config = (await prettier.resolveConfig(FEATURES_JSON)) ?? {}; + const formatted = await prettier.format(JSON.stringify(data), { + ...config, + filepath: FEATURES_JSON, + }); + fs.writeFileSync(FEATURES_JSON, formatted); +} + +async function main() { + const proposals = loadProposals(); + const { data, originalKeys } = loadFeaturesData(); + const existingByKey = buildExistingByKeyMap(data.features); + const seen = new Set(); + + for (const proposal of proposals) { + reconcileProposal(proposal, { + features: data.features, + existingByKey, + seen, + }); + } + + reportUnseenFeatures(originalKeys, seen); + + if (problems.length > 0) { + throw new Error( + `${problems.length} problem(s) found - see ::error:: annotations above` + ); + } + + data.features = Object.fromEntries( + Object.entries(data.features).sort(([a], [b]) => + a < b ? -1 : a > b ? 1 : 0 + ) + ); + await writeFeaturesJson(data); +} + +await main();