diff --git a/package-lock.json b/package-lock.json index 9ce93a64223..433fb065d53 100644 --- a/package-lock.json +++ b/package-lock.json @@ -159,6 +159,7 @@ "vite-plugin-dts": "3.6.3", "vite-tsconfig-paths": "4.2.0", "vitest": "2.1.9", + "wrangler": "4.86.0", "xterm": "5.3.0", "xterm-addon-fit": "0.8.0" }, @@ -2455,6 +2456,32 @@ "dev": true, "license": "MIT" }, + "node_modules/@cloudflare/kv-asset-handler": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.4.2.tgz", + "integrity": "sha512-SIOD2DxrRRwQ+jgzlXCqoEFiKOFqaPjhnNTGKXSRLvp1HiOvapLaFG2kEr9dYQTYe8rKrd9uvDUzmAITeNyaHQ==", + "dev": true, + "license": "MIT OR Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@cloudflare/unenv-preset": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/@cloudflare/unenv-preset/-/unenv-preset-2.16.1.tgz", + "integrity": "sha512-ECxObrMfyTl5bhQf/lZCXwo5G6xX9IAUo+nDMKK4SZ8m4Jvvxp52vilxyySSWh2YTZz8+HQ07qGH/2rEom1vDw==", + "dev": true, + "license": "MIT OR Apache-2.0", + "peerDependencies": { + "unenv": "2.0.0-rc.24", + "workerd": ">1.20260305.0 <2.0.0-0" + }, + "peerDependenciesMeta": { + "workerd": { + "optional": true + } + } + }, "node_modules/@codemirror/autocomplete": { "version": "6.19.0", "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.19.0.tgz", @@ -6024,6 +6051,23 @@ "node": ">=12" } }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/netbsd-x64": { "version": "0.19.9", "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.9.tgz", @@ -6041,6 +6085,23 @@ "node": ">=12" } }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/openbsd-x64": { "version": "0.19.9", "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.9.tgz", @@ -6058,6 +6119,23 @@ "node": ">=12" } }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/sunos-x64": { "version": "0.19.9", "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.9.tgz", @@ -6474,6 +6552,544 @@ "node": ">=6.9.0" } }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, "node_modules/@inquirer/ansi": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.2.tgz", @@ -13394,6 +14010,71 @@ "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", "license": "MIT" }, + "node_modules/@poppinss/colors": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@poppinss/colors/-/colors-4.1.6.tgz", + "integrity": "sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^4.1.5" + } + }, + "node_modules/@poppinss/colors/node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/@poppinss/dumper": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/@poppinss/dumper/-/dumper-0.6.5.tgz", + "integrity": "sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@poppinss/colors": "^4.1.5", + "@sindresorhus/is": "^7.0.2", + "supports-color": "^10.0.0" + } + }, + "node_modules/@poppinss/dumper/node_modules/@sindresorhus/is": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.2.0.tgz", + "integrity": "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@poppinss/dumper/node_modules/supports-color": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz", + "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/@poppinss/exception": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@poppinss/exception/-/exception-1.2.3.tgz", + "integrity": "sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==", + "dev": true, + "license": "MIT" + }, "node_modules/@preact/signals-core": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@preact/signals-core/-/signals-core-1.14.1.tgz", @@ -15153,6 +15834,13 @@ "micromark-util-symbol": "^1.0.1" } }, + "node_modules/@speed-highlight/core": { + "version": "1.2.17", + "resolved": "https://registry.npmjs.org/@speed-highlight/core/-/core-1.2.17.tgz", + "integrity": "sha512-Z92FwKpCtfaW1V0jTU/fh3QzYEZN8wDwrzRIBoADCJfn4mJCNcJN/XegifX7BDrQ8/h9Xh/JnbyMchL0FqXrkg==", + "dev": true, + "license": "CC0-1.0" + }, "node_modules/@svgr/babel-plugin-add-jsx-attribute": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz", @@ -20770,6 +21458,10 @@ "resolved": "packages/playground/client", "link": true }, + "node_modules/@wp-playground/cloudflare-worker-memory-gate": { + "resolved": "packages/playground/cloudflare-worker", + "link": true + }, "node_modules/@wp-playground/common": { "resolved": "packages/playground/common", "link": true @@ -22590,6 +23282,13 @@ "ieee754": "^1.2.1" } }, + "node_modules/blake3-wasm": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/blake3-wasm/-/blake3-wasm-2.1.5.tgz", + "integrity": "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==", + "dev": true, + "license": "MIT" + }, "node_modules/blob-util": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/blob-util/-/blob-util-2.0.2.tgz", @@ -26914,6 +27613,16 @@ "is-arrayish": "^0.2.1" } }, + "node_modules/error-stack-parser-es": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/error-stack-parser-es/-/error-stack-parser-es-1.0.5.tgz", + "integrity": "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/es-abstract": { "version": "1.24.2", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz", @@ -40511,6 +41220,188 @@ "mini-svg-data-uri": "cli.js" } }, + "node_modules/miniflare": { + "version": "4.20260426.0", + "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20260426.0.tgz", + "integrity": "sha512-KM+v76d04qT+NsPfVKVQEgnnuLNE3uzCCl2QKMTJ5OXor5JbBm1vpkQwQ+l7o5ELCrZ74RnyKhJKLiJyUA39Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "0.8.1", + "sharp": "^0.34.5", + "undici": "7.24.8", + "workerd": "1.20260426.1", + "ws": "8.18.0", + "youch": "4.1.0-beta.10" + }, + "bin": { + "miniflare": "bootstrap.js" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/miniflare/node_modules/@cloudflare/workerd-darwin-64": { + "version": "1.20260426.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20260426.1.tgz", + "integrity": "sha512-Ch7DqsmYzSQRTY87pZpsGsFVz9VVBnLPnCBOHxKt1HH25a7oMu1w1PbPWqVmE0VerCLsj/TScX7Ob3v6E14TZw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/miniflare/node_modules/@cloudflare/workerd-darwin-arm64": { + "version": "1.20260426.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20260426.1.tgz", + "integrity": "sha512-0m0U8vaPRH25SpKjbSyRql6gmPe4rCsETRV2WW0qBnuMdKNr5Vh5/Uez80xVrfiCCRMTULGeg63Nqg2vg6CDOA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/miniflare/node_modules/@cloudflare/workerd-linux-64": { + "version": "1.20260426.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20260426.1.tgz", + "integrity": "sha512-C8LlC8uSYzg49y51n++75esxZmMp+Uz1OKHHA/4lkv6rjOTbcHQJuEwSLppjybVIXpv7A8MBhbu9iyCTvyv1mw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/miniflare/node_modules/@cloudflare/workerd-linux-arm64": { + "version": "1.20260426.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20260426.1.tgz", + "integrity": "sha512-ESVp/OIFMAqjQsa8BOP2BQQz5Vpfv6ncN6lNnIuNeOgsISQBdYk+LA60bwQHMud9tvmnSYtONp1zkZ8OQz+x6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/miniflare/node_modules/@cloudflare/workerd-windows-64": { + "version": "1.20260426.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20260426.1.tgz", + "integrity": "sha512-d3Xj/IjINRgNVwH+eKhpUn4xkkcEewbWXbOvBlapiirKWh5zl9m0Epi3qOqmjyRYK6MICqIGXg4qZBEt0lxudw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/miniflare/node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, + "node_modules/miniflare/node_modules/undici": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.24.8.tgz", + "integrity": "sha512-6KQ/+QxK49Z/p3HO6E5ZCZWNnCasyZLa5ExaVYyvPxUwKtbCPMKELJOqh7EqOle0t9cH/7d2TaaTRRa6Nhs4YQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/miniflare/node_modules/workerd": { + "version": "1.20260426.1", + "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20260426.1.tgz", + "integrity": "sha512-ELvGgN8c9oo+E6EPyecxk1TEf6/eAK4TxxQTW5mQ87C7jbjCzhMbg0P2ije49UBHV0dkBYPJcJvcklUltipl2A==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "bin": { + "workerd": "bin/workerd" + }, + "engines": { + "node": ">=16" + }, + "optionalDependencies": { + "@cloudflare/workerd-darwin-64": "1.20260426.1", + "@cloudflare/workerd-darwin-arm64": "1.20260426.1", + "@cloudflare/workerd-linux-64": "1.20260426.1", + "@cloudflare/workerd-linux-arm64": "1.20260426.1", + "@cloudflare/workerd-windows-64": "1.20260426.1" + } + }, "node_modules/minimalistic-assert": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", @@ -51178,6 +52069,23 @@ "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", "license": "MIT" }, + "node_modules/unenv": { + "version": "2.0.0-rc.24", + "resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.24.tgz", + "integrity": "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "pathe": "^2.0.3" + } + }, + "node_modules/unenv/node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, "node_modules/unicode-canonical-property-names-ecmascript": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", @@ -53333,6 +54241,580 @@ "dev": true, "license": "MIT" }, + "node_modules/wrangler": { + "version": "4.86.0", + "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.86.0.tgz", + "integrity": "sha512-9aa/gbF/HiUeeUEwyQpW5LDPBEzyt7iaE6xHwm0vk2Ly8A6J+jh03pzchqVnCCWR832mNyA28MD8oAYt0Kfvlw==", + "dev": true, + "license": "MIT OR Apache-2.0", + "dependencies": { + "@cloudflare/kv-asset-handler": "0.4.2", + "@cloudflare/unenv-preset": "2.16.1", + "blake3-wasm": "2.1.5", + "esbuild": "0.27.3", + "miniflare": "4.20260426.0", + "path-to-regexp": "6.3.0", + "unenv": "2.0.0-rc.24", + "workerd": "1.20260426.1" + }, + "bin": { + "wrangler": "bin/wrangler.js", + "wrangler2": "bin/wrangler.js" + }, + "engines": { + "node": ">=20.3.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + }, + "peerDependencies": { + "@cloudflare/workers-types": "^4.20260426.1" + }, + "peerDependenciesMeta": { + "@cloudflare/workers-types": { + "optional": true + } + } + }, + "node_modules/wrangler/node_modules/@cloudflare/workerd-darwin-64": { + "version": "1.20260426.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20260426.1.tgz", + "integrity": "sha512-Ch7DqsmYzSQRTY87pZpsGsFVz9VVBnLPnCBOHxKt1HH25a7oMu1w1PbPWqVmE0VerCLsj/TScX7Ob3v6E14TZw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/wrangler/node_modules/@cloudflare/workerd-darwin-arm64": { + "version": "1.20260426.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20260426.1.tgz", + "integrity": "sha512-0m0U8vaPRH25SpKjbSyRql6gmPe4rCsETRV2WW0qBnuMdKNr5Vh5/Uez80xVrfiCCRMTULGeg63Nqg2vg6CDOA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/wrangler/node_modules/@cloudflare/workerd-linux-64": { + "version": "1.20260426.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20260426.1.tgz", + "integrity": "sha512-C8LlC8uSYzg49y51n++75esxZmMp+Uz1OKHHA/4lkv6rjOTbcHQJuEwSLppjybVIXpv7A8MBhbu9iyCTvyv1mw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/wrangler/node_modules/@cloudflare/workerd-linux-arm64": { + "version": "1.20260426.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20260426.1.tgz", + "integrity": "sha512-ESVp/OIFMAqjQsa8BOP2BQQz5Vpfv6ncN6lNnIuNeOgsISQBdYk+LA60bwQHMud9tvmnSYtONp1zkZ8OQz+x6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/wrangler/node_modules/@cloudflare/workerd-windows-64": { + "version": "1.20260426.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20260426.1.tgz", + "integrity": "sha512-d3Xj/IjINRgNVwH+eKhpUn4xkkcEewbWXbOvBlapiirKWh5zl9m0Epi3qOqmjyRYK6MICqIGXg4qZBEt0lxudw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/wrangler/node_modules/@esbuild/aix-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/android-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/android-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/android-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/darwin-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/darwin-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/freebsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/linux-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/linux-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/linux-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/linux-loong64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/linux-mips64el": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/linux-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/linux-riscv64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/linux-s390x": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/linux-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/netbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/openbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/sunos-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/win32-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/win32-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/win32-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/esbuild": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" + } + }, + "node_modules/wrangler/node_modules/workerd": { + "version": "1.20260426.1", + "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20260426.1.tgz", + "integrity": "sha512-ELvGgN8c9oo+E6EPyecxk1TEf6/eAK4TxxQTW5mQ87C7jbjCzhMbg0P2ije49UBHV0dkBYPJcJvcklUltipl2A==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "bin": { + "workerd": "bin/workerd" + }, + "engines": { + "node": ">=16" + }, + "optionalDependencies": { + "@cloudflare/workerd-darwin-64": "1.20260426.1", + "@cloudflare/workerd-darwin-arm64": "1.20260426.1", + "@cloudflare/workerd-linux-64": "1.20260426.1", + "@cloudflare/workerd-linux-arm64": "1.20260426.1", + "@cloudflare/workerd-windows-64": "1.20260426.1" + } + }, "node_modules/wrap-ansi": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", @@ -53834,6 +55316,45 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/youch": { + "version": "4.1.0-beta.10", + "resolved": "https://registry.npmjs.org/youch/-/youch-4.1.0-beta.10.tgz", + "integrity": "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@poppinss/colors": "^4.1.5", + "@poppinss/dumper": "^0.6.4", + "@speed-highlight/core": "^1.2.7", + "cookie": "^1.0.2", + "youch-core": "^0.3.3" + } + }, + "node_modules/youch-core": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/youch-core/-/youch-core-0.3.3.tgz", + "integrity": "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@poppinss/exception": "^1.2.2", + "error-stack-parser-es": "^1.0.5" + } + }, + "node_modules/youch/node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/zod": { "version": "4.3.6", "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", @@ -54191,6 +55712,10 @@ "npm": ">=10.2.3" } }, + "packages/playground/cloudflare-worker": { + "name": "@wp-playground/cloudflare-worker-memory-gate", + "version": "0.0.0" + }, "packages/playground/common": { "name": "@wp-playground/common", "version": "3.1.47", diff --git a/package.json b/package.json index db394da6c80..28e0241d20b 100644 --- a/package.json +++ b/package.json @@ -217,6 +217,7 @@ "vite-plugin-dts": "3.6.3", "vite-tsconfig-paths": "4.2.0", "vitest": "2.1.9", + "wrangler": "4.86.0", "xterm": "5.3.0", "xterm-addon-fit": "0.8.0" }, diff --git a/packages/php-wasm/stream-compression/src/test/decode-remote-zip.spec.ts b/packages/php-wasm/stream-compression/src/test/decode-remote-zip.spec.ts new file mode 100644 index 00000000000..ec16cc7f45a --- /dev/null +++ b/packages/php-wasm/stream-compression/src/test/decode-remote-zip.spec.ts @@ -0,0 +1,298 @@ +import { collectBytes } from '../utils/collect-bytes'; +import { decodeRemoteZip } from '../zip/decode-remote-zip'; +import { encodeZip } from '../zip/encode-zip'; + +describe('decodeRemoteZip', () => { + it('emits selected files from range responses', async () => { + const zipBytes = await createLargeZip(); + const originalFetch = globalThis.fetch; + globalThis.fetch = createRangeFetch(zipBytes); + + try { + const decoder = new TextDecoder(); + const stream = await decodeRemoteZip( + 'https://example.com/archive.zip', + (entry) => decoder.decode(entry.path).endsWith('.txt') + ); + const files = []; + for await (const file of stream) { + files.push(file); + } + + expect(files).toHaveLength(2); + expect(files.map((file) => decoder.decode(file.path))).toEqual([ + 'selected-0.txt', + 'selected-1.txt', + ]); + expect(files.map((file) => decoder.decode(file.bytes))).toEqual([ + 'contents 0', + 'contents 1', + ]); + } finally { + globalThis.fetch = originalFetch; + } + }, 30_000); + + it('bounds range requests for widely distributed selected files', async () => { + const zipBytes = await createLargeZip(100); + const originalFetch = globalThis.fetch; + let rangeRequests = 0; + globalThis.fetch = createRangeFetch(zipBytes, () => rangeRequests++); + + try { + const decoder = new TextDecoder(); + const stream = await decodeRemoteZip( + 'https://example.com/archive.zip', + (entry) => decoder.decode(entry.path).endsWith('.txt') + ); + const files = []; + for await (const file of stream) { + files.push(file); + } + + expect(files).toHaveLength(100); + expect(rangeRequests).toBeLessThanOrEqual(49); + } finally { + globalThis.fetch = originalFetch; + } + }, 30_000); + + it('fails clearly without exceeding the request budget for a large central directory', async () => { + const zipBytes = await createLargeCentralDirectoryZip(); + const originalFetch = globalThis.fetch; + let requests = 0; + globalThis.fetch = createRangeFetch(zipBytes, () => requests++); + + try { + const stream = await decodeRemoteZip( + 'https://example.com/archive.zip', + (entry) => + new TextDecoder().decode(entry.path) === 'selected.txt', + { maxRequests: 3 } + ); + await expect(readAll(stream)).rejects.toThrow( + 'Remote ZIP request budget exhausted after 3 requests' + ); + expect(requests).toBe(1); + } finally { + globalThis.fetch = originalFetch; + } + }, 30_000); + + it('bounds concurrent live range response bodies', async () => { + const zipBytes = await createLargeZip(20, 100_000); + const originalFetch = globalThis.fetch; + const delayedFetch = createDelayedRangeFetch(zipBytes); + globalThis.fetch = delayedFetch.fetch; + + try { + const decoder = new TextDecoder(); + const stream = await decodeRemoteZip( + 'https://example.com/archive.zip', + (entry) => decoder.decode(entry.path).endsWith('.txt') + ); + const files = readAll(stream); + + await delayedFetch.waitForLiveResponses(10); + expect(delayedFetch.maximumLiveResponses).toBeLessThanOrEqual(10); + expect(delayedFetch.maximumLiveResponses).toBeGreaterThan(1); + delayedFetch.releaseBodies(); + await files; + } finally { + globalThis.fetch = originalFetch; + } + }, 30_000); + + it('propagates range failures through the returned stream', async () => { + const zipBytes = await createLargeZip(); + const originalFetch = globalThis.fetch; + globalThis.fetch = createRangeFetch(zipBytes, () => { + throw new Error('Range request failed'); + }); + + try { + const stream = await decodeRemoteZip( + 'https://example.com/archive.zip', + () => true + ); + await expect(readAll(stream)).rejects.toThrow( + 'Range request failed' + ); + } finally { + globalThis.fetch = originalFetch; + } + }, 30_000); +}); + +async function createLargeZip( + selectedFiles = 2, + noiseLength = selectedFiles === 2 ? 600_000 : 40_000 +): Promise { + let state = 0x12345678; + const files = []; + for (let index = 0; index < selectedFiles; index++) { + files.push(new File([`contents ${index}`], `selected-${index}.txt`)); + const noise = new Uint8Array(noiseLength); + for (let byteIndex = 0; byteIndex < noise.length; byteIndex++) { + state ^= state << 13; + state ^= state >>> 17; + state ^= state << 5; + noise[byteIndex] = state; + } + files.push(new File([noise], `ignored-${index}.bin`)); + } + + return await collectBytes( + encodeZip([ + ...files, + ...Array.from( + { length: 2_500 }, + (_, index) => new File([], `ignored-${index}.bin`) + ), + ]) + ); +} + +async function createLargeCentralDirectoryZip(): Promise { + let state = 0x12345678; + const noise = new Uint8Array(700_000); + for (let index = 0; index < noise.length; index++) { + state ^= state << 13; + state ^= state >>> 17; + state ^= state << 5; + noise[index] = state; + } + + return await collectBytes( + encodeZip([ + new File(['selected contents'], 'selected.txt'), + new File([noise], 'noise.bin'), + ...Array.from( + { length: 400 }, + (_, index) => + new File([], `ignored-${index}-${'x'.repeat(1_000)}.bin`) + ), + ]) + ); +} + +async function readAll(stream: ReadableStream) { + for await (const file of stream) { + // Consume the stream so asynchronous range errors surface to the reader. + void file; + } +} + +function createRangeFetch( + zipBytes: Uint8Array, + onRangeRequest?: () => void +): typeof fetch { + return async (_input, init) => { + if (init?.method === 'HEAD') { + return new Response(null, { + headers: { 'Content-Length': String(zipBytes.byteLength) }, + }); + } + + const range = new Headers(init?.headers).get('Range'); + if (!range) { + return new Response(zipBytes); + } + if (range !== 'bytes=0-0') { + onRangeRequest?.(); + } + + const match = /^bytes=(\d+)-(\d*)$/.exec(range); + if (!match) { + throw new Error(`Unexpected Range header: ${range}`); + } + const from = Number(match[1]); + const to = match[2] + ? Math.min(Number(match[2]), zipBytes.byteLength - 1) + : zipBytes.byteLength - 1; + const bytes = zipBytes.slice(from, to + 1); + return new Response(bytes, { + status: 206, + headers: { + 'Content-Length': String(bytes.byteLength), + 'Content-Range': `bytes ${from}-${to}/${zipBytes.byteLength}`, + }, + }); + }; +} + +function createDelayedRangeFetch(zipBytes: Uint8Array) { + let liveResponses = 0; + let maximumLiveResponses = 0; + let releaseBodies!: () => void; + const bodiesReleased = new Promise((resolve) => { + releaseBodies = resolve; + }); + let resolveWaitForLiveResponses!: () => void; + const liveResponsesReached = new Promise((resolve) => { + resolveWaitForLiveResponses = resolve; + }); + + return { + fetch: (async (_input, init) => { + if (init?.method === 'HEAD') { + return new Response(null, { + headers: { 'Content-Length': String(zipBytes.byteLength) }, + }); + } + + const range = new Headers(init?.headers).get('Range'); + if (!range) { + return new Response(zipBytes); + } + + const match = /^bytes=(\d+)-(\d*)$/.exec(range); + if (!match) { + throw new Error(`Unexpected Range header: ${range}`); + } + const from = Number(match[1]); + const to = match[2] + ? Math.min(Number(match[2]), zipBytes.byteLength - 1) + : zipBytes.byteLength - 1; + const bytes = zipBytes.slice(from, to + 1); + const delayBody = + range !== 'bytes=0-0' && from < zipBytes.byteLength / 2; + const body = delayBody + ? new ReadableStream({ + async pull(controller) { + liveResponses++; + maximumLiveResponses = Math.max( + maximumLiveResponses, + liveResponses + ); + if (maximumLiveResponses >= 10) { + resolveWaitForLiveResponses(); + } + await bodiesReleased; + controller.enqueue(bytes); + controller.close(); + liveResponses--; + }, + }) + : bytes; + + return new Response(body, { + status: 206, + headers: { + 'Content-Length': String(bytes.byteLength), + 'Content-Range': `bytes ${from}-${to}/${zipBytes.byteLength}`, + }, + }); + }) as typeof fetch, + get maximumLiveResponses() { + return maximumLiveResponses; + }, + releaseBodies, + waitForLiveResponses: async (count: number) => { + if (maximumLiveResponses >= count) { + return; + } + await liveResponsesReached; + }, + }; +} diff --git a/packages/php-wasm/stream-compression/src/zip/decode-remote-zip.ts b/packages/php-wasm/stream-compression/src/zip/decode-remote-zip.ts index c97f9ea5406..2e35f97b7e1 100644 --- a/packages/php-wasm/stream-compression/src/zip/decode-remote-zip.ts +++ b/packages/php-wasm/stream-compression/src/zip/decode-remote-zip.ts @@ -13,11 +13,17 @@ import type { IterableReadableStream } from '../utils/iterable-stream-polyfill'; const CENTRAL_DIRECTORY_END_SCAN_CHUNK_SIZE = 110 * 1024; const BATCH_DOWNLOAD_OF_FILES_IF_CLOSER_THAN = 10 * 1024; -const PREFER_RANGES_IF_FILE_LARGER_THAN = 1024 * 1024 * 1; +const DEFAULT_MAX_REQUESTS = 50; const fetchSemaphore = new Semaphore({ concurrency: 10 }); +const PREFER_RANGES_IF_FILE_LARGER_THAN = 1024 * 1024 * 1; const DEFAULT_PREDICATE = () => true; +export type DecodeRemoteZipOptions = { + /** The maximum number of requests used by the filtered range path. */ + maxRequests?: number; +}; + /** * Streams the contents of a remote zip file. * @@ -33,7 +39,8 @@ export async function decodeRemoteZip( url: string, predicate: ( dirEntry: CentralDirectoryEntry | FileEntry - ) => boolean = DEFAULT_PREDICATE + ) => boolean = DEFAULT_PREDICATE, + { maxRequests = DEFAULT_MAX_REQUESTS }: DecodeRemoteZipOptions = {} ) { if (predicate === DEFAULT_PREDICATE) { // If we're not filtering the zip contents, let's just @@ -42,7 +49,8 @@ export async function decodeRemoteZip( return decodeZip(response.body!); } - const contentLength = await fetchContentLength(url); + const requestBudget = new RequestBudget(maxRequests); + const contentLength = await fetchContentLength(url, requestBudget); if (contentLength <= PREFER_RANGES_IF_FILE_LARGER_THAN) { // If the zip is small enough, let's just grab it. const response = await fetch(url); @@ -51,7 +59,7 @@ export async function decodeRemoteZip( // Ensure ranges query support: // Fetch one byte - const response = await fetch(url, { + const response = await requestBudget.fetch(url, { headers: { // 0-0 looks weird, doesn't it? // The Range header is inclusive so it's actually @@ -83,10 +91,10 @@ export async function decodeRemoteZip( // We're good, let's clean up the other branch of the response stream. responseStream.cancel(); - const source = await createFetchSource(url, contentLength); + const source = await createFetchSource(url, contentLength, requestBudget); return streamCentralDirectoryEntries(source) .pipeThrough(filterStream(predicate)) - .pipeThrough(partitionNearbyEntries()) + .pipeThrough(partitionNearbyEntries(source.length, requestBudget)) .pipeThrough( fetchPartitionedEntries(source) ) as IterableReadableStream; @@ -99,15 +107,12 @@ export async function decodeRemoteZip( * @returns */ function streamCentralDirectoryEntries(source: BytesSource) { - let centralDirectoryStream: ReadableStream; + const centralDirectoryStream = streamCentralDirectoryBytes(source); return new ReadableStream({ - async start() { - centralDirectoryStream = await streamCentralDirectoryBytes(source); - }, async pull(controller) { const entry = await readCentralDirectoryEntry( - centralDirectoryStream + await centralDirectoryStream ); if (!entry) { controller.close(); @@ -131,10 +136,7 @@ async function streamCentralDirectoryBytes(source: BytesSource) { let chunkStart = source.length; do { chunkStart = Math.max(0, chunkStart - chunkSize); - const chunkEnd = Math.min( - chunkStart + chunkSize - 1, - source.length - 1 - ); + const chunkEnd = Math.min(chunkStart + chunkSize, source.length); const bytes = await collectBytes( await source.streamBytes(chunkStart, chunkEnd) ); @@ -160,7 +162,7 @@ async function streamCentralDirectoryBytes(source: BytesSource) { if (dirStart < chunkStart) { // We're missing some bytes, let's grab them const missingBytes = await collectBytes( - await source.streamBytes(dirStart, chunkStart - 1) + await source.streamBytes(dirStart, chunkStart) ); centralDirectory = concatUint8Array( missingBytes!, @@ -185,15 +187,23 @@ async function streamCentralDirectoryBytes(source: BytesSource) { * It may download some extra files living within the gaps * between the partitions. */ -function partitionNearbyEntries() { +function partitionNearbyEntries( + sourceLength: number, + requestBudget: RequestBudget +) { let lastFileEndsAt = 0; let currentChunk: CentralDirectoryEntry[] = []; + let batchDownloadThreshold: number | undefined; return new TransformStream({ transform(zipEntry, controller) { + batchDownloadThreshold ??= Math.max( + BATCH_DOWNLOAD_OF_FILES_IF_CLOSER_THAN, + Math.ceil(sourceLength / Math.max(1, requestBudget.remaining)) + ); // Byte distance too large, flush and start a new chunk if ( zipEntry.firstByteAt > - lastFileEndsAt + BATCH_DOWNLOAD_OF_FILES_IF_CLOSER_THAN + lastFileEndsAt + batchDownloadThreshold ) { controller.enqueue(currentChunk); currentChunk = []; @@ -216,115 +226,89 @@ function partitionNearbyEntries() { function fetchPartitionedEntries( source: BytesSource ): ReadableWritablePair { - /** - * This function implements a ReadableStream and a WritableStream - * instead of a TransformStream. This is intentional. - * - * In TransformStream, the `transform` function may return a - * promise. The next call to `transform` will be delayed until - * the promise resolves. This is a problem for us because we - * want to issue many fetch() requests in parallel. - * - * The only way to do that seems to be creating separate ReadableStream - * and WritableStream implementations. - */ let isWritableClosed = false; let requestsInProgress = 0; let readableController: ReadableStreamDefaultController; const byteStreams: Array< [CentralDirectoryEntry[], ReadableStream] > = []; - /** - * Receives chunks of CentralDirectoryEntries, and fetches - * the corresponding byte ranges from the remote zip file. - */ const writable = new WritableStream({ write(zipEntries, controller) { if (!zipEntries.length) { return; } - ++requestsInProgress; - // If the write() method returns a promise, the next - // call will be delayed until the promise resolves. - // Let's not return the promise, then. - // This will effectively issue many requests in parallel. - requestChunkRange(source, zipEntries) - .then((byteStream) => { - byteStreams.push([zipEntries, byteStream]); - }) - .catch((e) => { - controller.error(e); + + requestsInProgress++; + void fetchSemaphore + .run(async () => { + const bytes = await collectBytes( + await requestChunkRange(source, zipEntries) + ); + byteStreams.push([zipEntries, new Blob([bytes]).stream()]); }) - .finally(() => { - --requestsInProgress; - }); + .catch((error) => controller.error(error)) + .finally(() => requestsInProgress--); }, - abort() { + abort(reason) { isWritableClosed = true; - readableController.close(); + readableController.error(reason); }, - async close() { + close() { isWritableClosed = true; }, }); - /** - * Decodes zipped bytes into FileEntry objects. - */ const readable = new ReadableStream({ start(controller) { readableController = controller; }, async pull(controller) { while (true) { - const allChunksProcessed = + if ( isWritableClosed && !byteStreams.length && - requestsInProgress === 0; - if (allChunksProcessed) { + requestsInProgress === 0 + ) { controller.close(); return; } - // There's no bytes available, but the writable - // stream is still open or there are still requests - // in progress. Let's wait for more bytes. - const waitingForMoreBytes = !byteStreams.length; - if (waitingForMoreBytes) { + if (!byteStreams.length) { await new Promise((resolve) => setTimeout(resolve, 50)); continue; } - const [requestedPaths, stream] = byteStreams[0]; + const [zipEntries, stream] = byteStreams[0]; const file = await readFileEntry(stream); - // The stream is exhausted, let's remove it from the queue - // and try the next one. - const streamExhausted = !file; - if (streamExhausted) { + if (!file) { byteStreams.shift(); continue; } - // There may be some extra files between the ones we're - // interested in. Let's filter out any files that got - // intertwined in the byte stream. - const isOneOfRequestedPaths = requestedPaths.find( - (entry) => entry.path === file.path - ); - if (!isOneOfRequestedPaths) { - continue; + if ( + zipEntries.some((entry) => + uint8ArraysEqual(entry.path, file.path) + ) + ) { + controller.enqueue(file); + return; } - - // Finally! We've got a file we're interested in. - controller.enqueue(file); - break; } }, }); - return { - readable, - writable, - }; + return { readable, writable }; +} + +function uint8ArraysEqual(left: Uint8Array, right: Uint8Array): boolean { + if (left.length !== right.length) { + return false; + } + for (let index = 0; index < left.length; index++) { + if (left[index] !== right[index]) { + return false; + } + } + return true; } /** @@ -337,24 +321,19 @@ async function requestChunkRange( source: BytesSource, zipEntries: CentralDirectoryEntry[] ) { - const release = await fetchSemaphore.acquire(); - try { - const lastZipEntry = zipEntries[zipEntries.length - 1]; - const substream = await source.streamBytes( - zipEntries[0].firstByteAt, - lastZipEntry.lastByteAt - ); - return substream; - } finally { - release(); - } + const lastZipEntry = zipEntries[zipEntries.length - 1]; + return await source.streamBytes( + zipEntries[0].firstByteAt, + lastZipEntry.lastByteAt + ); } /** * Fetches the Content-Length header from a remote URL. */ -async function fetchContentLength(url: string) { - return await fetch(url, { method: 'HEAD' }) +async function fetchContentLength(url: string, requestBudget: RequestBudget) { + return await requestBudget + .fetch(url, { method: 'HEAD' }) .then((response) => response.headers.get('Content-Length')) .then((contentLength) => { if (!contentLength) { @@ -383,27 +362,54 @@ type BytesSource = { ) => Promise>; }; +class RequestBudget { + private requestsMade = 0; + private readonly maxRequests: number; + + constructor(maxRequests: number) { + this.maxRequests = maxRequests; + } + + get remaining() { + return this.maxRequests - this.requestsMade; + } + + async fetch(url: string, init?: RequestInit): Promise { + if (this.remaining <= 0) { + throw new Error( + `Remote ZIP request budget exhausted after ${this.maxRequests} requests` + ); + } + this.requestsMade++; + return await fetch(url, init); + } +} + /** * Creates a BytesSource enabling fetching ranges of bytes * from a remote URL. */ async function createFetchSource( url: string, - contentLength?: number + contentLength: number, + requestBudget: RequestBudget ): Promise { - if (contentLength === undefined) { - contentLength = await fetchContentLength(url); - } - return { length: contentLength, - streamBytes: async (from: number, to: number) => - await fetch(url, { + streamBytes: async (from: number, to: number) => { + const response = await requestBudget.fetch(url, { headers: { // The Range header is inclusive, so we need to subtract 1 Range: `bytes=${from}-${to - 1}`, 'Accept-Encoding': 'none', }, - }).then((response) => response.body!), + }); + if (response.status !== 206 || !response.body) { + throw new Error( + `Expected a partial zip range response, received ${response.status} ${response.statusText}` + ); + } + return response.body; + }, }; } diff --git a/packages/php-wasm/stream-compression/src/zip/decode-zip.ts b/packages/php-wasm/stream-compression/src/zip/decode-zip.ts index 3cd00e4738d..cdfd6ae91f9 100644 --- a/packages/php-wasm/stream-compression/src/zip/decode-zip.ts +++ b/packages/php-wasm/stream-compression/src/zip/decode-zip.ts @@ -281,10 +281,8 @@ export async function readCentralDirectoryEntry( centralDirectory.firstByteAt! + FILE_HEADER_SIZE + pathLength + - fileCommentLength + extraLength! + - centralDirectory.compressedSize! - - 1; + centralDirectory.compressedSize!; centralDirectory['path'] = await collectBytes(stream, pathLength); centralDirectory['isDirectory'] = endsWithSlash(centralDirectory.path!); diff --git a/packages/php-wasm/stream-compression/src/zip/types.ts b/packages/php-wasm/stream-compression/src/zip/types.ts index e34bbe7be62..de2346f17bc 100644 --- a/packages/php-wasm/stream-compression/src/zip/types.ts +++ b/packages/php-wasm/stream-compression/src/zip/types.ts @@ -1,4 +1,4 @@ -export const FILE_HEADER_SIZE = 32; +export const FILE_HEADER_SIZE = 30; export const SIGNATURE_FILE = 67324752 as const; export const SIGNATURE_CENTRAL_DIRECTORY = 33639248 as const; export const SIGNATURE_CENTRAL_DIRECTORY_END = 101010256 as const; diff --git a/packages/playground/cloudflare-worker/.eslintrc.json b/packages/playground/cloudflare-worker/.eslintrc.json new file mode 100644 index 00000000000..1d3309e3c60 --- /dev/null +++ b/packages/playground/cloudflare-worker/.eslintrc.json @@ -0,0 +1,4 @@ +{ + "extends": ["../../../.eslintrc.json"], + "ignorePatterns": ["!**/*"] +} diff --git a/packages/playground/cloudflare-worker/.gitignore b/packages/playground/cloudflare-worker/.gitignore new file mode 100644 index 00000000000..cfd84630a3a --- /dev/null +++ b/packages/playground/cloudflare-worker/.gitignore @@ -0,0 +1,3 @@ +.wrangler/ +dist/ +node_modules/ diff --git a/packages/playground/cloudflare-worker/README.md b/packages/playground/cloudflare-worker/README.md new file mode 100644 index 00000000000..46cdc8c2e96 --- /dev/null +++ b/packages/playground/cloudflare-worker/README.md @@ -0,0 +1,72 @@ +# Cloudflare Workers PHP-WASM memory gate + +This isolated experiment is the first acceptance gate for [issue #69](https://github.com/WordPress/wordpress-playground/issues/69). It loads the current PHP 8.5.8 Asyncify artifact, executes PHP, and returns deterministic JSON. It does not include WordPress, SQLite, Durable Objects, R2, MDI, or persistent storage. + +The Worker imports `packages/php-wasm/web-builds/8-5/asyncify/php_8_5.js` and passes Wrangler's precompiled import of its `8_5_8/php_8_5.wasm` file to the loader's `instantiateWasm` hook. This avoids browser XHR/fetch acquisition and satisfies workerd's precompiled-Wasm contract. The binary remains owned by the PHP-WASM build and is not copied here. + +## Setup and local development + +The root dependency graph is required because the Worker imports source from the monorepo's PHP-WASM packages. Use Node 22 from `.nvmrc`: + +```sh +nvm use +npm ci +npm exec -- wrangler dev --config packages/playground/cloudflare-worker/wrangler.jsonc +``` + +Local workerd is useful for loader and response wiring only. It is not evidence that the remote Cloudflare isolate stays within its memory limit. + +The remote ZIP probe exercises range decoding across many PHP, JSON, HTML, CSS, +JavaScript, font, and image entries from the current WordPress archive. Use an +isolated persistence directory to avoid reusing incompatible Miniflare state: + +```sh +npm exec -- wrangler dev --config packages/playground/cloudflare-worker/wrangler.jsonc --persist-to /tmp/playground-cloudflare-worker +curl --fail --show-error 'http://127.0.0.1:8787/?probe=remote-zip' +``` + +Success returns `includesVersion: true` and at least 100 `decodedEntries`. + +## Remote acceptance gate + +Package without deploying: + +```sh +npm exec -- wrangler deploy --dry-run --config packages/playground/cloudflare-worker/wrangler.jsonc +``` + +Upload an authenticated preview version without routing production traffic: + +```sh +npm exec -- wrangler versions upload --preview-alias memory-gate --config packages/playground/cloudflare-worker/wrangler.jsonc +``` + +Wrangler prints the versioned and aliased preview URLs after the upload. The Worker must already exist, and Cloudflare authentication plus a paid Workers plan are required because this bundle exceeds the free plan's compressed Worker size limit. + +For a normal account deployment, authenticate separately and run: + +```sh +npm exec -- wrangler deploy --config packages/playground/cloudflare-worker/wrangler.jsonc +``` + +Call the URL printed by Wrangler with a unique `run` query parameter. Success is JSON with `marker` equal to `cloudflare-php-wasm-memory-gate`, a PHP 8.5.8 `php_version`, and request/isolate correlation fields. Only that real remote response is a memory acceptance result for issue #69. An isolate-memory deployment or request failure is negative evidence. + +Cloudflare intentionally freezes `performance.now()` and `Date.now()` during CPU and Wasm execution, so in-Worker elapsed timers are not valid performance evidence. Stream preview logs in another terminal and correlate each request's unique `run` query value with Cloudflare's authoritative invocation `CPUTimeMs` and `WallTimeMs`. The response's `isolate_id` and `initialized_for_request` identify cold and warm execution: + +```sh +npm exec -- wrangler tail playground-php-wasm-memory-gate --format json +curl --fail --show-error 'https:///?run=cold-1' +curl --fail --show-error 'https:///?run=warm-1' +``` + +Repeated requests may reach different isolates. A response with `initialized_for_request: false` proves warm reuse of the named `isolate_id`; a later cold isolate does not by itself prove memory eviction. + +Clean up a normal deployment after recording the result: + +```sh +npm exec -- wrangler delete playground-php-wasm-memory-gate +``` + +## Limits under test + +The raw PHP 8.5 Asyncify Wasm artifact is 21,019,221 bytes (about 20.0 MiB), below Cloudflare's 25 MiB individual static asset limit. This Worker intentionally uses a Wasm module import rather than Static Assets because the Emscripten loader needs a synchronous `WebAssembly.Module` in its `instantiateWasm` callback. Cloudflare's compressed Worker bundle limit still applies; record the dry-run compressed size alongside the raw Wasm size. The remote isolate memory limit remains the only acceptance gate. diff --git a/packages/playground/cloudflare-worker/package.json b/packages/playground/cloudflare-worker/package.json new file mode 100644 index 00000000000..6c9bb602e06 --- /dev/null +++ b/packages/playground/cloudflare-worker/package.json @@ -0,0 +1,6 @@ +{ + "name": "@wp-playground/cloudflare-worker-memory-gate", + "version": "0.0.0", + "private": true, + "description": "Cloudflare Workers PHP-WASM memory gate experiment" +} diff --git a/packages/playground/cloudflare-worker/project.json b/packages/playground/cloudflare-worker/project.json new file mode 100644 index 00000000000..fc0e036fa8b --- /dev/null +++ b/packages/playground/cloudflare-worker/project.json @@ -0,0 +1,34 @@ +{ + "name": "playground-cloudflare-worker-memory-gate", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "packages/playground/cloudflare-worker/src", + "projectType": "application", + "targets": { + "typecheck": { + "executor": "nx:run-commands", + "options": { + "command": "tsc -p packages/playground/cloudflare-worker/tsconfig.typecheck.json --noEmit" + } + }, + "test": { + "executor": "@nx/vitest:test", + "outputs": [ + "{workspaceRoot}/coverage/packages/playground/cloudflare-worker" + ], + "options": { + "reportsDirectory": "../../../coverage/packages/playground/cloudflare-worker" + } + }, + "lint": { + "executor": "@nx/eslint:lint", + "outputs": ["{options.outputFile}"], + "options": { + "useFlatConfig": false, + "lintFilePatterns": [ + "packages/playground/cloudflare-worker/**/*.ts" + ], + "maxWarnings": 0 + } + } + } +} diff --git a/packages/playground/cloudflare-worker/src/php-wasm-universal.d.ts b/packages/playground/cloudflare-worker/src/php-wasm-universal.d.ts new file mode 100644 index 00000000000..36435e0ba43 --- /dev/null +++ b/packages/playground/cloudflare-worker/src/php-wasm-universal.d.ts @@ -0,0 +1,18 @@ +declare module '@php-wasm/universal/php' { + export class PHP { + constructor(runtimeId: number); + run(request: { code: string }): Promise<{ text: string }>; + } +} + +declare module '@php-wasm/universal/load-php-runtime' { + export function loadPHPRuntime( + loader: { + dependencyFilename: string; + dependenciesTotalSize: number; + phpWasmAsyncMode: 'asyncify'; + init: unknown; + }, + options: { instantiateWasm: unknown } + ): Promise; +} diff --git a/packages/playground/cloudflare-worker/src/runtime.spec.ts b/packages/playground/cloudflare-worker/src/runtime.spec.ts new file mode 100644 index 00000000000..336a4c3b279 --- /dev/null +++ b/packages/playground/cloudflare-worker/src/runtime.spec.ts @@ -0,0 +1,46 @@ +import { describe, expect, it } from 'vitest'; +import { + healthResponse, + instantiatePrecompiledWasm, + type HealthPayload, +} from './runtime'; + +describe('instantiatePrecompiledWasm', () => { + it("passes the precompiled module to Emscripten's instance callback", () => { + const module = new WebAssembly.Module( + new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0]) + ); + let receivedModule: WebAssembly.Module | undefined; + let receivedInstance: WebAssembly.Instance | undefined; + instantiatePrecompiledWasm(module)({}, (instance, wasmModule) => { + receivedInstance = instance; + receivedModule = wasmModule; + }); + expect(receivedModule).toBe(module); + expect(receivedInstance).toBeInstanceOf(WebAssembly.Instance); + }); +}); + +describe('healthResponse', () => { + it('returns JSON without coupling the response contract to a Worker runtime', async () => { + const payload: HealthPayload = { + php_version: '8.5.8', + marker: 'cloudflare-php-wasm-memory-gate', + initialization_scope: 'isolate', + initialized_for_request: true, + isolate_id: 'isolate-id', + request_id: 'request-id', + artifact: { + php_version: '8.5.8', + async_mode: 'asyncify', + loader: '@php-wasm/web-8-5/asyncify/php_8_5.js', + wasm_bytes: 21019221, + }, + }; + const response = healthResponse(payload); + expect(response.headers.get('content-type')).toBe( + 'application/json; charset=utf-8' + ); + expect(await response.json()).toEqual(payload); + }); +}); diff --git a/packages/playground/cloudflare-worker/src/runtime.ts b/packages/playground/cloudflare-worker/src/runtime.ts new file mode 100644 index 00000000000..2f586d9b1a7 --- /dev/null +++ b/packages/playground/cloudflare-worker/src/runtime.ts @@ -0,0 +1,34 @@ +export const HEALTH_MARKER = 'cloudflare-php-wasm-memory-gate'; + +export type HealthPayload = { + php_version: string; + marker: string; + initialization_scope: 'isolate'; + initialized_for_request: boolean; + isolate_id: string; + request_id: string; + artifact: { + php_version: '8.5.8'; + async_mode: 'asyncify'; + loader: string; + wasm_bytes: number; + }; +}; + +export function instantiatePrecompiledWasm(module: WebAssembly.Module) { + return ( + imports: WebAssembly.Imports, + receiveInstance: ( + instance: WebAssembly.Instance, + module: WebAssembly.Module + ) => void + ) => { + receiveInstance(new WebAssembly.Instance(module, imports), module); + }; +} + +export function healthResponse(payload: HealthPayload): Response { + return new Response(JSON.stringify(payload), { + headers: { 'content-type': 'application/json; charset=utf-8' }, + }); +} diff --git a/packages/playground/cloudflare-worker/src/wasm.d.ts b/packages/playground/cloudflare-worker/src/wasm.d.ts new file mode 100644 index 00000000000..752cae7246b --- /dev/null +++ b/packages/playground/cloudflare-worker/src/wasm.d.ts @@ -0,0 +1,4 @@ +declare module '*.wasm' { + const module: WebAssembly.Module; + export default module; +} diff --git a/packages/playground/cloudflare-worker/src/worker.ts b/packages/playground/cloudflare-worker/src/worker.ts new file mode 100644 index 00000000000..4eb737be8f3 --- /dev/null +++ b/packages/playground/cloudflare-worker/src/worker.ts @@ -0,0 +1,116 @@ +import { loadPHPRuntime } from '@php-wasm/universal/load-php-runtime'; +import { PHP } from '@php-wasm/universal/php'; +// Wrangler bundles the source package because workspace package artifacts are not prebuilt. +// eslint-disable-next-line @nx/enforce-module-boundaries +import { decodeRemoteZip } from '../../../php-wasm/stream-compression/src'; +import { + dependenciesTotalSize, + init, +} from '@php-wasm/web-8-5/asyncify/php_8_5'; +// Wrangler must see this relative Wasm module import to compile it for workerd. +// eslint-disable-next-line @nx/enforce-module-boundaries +import phpWasmModule from '../../../php-wasm/web-builds/8-5/asyncify/8_5_8/php_8_5.wasm'; +import { + HEALTH_MARKER, + healthResponse, + instantiatePrecompiledWasm, + type HealthPayload, +} from '@wp-playground/cloudflare-worker-memory-gate/runtime'; + +const loaderPath = '@php-wasm/web-8-5/asyncify/php_8_5.js'; +const phpVersion = '8.5.8' as const; +const wordpressArchiveUrl = 'https://wordpress.org/latest.zip'; +const remoteZipRequestBudget = 50; +let isolateId: string | undefined; +let runtimePromise: Promise | undefined; + +export default { + async fetch(request: Request): Promise { + const url = new URL(request.url); + if (url.searchParams.get('probe') === 'remote-zip') { + return remoteZipProbe(); + } + + const initializedForRequest = runtimePromise === undefined; + const currentIsolateId = (isolateId ??= crypto.randomUUID()); + const php = await (runtimePromise ??= loadRuntime()); + const output = await php.run({ + code: ` PHP_VERSION, 'marker' => '${HEALTH_MARKER}']);`, + }); + const phpResult = JSON.parse(output.text) as Pick< + HealthPayload, + 'php_version' | 'marker' + >; + const requestId = url.searchParams.get('run') ?? crypto.randomUUID(); + return healthResponse({ + ...phpResult, + initialization_scope: 'isolate', + initialized_for_request: initializedForRequest, + isolate_id: currentIsolateId, + request_id: requestId, + artifact: { + php_version: phpVersion, + async_mode: 'asyncify', + loader: loaderPath, + wasm_bytes: dependenciesTotalSize, + }, + }); + }, +}; + +async function remoteZipProbe(): Promise { + const decoder = new TextDecoder(); + const expectedPath = 'wordpress/wp-includes/version.php'; + let predicateEntries = 0; + let decodedEntries = 0; + let includesVersion = false; + const stream = await decodeRemoteZip( + wordpressArchiveUrl, + (entry) => { + predicateEntries++; + return isWordPressRuntimeAsset(decoder.decode(entry.path)); + }, + { maxRequests: remoteZipRequestBudget } + ); + for await (const entry of stream) { + decodedEntries++; + const path = + entry instanceof File ? entry.name : decoder.decode(entry.path); + includesVersion ||= path === expectedPath; + } + if (!includesVersion || decodedEntries < 100) { + throw new Error( + `Expected ${expectedPath} and at least 100 entries, received ${decodedEntries}.` + ); + } + + return Response.json({ + marker: 'cloudflare-remote-zip-range-gate', + archive: wordpressArchiveUrl, + predicateEntries, + decodedEntries, + includesVersion, + }); +} + +function isWordPressRuntimeAsset(path: string): boolean { + return /\.(?:php|json|html?|css|m?js|svg|png|jpe?g|gif|webp|avif|woff2?|ttf|otf)$/i.test( + path + ); +} + +async function loadRuntime(): Promise { + const runtimeId = await loadPHPRuntime( + { + dependencyFilename: 'php_8_5.wasm', + dependenciesTotalSize, + phpWasmAsyncMode: 'asyncify', + init, + }, + { + // Wrangler supplies this import as a precompiled workerd Wasm module. + instantiateWasm: instantiatePrecompiledWasm(phpWasmModule), + } + ); + return new PHP(runtimeId); +} diff --git a/packages/playground/cloudflare-worker/tsconfig.json b/packages/playground/cloudflare-worker/tsconfig.json new file mode 100644 index 00000000000..b378a867ecc --- /dev/null +++ b/packages/playground/cloudflare-worker/tsconfig.json @@ -0,0 +1,24 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "allowJs": true, + "checkJs": false, + "paths": { + "@php-wasm/web-8-5/asyncify/php_8_5": [ + "packages/php-wasm/web-builds/8-5/asyncify/php_8_5.js" + ], + "@php-wasm/universal/load-php-runtime": [ + "packages/php-wasm/universal/src/lib/load-php-runtime.ts" + ], + "@php-wasm/universal/php": [ + "packages/php-wasm/universal/src/lib/php.ts" + ], + "@php-wasm/util": ["packages/php-wasm/util/src/index.ts"], + "@wp-playground/cloudflare-worker-memory-gate/runtime": [ + "packages/playground/cloudflare-worker/src/runtime.ts" + ] + }, + "types": ["vitest/globals"] + }, + "include": ["src/**/*.ts", "src/**/*.d.ts"] +} diff --git a/packages/playground/cloudflare-worker/tsconfig.typecheck.json b/packages/playground/cloudflare-worker/tsconfig.typecheck.json new file mode 100644 index 00000000000..2983e699f2d --- /dev/null +++ b/packages/playground/cloudflare-worker/tsconfig.typecheck.json @@ -0,0 +1,20 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "paths": { + "@php-wasm/web-8-5/asyncify/php_8_5": [ + "packages/php-wasm/web-builds/8-5/asyncify/php_8_5.js" + ], + "@php-wasm/universal/load-php-runtime": [ + "packages/playground/cloudflare-worker/src/php-wasm-universal.d.ts" + ], + "@php-wasm/universal/php": [ + "packages/playground/cloudflare-worker/src/php-wasm-universal.d.ts" + ], + "@php-wasm/util": ["packages/php-wasm/util/src/index.ts"], + "@wp-playground/cloudflare-worker-memory-gate/runtime": [ + "packages/playground/cloudflare-worker/src/runtime.ts" + ] + } + } +} diff --git a/packages/playground/cloudflare-worker/vitest.config.ts b/packages/playground/cloudflare-worker/vitest.config.ts new file mode 100644 index 00000000000..cb61456dde1 --- /dev/null +++ b/packages/playground/cloudflare-worker/vitest.config.ts @@ -0,0 +1,3 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ test: { environment: 'node' } }); diff --git a/packages/playground/cloudflare-worker/wrangler.jsonc b/packages/playground/cloudflare-worker/wrangler.jsonc new file mode 100644 index 00000000000..29b7903a5cd --- /dev/null +++ b/packages/playground/cloudflare-worker/wrangler.jsonc @@ -0,0 +1,17 @@ +{ + "name": "playground-php-wasm-memory-gate", + "main": "src/worker.ts", + "compatibility_date": "2025-10-01", + "compatibility_flags": ["nodejs_compat"], + "observability": { + "enabled": true, + "head_sampling_rate": 1, + }, + "rules": [ + { + "type": "CompiledWasm", + "globs": ["**/*.wasm"], + "fallthrough": false, + }, + ], +}