diff --git a/apps/chuzz/Cargo.toml b/apps/chuzz/Cargo.toml index b26a7f5..e8720a0 100644 --- a/apps/chuzz/Cargo.toml +++ b/apps/chuzz/Cargo.toml @@ -116,6 +116,10 @@ tauri.workspace = true tauri-runtime-blitz.workspace = true [dev-dependencies] +# `flavor = "multi_thread"` in the script-fetch test: the blocking fetch takes +# the `block_in_place` path, which only the multi-threaded runtime has, and +# that is the path the capture actually uses. +tokio = { workspace = true, features = ["macros", "rt", "rt-multi-thread", "sync", "time"] } # Assembles `fixtures/panel.wat` into a module in-process, so the capture test # owns its guest instead of borrowing blitz-wasm's demo, and so it needs no # wasm32 target and no nested cargo build. See the header of that file. diff --git a/apps/chuzz/frontend/bun.lock b/apps/chuzz/frontend/bun.lock index c72e9bd..3b0b29e 100644 --- a/apps/chuzz/frontend/bun.lock +++ b/apps/chuzz/frontend/bun.lock @@ -7,17 +7,19 @@ "dependencies": { "@chuzz/ui": "workspace:*", "@iconify/tailwind4": "^1.0.6", - "@pathscale/ui": "2.7.4", - "@solidjs/web": "2.0.0-rc.0", + "@pathscale/ui": "^2.11.10", + "@solidjs/web": "2.0.0-rc.4", "@tauri-apps/api": "^2.1.1", "clsx": "^2.1.1", "popmotion": "^11.0.5", - "solid-js": "2.0.0-rc.0", + "solid-js": "2.0.0-rc.4", "solid-layouts": "^0.2.1", "tailwind-merge": "^3.6.0", }, "devDependencies": { "@biomejs/biome": "^2.4.15", + "@iconify-json/lucide": "^1.2.127", + "@iconify-json/mdi": "^1.2.3", "@rsbuild/core": "^1.3.20", "@rsbuild/plugin-babel": "^1.0.5", "@rsbuild/plugin-solid": "^1.0.5", @@ -25,7 +27,7 @@ "@tailwindcss/postcss": "^4.1.7", "@testing-library/jest-dom": "^7.0.0", "@types/node": "^22.15.17", - "babel-preset-solid": "2.0.0-rc.0", + "babel-preset-solid": "^2.0.0-rc.2", "fork-ts-checker-webpack-plugin": "^9.1.0", "jsdom": "^30.0.0", "rsbuild-plugin-solid-layouts": "^0.2.1", @@ -40,14 +42,14 @@ "name": "@chuzz/ui", "version": "0.1.0", "peerDependencies": { - "@pathscale/ui": "^2.5.0", - "solid-js": "^1.9.5", - "solid-layouts": "^0.1.3", + "@pathscale/ui": "^2.11.9", + "solid-js": ">=2.0.0-rc.0", + "solid-layouts": "^0.2.1", }, }, }, "overrides": { - "babel-preset-solid": "2.0.0-rc.0", + "babel-preset-solid": "^2.0.0-rc.2", }, "packages": { "@adobe/css-tools": ["@adobe/css-tools@4.5.0", "", {}, "sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q=="], @@ -164,7 +166,7 @@ "@cyberalien/svg-utils": ["@cyberalien/svg-utils@1.2.19", "", { "dependencies": { "@iconify/types": "^2.0.0" } }, "sha512-paDJoDu+LhuH5Ma1q0BDqBpg6d16Xk22cZZrlg/s/J9oB8KgnnpU/snaEETJu2G1pwYcWQINUz6WUqu0Wm9k6A=="], - "@dom-expressions/babel-plugin-jsx": ["@dom-expressions/babel-plugin-jsx@0.50.0-next.42", "", { "dependencies": { "@babel/helper-module-imports": "7.18.6", "@babel/plugin-syntax-jsx": "^7.18.6", "@babel/types": "^7.20.7", "html-entities": "2.3.3", "parse5": "^7.1.2", "validate-html-nesting": "^1.2.1" }, "peerDependencies": { "@babel/core": "^7.20.12" } }, "sha512-ol24x9RW8loPyOTzC/mQzh/zAsrsPxyTG4WRxxRlwzNK2uBWIBftWN5IwmSV51zDQa7JcT6sE6kkXFCLUvsYIQ=="], + "@dom-expressions/babel-plugin-jsx": ["@dom-expressions/babel-plugin-jsx@0.50.0-next.44", "", { "dependencies": { "@babel/helper-module-imports": "7.18.6", "@babel/plugin-syntax-jsx": "^7.18.6", "@babel/types": "^7.20.7", "html-entities": "2.3.3", "parse5": "^7.1.2", "validate-html-nesting": "^1.2.1" }, "peerDependencies": { "@babel/core": "^7.20.12" } }, "sha512-f1kx6TeMQgaLVkGfuxMn0BkH0+HvsycFf1qPHbYoBqmBQd6Ag/EXOxkSpfcaC+UOBMSaF4BKXegGDchRNTWKrg=="], "@dom-expressions/compiler": ["@dom-expressions/compiler@0.50.0-next.43", "", { "optionalDependencies": { "@dom-expressions/compiler-darwin-arm64": "0.50.0-next.43", "@dom-expressions/compiler-darwin-x64": "0.50.0-next.43", "@dom-expressions/compiler-linux-arm64-gnu": "0.50.0-next.43", "@dom-expressions/compiler-linux-x64-gnu": "0.50.0-next.43", "@dom-expressions/compiler-wasm32-wasi": "0.50.0-next.43", "@dom-expressions/compiler-win32-x64-msvc": "0.50.0-next.43" } }, "sha512-ekEuAFLb868iGu6bw8KA2wgD3gYJpYYTjSbOtuXGlCZnWFA6Wx8Qf720ZYNlb40QjGe3/2kUHTtn47JfaCwPcA=="], @@ -188,6 +190,10 @@ "@exodus/bytes": ["@exodus/bytes@1.15.1", "", { "peerDependencies": { "@noble/hashes": "^1.8.0 || ^2.0.0" }, "optionalPeers": ["@noble/hashes"] }, "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q=="], + "@iconify-json/lucide": ["@iconify-json/lucide@1.2.127", "", { "dependencies": { "@iconify/types": "*" } }, "sha512-7KuCCpkly00kefna4wEnCr8l2HFRkbn4mD4zRib8wCSbrBmnR/l57ExpU6j5jv/Bo+qHpWuHpDtcqceQ0ptArw=="], + + "@iconify-json/mdi": ["@iconify-json/mdi@1.2.3", "", { "dependencies": { "@iconify/types": "*" } }, "sha512-O3cLwbDOK7NNDf2ihaQOH5F9JglnulNDFV7WprU2dSoZu3h3cWH//h74uQAB87brHmvFVxIOkuBX2sZSzYhScg=="], + "@iconify/tailwind4": ["@iconify/tailwind4@1.2.3", "", { "dependencies": { "@iconify/tools": "^5.0.5", "@iconify/types": "^2.0.0", "@iconify/utils": "^3.1.0" }, "peerDependencies": { "tailwindcss": ">= 4.0.0" } }, "sha512-z8SKiMHRASJKF/IY//87MF88lcB7ulxh8vlhQXXLWsBkNtOh6ese9R41MyGpQeqXdRvQVt+/fX2glQtHFjQ+MA=="], "@iconify/tools": ["@iconify/tools@5.0.12", "", { "dependencies": { "@cyberalien/svg-utils": "^1.2.15", "@iconify/types": "^2.0.0", "@iconify/utils": "^3.1.3", "fflate": "^0.8.3", "modern-tar": "^0.7.6", "pathe": "^2.0.3", "svgo": "^4.0.1" } }, "sha512-aFPwSFmFphUPVjNLUkgxgUPSPVgTEEjv0mE7NgvfoQBuE5TtsMrKa4HTY65cM5oXZ2a1xKQcW/RKhiOKEiAarw=="], @@ -224,7 +230,7 @@ "@oxc-project/types": ["@oxc-project/types@0.143.0", "", {}, "sha512-u6JZdLBTLotrNC9Vd6vPssINdzcCzleKAH6EJKImQb7GtYvX5keN2dxkoK44stCc4tffE6QQRtZTXVSzsLUlWA=="], - "@pathscale/ui": ["@pathscale/ui@2.7.4", "", { "dependencies": { "@solidjs/web": "2.0.0-rc.0", "clsx": "^2.1.1", "tailwind-merge": "^3.6.0" }, "peerDependencies": { "@standard-schema/spec": "^1.0.0", "popmotion": "^11.0.5", "solid-js": ">=2.0.0-rc.0", "solid-layouts": "^0.2.0" }, "optionalPeers": ["@standard-schema/spec", "popmotion"] }, "sha512-aPKnbtFEi3tDqYUEv4Vp+nlOPryH/51geJfY8o09ybSrFzC7dqMCl3vR4HaSDCwH+fLjihZZCLQwv8SYpvTkow=="], + "@pathscale/ui": ["@pathscale/ui@2.11.10", "", { "dependencies": { "clsx": "^2.1.1", "tailwind-merge": "^3.6.0" }, "peerDependencies": { "@solidjs/web": ">=2.0.0-rc.0", "@standard-schema/spec": "^1.0.0", "popmotion": "^11.0.5", "solid-js": ">=2.0.0-rc.0", "solid-layouts": "^0.2.0" }, "optionalPeers": ["@standard-schema/spec", "popmotion"] }, "sha512-3h03mALgWQse1Ebr6CusnYOz/CWGRHIxMcMdM3K7DoBdAq5VKtXdkIF7BQoOlHBnLv/l2grNdaQz2M3oLHpG0A=="], "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.2.3", "", { "os": "android", "cpu": "arm64" }, "sha512-zrJtHDcaZJ1Fp7xf4hNl+7seH9Cn/N5TwLYkhgXREtBwAd/jaqW3uqeHxpDugJLVICWg4eW44kOQEGJ1r6jCGw=="], @@ -288,13 +294,13 @@ "@rspack/lite-tapable": ["@rspack/lite-tapable@1.1.5", "", {}, "sha512-uzB782zJbFTM3ta+e2Glikx36dca/6Y+DXyvFN+wb0Tx5ItIW+g03A0t3amP3LGzPHSkb0k81VHCm4jxLQwfag=="], - "@solidjs/signals": ["@solidjs/signals@2.0.0-rc.0", "", {}, "sha512-oKZSfvsCcKw1uJjOGbUkJ+OqlhXLHtZ+rShSyu9KH0lUH7UUwfMfsKeh81JPiQxDDg4YLhEwI38hg0JkwzTdvA=="], + "@solidjs/signals": ["@solidjs/signals@2.0.0-rc.4", "", {}, "sha512-l7P0g8+2pnNscaIPOGDMhv0boaidGAsvR8QN66JySIWNMvVnuq2ayv7ZnvP+IR00DJC+RqCNnnTG/UCdxf+h8g=="], "@solidjs/testing-library": ["@solidjs/testing-library@1.0.0-beta.2", "", { "dependencies": { "@testing-library/dom": "^10.4.1" }, "peerDependencies": { "@solidjs/web": ">=2.0.0", "solid-js": ">=2.0.0" } }, "sha512-TLhQ5IUT/fdDfqa4X2rkQWB28Y+zEwi6mK/TVTeiQlEHG63eK2jfgwNYf2NtQoPh2c3ihLilsCzxABiSTP3JoQ=="], "@solidjs/vite-plugin": ["@solidjs/vite-plugin@3.0.0-next.31", "", { "dependencies": { "@ampproject/remapping": "^2.3.0", "@babel/core": "^7.23.3", "@dom-expressions/compiler": "^0.50.0-next.43", "@types/babel__core": "^7.20.4", "babel-preset-solid": "^2.0.0-rc.0", "merge-anything": "^5.1.7", "vitefu": "^1.0.4" }, "peerDependencies": { "@solidjs/start-devtools": "^1.0.0-next.0", "@solidjs/web": "^2.0.0-rc.0", "@testing-library/jest-dom": "^5.16.6 || ^5.17.0 || ^6.*", "solid-js": "^2.0.0-rc.0", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0" }, "optionalPeers": ["@solidjs/start-devtools", "@testing-library/jest-dom"] }, "sha512-iS9zz4MdzQwZyXGtEYKxFu9fHjX6fs0CWWQTQZ54xaaPKPOcv1sXJtxZ+th7EsmGlmeUNP0jBYoB9qxkd47tiQ=="], - "@solidjs/web": ["@solidjs/web@2.0.0-rc.0", "", { "dependencies": { "seroval": "~1.5.4", "seroval-plugins": "~1.5.4" }, "peerDependencies": { "solid-js": "^2.0.0-rc.0" } }, "sha512-pYSaA9+dH8H1h/d/ZF/P2kR6omfzFGNcdzKhWTcg9fJghXhn8+5UrXUr2iYxDdYNOXZzxxFQhYHSJ7P4HKDqgw=="], + "@solidjs/web": ["@solidjs/web@2.0.0-rc.4", "", { "dependencies": { "seroval": "~1.5.4", "seroval-plugins": "~1.5.4" }, "peerDependencies": { "solid-js": "^2.0.0-rc.4" } }, "sha512-acM9W0FByf0aJDMG5kMbHr0BLTjvFIg8lxwLwHzsu3ybWSmsP1FiOOs2RHNItve/AiX+MGR9CQBCazmnfPykEA=="], "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], @@ -454,7 +460,7 @@ "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], - "babel-preset-solid": ["babel-preset-solid@2.0.0-rc.0", "", { "dependencies": { "@dom-expressions/babel-plugin-jsx": "0.50.0-next.42" }, "peerDependencies": { "@babel/core": "^7.0.0", "solid-js": "^2.0.0-rc.0" }, "optionalPeers": ["solid-js"] }, "sha512-Ap2/QQY3pICj+Q0VM/RnIOpZo7e6icZnUA0oBJuhqzoCrljqMNo3eFb2OeEa4pUQeFREJOlex4Bt1ggwrcgC8w=="], + "babel-preset-solid": ["babel-preset-solid@2.0.0-rc.2", "", { "dependencies": { "@dom-expressions/babel-plugin-jsx": "0.50.0-next.44" }, "peerDependencies": { "@babel/core": "^7.0.0", "solid-js": "^2.0.0-rc.2" }, "optionalPeers": ["solid-js"] }, "sha512-Venq++Aa6+RzGIAyS5vSo3kX1fE8zRreEPCh1eTh5DTjlTft9qZGIG14RSZDv/sjVxT8x5Gs9+21PzxuLe8U+g=="], "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], @@ -814,11 +820,11 @@ "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], - "solid-js": ["solid-js@2.0.0-rc.0", "", { "dependencies": { "@solidjs/signals": "^2.0.0-rc.0", "csstype": "^3.1.0", "seroval": "~1.5.4", "seroval-plugins": "~1.5.4" } }, "sha512-3enTJ71VL69nM5p/it2InVBDBt316Cqfij+F0S7VuHZLITc8YV7Rvavjoy52nAKKxYWXM+BcXh2K7KcLTf1zdQ=="], + "solid-js": ["solid-js@2.0.0-rc.4", "", { "dependencies": { "@solidjs/signals": "^2.0.0-rc.4", "csstype": "^3.1.0", "seroval": "~1.5.4", "seroval-plugins": "~1.5.4" } }, "sha512-hSkDmtduesjFtvzjNyqLphrqiSvhSD5+njkPQUR+9YEoR60MMWtuv36SbLL4OucI0Ronof3Jc+AsUp0oWffwMg=="], "solid-layouts": ["solid-layouts@0.2.1", "", { "peerDependencies": { "@solidjs/web": "^2.0.0-rc.0", "solid-js": "^1.9.14 || ^2.0.0-rc.0" }, "optionalPeers": ["@solidjs/web"] }, "sha512-afgDiwRhBNimxCrEUfh2+6rO18ti6X1gFz5peZ4BPycDYGyeBrxLDy97C4BmmmwgqPkLZrIbbQEUJ4i32W9TPA=="], - "solid-layouts-oxc": ["solid-layouts-oxc@0.2.1", "", { "bin": { "solid-layouts-library": "bin/solid-layouts-library.js", "solid-layouts-application": "bin/solid-layouts-application.js", "solid-layouts-lint": "bin/solid-layouts-lint.js" } }, "sha512-u245kTqsNzYOEwpKxaTQOIubOgkXBQDuwP7xKzQ8MNFMHsNP6FrgtTA0tEf5UcUcE+WEyjl17VRRKnmUChat2w=="], + "solid-layouts-oxc": ["solid-layouts-oxc@0.2.1", "", { "bin": { "solid-layouts-lint": "bin/solid-layouts-lint.js", "solid-layouts-library": "bin/solid-layouts-library.js", "solid-layouts-application": "bin/solid-layouts-application.js" } }, "sha512-u245kTqsNzYOEwpKxaTQOIubOgkXBQDuwP7xKzQ8MNFMHsNP6FrgtTA0tEf5UcUcE+WEyjl17VRRKnmUChat2w=="], "solid-refresh": ["solid-refresh@0.6.3", "", { "dependencies": { "@babel/generator": "^7.23.6", "@babel/helper-module-imports": "^7.22.15", "@babel/types": "^7.23.6" }, "peerDependencies": { "solid-js": "^1.3" } }, "sha512-F3aPsX6hVw9ttm5LYlth8Q15x6MlI/J3Dn+o3EQyRTtTxidepSTwAYdozt01/YA+7ObcciagGEyXIopGZzQtbA=="], @@ -980,6 +986,8 @@ "pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], + "rsbuild-plugin-solid-layouts/solid-layouts-oxc": ["solid-layouts-oxc@0.2.2", "", { "bin": { "solid-layouts-lint": "bin/solid-layouts-lint.js", "solid-layouts-library": "bin/solid-layouts-library.js", "solid-layouts-application": "bin/solid-layouts-application.js" } }, "sha512-45rU4MTsRj9FNzVaXDtttVsSDb+/bojuqUT9IJy875u7bGboz616vkr2S9jg/FKDj3rjFz8pDLKlhcAtHDL6fg=="], + "svgo/commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="], "vite/lightningcss": ["lightningcss@1.33.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.33.0", "lightningcss-darwin-arm64": "1.33.0", "lightningcss-darwin-x64": "1.33.0", "lightningcss-freebsd-x64": "1.33.0", "lightningcss-linux-arm-gnueabihf": "1.33.0", "lightningcss-linux-arm64-gnu": "1.33.0", "lightningcss-linux-arm64-musl": "1.33.0", "lightningcss-linux-x64-gnu": "1.33.0", "lightningcss-linux-x64-musl": "1.33.0", "lightningcss-win32-arm64-msvc": "1.33.0", "lightningcss-win32-x64-msvc": "1.33.0" } }, "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA=="], diff --git a/apps/chuzz/frontend/local-ui/bundle/package.json b/apps/chuzz/frontend/local-ui/bundle/package.json index 8f61f39..d99691f 100644 --- a/apps/chuzz/frontend/local-ui/bundle/package.json +++ b/apps/chuzz/frontend/local-ui/bundle/package.json @@ -21,9 +21,9 @@ "**/*" ], "peerDependencies": { - "@pathscale/ui": "^2.5.0", - "solid-js": "^1.9.5", - "solid-layouts": "^0.1.3" + "@pathscale/ui": "^2.11.9", + "solid-js": ">=2.0.0-rc.0", + "solid-layouts": "^0.2.1" }, "solidLayouts": "./layouts.manifest.json" } diff --git a/apps/chuzz/frontend/local-ui/package.json b/apps/chuzz/frontend/local-ui/package.json index 09203a8..2e390f3 100644 --- a/apps/chuzz/frontend/local-ui/package.json +++ b/apps/chuzz/frontend/local-ui/package.json @@ -9,8 +9,8 @@ "**/*.css" ], "peerDependencies": { - "@pathscale/ui": "^2.5.0", - "solid-js": "^1.9.5", - "solid-layouts": "^0.1.3" + "@pathscale/ui": "^2.11.9", + "solid-js": ">=2.0.0-rc.0", + "solid-layouts": "^0.2.1" } } diff --git a/apps/chuzz/frontend/package.json b/apps/chuzz/frontend/package.json index 6792f87..5e09972 100644 --- a/apps/chuzz/frontend/package.json +++ b/apps/chuzz/frontend/package.json @@ -29,17 +29,19 @@ "dependencies": { "@chuzz/ui": "workspace:*", "@iconify/tailwind4": "^1.0.6", - "@pathscale/ui": "2.7.4", - "@solidjs/web": "2.0.0-rc.0", + "@pathscale/ui": "^2.11.10", + "@solidjs/web": "2.0.0-rc.4", "@tauri-apps/api": "^2.1.1", "clsx": "^2.1.1", "popmotion": "^11.0.5", - "solid-js": "2.0.0-rc.0", + "solid-js": "2.0.0-rc.4", "solid-layouts": "^0.2.1", "tailwind-merge": "^3.6.0" }, "devDependencies": { "@biomejs/biome": "^2.4.15", + "@iconify-json/lucide": "^1.2.127", + "@iconify-json/mdi": "^1.2.3", "@rsbuild/core": "^1.3.20", "@rsbuild/plugin-babel": "^1.0.5", "@rsbuild/plugin-solid": "^1.0.5", @@ -47,7 +49,7 @@ "@tailwindcss/postcss": "^4.1.7", "@testing-library/jest-dom": "^7.0.0", "@types/node": "^22.15.17", - "babel-preset-solid": "2.0.0-rc.0", + "babel-preset-solid": "^2.0.0-rc.2", "fork-ts-checker-webpack-plugin": "^9.1.0", "jsdom": "^30.0.0", "rsbuild-plugin-solid-layouts": "^0.2.1", @@ -58,6 +60,6 @@ "vitest": "^4.1.10" }, "overrides": { - "babel-preset-solid": "2.0.0-rc.0" + "babel-preset-solid": "^2.0.0-rc.2" } } diff --git a/apps/chuzz/frontend/src/styles/theme.css b/apps/chuzz/frontend/src/styles/theme.css index 1e2fee1..455d212 100644 --- a/apps/chuzz/frontend/src/styles/theme.css +++ b/apps/chuzz/frontend/src/styles/theme.css @@ -598,9 +598,9 @@ } /* - * `@theme` emits the app-specific variables outside the DaisyUI theme layer. - * Keep their light counterparts outside that layer too, or the unlayered dark - * values win the cascade even when the root carries `data-color-mode=light`. + * `@theme` emits the app-specific variables outside the theme layer. Keep their + * light counterparts outside that layer too, or the unlayered dark values win + * the cascade even when the root carries `data-color-mode=light`. */ [data-theme="24x-dark"][data-color-mode="light"] { /* Tailwind's `white/N` utilities are translucent interface surfaces in this app. */ diff --git a/apps/chuzz/src/browser.rs b/apps/chuzz/src/browser.rs index 01200a1..504899c 100644 --- a/apps/chuzz/src/browser.rs +++ b/apps/chuzz/src/browser.rs @@ -274,6 +274,20 @@ struct WasmPage { module: std::path::PathBuf, } +/// How long the window waits for a script the page asked for while running. +/// +/// Shorter than the capture's, and the reason is worth stating plainly: +/// `ScriptFetcher::fetch` is synchronous and the page's scripts run on the UI +/// thread, so this blocks the whole window, other tabs included, for as long +/// as it waits. The alternative is what happened before, which was to drop the +/// script and render a page missing whatever it was going to build. A short +/// stall is the better of the two, but only a short one; a page cannot be +/// allowed to freeze the browser because one of its servers went quiet. +/// +/// The real answer is an asynchronous script-loading path in the engine, which +/// would not need to choose. +const WINDOW_SCRIPT_DEADLINE: std::time::Duration = std::time::Duration::from_secs(5); + struct BrowserInner { state: Mutex, log: Mutex, @@ -556,7 +570,11 @@ impl Browser { // certain the document is exactly what the page said. let mut page = blitz_script::ScriptDocument::from_html(&html, make_config()) - .with_fetcher(PrefetchedScripts { scripts }); + .with_fetcher(crate::script_fetch::PageScripts::new( + scripts, + Arc::clone(&self.0.net), + WINDOW_SCRIPT_DEADLINE, + )); page.eval(WEB_API_SHIM); page.execute_scripts(); let title = page @@ -573,7 +591,11 @@ impl Browser { wasm: None, } => { let mut page = blitz_script::ScriptDocument::from_html(&html, make_config()) - .with_fetcher(PrefetchedScripts { scripts }); + .with_fetcher(crate::script_fetch::PageScripts::new( + scripts, + Arc::clone(&self.0.net), + WINDOW_SCRIPT_DEADLINE, + )); page.eval(WEB_API_SHIM); page.execute_scripts(); let title = page @@ -653,20 +675,6 @@ impl NavigationProvider for PageNavigation { } } -struct PrefetchedScripts { - scripts: HashMap, -} - -impl blitz_script::ScriptFetcher for PrefetchedScripts { - fn fetch(&self, url: &Url) -> Result { - self.scripts - .get(url) - .cloned() - .map(Ok) - .unwrap_or_else(|| blitz_script::DefaultScriptFetcher.fetch(url)) - } -} - /// Build a page document by letting a WebAssembly guest construct it. /// /// The config is the page config the HTML path uses, so the guest's document diff --git a/apps/chuzz/src/capture.rs b/apps/chuzz/src/capture.rs index 7fd9ae7..1d8b0c9 100644 --- a/apps/chuzz/src/capture.rs +++ b/apps/chuzz/src/capture.rs @@ -45,6 +45,29 @@ fn capture_scale() -> f32 { .unwrap_or(1.0) } +/// CSS pixel size to lay the page out at. Defaults to 1440 by 960. +/// +/// A capture is only comparable against a reference browser when both laid the +/// page out at the same width: every responsive breakpoint, every percentage +/// width and every centred box moves with it. These were exported by +/// `scripts/render-check.sh` and read by nothing, so a run at another size +/// silently produced the default and the resulting diff was all viewport and +/// no signal. +pub fn capture_viewport() -> (u32, u32) { + fn dimension(name: &str, fallback: u32) -> u32 { + std::env::var(name) + .ok() + .and_then(|value| value.parse().ok()) + .filter(|pixels: &u32| *pixels > 0) + .unwrap_or(fallback) + } + + ( + dimension("CHUZZ_CAPTURE_WIDTH", 1440), + dimension("CHUZZ_CAPTURE_HEIGHT", 960), + ) +} + /// Which colour scheme to render at. Defaults to dark. /// /// A site that respects `prefers-color-scheme` is a different page in each, so diff --git a/apps/chuzz/src/document_loader.rs b/apps/chuzz/src/document_loader.rs index b29b79d..a4a49de 100644 --- a/apps/chuzz/src/document_loader.rs +++ b/apps/chuzz/src/document_loader.rs @@ -29,6 +29,13 @@ use std::collections::HashMap; pub type NetProvider = blitz_net::Provider; +/// How long a capture waits for a script the page asked for while running. +/// +/// Longer than the window's, because nobody is watching a capture and a +/// dropped script costs the very fidelity the capture exists to measure. +#[cfg(all(feature = "capture", feature = "javascript"))] +const CAPTURE_SCRIPT_DEADLINE: std::time::Duration = std::time::Duration::from_secs(10); + /// Web APIs the script engine does not provide. /// /// Boa is a JavaScript engine, not a browser: it supplies the language, and @@ -312,23 +319,6 @@ pub(crate) const WEB_API_SHIM: &str = r#" } })(); "#; -/// Serves the sources prefetched above, falling back to the default fetcher -/// for `file:` and `data:` URLs. -#[cfg(all(feature = "capture", feature = "javascript"))] -struct PrefetchedScripts { - scripts: HashMap, -} - -#[cfg(all(feature = "capture", feature = "javascript"))] -impl blitz_script::ScriptFetcher for PrefetchedScripts { - fn fetch(&self, url: &Url) -> Result { - if let Some(source) = self.scripts.get(url) { - return Ok(source.clone()); - } - blitz_script::DefaultScriptFetcher.fetch(url) - } -} - /// Load a page outside the browser, for headless capture. /// /// Shares the fetch, decompression, script execution and web-API shim with the @@ -403,7 +393,11 @@ pub async fn load_for_capture( scripts.insert(url, decode_body(&bytes)); } } - let mut document = document.with_fetcher(PrefetchedScripts { scripts }); + let mut document = document.with_fetcher(crate::script_fetch::PageScripts::new( + scripts, + Arc::clone(&net_provider), + CAPTURE_SCRIPT_DEADLINE, + )); document.eval(WEB_API_SHIM); document.execute_scripts(); // Pump the script runtime until the page has built its DOM, then keep diff --git a/apps/chuzz/src/script_fetch.rs b/apps/chuzz/src/script_fetch.rs new file mode 100644 index 0000000..fd7708f --- /dev/null +++ b/apps/chuzz/src/script_fetch.rs @@ -0,0 +1,217 @@ +//! Fetching the scripts a page asks for after it has started running. +//! +//! Both loading paths prefetch: they walk the parsed HTML, load every script it +//! names, and serve those from a map. That map cannot contain a script the page +//! builds for itself, because nobody knew the URL until the page was already +//! running. Such a request used to reach `DefaultScriptFetcher`, which serves +//! `file:` and `data:` only, so it was refused with `unsupported URL scheme for +//! script: https` and the script was dropped. +//! +//! Measured over a hundred-site corpus that was the most common single defect, +//! reaching a quarter of the sites. It hides well, because the scripts the +//! parser found load perfectly: a page fails only in the parts it assembles +//! itself. + +use std::collections::HashMap; +use std::sync::Arc; +use std::time::Duration; + +use blitz_traits::net::{Request, Url}; + +use crate::decode::decode_body; +use crate::document_loader::NetProvider; + +/// Serves prefetched sources, and fetches anything else from the network. +pub struct PageScripts { + scripts: HashMap, + net: Arc, + /// Used only when this is called from outside a runtime. Inside one, the + /// current handle is the right one: it is the runtime already driving the + /// page, and a second would run the fetch on a pool with its own shutdown. + runtime: Option, + /// How long a runtime-discovered script may take before the page moves on. + /// + /// Bounded because this blocks script execution. A server that accepts the + /// connection and never answers would otherwise hang the caller, and a + /// missing script is a far better outcome than a page that never finishes. + deadline: Duration, +} + +impl PageScripts { + pub fn new(scripts: HashMap, net: Arc, deadline: Duration) -> Self { + Self { + scripts, + net, + runtime: tokio::runtime::Handle::try_current().ok(), + deadline, + } + } + + fn fetch_over_network(&self, url: &Url) -> Result { + let net = Arc::clone(&self.net); + let target = url.clone(); + let deadline = self.deadline; + let fetch = async move { + tokio::time::timeout(deadline, net.fetch_async(Request::get(target))).await + }; + + // Called from inside the runtime driving the page, so blocking the + // thread outright would stall the executor being waited on. + // `block_in_place` hands the other tasks to a sibling worker first; it + // exists only on the multi-threaded runtime, hence the flavour test + // rather than an unwrap that would be right until someone built a + // current-thread one. + let joined = match tokio::runtime::Handle::try_current() { + Ok(handle) if handle.runtime_flavor() == tokio::runtime::RuntimeFlavor::MultiThread => { + tokio::task::block_in_place(|| handle.block_on(fetch)) + } + // Not on a runtime: nothing here is waiting on this thread. + Err(_) => match &self.runtime { + Some(handle) => handle.block_on(fetch), + None => { + return Err(blitz_script::FetchError::InvalidData( + "no runtime available to fetch a script".to_owned(), + )); + } + }, + // A current-thread runtime has one worker and it is this one, so + // blocking it and re-entering it both deadlock. Refusing the script + // drops it exactly as this path did before, which is bad but + // survivable; a load that never returns is not. + Ok(_) => { + return Err(blitz_script::FetchError::InvalidData( + "cannot fetch a script from a current-thread runtime".to_owned(), + )); + } + }; + + match joined { + Ok(Ok((_, bytes))) => Ok(decode_body(&bytes)), + Ok(Err(error)) => Err(blitz_script::FetchError::InvalidData(format!("{error:?}"))), + Err(_) => Err(blitz_script::FetchError::InvalidData(format!( + "script fetch timed out after {}s", + deadline.as_secs() + ))), + } + } +} + +impl blitz_script::ScriptFetcher for PageScripts { + fn fetch(&self, url: &Url) -> Result { + if let Some(source) = self.scripts.get(url) { + return Ok(source.clone()); + } + match url.scheme() { + "http" | "https" => self.fetch_over_network(url), + // `file:` and `data:` still belong to the default fetcher: it + // decodes data URLs correctly and needs no network. + _ => blitz_script::DefaultScriptFetcher.fetch(url), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// A script the page asks for at runtime is fetched, not refused. + /// + /// The prefetch map is deliberately empty: that is the state a page reaches + /// when its own JavaScript builds a `