From 3efa69db63a6470f726a32c17454d936269a66c3 Mon Sep 17 00:00:00 2001 From: Jonny Burger Date: Tue, 17 Feb 2026 18:06:23 +0100 Subject: [PATCH 01/56] Docs: Upgrade to new Shiki Twoslasher --- bun.lock | 93 +++-- packages/docs/docs/maps.mdx | 3 - .../docs/docs/rive/remotionrivecanvas.mdx | 2 + packages/docs/docs/use-video-texture.mdx | 2 + .../theme/CodeBlock/styles.css | 38 ++- packages/docs/package.json | 3 + packages/docs/prewarm-twoslash.ts | 24 +- packages/docs/twoslash-worker.cjs | 66 ---- packages/docs/twoslash-worker.ts | 90 +++++ packages/docusaurus-plugin/package.json | 6 +- packages/docusaurus-plugin/src/caching.ts | 88 +++-- .../src/exceptionMessageDOM.ts | 21 +- packages/docusaurus-plugin/src/shiki.ts | 319 ++++++++---------- packages/docusaurus-plugin/src/unist-types.ts | 4 +- 14 files changed, 426 insertions(+), 333 deletions(-) delete mode 100644 packages/docs/twoslash-worker.cjs create mode 100644 packages/docs/twoslash-worker.ts diff --git a/bun.lock b/bun.lock index 1ad451bad0c..07d0a840454 100644 --- a/bun.lock +++ b/bun.lock @@ -425,6 +425,7 @@ "@remotion/whisper-web": "workspace:*", "@remotion/zod-types": "workspace:*", "@rive-app/canvas-advanced": "2.31.5", + "@shikijs/twoslash": "3.22.0", "@shopify/react-native-skia": "2.0.0", "@swc/core": "^1.3.80", "@turf/turf": "7.3.2", @@ -452,7 +453,9 @@ "react-dom": "catalog:", "remark-shiki-twoslash": "^3.1.3", "remotion": "workspace:*", + "shiki": "3.22.0", "three": "catalog:", + "twoslash": "0.3.6", "uuid": "^8.3.2", "zod": "catalog:", }, @@ -475,11 +478,11 @@ "version": "4.0.423", "dependencies": { "@docusaurus/types": "3.6.0", + "@shikijs/twoslash": "3.22.0", "@types/dom-webcodecs": "catalog:", - "@typescript/twoslash": "3.2.1", "fenceparser": "^2.2.0", - "shiki": "0.10.1", - "shiki-twoslash": "3.1.2", + "shiki": "3.22.0", + "twoslash": "0.3.6", "unified": "^11.0.5", "unist-util-visit": "^5.0.0", }, @@ -3825,17 +3828,19 @@ "@rushstack/eslint-patch": ["@rushstack/eslint-patch@1.10.4", "", {}, "sha512-WJgX9nzTqknM393q1QJDJmoW28kUfEnybeTfVNcNAPnIx210RXm2DiXiHzfNPJNIUUb1tJnz/l4QGtJ30PgWmA=="], - "@shikijs/core": ["@shikijs/core@3.11.0", "", { "dependencies": { "@shikijs/types": "3.11.0", "@shikijs/vscode-textmate": "10.0.2", "@types/hast": "3.0.4", "hast-util-to-html": "9.0.5" } }, "sha512-oJwU+DxGqp6lUZpvtQgVOXNZcVsirN76tihOLBmwILkKuRuwHteApP8oTXmL4tF5vS5FbOY0+8seXmiCoslk4g=="], + "@shikijs/core": ["@shikijs/core@3.22.0", "", { "dependencies": { "@shikijs/types": "3.22.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-iAlTtSDDbJiRpvgL5ugKEATDtHdUVkqgHDm/gbD2ZS9c88mx7G1zSYjjOxp5Qa0eaW0MAQosFRmJSk354PRoQA=="], - "@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.11.0", "", { "dependencies": { "@shikijs/types": "3.11.0", "@shikijs/vscode-textmate": "10.0.2", "oniguruma-to-es": "4.3.3" } }, "sha512-6/ov6pxrSvew13k9ztIOnSBOytXeKs5kfIR7vbhdtVRg+KPzvp2HctYGeWkqv7V6YIoLicnig/QF3iajqyElZA=="], + "@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.22.0", "", { "dependencies": { "@shikijs/types": "3.22.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.4" } }, "sha512-jdKhfgW9CRtj3Tor0L7+yPwdG3CgP7W+ZEqSsojrMzCjD1e0IxIbwUMDDpYlVBlC08TACg4puwFGkZfLS+56Tw=="], - "@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@3.11.0", "", { "dependencies": { "@shikijs/types": "3.11.0", "@shikijs/vscode-textmate": "10.0.2" } }, "sha512-4DwIjIgETK04VneKbfOE4WNm4Q7WC1wo95wv82PoHKdqX4/9qLRUwrfKlmhf0gAuvT6GHy0uc7t9cailk6Tbhw=="], + "@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@3.22.0", "", { "dependencies": { "@shikijs/types": "3.22.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-DyXsOG0vGtNtl7ygvabHd7Mt5EY8gCNqR9Y7Lpbbd/PbJvgWrqaKzH1JW6H6qFkuUa8aCxoiYVv8/YfFljiQxA=="], - "@shikijs/langs": ["@shikijs/langs@3.11.0", "", { "dependencies": { "@shikijs/types": "3.11.0" } }, "sha512-Njg/nFL4HDcf/ObxcK2VeyidIq61EeLmocrwTHGGpOQx0BzrPWM1j55XtKQ1LvvDWH15cjQy7rg96aJ1/l63uw=="], + "@shikijs/langs": ["@shikijs/langs@3.22.0", "", { "dependencies": { "@shikijs/types": "3.22.0" } }, "sha512-x/42TfhWmp6H00T6uwVrdTJGKgNdFbrEdhaDwSR5fd5zhQ1Q46bHq9EO61SCEWJR0HY7z2HNDMaBZp8JRmKiIA=="], - "@shikijs/themes": ["@shikijs/themes@3.11.0", "", { "dependencies": { "@shikijs/types": "3.11.0" } }, "sha512-BhhWRzCTEk2CtWt4S4bgsOqPJRkapvxdsifAwqP+6mk5uxboAQchc0etiJ0iIasxnMsb764qGD24DK9albcU9Q=="], + "@shikijs/themes": ["@shikijs/themes@3.22.0", "", { "dependencies": { "@shikijs/types": "3.22.0" } }, "sha512-o+tlOKqsr6FE4+mYJG08tfCFDS+3CG20HbldXeVoyP+cYSUxDhrFf3GPjE60U55iOkkjbpY2uC3It/eeja35/g=="], - "@shikijs/types": ["@shikijs/types@3.11.0", "", { "dependencies": { "@shikijs/vscode-textmate": "10.0.2", "@types/hast": "3.0.4" } }, "sha512-RB7IMo2E7NZHyfkqAuaf4CofyY8bPzjWPjJRzn6SEak3b46fIQyG6Vx5fG/obqkfppQ+g8vEsiD7Uc6lqQt32Q=="], + "@shikijs/twoslash": ["@shikijs/twoslash@3.22.0", "", { "dependencies": { "@shikijs/core": "3.22.0", "@shikijs/types": "3.22.0", "twoslash": "^0.3.6" }, "peerDependencies": { "typescript": ">=5.5.0" } }, "sha512-GO27UPN+kegOMQvC+4XcLt0Mttyg+n16XKjmoKjdaNZoW+sOJV7FLdv2QKauqUDws6nE3EQPD+TFHEdyyoUBDw=="], + + "@shikijs/types": ["@shikijs/types@3.22.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-491iAekgKDBFE67z70Ok5a8KBMsQ2IJwOWw3us/7ffQkIBCyOQfm/aNwVMBUriP02QshIfgHCBSIYAl3u2eWjg=="], "@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="], @@ -4607,9 +4612,9 @@ "@typescript/native-preview-win32-x64": ["@typescript/native-preview-win32-x64@7.0.0-dev.20260217.1", "", { "os": "win32", "cpu": "x64" }, "sha512-bje8xm+b0q3OSCY01/MZdDBnVISURunL9rCAob8pAu8qtRMvKYGWUsYwTH+fXGUViPAmdlafFd2E82K9VJYmHA=="], - "@typescript/twoslash": ["@typescript/twoslash@3.2.1", "", { "dependencies": { "@typescript/vfs": "1.4.0", "debug": "4.4.0", "lz-string": "1.5.0" } }, "sha512-tS4gLwOe1WCDspqBXhQCb2ESUqzEd1tOkmKpiZ1O+W1x+9l+9njETuXFkLErtH9is/uD1GSvClDjk/tEOJktjQ=="], + "@typescript/twoslash": ["@typescript/twoslash@3.1.0", "", { "dependencies": { "@typescript/vfs": "1.3.5", "debug": "4.4.0", "lz-string": "1.5.0" } }, "sha512-kTwMUQ8xtAZaC4wb2XuLkPqFVBj2dNBueMQ89NWEuw87k2nLBbuafeG5cob/QEr6YduxIdTVUjix0MtC7mPlmg=="], - "@typescript/vfs": ["@typescript/vfs@1.4.0", "", { "dependencies": { "debug": "4.4.0" } }, "sha512-Pood7yv5YWMIX+yCHo176OnF8WUlKGImFG7XlsuH14Zb1YN5+dYD3uUtS7lqZtsH7tAveNUi2NzdpQCN0yRbaw=="], + "@typescript/vfs": ["@typescript/vfs@1.6.3", "", { "dependencies": { "debug": "^4.1.1" }, "peerDependencies": { "typescript": "*" } }, "sha512-8Qs6/Tj2B8Uyo4lYJkopdCtrsfpF/ZlbTXK13Nq6JKN+Ih8FF9Oxg97gEp+zIS96wmkMdWUIETl35Yt9BITeiw=="], "@ungap/structured-clone": ["@ungap/structured-clone@1.2.0", "", {}, "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ=="], @@ -6473,7 +6478,7 @@ "oniguruma-parser": ["oniguruma-parser@0.12.1", "", {}, "sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w=="], - "oniguruma-to-es": ["oniguruma-to-es@4.3.3", "", { "dependencies": { "oniguruma-parser": "0.12.1", "regex": "6.0.1", "regex-recursion": "6.0.2" } }, "sha512-rPiZhzC3wXwE59YQMRDodUwwT9FZ9nNBwQQfsd1wfdtlKEyCdRV0avrTcSZ5xlIvGRVPd/cx6ZN45ECmS39xvg=="], + "oniguruma-to-es": ["oniguruma-to-es@4.3.4", "", { "dependencies": { "oniguruma-parser": "^0.12.1", "regex": "^6.0.1", "regex-recursion": "^6.0.2" } }, "sha512-3VhUGN3w2eYxnTzHn+ikMI+fp/96KoRSVK9/kMTcFqj1NRDh2IhQCKvYxDnWePKRXY/AqH+Fuiyb7VHSzBjHfA=="], "open": ["open@8.4.2", "", { "dependencies": { "define-lazy-prop": "2.0.0", "is-docker": "2.2.1", "is-wsl": "2.2.0" } }, "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ=="], @@ -7121,7 +7126,7 @@ "shell-quote": ["shell-quote@1.8.1", "", {}, "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA=="], - "shiki": ["shiki@0.10.1", "", { "dependencies": { "jsonc-parser": "3.2.0", "vscode-oniguruma": "1.7.0", "vscode-textmate": "5.2.0" } }, "sha512-VsY7QJVzU51j5o1+DguUd+6vmCmZ5v/6gYu4vyYAhzjuNQU6P/vmSy4uQaOhvje031qQMiW0d2BwgMH52vqMng=="], + "shiki": ["shiki@3.22.0", "", { "dependencies": { "@shikijs/core": "3.22.0", "@shikijs/engine-javascript": "3.22.0", "@shikijs/engine-oniguruma": "3.22.0", "@shikijs/langs": "3.22.0", "@shikijs/themes": "3.22.0", "@shikijs/types": "3.22.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-LBnhsoYEe0Eou4e1VgJACes+O6S6QC0w71fCSp5Oya79inkwkm15gQ1UF6VtQ8j/taMDh79hAB49WUk8ALQW3g=="], "shiki-twoslash": ["shiki-twoslash@3.1.2", "", { "dependencies": { "@typescript/twoslash": "3.1.0", "@typescript/vfs": "1.3.4", "fenceparser": "1.1.1", "shiki": "0.10.1" }, "peerDependencies": { "typescript": "5.8.2" } }, "sha512-JBcRIIizi+exIA/OUhYkV6jtyeZco0ykCkIRd5sgwIt1Pm4pz+maoaRZpm6SkhPwvif4fCA7xOtJOykhpIV64Q=="], @@ -7501,11 +7506,11 @@ "tw-animate-css": ["tw-animate-css@1.4.0", "", {}, "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ=="], - "twoslash": ["twoslash@0.3.1", "", { "dependencies": { "@typescript/vfs": "1.6.1", "twoslash-protocol": "0.3.1" }, "peerDependencies": { "typescript": "5.8.2" } }, "sha512-OGqMTGvqXTcb92YQdwGfEdK0nZJA64Aj/ChLOelbl3TfYch2IoBST0Yx4C0LQ7Lzyqm9RpgcpgDxeXQIz4p2Kg=="], + "twoslash": ["twoslash@0.3.6", "", { "dependencies": { "@typescript/vfs": "^1.6.2", "twoslash-protocol": "0.3.6" }, "peerDependencies": { "typescript": "^5.5.0" } }, "sha512-VuI5OKl+MaUO9UIW3rXKoPgHI3X40ZgB/j12VY6h98Ae1mCBihjPvhOPeJWlxCYcmSbmeZt5ZKkK0dsVtp+6pA=="], "twoslash-cdn": ["twoslash-cdn@0.3.1", "", { "dependencies": { "twoslash": "0.3.1" }, "peerDependencies": { "typescript": "5.8.2" } }, "sha512-JbYbEIG82SlBVD03s7PW+VC5cV9zgWHCtdVkBEc38Kqz8D8ZAgPkthzPAj3isaiJo5xTu/Q/R4NYgihcMVmFXQ=="], - "twoslash-protocol": ["twoslash-protocol@0.3.1", "", {}, "sha512-BMePTL9OkuNISSyyMclBBhV2s9++DiOCyhhCoV5Kaht6eaWLwVjCCUJHY33eZJPsyKeZYS8Wzz0h+XI01VohVw=="], + "twoslash-protocol": ["twoslash-protocol@0.3.6", "", {}, "sha512-FHGsJ9Q+EsNr5bEbgG3hnbkvEBdW5STgPU824AHUjB4kw0Dn4p8tABT7Ncg1Ie6V0+mDg3Qpy41VafZXcQhWMA=="], "type": ["type@1.2.0", "", {}, "sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg=="], @@ -9361,6 +9366,10 @@ "@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], + "@typescript/twoslash/@typescript/vfs": ["@typescript/vfs@1.3.5", "", { "dependencies": { "debug": "4.4.1" } }, "sha512-pI8Saqjupf9MfLw7w2+og+fmb0fZS0J6vsKXXrp4/PDXEFvntgzXmChCXC/KefZZS0YGS6AT8e0hGAJcTsdJlg=="], + + "@typescript/vfs/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + "@use-gesture/react/react": ["react@19.0.0", "", {}, "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ=="], "@vanilla-extract/babel-plugin-debug-ids/@babel/core": ["@babel/core@7.27.4", "", { "dependencies": { "@ampproject/remapping": "2.3.0", "@babel/code-frame": "7.27.1", "@babel/generator": "7.27.5", "@babel/helper-compilation-targets": "7.27.2", "@babel/helper-module-transforms": "7.27.3", "@babel/helpers": "7.27.6", "@babel/parser": "7.27.5", "@babel/template": "7.27.2", "@babel/traverse": "7.27.4", "@babel/types": "7.27.6", "convert-source-map": "2.0.0", "debug": "4.4.1", "gensync": "1.0.0-beta.2", "json5": "2.2.3", "semver": "6.3.1" } }, "sha512-bXYxrXFubeYdvB0NhD/NBB3Qi6aZeV20GOWVI47t2dkecCEoneR4NPVcb7abpXDEvejgrUfFtG6vG/zxAKmg+g=="], @@ -10313,12 +10322,12 @@ "remark-shiki-twoslash/@types/unist": ["@types/unist@2.0.6", "", {}, "sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ=="], - "remark-shiki-twoslash/@typescript/twoslash": ["@typescript/twoslash@3.1.0", "", { "dependencies": { "@typescript/vfs": "1.3.5", "debug": "4.4.0", "lz-string": "1.5.0" } }, "sha512-kTwMUQ8xtAZaC4wb2XuLkPqFVBj2dNBueMQ89NWEuw87k2nLBbuafeG5cob/QEr6YduxIdTVUjix0MtC7mPlmg=="], - "remark-shiki-twoslash/@typescript/vfs": ["@typescript/vfs@1.3.4", "", { "dependencies": { "debug": "4.4.0" } }, "sha512-RbyJiaAGQPIcAGWFa3jAXSuAexU4BFiDRF1g3hy7LmRqfNpYlTQWGXjcrOaVZjJ8YkkpuwG0FcsYvtWQpd9igQ=="], "remark-shiki-twoslash/fenceparser": ["fenceparser@1.1.1", "", {}, "sha512-VdkTsK7GWLT0VWMK5S5WTAPn61wJ98WPFwJiRHumhg4ESNUO/tnkU8bzzzc62o6Uk1SVhuZFLnakmDA4SGV7wA=="], + "remark-shiki-twoslash/shiki": ["shiki@0.10.1", "", { "dependencies": { "jsonc-parser": "3.2.0", "vscode-oniguruma": "1.7.0", "vscode-textmate": "5.2.0" } }, "sha512-VsY7QJVzU51j5o1+DguUd+6vmCmZ5v/6gYu4vyYAhzjuNQU6P/vmSy4uQaOhvje031qQMiW0d2BwgMH52vqMng=="], + "remark-shiki-twoslash/tslib": ["tslib@2.1.0", "", {}, "sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A=="], "remark-shiki-twoslash/unist-util-visit": ["unist-util-visit@2.0.3", "", { "dependencies": { "@types/unist": "2.0.6", "unist-util-is": "4.1.0", "unist-util-visit-parents": "3.1.1" } }, "sha512-iJ4/RczbJMkD0712mGktuGpm/U4By4FfDonL7N/9tATGIF4imikjOuagyMY53tnZq3NP6BcmlrHhEKAfGWjh7Q=="], @@ -10369,12 +10378,12 @@ "sharp/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], - "shiki-twoslash/@typescript/twoslash": ["@typescript/twoslash@3.1.0", "", { "dependencies": { "@typescript/vfs": "1.3.5", "debug": "4.4.0", "lz-string": "1.5.0" } }, "sha512-kTwMUQ8xtAZaC4wb2XuLkPqFVBj2dNBueMQ89NWEuw87k2nLBbuafeG5cob/QEr6YduxIdTVUjix0MtC7mPlmg=="], - "shiki-twoslash/@typescript/vfs": ["@typescript/vfs@1.3.4", "", { "dependencies": { "debug": "4.4.0" } }, "sha512-RbyJiaAGQPIcAGWFa3jAXSuAexU4BFiDRF1g3hy7LmRqfNpYlTQWGXjcrOaVZjJ8YkkpuwG0FcsYvtWQpd9igQ=="], "shiki-twoslash/fenceparser": ["fenceparser@1.1.1", "", {}, "sha512-VdkTsK7GWLT0VWMK5S5WTAPn61wJ98WPFwJiRHumhg4ESNUO/tnkU8bzzzc62o6Uk1SVhuZFLnakmDA4SGV7wA=="], + "shiki-twoslash/shiki": ["shiki@0.10.1", "", { "dependencies": { "jsonc-parser": "3.2.0", "vscode-oniguruma": "1.7.0", "vscode-textmate": "5.2.0" } }, "sha512-VsY7QJVzU51j5o1+DguUd+6vmCmZ5v/6gYu4vyYAhzjuNQU6P/vmSy4uQaOhvje031qQMiW0d2BwgMH52vqMng=="], + "simple-swizzle/is-arrayish": ["is-arrayish@0.3.2", "", {}, "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="], "sitemap/@types/node": ["@types/node@17.0.17", "", {}, "sha512-e8PUNQy1HgJGV3iU/Bp2+D/DXh3PYeyli8LgIwsQcs1Ar1LoaWHSIT6Rw+H2rNJmiq6SNWiDytfx8+gYj7wDHw=="], @@ -10577,7 +10586,7 @@ "tunnel-rat/zustand": ["zustand@4.5.5", "", { "dependencies": { "use-sync-external-store": "1.2.2" }, "peerDependencies": { "@types/react": "19.0.0", "react": "19.0.0" }, "optionalPeers": ["@types/react", "react"] }, "sha512-+0PALYNJNgK6hldkgDq2vLrw5f6g/jCInz52n9RTpropGgeAf/ioFUCdtsjCqu4gNhW9D01rUQBROoRjdzyn2Q=="], - "twoslash/@typescript/vfs": ["@typescript/vfs@1.6.1", "", { "dependencies": { "debug": "4.4.1" }, "peerDependencies": { "typescript": "5.8.2" } }, "sha512-JwoxboBh7Oz1v38tPbkrZ62ZXNHAk9bJ7c9x0eI5zBfBnBYGhURdbnh7Z4smN/MV48Y5OCcZb58n972UtbazsA=="], + "twoslash-cdn/twoslash": ["twoslash@0.3.1", "", { "dependencies": { "@typescript/vfs": "1.6.1", "twoslash-protocol": "0.3.1" }, "peerDependencies": { "typescript": "5.8.2" } }, "sha512-OGqMTGvqXTcb92YQdwGfEdK0nZJA64Aj/ChLOelbl3TfYch2IoBST0Yx4C0LQ7Lzyqm9RpgcpgDxeXQIz4p2Kg=="], "typescript-eslint/@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.21.0", "", { "dependencies": { "@eslint-community/regexpp": "4.12.1", "@typescript-eslint/scope-manager": "8.21.0", "@typescript-eslint/type-utils": "8.21.0", "@typescript-eslint/utils": "8.21.0", "@typescript-eslint/visitor-keys": "8.21.0", "graphemer": "1.4.0", "ignore": "5.3.2", "natural-compare": "1.4.0", "ts-api-utils": "2.0.1" }, "peerDependencies": { "@typescript-eslint/parser": "8.21.0", "eslint": "9.19.0", "typescript": "5.7.3" } }, "sha512-eTH+UOR4I7WbdQnG4Z48ebIA6Bgi7WO8HvFEneeYBxG8qCOYgTOFPSg6ek9ITIDvGjDQzWHcoWHCDO2biByNzA=="], @@ -10813,6 +10822,18 @@ "@algolia/autocomplete-shared/algoliasearch/@algolia/requester-node-http": ["@algolia/requester-node-http@5.18.0", "", { "dependencies": { "@algolia/client-common": "5.18.0" } }, "sha512-tZCqDrqJ2YE2I5ukCQrYN8oiF6u3JIdCxrtKq+eniuLkjkO78TKRnXrVcKZTmfFJyyDK8q47SfDcHzAA3nHi6w=="], + "@astrojs/markdown-remark/shiki/@shikijs/core": ["@shikijs/core@3.11.0", "", { "dependencies": { "@shikijs/types": "3.11.0", "@shikijs/vscode-textmate": "10.0.2", "@types/hast": "3.0.4", "hast-util-to-html": "9.0.5" } }, "sha512-oJwU+DxGqp6lUZpvtQgVOXNZcVsirN76tihOLBmwILkKuRuwHteApP8oTXmL4tF5vS5FbOY0+8seXmiCoslk4g=="], + + "@astrojs/markdown-remark/shiki/@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.11.0", "", { "dependencies": { "@shikijs/types": "3.11.0", "@shikijs/vscode-textmate": "10.0.2", "oniguruma-to-es": "4.3.3" } }, "sha512-6/ov6pxrSvew13k9ztIOnSBOytXeKs5kfIR7vbhdtVRg+KPzvp2HctYGeWkqv7V6YIoLicnig/QF3iajqyElZA=="], + + "@astrojs/markdown-remark/shiki/@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@3.11.0", "", { "dependencies": { "@shikijs/types": "3.11.0", "@shikijs/vscode-textmate": "10.0.2" } }, "sha512-4DwIjIgETK04VneKbfOE4WNm4Q7WC1wo95wv82PoHKdqX4/9qLRUwrfKlmhf0gAuvT6GHy0uc7t9cailk6Tbhw=="], + + "@astrojs/markdown-remark/shiki/@shikijs/langs": ["@shikijs/langs@3.11.0", "", { "dependencies": { "@shikijs/types": "3.11.0" } }, "sha512-Njg/nFL4HDcf/ObxcK2VeyidIq61EeLmocrwTHGGpOQx0BzrPWM1j55XtKQ1LvvDWH15cjQy7rg96aJ1/l63uw=="], + + "@astrojs/markdown-remark/shiki/@shikijs/themes": ["@shikijs/themes@3.11.0", "", { "dependencies": { "@shikijs/types": "3.11.0" } }, "sha512-BhhWRzCTEk2CtWt4S4bgsOqPJRkapvxdsifAwqP+6mk5uxboAQchc0etiJ0iIasxnMsb764qGD24DK9albcU9Q=="], + + "@astrojs/markdown-remark/shiki/@shikijs/types": ["@shikijs/types@3.11.0", "", { "dependencies": { "@shikijs/vscode-textmate": "10.0.2", "@types/hast": "3.0.4" } }, "sha512-RB7IMo2E7NZHyfkqAuaf4CofyY8bPzjWPjJRzn6SEak3b46fIQyG6Vx5fG/obqkfppQ+g8vEsiD7Uc6lqQt32Q=="], + "@astrojs/react/@vitejs/plugin-react/@babel/core": ["@babel/core@7.26.0", "", { "dependencies": { "@ampproject/remapping": "2.3.0", "@babel/code-frame": "7.26.2", "@babel/generator": "7.26.2", "@babel/helper-compilation-targets": "7.25.9", "@babel/helper-module-transforms": "7.26.0", "@babel/helpers": "7.26.0", "@babel/parser": "7.26.2", "@babel/template": "7.25.9", "@babel/traverse": "7.25.9", "@babel/types": "7.26.0", "convert-source-map": "2.0.0", "debug": "4.4.1", "gensync": "1.0.0-beta.2", "json5": "2.2.3", "semver": "6.3.1" } }, "sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg=="], "@astrojs/react/@vitejs/plugin-react/@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "7.25.9" }, "peerDependencies": { "@babel/core": "7.26.0" } }, "sha512-y8quW6p0WHkEhmErnfe58r7x0A70uKphQm8Sp8cV7tjNQwK56sNVK0M73LK3WuYmsuyrftut4xAkjjgU0twaMg=="], @@ -12335,6 +12356,8 @@ "@typescript-eslint/utils/eslint-scope/estraverse": ["estraverse@4.3.0", "", {}, "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw=="], + "@typescript/twoslash/@typescript/vfs/debug": ["debug@4.4.1", "", { "dependencies": { "ms": "2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], + "@vanilla-extract/babel-plugin-debug-ids/@babel/core/@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "7.27.1", "js-tokens": "4.0.0", "picocolors": "1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], "@vanilla-extract/babel-plugin-debug-ids/@babel/core/@babel/generator": ["@babel/generator@7.27.5", "", { "dependencies": { "@babel/parser": "7.27.5", "@babel/types": "7.27.6", "@jridgewell/gen-mapping": "0.3.5", "@jridgewell/trace-mapping": "0.3.25", "jsesc": "3.0.2" } }, "sha512-ZGhA37l0e/g2s1Cnzdix0O3aLYm66eF8aufiVteOgnwxgnRP8GoyMj7VWsgWnQbVKXyge7hqrFh2K2TQM6t1Hw=="], @@ -12495,6 +12518,18 @@ "astro/sharp/detect-libc": ["detect-libc@2.0.4", "", {}, "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA=="], + "astro/shiki/@shikijs/core": ["@shikijs/core@3.11.0", "", { "dependencies": { "@shikijs/types": "3.11.0", "@shikijs/vscode-textmate": "10.0.2", "@types/hast": "3.0.4", "hast-util-to-html": "9.0.5" } }, "sha512-oJwU+DxGqp6lUZpvtQgVOXNZcVsirN76tihOLBmwILkKuRuwHteApP8oTXmL4tF5vS5FbOY0+8seXmiCoslk4g=="], + + "astro/shiki/@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.11.0", "", { "dependencies": { "@shikijs/types": "3.11.0", "@shikijs/vscode-textmate": "10.0.2", "oniguruma-to-es": "4.3.3" } }, "sha512-6/ov6pxrSvew13k9ztIOnSBOytXeKs5kfIR7vbhdtVRg+KPzvp2HctYGeWkqv7V6YIoLicnig/QF3iajqyElZA=="], + + "astro/shiki/@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@3.11.0", "", { "dependencies": { "@shikijs/types": "3.11.0", "@shikijs/vscode-textmate": "10.0.2" } }, "sha512-4DwIjIgETK04VneKbfOE4WNm4Q7WC1wo95wv82PoHKdqX4/9qLRUwrfKlmhf0gAuvT6GHy0uc7t9cailk6Tbhw=="], + + "astro/shiki/@shikijs/langs": ["@shikijs/langs@3.11.0", "", { "dependencies": { "@shikijs/types": "3.11.0" } }, "sha512-Njg/nFL4HDcf/ObxcK2VeyidIq61EeLmocrwTHGGpOQx0BzrPWM1j55XtKQ1LvvDWH15cjQy7rg96aJ1/l63uw=="], + + "astro/shiki/@shikijs/themes": ["@shikijs/themes@3.11.0", "", { "dependencies": { "@shikijs/types": "3.11.0" } }, "sha512-BhhWRzCTEk2CtWt4S4bgsOqPJRkapvxdsifAwqP+6mk5uxboAQchc0etiJ0iIasxnMsb764qGD24DK9albcU9Q=="], + + "astro/shiki/@shikijs/types": ["@shikijs/types@3.11.0", "", { "dependencies": { "@shikijs/vscode-textmate": "10.0.2", "@types/hast": "3.0.4" } }, "sha512-RB7IMo2E7NZHyfkqAuaf4CofyY8bPzjWPjJRzn6SEak3b46fIQyG6Vx5fG/obqkfppQ+g8vEsiD7Uc6lqQt32Q=="], + "astro/vite/postcss": ["postcss@8.5.5", "", { "dependencies": { "nanoid": "3.3.11", "picocolors": "1.1.1", "source-map-js": "1.2.1" } }, "sha512-d/jtm+rdNT8tpXuHY5MMtcbJFBkhXE6593XVR9UoGCH8jSFGci7jGvMGH5RYd5PBJW+00NZQt6gf7CbagJCrhg=="], "autoprefixer/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.167", "", {}, "sha512-LxcRvnYO5ez2bMOFpbuuVuAI5QNeY1ncVytE/KXaL6ZNfzX1yPlAO0nSOyIHx2fVAuUprMqPs/TdVhUFZy7SIQ=="], @@ -13267,8 +13302,6 @@ "remark-mdx/mdast-util-mdx/mdast-util-to-markdown": ["mdast-util-to-markdown@1.5.0", "", { "dependencies": { "@types/mdast": "3.0.10", "@types/unist": "2.0.6", "longest-streak": "3.1.0", "mdast-util-phrasing": "3.0.1", "mdast-util-to-string": "3.1.1", "micromark-util-decode-string": "1.0.2", "unist-util-visit": "4.1.2", "zwitch": "2.0.4" } }, "sha512-bbv7TPv/WC49thZPg3jXuqzuvI45IL2EVAr/KxF0BSdHsU0ceFHOmwQn6evxAh1GaoK/6GQ1wp4R4oW2+LFL/A=="], - "remark-shiki-twoslash/@typescript/twoslash/@typescript/vfs": ["@typescript/vfs@1.3.5", "", { "dependencies": { "debug": "4.4.1" } }, "sha512-pI8Saqjupf9MfLw7w2+og+fmb0fZS0J6vsKXXrp4/PDXEFvntgzXmChCXC/KefZZS0YGS6AT8e0hGAJcTsdJlg=="], - "remark-shiki-twoslash/unist-util-visit/unist-util-is": ["unist-util-is@4.1.0", "", {}, "sha512-ZOQSsnce92GrxSqlnEEseX0gi7GH9zTJZ0p9dtu87WRb/37mMPO2Ilx1s/t9vBHrFhbgweUwb+t7cIn5dxPhZg=="], "remark-shiki-twoslash/unist-util-visit/unist-util-visit-parents": ["unist-util-visit-parents@3.1.1", "", { "dependencies": { "@types/unist": "2.0.6", "unist-util-is": "4.1.0" } }, "sha512-1KROIZWo6bcMrZEwiH2UrXDyalAa0uqzWCxCJj6lPOvTve2WkfgCytoDTPaMnodXh1WrXOq0haVYHj99ynJlsg=="], @@ -13325,8 +13358,6 @@ "serve-index/http-errors/statuses": ["statuses@1.5.0", "", {}, "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA=="], - "shiki-twoslash/@typescript/twoslash/@typescript/vfs": ["@typescript/vfs@1.3.5", "", { "dependencies": { "debug": "4.4.1" } }, "sha512-pI8Saqjupf9MfLw7w2+og+fmb0fZS0J6vsKXXrp4/PDXEFvntgzXmChCXC/KefZZS0YGS6AT8e0hGAJcTsdJlg=="], - "string-width/strip-ansi/ansi-regex": ["ansi-regex@6.0.1", "", {}, "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA=="], "style-loader/webpack/@webassemblyjs/ast": ["@webassemblyjs/ast@1.12.1", "", { "dependencies": { "@webassemblyjs/helper-numbers": "1.11.6", "@webassemblyjs/helper-wasm-bytecode": "1.11.6" } }, "sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg=="], @@ -13719,7 +13750,9 @@ "tunnel-rat/zustand/use-sync-external-store": ["use-sync-external-store@1.2.2", "", { "peerDependencies": { "react": "19.0.0" } }, "sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw=="], - "twoslash/@typescript/vfs/debug": ["debug@4.4.1", "", { "dependencies": { "ms": "2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], + "twoslash-cdn/twoslash/@typescript/vfs": ["@typescript/vfs@1.6.1", "", { "dependencies": { "debug": "4.4.1" }, "peerDependencies": { "typescript": "5.8.2" } }, "sha512-JwoxboBh7Oz1v38tPbkrZ62ZXNHAk9bJ7c9x0eI5zBfBnBYGhURdbnh7Z4smN/MV48Y5OCcZb58n972UtbazsA=="], + + "twoslash-cdn/twoslash/twoslash-protocol": ["twoslash-protocol@0.3.1", "", {}, "sha512-BMePTL9OkuNISSyyMclBBhV2s9++DiOCyhhCoV5Kaht6eaWLwVjCCUJHY33eZJPsyKeZYS8Wzz0h+XI01VohVw=="], "typescript-eslint/@typescript-eslint/eslint-plugin/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.21.0", "", { "dependencies": { "@typescript-eslint/types": "8.21.0", "@typescript-eslint/visitor-keys": "8.21.0" } }, "sha512-G3IBKz0/0IPfdeGRMbp+4rbjfSSdnGkXsM/pFZA8zM9t9klXDnB/YnKOBQ0GoPmoROa4bCq2NeHgJa5ydsQ4mA=="], @@ -13933,6 +13966,8 @@ "yargs/string-width/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], + "@astrojs/markdown-remark/shiki/@shikijs/engine-javascript/oniguruma-to-es": ["oniguruma-to-es@4.3.3", "", { "dependencies": { "oniguruma-parser": "0.12.1", "regex": "6.0.1", "regex-recursion": "6.0.2" } }, "sha512-rPiZhzC3wXwE59YQMRDodUwwT9FZ9nNBwQQfsd1wfdtlKEyCdRV0avrTcSZ5xlIvGRVPd/cx6ZN45ECmS39xvg=="], + "@astrojs/react/@vitejs/plugin-react/@babel/core/@babel/parser": ["@babel/parser@7.26.2", "", { "dependencies": { "@babel/types": "7.26.0" }, "bin": "./bin/babel-parser.js" }, "sha512-DWMCZH9WA4Maitz2q21SRKHo9QXZxkDsbNZoVD62gusNtNBBqDg9i7uOhASfTfIGNzW+O+r7+jAlM8dwphcJKQ=="], "@astrojs/react/@vitejs/plugin-react/@babel/core/@babel/types": ["@babel/types@7.26.0", "", { "dependencies": { "@babel/helper-string-parser": "7.25.9", "@babel/helper-validator-identifier": "7.25.9" } }, "sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA=="], @@ -16187,6 +16222,8 @@ "astro/sharp/@img/sharp-wasm32/@emnapi/runtime": ["@emnapi/runtime@1.5.0", "", { "dependencies": { "tslib": "2.8.1" } }, "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ=="], + "astro/shiki/@shikijs/engine-javascript/oniguruma-to-es": ["oniguruma-to-es@4.3.3", "", { "dependencies": { "oniguruma-parser": "0.12.1", "regex": "6.0.1", "regex-recursion": "6.0.2" } }, "sha512-rPiZhzC3wXwE59YQMRDodUwwT9FZ9nNBwQQfsd1wfdtlKEyCdRV0avrTcSZ5xlIvGRVPd/cx6ZN45ECmS39xvg=="], + "babel-loader/webpack/@webassemblyjs/ast/@webassemblyjs/helper-numbers": ["@webassemblyjs/helper-numbers@1.11.6", "", { "dependencies": { "@webassemblyjs/floating-point-hex-parser": "1.11.6", "@webassemblyjs/helper-api-error": "1.11.6", "@xtuc/long": "4.2.2" } }, "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g=="], "babel-loader/webpack/@webassemblyjs/ast/@webassemblyjs/helper-wasm-bytecode": ["@webassemblyjs/helper-wasm-bytecode@1.11.6", "", {}, "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA=="], @@ -16801,8 +16838,6 @@ "remark-mdx/mdast-util-mdx/mdast-util-to-markdown/unist-util-visit": ["unist-util-visit@4.1.2", "", { "dependencies": { "@types/unist": "2.0.6", "unist-util-is": "5.2.0", "unist-util-visit-parents": "5.1.3" } }, "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg=="], - "remark-shiki-twoslash/@typescript/twoslash/@typescript/vfs/debug": ["debug@4.4.1", "", { "dependencies": { "ms": "2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], - "renderkid/css-select/domutils/dom-serializer": ["dom-serializer@1.3.2", "", { "dependencies": { "domelementtype": "2.3.0", "domhandler": "4.3.1", "entities": "2.2.0" } }, "sha512-5c54Bk5Dw4qAxNOI1pFEizPSjVsx5+bpJKmL2kPn8JhBUq2q09tTCa3mjijun2NfK78NMouDYNMBkOrPZiS+ig=="], "renderkid/htmlparser2/domutils/dom-serializer": ["dom-serializer@1.3.2", "", { "dependencies": { "domelementtype": "2.3.0", "domhandler": "4.3.1", "entities": "2.2.0" } }, "sha512-5c54Bk5Dw4qAxNOI1pFEizPSjVsx5+bpJKmL2kPn8JhBUq2q09tTCa3mjijun2NfK78NMouDYNMBkOrPZiS+ig=="], @@ -16845,8 +16880,6 @@ "sass-loader/webpack/terser-webpack-plugin/serialize-javascript": ["serialize-javascript@6.0.1", "", { "dependencies": { "randombytes": "2.1.0" } }, "sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w=="], - "shiki-twoslash/@typescript/twoslash/@typescript/vfs/debug": ["debug@4.4.1", "", { "dependencies": { "ms": "2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], - "style-loader/webpack/@webassemblyjs/ast/@webassemblyjs/helper-numbers": ["@webassemblyjs/helper-numbers@1.11.6", "", { "dependencies": { "@webassemblyjs/floating-point-hex-parser": "1.11.6", "@webassemblyjs/helper-api-error": "1.11.6", "@xtuc/long": "4.2.2" } }, "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g=="], "style-loader/webpack/@webassemblyjs/ast/@webassemblyjs/helper-wasm-bytecode": ["@webassemblyjs/helper-wasm-bytecode@1.11.6", "", {}, "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA=="], @@ -17195,6 +17228,8 @@ "terser-webpack-plugin/schema-utils/ajv-keywords/ajv": ["ajv@8.11.0", "", { "dependencies": { "fast-deep-equal": "3.1.3", "json-schema-traverse": "1.0.0", "require-from-string": "2.0.2", "uri-js": "4.4.1" } }, "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg=="], + "twoslash-cdn/twoslash/@typescript/vfs/debug": ["debug@4.4.1", "", { "dependencies": { "ms": "2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], + "typescript-eslint/@typescript-eslint/eslint-plugin/@typescript-eslint/scope-manager/@typescript-eslint/types": ["@typescript-eslint/types@8.21.0", "", {}, "sha512-PAL6LUuQwotLW2a8VsySDBwYMm129vFm4tMVlylzdoTybTHaAi0oBp7Ac6LhSrHHOdLM3efH+nAR6hAWoMF89A=="], "typescript-eslint/@typescript-eslint/eslint-plugin/@typescript-eslint/type-utils/@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.21.0", "", { "dependencies": { "@typescript-eslint/types": "8.21.0", "@typescript-eslint/visitor-keys": "8.21.0", "debug": "4.4.1", "fast-glob": "3.3.2", "is-glob": "4.0.3", "minimatch": "9.0.5", "semver": "7.6.3", "ts-api-utils": "2.1.0" }, "peerDependencies": { "typescript": "5.7.3" } }, "sha512-x+aeKh/AjAArSauz0GiQZsjT8ciadNMHdkUSwBB9Z6PrKc/4knM4g3UfHml6oDJmKC88a6//cdxnO/+P2LkMcg=="], diff --git a/packages/docs/docs/maps.mdx b/packages/docs/docs/maps.mdx index 245d98baed9..d8165dde842 100644 --- a/packages/docs/docs/maps.mdx +++ b/packages/docs/docs/maps.mdx @@ -182,7 +182,6 @@ const lineCoordinates = [ [1, 1], ]; const progress = 0.5; -// @ts-expect-error (Only in docs, should work in your project) // ---cut--- const routeLine = turf.lineString(lineCoordinates); const routeDistance = turf.length(routeLine); @@ -217,7 +216,6 @@ useEffect(() => { const handle = delayRender('Moving camera...'); - // @ts-expect-error (Only in docs, should work in your project) const routeDistance = turf.length(turf.lineString(lineCoordinates)); const progress = Math.max( @@ -227,7 +225,6 @@ useEffect(() => { }), ); - // @ts-expect-error (Only in docs, should work in your project) const alongRoute = turf.along(turf.lineString(lineCoordinates), routeDistance * progress).geometry.coordinates; const camera = map.getFreeCameraOptions(); diff --git a/packages/docs/docs/rive/remotionrivecanvas.mdx b/packages/docs/docs/rive/remotionrivecanvas.mdx index 21623ca8a4f..32cb3ab61e0 100644 --- a/packages/docs/docs/rive/remotionrivecanvas.mdx +++ b/packages/docs/docs/rive/remotionrivecanvas.mdx @@ -12,6 +12,7 @@ This component can render a [Rive](https://rive.app/) animation so it synchroniz ## Example ```tsx twoslash +import React from 'react'; import {RemotionRiveCanvas} from '@remotion/rive'; function App() { @@ -117,6 +118,7 @@ export const MyComp: React.FC = () => { You can attach a ref to the component to access the Rive Canvas instance. ```tsx twoslash title="MyComp.tsx" +import React from 'react'; import {RemotionRiveCanvas, RiveCanvasRef} from '@remotion/rive'; import {useEffect} from 'react'; diff --git a/packages/docs/docs/use-video-texture.mdx b/packages/docs/docs/use-video-texture.mdx index ae4752f6647..26c11f6a131 100644 --- a/packages/docs/docs/use-video-texture.mdx +++ b/packages/docs/docs/use-video-texture.mdx @@ -30,6 +30,7 @@ const MyVideo = () => { To convert the video to a video texture, place the `useVideoTexture()` hook in the same component. ```tsx twoslash +import React from 'react'; const videoRef: React.MutableRefObject = React.useRef(null); // ---cut--- import {useVideoTexture} from '@remotion/three'; @@ -42,6 +43,7 @@ const texture = useVideoTexture(videoRef); The return type of it is a `THREE.VideoTexture | null` which you can assign as a `map` to for example `meshBasicMaterial`. We recommend to only render the material when the texture is not `null` to prevent bugs. ```tsx twoslash +import React from 'react'; import {useVideoTexture} from '@remotion/three'; const videoRef: React.MutableRefObject = React.useRef(null); const videoTexture = useVideoTexture(videoRef); diff --git a/packages/docs/docusaurus-theme-shiki-twoslash/theme/CodeBlock/styles.css b/packages/docs/docusaurus-theme-shiki-twoslash/theme/CodeBlock/styles.css index 252717d9fad..a05bd93a495 100644 --- a/packages/docs/docusaurus-theme-shiki-twoslash/theme/CodeBlock/styles.css +++ b/packages/docs/docusaurus-theme-shiki-twoslash/theme/CodeBlock/styles.css @@ -28,7 +28,7 @@ pre.shiki.with-title { padding-top: 2.5rem; } -/** +/** * Copy-to-Clipboard Button * Taken from https://github.com/facebook/docusaurus/blob/ed9d2a26f5a7b8096804ae1b3a4fffc504f8f90d/packages/docusaurus-theme-classic/src/theme/CodeBlock/styles.module.css * which is under MIT License as per the banner @@ -67,15 +67,21 @@ pre.shiki { padding: 0; } -/* Sets a horizontal padding for all the lines */ -pre.shiki div.line, -pre.shiki div.meta-line { +/* Sets a horizontal padding for all the lines (shiki v3 uses span.line) */ +pre.shiki .line, +pre.shiki .meta-line { padding-left: var(--ifm-pre-padding); padding-right: var(--ifm-pre-padding); font-family: monospace; } -/* Sets vertical padding for the container */ +/* Sets vertical padding for the container — shiki v3 wraps in directly */ +pre.shiki > code { + padding: var(--ifm-pre-padding) 0; + display: block; +} + +/* Also support old .code-container wrapper if rendererClassic produces it */ pre.shiki > .code-container { padding: var(--ifm-pre-padding) 0; } @@ -89,7 +95,7 @@ pre.shiki { border-color: var(--ifm-color-emphasis-300); } -/* Hide language identifiers */ +/* Hide language identifiers (kept for backwards compat, no longer generated in v3) */ pre.shiki .language-id { display: none; } @@ -100,31 +106,31 @@ pre.shiki:hover .dim { filter: none; } -pre.shiki div.dim { +pre.shiki .dim { opacity: 0.5; filter: grayscale(1); transition: opacity 200ms ease-in-out; } -pre.shiki div.dim, -pre.shiki div.highlight { +pre.shiki .dim, +pre.shiki .highlight { margin: 0; /* To avoid flickering on the highlighted lines focus */ border-left: 2px solid transparent; } -pre.shiki div.highlight { +pre.shiki .highlight { opacity: 1; transition: background-color 200ms ease-in-out; } -pre.shiki:hover div.highlight { +pre.shiki:hover .highlight { background-color: var(--ifm-hover-overlay); border-left: 2px solid var(--ifm-color-primary); width: 100%; } -pre.shiki div.line { +pre.shiki .line { min-height: 1rem; } @@ -176,11 +182,11 @@ pre .query { display: inline-block; } -/** +/** * In order to have the 'popped out' style design and to not break the layout * we need to place a fake and un-selectable copy of the error which _isn't_ broken out * behind the actual error message. - * This sections keeps both of those two in in sync + * This sections keeps both of those two in in sync */ pre .error, @@ -292,8 +298,8 @@ data-lsp { @media (prefers-reduced-motion: reduce) { data-lsp, pre .code-container > a, - pre.shiki div.dim, - pre.shiki div.highlight { + pre.shiki .dim, + pre.shiki .highlight { transition: none; } } diff --git a/packages/docs/package.json b/packages/docs/package.json index 33789cbbb8e..65a7a3e9630 100644 --- a/packages/docs/package.json +++ b/packages/docs/package.json @@ -43,6 +43,9 @@ "@remotion/cloudrun": "workspace:*", "@remotion/design": "workspace:*", "@remotion/docusaurus-plugin": "workspace:*", + "shiki": "3.22.0", + "@shikijs/twoslash": "3.22.0", + "twoslash": "0.3.6", "@remotion/enable-scss": "workspace:*", "@remotion/eslint-config-internal": "workspace:*", "@remotion/fonts": "workspace:*", diff --git a/packages/docs/prewarm-twoslash.ts b/packages/docs/prewarm-twoslash.ts index f96ea9b29ad..6925cfa8c83 100644 --- a/packages/docs/prewarm-twoslash.ts +++ b/packages/docs/prewarm-twoslash.ts @@ -15,13 +15,16 @@ import {join, resolve} from 'path'; const DOCS_ROOT = resolve(import.meta.dirname); const CACHE_ROOT = join(DOCS_ROOT, 'node_modules', '.cache', 'twoslash'); -const WORKER_PATH = join(DOCS_ROOT, 'twoslash-worker.cjs'); -const NUM_WORKERS = cpus().length; +const WORKER_PATH = join(DOCS_ROOT, 'twoslash-worker.ts'); +const NUM_WORKERS = process.env.VERCEL + ? cpus().length + : Math.max(1, cpus().length - 2); const pluginDir = join(DOCS_ROOT, '..', 'docusaurus-plugin'); const pluginRequire = createRequire(join(pluginDir, 'package.json')); -const shikiVersion = pluginRequire('@typescript/twoslash/package.json') +const twoslashVersion = pluginRequire('twoslash/package.json') .version as string; +const shikiVersion = pluginRequire('shiki/package.json').version as string; const tsVersion = pluginRequire('typescript/package.json').version as string; interface TwoslashBlock { @@ -34,7 +37,9 @@ interface TwoslashBlock { function computeCachePath(code: string): string { const shasum = createHash('sha1'); const codeSha = shasum - .update(`${code}-${shikiVersion}-${tsVersion}`) + .update( + `${code}-${twoslashVersion}-${shikiVersion}-${tsVersion}-github-dark`, + ) .digest('hex'); return join(CACHE_ROOT, `${codeSha}.json`); } @@ -155,7 +160,7 @@ function runWorker( const tmpFile = join(DOCS_ROOT, `.twoslash-work-${workerId}.json`); writeFileSync(tmpFile, JSON.stringify(workItems)); - const child = spawn('node', [WORKER_PATH, tmpFile], { + const child = spawn('bun', ['run', WORKER_PATH, tmpFile], { cwd: DOCS_ROOT, stdio: ['ignore', 'pipe', 'pipe'], }); @@ -294,6 +299,15 @@ async function main() { allTimings.sort((a, b) => b.ms - a.ms); console.log(`\nTwoslash pre-warm: ${totalCompleted} blocks in ${totalTime}s using ${numWorkers} workers (${totalErrors} errors)`); + const errorTimings = allTimings.filter((t) => t.error); + if (errorTimings.length > 0) { + console.log(`\nErrors:`); + for (const t of errorTimings) { + const files = t.sourceFiles.join(', '); + console.log(` ${t.ms}ms - ${files} ERROR: ${t.error}`); + } + } + console.log(`\nSlowest snippets:`); for (const t of allTimings.slice(0, 30)) { const files = t.sourceFiles.join(', '); diff --git a/packages/docs/twoslash-worker.cjs b/packages/docs/twoslash-worker.cjs deleted file mode 100644 index e015ff252c5..00000000000 --- a/packages/docs/twoslash-worker.cjs +++ /dev/null @@ -1,66 +0,0 @@ -const {createRequire} = require('module'); -const {writeFileSync, existsSync, mkdirSync, readFileSync} = require('fs'); -const {dirname, join} = require('path'); - -// Resolve from the docusaurus-plugin which has these as dependencies -const pluginDir = join(__dirname, '..', 'docusaurus-plugin'); -const pluginRequire = createRequire(join(pluginDir, 'package.json')); -const {runTwoSlash} = pluginRequire('shiki-twoslash'); -const {ScriptTarget, ModuleKind} = pluginRequire('typescript'); - -const settings = { - defaultCompilerOptions: { - types: ['node'], - target: ScriptTarget.ESNext, - module: ModuleKind.ESNext, - }, -}; - -// Read work items from the file passed as argument -const workFile = process.argv[2]; -const items = JSON.parse(readFileSync(workFile, 'utf8')); - -let completed = 0; -let errors = 0; -const timings = []; - -for (const item of items) { - const start = performance.now(); - try { - const results = runTwoSlash(item.code, item.lang, settings); - const dir = dirname(item.cachePath); - if (!existsSync(dir)) mkdirSync(dir, {recursive: true}); - writeFileSync(item.cachePath, JSON.stringify(results), 'utf8'); - completed++; - timings.push({ - cachePath: item.cachePath, - ms: Math.round(performance.now() - start), - }); - } catch (error) { - errors++; - timings.push({ - cachePath: item.cachePath, - ms: Math.round(performance.now() - start), - error: error.message.slice(0, 200), - }); - } - - // Report progress every 10 items - if ((completed + errors) % 10 === 0) { - process.stdout.write( - JSON.stringify({completed, errors, total: items.length, timings}) + - '\n', - ); - } -} - -// Final report -process.stdout.write( - JSON.stringify({ - completed, - errors, - total: items.length, - done: true, - timings, - }) + '\n', -); diff --git a/packages/docs/twoslash-worker.ts b/packages/docs/twoslash-worker.ts new file mode 100644 index 00000000000..ec64e6b7459 --- /dev/null +++ b/packages/docs/twoslash-worker.ts @@ -0,0 +1,90 @@ +import {rendererClassic, transformerTwoslash} from '@shikijs/twoslash'; +import {existsSync, mkdirSync, readFileSync, writeFileSync} from 'fs'; +import {dirname} from 'path'; +import {createHighlighter} from 'shiki'; +import {createTwoslasher} from 'twoslash'; + +interface WorkItem { + code: string; + lang: string; + cachePath: string; +} + +// Read work items from the file passed as argument +const workFile = process.argv[2]; +const items: WorkItem[] = JSON.parse(readFileSync(workFile, 'utf8')); + +// Create Language Service ONCE per worker (5-20x faster for batch) +const twoslasher = createTwoslasher({ + compilerOptions: { + types: ['node'], + target: 99 /* ESNext */, + module: 99 /* ESNext */, + jsx: 4 /* ReactJSX */, + }, +}); + +// Collect unique languages from work items +const uniqueLangs = [...new Set(items.map((item) => item.lang))]; + +// Create highlighter ONCE with all needed languages +const highlighter = await createHighlighter({ + themes: ['github-dark'], + langs: uniqueLangs, +}); + +let completed = 0; +let errors = 0; +const timings: Array<{cachePath: string; ms: number; error?: string}> = []; + +for (const item of items) { + const start = performance.now(); + try { + const html = highlighter.codeToHtml(item.code, { + lang: item.lang, + theme: 'github-dark', + transformers: [ + transformerTwoslash({ + twoslasher, + renderer: rendererClassic(), + explicitTrigger: false, + }), + ], + }); + + const dir = dirname(item.cachePath); + if (!existsSync(dir)) mkdirSync(dir, {recursive: true}); + writeFileSync(item.cachePath, html, 'utf8'); + completed++; + timings.push({ + cachePath: item.cachePath, + ms: Math.round(performance.now() - start), + }); + } catch (error) { + errors++; + timings.push({ + cachePath: item.cachePath, + ms: Math.round(performance.now() - start), + error: (error as Error).message.slice(0, 200), + }); + } + + // Report progress every 10 items + if ((completed + errors) % 10 === 0) { + process.stdout.write( + JSON.stringify({completed, errors, total: items.length, timings}) + + '\n', + ); + } +} + +// Final report +process.stdout.write( + JSON.stringify({ + completed, + errors, + total: items.length, + done: true, + timings, + }) + '\n', +); diff --git a/packages/docusaurus-plugin/package.json b/packages/docusaurus-plugin/package.json index 94ac49492c6..ff4948b817a 100644 --- a/packages/docusaurus-plugin/package.json +++ b/packages/docusaurus-plugin/package.json @@ -14,12 +14,12 @@ "url": "https://github.com/remotion-dev/remotion/issues" }, "dependencies": { - "shiki-twoslash": "3.1.2", + "shiki": "3.22.0", + "@shikijs/twoslash": "3.22.0", + "twoslash": "0.3.6", "unified": "^11.0.5", "unist-util-visit": "^5.0.0", "fenceparser": "^2.2.0", - "shiki": "0.10.1", - "@typescript/twoslash": "3.2.1", "@docusaurus/types": "3.6.0", "@types/dom-webcodecs": "catalog:" }, diff --git a/packages/docusaurus-plugin/src/caching.ts b/packages/docusaurus-plugin/src/caching.ts index bd1f8f66aab..ecc4e9d58a8 100644 --- a/packages/docusaurus-plugin/src/caching.ts +++ b/packages/docusaurus-plugin/src/caching.ts @@ -1,29 +1,66 @@ /* eslint-disable no-console */ -import type {TwoSlashReturn} from '@typescript/twoslash'; -import type {UserConfigSettings} from 'shiki-twoslash'; -import {runTwoSlash} from 'shiki-twoslash'; -import {ModuleKind, ScriptTarget} from 'typescript'; +import {rendererClassic, transformerTwoslash} from '@shikijs/twoslash'; +import type {HighlighterGeneric} from 'shiki/core'; +import {createTwoslasher} from 'twoslash'; + +let cachedTwoslasher: ReturnType | null = null; + +function getTwoslasher() { + if (!cachedTwoslasher) { + cachedTwoslasher = createTwoslasher({ + compilerOptions: { + types: ['node'], + target: 99 /* ESNext */, + module: 99 /* ESNext */, + jsx: 4 /* ReactJSX */, + }, + }); + } + + return cachedTwoslasher; +} /** - * Keeps a cache of the JSON responses to a twoslash call in node_modules/.cache/twoslash - * which should keep CI times down (e.g. the epub vs the handbook etc) - but also during - * dev time, where it can be super useful. + * Keeps a cache of the HTML responses in node_modules/.cache/twoslash + * which should keep CI times down — but also during dev time. + * Returns an HTML string (final rendered output). */ export const cachedTwoslashCall = ( code: string, lang: string, - settings: UserConfigSettings, -): TwoSlashReturn => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + highlighter: HighlighterGeneric, +): string => { const {createHash} = require('crypto'); const {readFileSync, existsSync, mkdirSync, writeFileSync} = require('fs'); const {join} = require('path'); - const shikiVersion = require('@typescript/twoslash/package.json').version; - const tsVersion = require('typescript/package.json').version; + const {createRequire} = require('module'); + const _require = createRequire(__filename); + const readPkgVersion = (pkg: string) => { + const entryPath = _require.resolve(pkg); + let dir = require('path').dirname(entryPath); + while (dir !== '/') { + const p = join(dir, 'package.json'); + if (existsSync(p)) { + return JSON.parse(readFileSync(p, 'utf8')).version as string; + } + + dir = require('path').dirname(dir); + } + + return 'unknown'; + }; + + const twoslashVersion = readPkgVersion('twoslash'); + const shikiVersion = readPkgVersion('shiki'); + const tsVersion = readPkgVersion('typescript'); const shasum = createHash('sha1'); const codeSha = shasum - .update(`${code}-${shikiVersion}-${tsVersion}`) + .update( + `${code}-${twoslashVersion}-${shikiVersion}-${tsVersion}-github-dark`, + ) .digest('hex'); const getNmCache = () => { @@ -32,25 +69,30 @@ export const cachedTwoslashCall = ( }; const cacheRoot = getNmCache(); - const cachePath = join(cacheRoot, `${codeSha}.json`); if (existsSync(cachePath)) { if (process.env.debug) console.log(`Using cached twoslash results from ${cachePath}`); - return JSON.parse(readFileSync(cachePath, 'utf8')); + return readFileSync(cachePath, 'utf8'); } - const results = runTwoSlash(code, lang, { - ...settings, - defaultCompilerOptions: { - ...settings.defaultCompilerOptions, - target: ScriptTarget.ESNext, - module: ModuleKind.ESNext, - }, + const twoslasher = getTwoslasher(); + + const html = highlighter.codeToHtml(code, { + lang, + theme: 'github-dark', + transformers: [ + transformerTwoslash({ + twoslasher, + renderer: rendererClassic(), + explicitTrigger: false, + }), + ], }); + if (!existsSync(cacheRoot)) mkdirSync(cacheRoot, {recursive: true}); - writeFileSync(cachePath, JSON.stringify(results), 'utf8'); - return results; + writeFileSync(cachePath, html, 'utf8'); + return html; }; diff --git a/packages/docusaurus-plugin/src/exceptionMessageDOM.ts b/packages/docusaurus-plugin/src/exceptionMessageDOM.ts index 8b9ba1cde33..3f640b0e376 100644 --- a/packages/docusaurus-plugin/src/exceptionMessageDOM.ts +++ b/packages/docusaurus-plugin/src/exceptionMessageDOM.ts @@ -1,5 +1,4 @@ /* eslint-disable no-console */ -import {TwoslashError} from '@typescript/twoslash'; import type {Node} from './unist-types'; export function escapeHtml(html: string) { @@ -89,7 +88,23 @@ export const setupNodeForTwoslashException = ( } `; - const bodyFromTwoslashError = (err: TwoslashError) => { + const isTwoslashError = ( + err: unknown, + ): err is {title: string; description: string; recommendation: string} => { + return ( + typeof err === 'object' && + err !== null && + 'title' in err && + 'description' in err && + 'recommendation' in err + ); + }; + + const bodyFromTwoslashError = (err: { + title: string; + description: string; + recommendation: string; + }) => { return `

${escapeHtml(err.title)}

${escapeHtml(err.description).replace(/(?:\r\n|\r|\n)/g, '
')}

@@ -124,7 +139,7 @@ export const setupNodeForTwoslashException = ( body = String(error); eLog(`### Unexpected error:`); eLog(error); - } else if (error instanceof TwoslashError) { + } else if (isTwoslashError(error)) { body = bodyFromTwoslashError(error); eLog(`### Twoslash error: ${error.title}`); eLog(error.description); diff --git a/packages/docusaurus-plugin/src/shiki.ts b/packages/docusaurus-plugin/src/shiki.ts index 35cf5652a6d..b467c3fbc36 100644 --- a/packages/docusaurus-plugin/src/shiki.ts +++ b/packages/docusaurus-plugin/src/shiki.ts @@ -1,10 +1,6 @@ -import {TwoslashError} from '@typescript/twoslash'; - import {lex, parse} from 'fenceparser'; -import type {Highlighter} from 'shiki'; -import {getHighlighter} from 'shiki'; -import type {UserConfigSettings} from 'shiki-twoslash'; -import {renderCodeToHTML} from 'shiki-twoslash'; +import type {BundledLanguage, BundledTheme, HighlighterGeneric} from 'shiki'; +import {createHighlighter} from 'shiki'; import type {BuildVisitor} from 'unist-util-visit'; import {visit} from 'unist-util-visit'; import {cachedTwoslashCall} from './caching'; @@ -20,116 +16,18 @@ type Fence = { meta: OBJECT; }; -// A set of includes which can be pulled via a set ID -const includes = new Map(); - -function getHTML({ - code, - fence, - highlighters, - twoslash, - twoslashSettings, -}: { - code: string; - fence: Fence; - highlighters: Highlighter[]; - twoslash: any; - twoslashSettings: UserConfigSettings; -}) { - // Shiki doesn't respect json5 as an input, so switch it - // to json, which can handle comments in the syntax highlight - if (fence.lang === 'json5') { - fence.lang = 'json'; - } - - let results; - // Support 'twoslash' includes - if (fence.lang === 'twoslash') { - if (!fence.meta.include || typeof fence.meta.include !== 'string') { - throw new Error( - "A twoslash code block needs a pragma like 'twoslash include [name]'", - ); - } - - addIncludes(includes, fence.meta.include, code); - results = twoslashSettings.wrapFragments - ? `
` - : ''; - } else { - // All good, get each highlighter and render the shiki output for it - const output = highlighters.map((highlighter) => { - // @ts-expect-error - const themeName = highlighter.customName - .split('/') - .pop() - .replace('.json', ''); - const html = renderCodeToHTML( - code, - fence.lang, - fence.meta as any, - {themeName, ...twoslashSettings}, - highlighter, - twoslash, - ); - return html.replace( - '', - '', - ); - }); - results = output.join('\n'); - if (highlighters.length > 1 && twoslashSettings.wrapFragments) { - results = `
${results}
`; - } - } - - return results; +interface TwoslashSettings { + themes?: string[]; + theme?: string; + wrapFragments?: boolean; + ignoreCodeblocksWithCodefenceMeta?: string[]; + alwayRaiseForTwoslashExceptions?: boolean; + defaultCompilerOptions?: Record; + [key: string]: unknown; } -/** - * Runs twoslash across an AST node, switching out the text content, and lang - * and adding a `twoslash` property to the node. - */ -export const runTwoSlashOnNode = ( - code: string, - {lang, meta}: {lang: string; meta: Record}, - settings = {}, -) => { - // Only run twoslash when the meta has the attribute twoslash - if (meta.twoslash) { - const importedCode = replaceIncludesInCode(includes, code); - return cachedTwoslashCall(importedCode, lang, settings); - } - - return undefined; -}; - -// To make sure we only have one highlighter per theme in a process -const highlighterCache = new Map(); - -/** Sets up the highlighters, and cache's for recalls */ -export const highlightersFromSettings = ( - settings: UserConfigSettings, -): Promise => { - // console.log("i should only log once per theme") - // ^ uncomment this to debug if required - const themes = - settings.themes || (settings.theme ? [settings.theme] : ['dark-plus']); - - return Promise.all( - themes.map(async (theme) => { - // You can put a string, a path, or the JSON theme obj - const themeName = typeof theme === 'string' ? theme : theme.name; - const highlighter = await getHighlighter({ - ...settings, - theme, - themes: undefined, - }); - // @ts-expect-error - highlighter.customName = themeName; - return highlighter; - }), - ); -}; +// A set of includes which can be pulled via a set ID +const includes = new Map(); const parsingNewFile = () => includes.clear(); @@ -155,6 +53,51 @@ const parseFence = (fence: string): Fence => { }; }; +// To make sure we only have one highlighter per settings in a process +const highlighterCache = new Map< + TwoslashSettings, + Promise> +>(); + +const ALL_LANGS: BundledLanguage[] = [ + 'tsx', + 'typescript', + 'jsx', + 'javascript', + 'json', + 'bash', + 'shellscript', + 'css', + 'html', + 'diff', + 'yaml', + 'toml', + 'docker', + 'python', + 'ruby', + 'go', + 'php', + 'markdown', + 'ini', +]; + +/** Creates a highlighter and caches for reuse */ +const getHighlighterInstance = ( + settings: TwoslashSettings, +): Promise> => { + if (!highlighterCache.has(settings)) { + highlighterCache.set( + settings, + createHighlighter({ + themes: ['github-dark'], + langs: ALL_LANGS, + }), + ); + } + + return highlighterCache.get(settings)!; +}; + // --- The Remark API --- /** @@ -162,23 +105,21 @@ const parseFence = (fence: string): Fence => { */ const remarkVisitor = ( - highlighters: Highlighter[], - twoslashSettings: UserConfigSettings = {}, + highlighter: HighlighterGeneric, + twoslashSettings: TwoslashSettings = {}, ): BuildVisitor => (node: Node) => { const code = node; - let fence; + let fence: Fence; try { fence = parseFence([node.lang, node.meta].filter(Boolean).join(' ')); } catch { - const twoslashError = new TwoslashError( - 'Codefence error', - 'Could not parse the codefence for this code sample', - "It's usually an unclosed string", + return setupNodeForTwoslashException( code.value, + node, + new Error('Could not parse the codefence for this code sample'), ); - return setupNodeForTwoslashException(code.value, node, twoslashError); } // Do nothing if the node has an attribute to ignore @@ -192,40 +133,81 @@ const remarkVisitor = return; } - let twoslash; - try { - // By allowing node.twoslash to already exist you can set it up yourself in a browser - twoslash = - node.twoslash || runTwoSlashOnNode(code.value, fence, twoslashSettings); - } catch (error) { - const shouldAlwaysRaise = - process && process.env && Boolean(process.env.CI); - // @ts-expect-error - const yeahButNotInTests = typeof jest === 'undefined'; + let shikiHTML: string; - if ( - (shouldAlwaysRaise && yeahButNotInTests) || - twoslashSettings.alwayRaiseForTwoslashExceptions - ) { - throw error; - } else { - return setupNodeForTwoslashException(code.value, node, error as Error); + if (fence.lang === 'twoslash') { + // Support 'twoslash' includes + if (!fence.meta.include || typeof fence.meta.include !== 'string') { + throw new Error( + "A twoslash code block needs a pragma like 'twoslash include [name]'", + ); } - } - if (twoslash) { - node.value = twoslash.code; - node.lang = twoslash.extension; - node.twoslash = twoslash; - } + addIncludes(includes, fence.meta.include, node.value); + shikiHTML = twoslashSettings.wrapFragments + ? `
` + : ''; + } else if (fence.meta.twoslash) { + // Twoslash code block — use cached call + const importedCode = replaceIncludesInCode(includes, node.value); + try { + shikiHTML = cachedTwoslashCall(importedCode, fence.lang, highlighter); + } catch (error) { + const shouldAlwaysRaise = + process && process.env && Boolean(process.env.CI); + + if ( + shouldAlwaysRaise || + twoslashSettings.alwayRaiseForTwoslashExceptions + ) { + throw error; + } else { + return setupNodeForTwoslashException( + code.value, + node, + error as Error, + ); + } + } + + // Add copy button + shikiHTML = shikiHTML.replace( + '', + '', + ); + } else { + // Regular (non-twoslash) code block + const langAliases: Record = { + json5: 'json', + js: 'javascript', + ts: 'typescript', + sh: 'bash', + rb: 'ruby', + md: 'markdown', + txt: 'plaintext', + }; + const resolvedLang = + langAliases[fence.lang] || fence.lang || 'plaintext'; + + try { + shikiHTML = highlighter.codeToHtml(node.value, { + lang: resolvedLang, + theme: 'github-dark', + }); + } catch { + // Fallback: if language is not supported, render as plaintext + shikiHTML = highlighter.codeToHtml(node.value, { + lang: 'plaintext', + theme: 'github-dark', + }); + } - const shikiHTML = getHTML({ - code: node.value, - fence, - highlighters, - twoslash, - twoslashSettings, - }); + // Add copy button + shikiHTML = shikiHTML.replace( + '', + '', + ); + } // @ts-expect-error node.type = 'mdxJsxFlowElement'; @@ -323,39 +305,12 @@ const remarkVisitor = * Synchronous outer function, async inner function, which is how the remark * async API works. */ -export function remarkTwoslash(settings: UserConfigSettings = {}) { - if (!highlighterCache.has(settings)) { - highlighterCache.set(settings, highlightersFromSettings(settings)); - } - +export function remarkTwoslash(settings: TwoslashSettings = {}) { const transform = async (markdownAST: Node) => { - const highlighters = await highlighterCache.get(settings); + const highlighter = await getHighlighterInstance(settings); parsingNewFile(); - visit(markdownAST, 'code', remarkVisitor(highlighters, settings)); + visit(markdownAST, 'code', remarkVisitor(highlighter, settings)); }; return transform; } - -// --- The Markdown-it API --- - -/** Only the inner function exposed as a synchronous API for markdown-it */ - -export const transformAttributesToHTML = ( - code: string, - fenceString: string, - highlighters: Highlighter[], - settings: UserConfigSettings, -) => { - const fence = parseFence(fenceString); - - const twoslash = runTwoSlashOnNode(code, fence, settings); - const newCode = (twoslash && twoslash.code) || code; - return getHTML({ - code: newCode, - fence, - highlighters, - twoslash, - twoslashSettings: settings, - }); -}; diff --git a/packages/docusaurus-plugin/src/unist-types.ts b/packages/docusaurus-plugin/src/unist-types.ts index fe4e4892778..bda2c87606f 100644 --- a/packages/docusaurus-plugin/src/unist-types.ts +++ b/packages/docusaurus-plugin/src/unist-types.ts @@ -1,7 +1,5 @@ // ## Interfaces -import type {TwoSlashReturn} from '@typescript/twoslash'; - /** * Info associated with nodes by the ecosystem. * @@ -108,7 +106,7 @@ export interface Node { lang: string; meta?: string; children?: Node[]; - twoslash?: TwoSlashReturn; + twoslash?: unknown; } /** From 43281dd0a687dc6f6223ac1ac14413627c0cbeae Mon Sep 17 00:00:00 2001 From: Jonny Burger Date: Tue, 17 Feb 2026 18:18:42 +0100 Subject: [PATCH 02/56] Update twoslash-worker.ts --- packages/docs/twoslash-worker.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/docs/twoslash-worker.ts b/packages/docs/twoslash-worker.ts index ec64e6b7459..5487edf99e4 100644 --- a/packages/docs/twoslash-worker.ts +++ b/packages/docs/twoslash-worker.ts @@ -33,6 +33,12 @@ const highlighter = await createHighlighter({ langs: uniqueLangs, }); +const transformer = transformerTwoslash({ + twoslasher, + renderer: rendererClassic(), + explicitTrigger: false, +}); + let completed = 0; let errors = 0; const timings: Array<{cachePath: string; ms: number; error?: string}> = []; @@ -43,13 +49,7 @@ for (const item of items) { const html = highlighter.codeToHtml(item.code, { lang: item.lang, theme: 'github-dark', - transformers: [ - transformerTwoslash({ - twoslasher, - renderer: rendererClassic(), - explicitTrigger: false, - }), - ], + transformers: [transformer], }); const dir = dirname(item.cachePath); From 8fca3061581962fe33d0aec04c4442d64cd23e9a Mon Sep 17 00:00:00 2001 From: Jonny Burger Date: Tue, 17 Feb 2026 18:41:12 +0100 Subject: [PATCH 03/56] Fix twoslash workers hanging: force exit and add timeout/stderr logging The worker process never exited because createTwoslasher holds a TS language service with open handles and neither it nor shiki expose dispose(). Added process.exit(0) after final output, a 5-minute timeout per worker, and stderr forwarding so crashes are visible. Co-Authored-By: Claude Opus 4.6 --- packages/docs/prewarm-twoslash.ts | 48 ++++++++++++++++++++++++------- packages/docs/twoslash-worker.ts | 3 ++ 2 files changed, 41 insertions(+), 10 deletions(-) diff --git a/packages/docs/prewarm-twoslash.ts b/packages/docs/prewarm-twoslash.ts index 6925cfa8c83..6a564c0a2af 100644 --- a/packages/docs/prewarm-twoslash.ts +++ b/packages/docs/prewarm-twoslash.ts @@ -152,6 +152,8 @@ interface WorkerResult { timings: TimingEntry[]; } +const WORKER_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes per worker + function runWorker( workItems: TwoslashBlock[], workerId: number, @@ -166,6 +168,19 @@ function runWorker( }); let lastReport: WorkerResult = {completed: 0, errors: 0, timings: []}; + let settled = false; + + const timeout = setTimeout(() => { + if (!settled) { + settled = true; + console.error(`Worker ${workerId} timed out after ${WORKER_TIMEOUT_MS / 1000}s, killing...`); + child.kill('SIGKILL'); + try { + unlinkSync(tmpFile); + } catch {} + resolvePromise(lastReport); + } + }, WORKER_TIMEOUT_MS); child.stdout.on('data', (data: Buffer) => { const lines = data.toString().trim().split('\n'); @@ -178,20 +193,33 @@ function runWorker( } }); - child.stderr.on('data', () => {}); + child.stderr.on('data', (data: Buffer) => { + process.stderr.write(`[worker ${workerId}] ${data}`); + }); - child.on('close', () => { - try { - unlinkSync(tmpFile); - } catch {} - resolvePromise(lastReport); + child.on('close', (code) => { + clearTimeout(timeout); + if (!settled) { + settled = true; + if (code !== 0 && code !== null) { + console.error(`Worker ${workerId} exited with code ${code}`); + } + try { + unlinkSync(tmpFile); + } catch {} + resolvePromise(lastReport); + } }); child.on('error', (err) => { - try { - unlinkSync(tmpFile); - } catch {} - reject(err); + clearTimeout(timeout); + if (!settled) { + settled = true; + try { + unlinkSync(tmpFile); + } catch {} + reject(err); + } }); }); } diff --git a/packages/docs/twoslash-worker.ts b/packages/docs/twoslash-worker.ts index 5487edf99e4..68531f2b01e 100644 --- a/packages/docs/twoslash-worker.ts +++ b/packages/docs/twoslash-worker.ts @@ -88,3 +88,6 @@ process.stdout.write( timings, }) + '\n', ); + +// Force exit — createTwoslasher holds a TS language service that keeps the process alive +process.exit(0); From 04393b5d120d781867c60c31c2c5c7a585fbdcb9 Mon Sep 17 00:00:00 2001 From: Jonny Burger Date: Tue, 17 Feb 2026 18:50:58 +0100 Subject: [PATCH 04/56] Exit with code 1 on twoslash errors, force exit 0 on success The main process was silently swallowing worker errors and never exiting, causing the Vercel build to hang. Now errors are printed and cause exit 1, and success calls process.exit(0) to avoid dangling handles keeping the process alive. Co-Authored-By: Claude Opus 4.6 --- packages/docs/prewarm-twoslash.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/docs/prewarm-twoslash.ts b/packages/docs/prewarm-twoslash.ts index 6a564c0a2af..6ef9fce7aeb 100644 --- a/packages/docs/prewarm-twoslash.ts +++ b/packages/docs/prewarm-twoslash.ts @@ -342,6 +342,13 @@ async function main() { const status = t.error ? ` ERROR: ${t.error}` : ''; console.log(` ${t.ms}ms - ${files}${status}`); } + + if (totalErrors > 0) { + console.error(`\n${totalErrors} twoslash errors — failing build`); + process.exit(1); + } + + process.exit(0); } main().catch((e) => { From de2cdfb147399d026718f37bfb49b6a9223a2cf1 Mon Sep 17 00:00:00 2001 From: Jonny Burger Date: Tue, 17 Feb 2026 18:51:37 +0100 Subject: [PATCH 05/56] Remove silent catch in progress interval, check dir existence instead Co-Authored-By: Claude Opus 4.6 --- packages/docs/prewarm-twoslash.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/docs/prewarm-twoslash.ts b/packages/docs/prewarm-twoslash.ts index 6ef9fce7aeb..aa75b25670b 100644 --- a/packages/docs/prewarm-twoslash.ts +++ b/packages/docs/prewarm-twoslash.ts @@ -301,11 +301,11 @@ async function main() { const workerPromises = chunks.map((chunk, i) => runWorker(chunk, i)); const progressInterval = setInterval(() => { - try { - const cached = readdirSync(CACHE_ROOT).length; - const elapsed = ((performance.now() - startTime) / 1000).toFixed(0); - console.log(` ${elapsed}s elapsed, ~${cached} cached`); - } catch {} + const cached = existsSync(CACHE_ROOT) + ? readdirSync(CACHE_ROOT).length + : 0; + const elapsed = ((performance.now() - startTime) / 1000).toFixed(0); + console.log(` ${elapsed}s elapsed, ~${cached} cached`); }, 15000); const results = await Promise.all(workerPromises); From dcfdf8bd42569856952ab2814bf99bb93b61f90e Mon Sep 17 00:00:00 2001 From: Jonny Burger Date: Tue, 17 Feb 2026 19:01:22 +0100 Subject: [PATCH 06/56] Update prewarm-twoslash.ts --- packages/docs/prewarm-twoslash.ts | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/packages/docs/prewarm-twoslash.ts b/packages/docs/prewarm-twoslash.ts index aa75b25670b..592bddba2ad 100644 --- a/packages/docs/prewarm-twoslash.ts +++ b/packages/docs/prewarm-twoslash.ts @@ -17,7 +17,7 @@ const DOCS_ROOT = resolve(import.meta.dirname); const CACHE_ROOT = join(DOCS_ROOT, 'node_modules', '.cache', 'twoslash'); const WORKER_PATH = join(DOCS_ROOT, 'twoslash-worker.ts'); const NUM_WORKERS = process.env.VERCEL - ? cpus().length + ? Math.max(1, cpus().length - 1) : Math.max(1, cpus().length - 2); const pluginDir = join(DOCS_ROOT, '..', 'docusaurus-plugin'); @@ -173,7 +173,9 @@ function runWorker( const timeout = setTimeout(() => { if (!settled) { settled = true; - console.error(`Worker ${workerId} timed out after ${WORKER_TIMEOUT_MS / 1000}s, killing...`); + console.error( + `Worker ${workerId} timed out after ${WORKER_TIMEOUT_MS / 1000}s, killing...`, + ); child.kill('SIGKILL'); try { unlinkSync(tmpFile); @@ -245,7 +247,12 @@ async function main() { for (const file of allFiles) { const content = readFileSync(file, 'utf8'); allBlocks.push( - ...extractTwoslashBlocks(content, file, validCachePaths, cachePathToFiles), + ...extractTwoslashBlocks( + content, + file, + validCachePaths, + cachePathToFiles, + ), ); } @@ -301,15 +308,14 @@ async function main() { const workerPromises = chunks.map((chunk, i) => runWorker(chunk, i)); const progressInterval = setInterval(() => { - const cached = existsSync(CACHE_ROOT) - ? readdirSync(CACHE_ROOT).length - : 0; + const cached = existsSync(CACHE_ROOT) ? readdirSync(CACHE_ROOT).length : 0; const elapsed = ((performance.now() - startTime) / 1000).toFixed(0); console.log(` ${elapsed}s elapsed, ~${cached} cached`); }, 15000); const results = await Promise.all(workerPromises); clearInterval(progressInterval); + console.log('Workers completed'); const totalCompleted = results.reduce((s, r) => s + r.completed, 0); const totalErrors = results.reduce((s, r) => s + r.errors, 0); @@ -326,7 +332,9 @@ async function main() { allTimings.sort((a, b) => b.ms - a.ms); - console.log(`\nTwoslash pre-warm: ${totalCompleted} blocks in ${totalTime}s using ${numWorkers} workers (${totalErrors} errors)`); + console.log( + `\nTwoslash pre-warm: ${totalCompleted} blocks in ${totalTime}s using ${numWorkers} workers (${totalErrors} errors)`, + ); const errorTimings = allTimings.filter((t) => t.error); if (errorTimings.length > 0) { console.log(`\nErrors:`); From bf85c29538145ec6b6b9e01ffc0b328ff32afc8a Mon Sep 17 00:00:00 2001 From: Igor Samokhovets Date: Tue, 17 Feb 2026 20:24:19 +0100 Subject: [PATCH 07/56] `docs`: Document browser support requirements for client-side rendering --- packages/docs/docs/client-side-rendering/index.mdx | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/docs/docs/client-side-rendering/index.mdx b/packages/docs/docs/client-side-rendering/index.mdx index 0d200d334e3..c68e4264b4f 100644 --- a/packages/docs/docs/client-side-rendering/index.mdx +++ b/packages/docs/docs/client-side-rendering/index.mdx @@ -23,6 +23,16 @@ Unlike server-side rendering with [`@remotion/renderer`](/docs/renderer), client - Limited to a subset of HTML elements - see [limitations](/docs/client-side-rendering/limitations) - No bundling step - takes components and video config directly +## Browser support + +Client-side rendering requires the [WebCodecs API](https://developer.mozilla.org/en-US/docs/Web/API/WebCodecs_API), which limits browser support to: + +| Browser | Minimum version | +| ------- | --------------- | +| Chrome | 94+ | +| Firefox | 130+ | +| Safari | 26+ | + ## APIs The package provides APIs called [`renderStillOnWeb()`](/docs/web-renderer/render-still-on-web) and [`renderMediaOnWeb()`](/docs/web-renderer/render-media-on-web). From fed2a96ad210e89c4c5953c9dd7bfcee5255ec5e Mon Sep 17 00:00:00 2001 From: Igor Samokhovets Date: Tue, 17 Feb 2026 20:51:43 +0100 Subject: [PATCH 08/56] `docs`: Document Firefox AAC encoding limitation for client-side rendering --- packages/docs/docs/client-side-rendering/limitations.mdx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/docs/docs/client-side-rendering/limitations.mdx b/packages/docs/docs/client-side-rendering/limitations.mdx index 22ea72a49e5..1f1aaea08ce 100644 --- a/packages/docs/docs/client-side-rendering/limitations.mdx +++ b/packages/docs/docs/client-side-rendering/limitations.mdx @@ -146,6 +146,10 @@ const MyComp = () => { The limitations from [`@remotion/media`](/docs/media/support) apply. +## Firefox does not support AAC encoding + +Firefox does not support encoding AAC audio via WebCodecs. When rendering in Firefox, use Opus audio (WebM container) instead of AAC (MP4 container). + ## Single concurrency There is no multithreading for client-side rendering. From aca771ac200da5e7434e0d630de0047cd254095a Mon Sep 17 00:00:00 2001 From: Igor Samokhovets Date: Tue, 17 Feb 2026 20:52:32 +0100 Subject: [PATCH 09/56] `docs`: Add WebCodecs requirement note to client-side rendering limitations --- packages/docs/docs/client-side-rendering/limitations.mdx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/docs/docs/client-side-rendering/limitations.mdx b/packages/docs/docs/client-side-rendering/limitations.mdx index 1f1aaea08ce..581dff16b68 100644 --- a/packages/docs/docs/client-side-rendering/limitations.mdx +++ b/packages/docs/docs/client-side-rendering/limitations.mdx @@ -10,6 +10,8 @@ Very experimental feature - expect bugs and breaking changes at any time. [Track progress on GitHub](https://github.com/remotion-dev/remotion/issues/5913) and discuss in the [`#web-renderer`](https://remotion.dev/discord) channel on Discord. ::: +The browser must support the [WebCodecs API](https://developer.mozilla.org/en-US/docs/Web/API/WebCodecs_API). See [Browser support](/docs/client-side-rendering#browser-support) for minimum browser versions. + Unlike server-side rendering, where a full screenshot it taken, in client-side rendering the layout and styles of your video are being emulated and drawn to a canvas. It is not feasible to support all CSS properties and factors affecting the visual style of a page, so only the most important styling primitives are supported. From d7707a89ddce9917bbf84943740a452720bfb143 Mon Sep 17 00:00:00 2001 From: Ayushman Date: Wed, 18 Feb 2026 11:42:06 +0530 Subject: [PATCH 10/56] Studio: In Safari, a user can select some things #6555 --- packages/studio/src/helpers/inject-css.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/packages/studio/src/helpers/inject-css.ts b/packages/studio/src/helpers/inject-css.ts index 0e8b503d7b9..b985fea0c48 100644 --- a/packages/studio/src/helpers/inject-css.ts +++ b/packages/studio/src/helpers/inject-css.ts @@ -21,9 +21,22 @@ const makeDefaultGlobalCSS = () => { /* Override Chakra UI position: relative on body */ position: static !important; } + + #__remotion-studio-container { + user-select: none; + -webkit-user-select: none; + } + + #__remotion-studio-container input, + #__remotion-studio-container textarea, + #__remotion-studio-container [contenteditable] { + user-select: text; + -webkit-user-select: text; + } .remotion-splitter { user-select: none; + -webkit-user-select: none; } .remotion-splitter-horizontal { From a304048d56347d2090a994dbf219e1712af0978a Mon Sep 17 00:00:00 2001 From: JonnyBurger Date: Wed, 18 Feb 2026 08:03:00 +0100 Subject: [PATCH 11/56] Convert overrideWidth and overrideHeight to AnyRemotionOption pattern Migrates the standalone config helpers in packages/cli/src/config/{width,height}.ts to proper AnyRemotionOption definitions in packages/renderer/src/options/, following the same pattern as scaleOption, concurrencyOption, etc. Co-Authored-By: Claude Opus 4.6 --- packages/cli/src/config/height.ts | 13 ----- packages/cli/src/config/index.ts | 10 ++-- packages/cli/src/config/width.ts | 15 ------ packages/cli/src/get-cli-options.ts | 8 ++- packages/cli/src/parse-command-line.ts | 14 ++--- packages/docs/docs/cli/render.mdx | 4 +- packages/docs/docs/config.mdx | 7 ++- packages/docs/docs/lambda/cli/render.mdx | 4 +- packages/renderer/src/options/index.tsx | 4 ++ .../renderer/src/options/override-height.tsx | 51 +++++++++++++++++++ .../renderer/src/options/override-width.tsx | 51 +++++++++++++++++++ 11 files changed, 127 insertions(+), 54 deletions(-) delete mode 100644 packages/cli/src/config/height.ts delete mode 100644 packages/cli/src/config/width.ts create mode 100644 packages/renderer/src/options/override-height.tsx create mode 100644 packages/renderer/src/options/override-width.tsx diff --git a/packages/cli/src/config/height.ts b/packages/cli/src/config/height.ts deleted file mode 100644 index 60156bd526e..00000000000 --- a/packages/cli/src/config/height.ts +++ /dev/null @@ -1,13 +0,0 @@ -import {validateDimension} from '../validate'; - -let specifiedHeight: number | null; - -export const overrideHeight = (newHeight: number) => { - validateDimension(newHeight, 'height', 'passed to `overrideHeight()`'); - - specifiedHeight = newHeight; -}; - -export const getHeight = (): number | null => { - return specifiedHeight; -}; diff --git a/packages/cli/src/config/index.ts b/packages/cli/src/config/index.ts index 3f9463fb34a..7a0ec0c1159 100644 --- a/packages/cli/src/config/index.ts +++ b/packages/cli/src/config/index.ts @@ -44,7 +44,6 @@ import { setFfmpegOverrideFunction, } from './ffmpeg-override'; import {setFrameRange} from './frame-range'; -import {getHeight, overrideHeight} from './height'; import {setImageSequence} from './image-sequence'; import {getMetadata, setMetadata} from './metadata'; import {getShouldOpenBrowser, setShouldOpenBrowser} from './open-browser'; @@ -57,7 +56,6 @@ import { getWebpackPolling, setWebpackPollingInMilliseconds, } from './webpack-poll'; -import {getWidth, overrideWidth} from './width'; export type {Concurrency, WebpackConfiguration, WebpackOverrideFn}; @@ -117,6 +115,8 @@ const { userAgentOption, disableWebSecurityOption, ignoreCertificateErrorsOption, + overrideHeightOption, + overrideWidthOption, } = BrowserSafeApis.options; declare global { @@ -702,8 +702,8 @@ export const Config: FlatConfig = { setVideoBitrate: videoBitrateOption.setConfig, setAudioLatencyHint: audioLatencyHintOption.setConfig, setForSeamlessAacConcatenation: forSeamlessAacConcatenationOption.setConfig, - overrideHeight, - overrideWidth, + overrideHeight: overrideHeightOption.setConfig, + overrideWidth: overrideWidthOption.setConfig, overrideFfmpegCommand: setFfmpegOverrideFunction, setAudioCodec: audioCodecOption.setConfig, setOffthreadVideoCacheSizeInBytes: (size) => { @@ -746,8 +746,6 @@ export const ConfigInternals = { getMaxTimelineTracks: StudioServerInternals.getMaxTimelineTracks, defaultOverrideFunction, getFfmpegOverrideFunction, - getHeight, - getWidth, getMetadata, getEntryPoint, getWebpackPolling, diff --git a/packages/cli/src/config/width.ts b/packages/cli/src/config/width.ts deleted file mode 100644 index 603b1a4c4ff..00000000000 --- a/packages/cli/src/config/width.ts +++ /dev/null @@ -1,15 +0,0 @@ -import {validateDimension} from '../validate'; - -let passedWidth: number | null = null; - -export const overrideWidth = (newWidth: number) => { - if (typeof newWidth !== 'number') { - validateDimension(newWidth, 'width', 'passed to `setWidth()`'); - } - - passedWidth = newWidth; -}; - -export const getWidth = (): number | null => { - return passedWidth; -}; diff --git a/packages/cli/src/get-cli-options.ts b/packages/cli/src/get-cli-options.ts index 3a57a0ca11c..8a65ada0ed0 100644 --- a/packages/cli/src/get-cli-options.ts +++ b/packages/cli/src/get-cli-options.ts @@ -64,8 +64,12 @@ export const getCliOptions = (options: { commandLine: parsedCli, }).value; - const height = ConfigInternals.getHeight(); - const width = ConfigInternals.getWidth(); + const height = BrowserSafeApis.options.overrideHeightOption.getValue({ + commandLine: parsedCli, + }).value; + const width = BrowserSafeApis.options.overrideWidthOption.getValue({ + commandLine: parsedCli, + }).value; RenderInternals.validateConcurrency({ value: concurrency, diff --git a/packages/cli/src/parse-command-line.ts b/packages/cli/src/parse-command-line.ts index 9d8e5e62ce9..abafe1e4484 100644 --- a/packages/cli/src/parse-command-line.ts +++ b/packages/cli/src/parse-command-line.ts @@ -42,6 +42,8 @@ const { userAgentOption, disableWebSecurityOption, ignoreCertificateErrorsOption, + overrideHeightOption, + overrideWidthOption, } = BrowserSafeApis.options; export type CommandLineOptions = { @@ -109,8 +111,8 @@ export type CommandLineOptions = { ['disable-keyboard-shortcuts']: boolean; ['enable-experimental-client-side-rendering']: boolean; muted: boolean; - height: number; - width: number; + [overrideHeightOption.cliFlag]: TypeOfOption; + [overrideWidthOption.cliFlag]: TypeOfOption; runs: number; concurrencies: string; [enforceAudioOption.cliFlag]: TypeOfOption; @@ -141,14 +143,6 @@ export const parseCommandLine = () => { Config.setCachingEnabled(parsedCli['bundle-cache'] !== 'false'); } - if (parsedCli.height) { - Config.overrideHeight(parsedCli.height); - } - - if (parsedCli.width) { - Config.overrideWidth(parsedCli.width); - } - if (parsedCli.frames) { ConfigInternals.setFrameRangeFromCli(parsedCli.frames); } diff --git a/packages/docs/docs/cli/render.mdx b/packages/docs/docs/cli/render.mdx index 2b3a9141682..ad2fb973bdd 100644 --- a/packages/docs/docs/cli/render.mdx +++ b/packages/docs/docs/cli/render.mdx @@ -33,11 +33,11 @@ Inline JSON string isn't supported on Windows shells because it removes the `"` ### `--height` -[Overrides composition height.](/docs/config#overrideheight) + ### `--width` -[Overrides composition width.](/docs/config#overridewidth) + ### `--concurrency` diff --git a/packages/docs/docs/config.mdx b/packages/docs/docs/config.mdx index 348ddfa60c8..13c836dc8f2 100644 --- a/packages/docs/docs/config.mdx +++ b/packages/docs/docs/config.mdx @@ -610,7 +610,7 @@ The [command line flag](/docs/cli/render#--sequence) `--sequence` will take prec ## `overrideHeight()` -Overrides the height of the rendered video. + ```ts twoslash title="remotion.config.ts" import {Config} from '@remotion/cli/config'; @@ -619,11 +619,10 @@ Config.overrideHeight(600); ``` The [command line flag](/docs/cli/render#--height) `--height` will take precedence over this option. -(see h264 validation?) ## `overrideWidth()` -Overrides the width of the rendered video. + ```ts twoslash title="remotion.config.ts" import {Config} from '@remotion/cli/config'; @@ -631,7 +630,7 @@ import {Config} from '@remotion/cli/config'; Config.overrideWidth(900); ``` -The [command line flag](/docs/cli/render#--width) `--width` will take precedence over this option +The [command line flag](/docs/cli/render#--width) `--width` will take precedence over this option. ## `setCrf()` diff --git a/packages/docs/docs/lambda/cli/render.mdx b/packages/docs/docs/lambda/cli/render.mdx index eddc8ed12f4..17c73bd607c 100644 --- a/packages/docs/docs/lambda/cli/render.mdx +++ b/packages/docs/docs/lambda/cli/render.mdx @@ -226,11 +226,11 @@ Sets a webhook secret for the webhook (see above). [`renderMediaOnLambda() -> we ### `--height` -[Overrides composition height.](/docs/config#overrideheight) + ### `--width` -[Overrides composition width.](/docs/config#overridewidth) + ### `--function-name` diff --git a/packages/renderer/src/options/index.tsx b/packages/renderer/src/options/index.tsx index bb1e7dcb3f6..c9eaaca2955 100644 --- a/packages/renderer/src/options/index.tsx +++ b/packages/renderer/src/options/index.tsx @@ -45,6 +45,8 @@ import {offthreadVideoCacheSizeInBytesOption} from './offthreadvideo-cache-size' import {offthreadVideoThreadsOption} from './offthreadvideo-threads'; import {onBrowserDownloadOption} from './on-browser-download'; import type {AnyRemotionOption} from './option'; +import {overrideHeightOption} from './override-height'; +import {overrideWidthOption} from './override-width'; import {overwriteOption} from './overwrite'; import {pixelFormatOption} from './pixel-format'; import {preferLosslessAudioOption} from './prefer-lossless'; @@ -133,6 +135,8 @@ export const allOptions = { stillImageFormatOption, userAgentOption, videoImageFormatOption, + overrideHeightOption, + overrideWidthOption, }; export type AvailableOptions = keyof typeof allOptions; diff --git a/packages/renderer/src/options/override-height.tsx b/packages/renderer/src/options/override-height.tsx new file mode 100644 index 00000000000..21bd3adaa43 --- /dev/null +++ b/packages/renderer/src/options/override-height.tsx @@ -0,0 +1,51 @@ +import type {AnyRemotionOption} from './option'; + +let currentHeight: number | null = null; + +const cliFlag = 'height' as const; + +export const overrideHeightOption = { + name: 'Override Height', + cliFlag, + description: () => <>Overrides the height of the composition., + ssrName: null, + docLink: 'https://www.remotion.dev/docs/config#overrideheight', + type: null as number | null, + getValue: ({commandLine}) => { + if (commandLine[cliFlag] !== undefined) { + const value = commandLine[cliFlag] as number; + if (typeof value !== 'number') { + throw new TypeError( + `--height must be a number, got ${JSON.stringify(value)}`, + ); + } + + return { + source: 'cli', + value, + }; + } + + if (currentHeight !== null) { + return { + source: 'config', + value: currentHeight, + }; + } + + return { + source: 'default', + value: null, + }; + }, + setConfig: (height) => { + if (typeof height !== 'number') { + throw new TypeError( + `overrideHeight() must receive a number, got ${JSON.stringify(height)}`, + ); + } + + currentHeight = height; + }, + id: cliFlag, +} satisfies AnyRemotionOption; diff --git a/packages/renderer/src/options/override-width.tsx b/packages/renderer/src/options/override-width.tsx new file mode 100644 index 00000000000..52049870d17 --- /dev/null +++ b/packages/renderer/src/options/override-width.tsx @@ -0,0 +1,51 @@ +import type {AnyRemotionOption} from './option'; + +let currentWidth: number | null = null; + +const cliFlag = 'width' as const; + +export const overrideWidthOption = { + name: 'Override Width', + cliFlag, + description: () => <>Overrides the width of the composition., + ssrName: null, + docLink: 'https://www.remotion.dev/docs/config#overridewidth', + type: null as number | null, + getValue: ({commandLine}) => { + if (commandLine[cliFlag] !== undefined) { + const value = commandLine[cliFlag] as number; + if (typeof value !== 'number') { + throw new TypeError( + `--width must be a number, got ${JSON.stringify(value)}`, + ); + } + + return { + source: 'cli', + value, + }; + } + + if (currentWidth !== null) { + return { + source: 'config', + value: currentWidth, + }; + } + + return { + source: 'default', + value: null, + }; + }, + setConfig: (width) => { + if (typeof width !== 'number') { + throw new TypeError( + `overrideWidth() must receive a number, got ${JSON.stringify(width)}`, + ); + } + + currentWidth = width; + }, + id: cliFlag, +} satisfies AnyRemotionOption; From c9ee901fb261ddb7b1bdb0697f191da1d8a3740e Mon Sep 17 00:00:00 2001 From: Ayushman Date: Wed, 18 Feb 2026 12:44:27 +0530 Subject: [PATCH 12/56] fix in #6555 --- packages/studio/src/components/Timeline/Timeline.tsx | 2 +- packages/studio/src/helpers/inject-css.ts | 11 +++-------- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/packages/studio/src/components/Timeline/Timeline.tsx b/packages/studio/src/components/Timeline/Timeline.tsx index f8c44970783..abc55c00115 100644 --- a/packages/studio/src/components/Timeline/Timeline.tsx +++ b/packages/studio/src/components/Timeline/Timeline.tsx @@ -98,7 +98,7 @@ export const Timeline: React.FC = () => {
diff --git a/packages/studio/src/helpers/inject-css.ts b/packages/studio/src/helpers/inject-css.ts index b985fea0c48..38b48773af6 100644 --- a/packages/studio/src/helpers/inject-css.ts +++ b/packages/studio/src/helpers/inject-css.ts @@ -22,17 +22,12 @@ const makeDefaultGlobalCSS = () => { position: static !important; } - #__remotion-studio-container { + + + .timeline-root { user-select: none; -webkit-user-select: none; } - - #__remotion-studio-container input, - #__remotion-studio-container textarea, - #__remotion-studio-container [contenteditable] { - user-select: text; - -webkit-user-select: text; - } .remotion-splitter { user-select: none; From bbea1df6f0578e4cf7e85aded48c9cacd4a95eed Mon Sep 17 00:00:00 2001 From: JonnyBurger Date: Wed, 18 Feb 2026 08:19:14 +0100 Subject: [PATCH 13/56] Add --fps and --duration CLI flags as AnyRemotionOption overrides Adds two new composition override options following the same pattern as --height and --width: `--fps` overrides the composition's FPS and `--duration` overrides its durationInFrames. Propagated through CLI, Lambda, Cloud Run, serverless, and docs. Co-Authored-By: Claude Opus 4.6 --- packages/cli/src/benchmark.ts | 4 ++ packages/cli/src/config/index.ts | 12 +++++ packages/cli/src/get-cli-options.ts | 9 ++++ ...get-composition-with-dimension-override.ts | 7 +++ packages/cli/src/parse-command-line.ts | 4 ++ packages/cli/src/render-flows/render.ts | 8 +++ packages/cli/src/render-flows/still.ts | 8 ++- .../cli/src/render-queue/process-still.ts | 4 +- .../cli/src/render-queue/process-video.ts | 4 +- packages/cli/src/render.tsx | 4 ++ packages/cli/src/still.ts | 6 ++- .../src/api/render-media-on-cloudrun.ts | 12 +++++ .../src/api/render-still-on-cloudrun.ts | 8 +++ .../cloudrun/src/cli/commands/render/index.ts | 8 ++- packages/cloudrun/src/cli/commands/still.ts | 6 +++ .../src/functions/helpers/payloads.ts | 4 ++ .../functions/render-media-single-thread.ts | 2 + .../functions/render-still-single-thread.ts | 2 + packages/docs/docs/cli/render.mdx | 8 +++ packages/docs/docs/config.mdx | 24 +++++++++ packages/docs/docs/lambda/cli/render.mdx | 8 +++ .../lambda-client/src/make-lambda-payload.ts | 10 ++++ .../src/render-media-on-lambda.ts | 4 ++ .../src/render-still-on-lambda.ts | 4 ++ .../src/test/concurrency-payload.test.ts | 4 ++ .../lambda/src/cli/commands/render/render.ts | 8 ++- packages/lambda/src/cli/commands/still.ts | 6 ++- packages/renderer/src/options/index.tsx | 4 ++ .../src/options/override-duration.tsx | 53 +++++++++++++++++++ .../renderer/src/options/override-fps.tsx | 51 ++++++++++++++++++ packages/serverless-client/src/constants.ts | 6 +++ packages/serverless/src/handlers/launch.ts | 2 + packages/serverless/src/handlers/start.ts | 2 + packages/serverless/src/handlers/still.ts | 2 + .../serverless/src/validate-composition.ts | 6 +++ 35 files changed, 305 insertions(+), 9 deletions(-) create mode 100644 packages/renderer/src/options/override-duration.tsx create mode 100644 packages/renderer/src/options/override-fps.tsx diff --git a/packages/cli/src/benchmark.ts b/packages/cli/src/benchmark.ts index db7c313f29a..1cb4c77a617 100644 --- a/packages/cli/src/benchmark.ts +++ b/packages/cli/src/benchmark.ts @@ -211,6 +211,8 @@ export const benchmarkCommand = async ( ffmpegOverride, height, width, + fps, + durationInFrames, concurrency: unparsedConcurrency, } = getCliOptions({ isStill: false, @@ -491,6 +493,8 @@ export const benchmarkCommand = async ( ...composition, width: width ?? composition.width, height: height ?? composition.height, + fps: fps ?? composition.fps, + durationInFrames: durationInFrames ?? composition.durationInFrames, }, crf: configFileCrf ?? null, envVariables, diff --git a/packages/cli/src/config/index.ts b/packages/cli/src/config/index.ts index 7a0ec0c1159..2256a33a51b 100644 --- a/packages/cli/src/config/index.ts +++ b/packages/cli/src/config/index.ts @@ -117,6 +117,8 @@ const { ignoreCertificateErrorsOption, overrideHeightOption, overrideWidthOption, + overrideFpsOption, + overrideDurationOption, } = BrowserSafeApis.options; declare global { @@ -390,6 +392,14 @@ declare global { * Overrides the width of a composition */ readonly overrideWidth: (newWidth: number) => void; + /** + * Overrides the FPS of a composition + */ + readonly overrideFps: (newFps: number) => void; + /** + * Overrides the duration in frames of a composition + */ + readonly overrideDuration: (newDuration: number) => void; /** * Set the ProRes profile. * This method is only valid if the codec has been set to 'prores'. @@ -704,6 +714,8 @@ export const Config: FlatConfig = { setForSeamlessAacConcatenation: forSeamlessAacConcatenationOption.setConfig, overrideHeight: overrideHeightOption.setConfig, overrideWidth: overrideWidthOption.setConfig, + overrideFps: overrideFpsOption.setConfig, + overrideDuration: overrideDurationOption.setConfig, overrideFfmpegCommand: setFfmpegOverrideFunction, setAudioCodec: audioCodecOption.setConfig, setOffthreadVideoCacheSizeInBytes: (size) => { diff --git a/packages/cli/src/get-cli-options.ts b/packages/cli/src/get-cli-options.ts index 8a65ada0ed0..4109df43438 100644 --- a/packages/cli/src/get-cli-options.ts +++ b/packages/cli/src/get-cli-options.ts @@ -70,6 +70,13 @@ export const getCliOptions = (options: { const width = BrowserSafeApis.options.overrideWidthOption.getValue({ commandLine: parsedCli, }).value; + const fps = BrowserSafeApis.options.overrideFpsOption.getValue({ + commandLine: parsedCli, + }).value; + const durationInFrames = + BrowserSafeApis.options.overrideDurationOption.getValue({ + commandLine: parsedCli, + }).value; RenderInternals.validateConcurrency({ value: concurrency, @@ -91,5 +98,7 @@ export const getCliOptions = (options: { ffmpegOverride: ConfigInternals.getFfmpegOverrideFunction(), height, width, + fps, + durationInFrames, }; }; diff --git a/packages/cli/src/get-composition-with-dimension-override.ts b/packages/cli/src/get-composition-with-dimension-override.ts index 89ed103933d..5cd2adf6210 100644 --- a/packages/cli/src/get-composition-with-dimension-override.ts +++ b/packages/cli/src/get-composition-with-dimension-override.ts @@ -13,6 +13,8 @@ import {getCompositionId} from './get-composition-id'; export const getCompositionWithDimensionOverride = async ({ height, width, + fps, + durationInFrames, args, compositionIdFromUi, chromiumOptions, @@ -35,6 +37,8 @@ export const getCompositionWithDimensionOverride = async ({ }: { height: number | null; width: number | null; + fps: number | null; + durationInFrames: number | null; args: (string | number)[]; compositionIdFromUi: string | null; timeoutInMilliseconds: number; @@ -88,6 +92,9 @@ export const getCompositionWithDimensionOverride = async ({ ...returnValue.config, height: height ?? returnValue.config.height, width: width ?? returnValue.config.width, + fps: fps ?? returnValue.config.fps, + durationInFrames: + durationInFrames ?? returnValue.config.durationInFrames, }, }; }; diff --git a/packages/cli/src/parse-command-line.ts b/packages/cli/src/parse-command-line.ts index abafe1e4484..a47faa45ce0 100644 --- a/packages/cli/src/parse-command-line.ts +++ b/packages/cli/src/parse-command-line.ts @@ -44,6 +44,8 @@ const { ignoreCertificateErrorsOption, overrideHeightOption, overrideWidthOption, + overrideFpsOption, + overrideDurationOption, } = BrowserSafeApis.options; export type CommandLineOptions = { @@ -113,6 +115,8 @@ export type CommandLineOptions = { muted: boolean; [overrideHeightOption.cliFlag]: TypeOfOption; [overrideWidthOption.cliFlag]: TypeOfOption; + [overrideFpsOption.cliFlag]: TypeOfOption; + [overrideDurationOption.cliFlag]: TypeOfOption; runs: number; concurrencies: string; [enforceAudioOption.cliFlag]: TypeOfOption; diff --git a/packages/cli/src/render-flows/render.ts b/packages/cli/src/render-flows/render.ts index 3f26e857bcc..2aca245daf2 100644 --- a/packages/cli/src/render-flows/render.ts +++ b/packages/cli/src/render-flows/render.ts @@ -80,6 +80,8 @@ export const renderVideoFlow = async ({ port, height, width, + fps, + durationInFrames, remainingArgs, compositionIdFromUi, entryPointReason, @@ -145,6 +147,8 @@ export const renderVideoFlow = async ({ port: number | null; height: number | null; width: number | null; + fps: number | null; + durationInFrames: number | null; remainingArgs: (string | number)[]; compositionIdFromUi: string | null; outputLocationFromUI: string | null; @@ -373,6 +377,8 @@ export const renderVideoFlow = async ({ await getCompositionWithDimensionOverride({ height, width, + fps, + durationInFrames, args: remainingArgs, compositionIdFromUi, browserExecutable, @@ -640,6 +646,8 @@ export const renderVideoFlow = async ({ ...config, width: width ?? config.width, height: height ?? config.height, + fps: fps ?? config.fps, + durationInFrames: durationInFrames ?? config.durationInFrames, }, crf: crf ?? null, envVariables, diff --git a/packages/cli/src/render-flows/still.ts b/packages/cli/src/render-flows/still.ts index 5e2699d1eb4..7521da8cfd9 100644 --- a/packages/cli/src/render-flows/still.ts +++ b/packages/cli/src/render-flows/still.ts @@ -58,6 +58,9 @@ export const renderStillFlow = async ({ chromiumOptions, envVariables, height, + width, + fps, + durationInFrames, serializedInputPropsWithCustomSchema, overwrite, port, @@ -66,7 +69,6 @@ export const renderStillFlow = async ({ jpegQuality, scale, stillFrame, - width, compositionIdFromUi, imageFormatFromUi, logLevel, @@ -104,6 +106,8 @@ export const renderStillFlow = async ({ publicDir: string | null; height: number | null; width: number | null; + fps: number | null; + durationInFrames: number | null; compositionIdFromUi: string | null; imageFormatFromUi: StillImageFormat | null; logLevel: LogLevel; @@ -255,6 +259,8 @@ export const renderStillFlow = async ({ await getCompositionWithDimensionOverride({ height, width, + fps, + durationInFrames, args: remainingArgs, compositionIdFromUi, browserExecutable, diff --git a/packages/cli/src/render-queue/process-still.ts b/packages/cli/src/render-queue/process-still.ts index f212fceef58..904e47ee740 100644 --- a/packages/cli/src/render-queue/process-still.ts +++ b/packages/cli/src/render-queue/process-still.ts @@ -56,6 +56,9 @@ export const processStill = async ({ entryPointReason: 'same as Studio', envVariables: job.envVariables, height: null, + width: null, + fps: null, + durationInFrames: null, fullEntryPoint, serializedInputPropsWithCustomSchema: job.serializedInputPropsWithCustomSchema, @@ -67,7 +70,6 @@ export const processStill = async ({ remainingArgs: [], scale: job.scale, stillFrame: job.frame, - width: null, compositionIdFromUi: job.compositionId, imageFormatFromUi: job.imageFormat, logLevel: job.logLevel, diff --git a/packages/cli/src/render-queue/process-video.ts b/packages/cli/src/render-queue/process-video.ts index 0965f1e0b64..eda077a1cc8 100644 --- a/packages/cli/src/render-queue/process-video.ts +++ b/packages/cli/src/render-queue/process-video.ts @@ -59,6 +59,9 @@ export const processVideoJob = async ({ entryPointReason: 'same as Studio', envVariables: job.envVariables, height: null, + width: null, + fps: null, + durationInFrames: null, fullEntryPoint, serializedInputPropsWithCustomSchema: job.serializedInputPropsWithCustomSchema, @@ -69,7 +72,6 @@ export const processVideoJob = async ({ jpegQuality: job.jpegQuality ?? undefined, remainingArgs: [], scale: job.scale, - width: null, compositionIdFromUi: job.compositionId, logLevel: job.logLevel, onProgress, diff --git a/packages/cli/src/render.tsx b/packages/cli/src/render.tsx index 7bb02756631..940cccd5881 100644 --- a/packages/cli/src/render.tsx +++ b/packages/cli/src/render.tsx @@ -102,6 +102,8 @@ export const render = async ( envVariables, height, width, + fps, + durationInFrames, ffmpegOverride, } = getCliOptions({ isStill: false, @@ -253,6 +255,8 @@ export const render = async ( port: getRendererPortFromConfigFileAndCliFlag(), height, width, + fps, + durationInFrames, remainingArgs, compositionIdFromUi: null, entryPointReason, diff --git a/packages/cli/src/still.ts b/packages/cli/src/still.ts index da126daefaa..ba51b8b1b2f 100644 --- a/packages/cli/src/still.ts +++ b/packages/cli/src/still.ts @@ -73,7 +73,7 @@ export const still = async ( process.exit(1); } - const {envVariables, height, inputProps, stillFrame, width} = getCliOptions({ + const {envVariables, height, inputProps, stillFrame, width, fps, durationInFrames} = getCliOptions({ isStill: true, logLevel, indent: false, @@ -160,6 +160,9 @@ export const still = async ( chromiumOptions, envVariables, height, + width, + fps, + durationInFrames, serializedInputPropsWithCustomSchema: NoReactInternals.serializeJSONWithSpecialTypes({ data: inputProps, @@ -173,7 +176,6 @@ export const still = async ( jpegQuality, scale, stillFrame, - width, compositionIdFromUi: null, imageFormatFromUi: null, logLevel, diff --git a/packages/cloudrun/src/api/render-media-on-cloudrun.ts b/packages/cloudrun/src/api/render-media-on-cloudrun.ts index a14fbcbbd4b..2fcecb5f69c 100644 --- a/packages/cloudrun/src/api/render-media-on-cloudrun.ts +++ b/packages/cloudrun/src/api/render-media-on-cloudrun.ts @@ -56,6 +56,8 @@ type InternalRenderMediaOnCloudrun = { chromiumOptions: ChromiumOptions | undefined; forceWidth: number | null; forceHeight?: number | null; + forceFps?: number | null; + forceDurationInFrames?: number | null; concurrency: number | string | null; preferLossless: boolean | undefined; indent: boolean; @@ -93,6 +95,8 @@ export type RenderMediaOnCloudrunInput = { chromiumOptions?: ChromiumOptions; forceWidth?: number | null; forceHeight?: number | null; + forceFps?: number | null; + forceDurationInFrames?: number | null; concurrency?: number | string | null; preferLossless?: boolean; downloadBehavior?: DownloadBehavior; @@ -135,6 +139,8 @@ const internalRenderMediaOnCloudrunRaw = async ({ muted, forceWidth, forceHeight, + forceFps, + forceDurationInFrames, logLevel, delayRenderTimeoutInMilliseconds, concurrency, @@ -195,6 +201,8 @@ const internalRenderMediaOnCloudrunRaw = async ({ outName, forceWidth, forceHeight, + forceFps, + forceDurationInFrames, type: 'media', logLevel: logLevel ?? 'info', delayRenderTimeoutInMilliseconds: delayRenderTimeoutInMilliseconds ?? null, @@ -334,6 +342,8 @@ export const renderMediaOnCloudrun = ({ muted, forceWidth, forceHeight, + forceFps, + forceDurationInFrames, logLevel, delayRenderTimeoutInMilliseconds, concurrency, @@ -382,6 +392,8 @@ export const renderMediaOnCloudrun = ({ muted: muted ?? false, forceWidth: forceWidth ?? null, forceHeight: forceHeight ?? null, + forceFps: forceFps ?? null, + forceDurationInFrames: forceDurationInFrames ?? null, logLevel: logLevel ?? 'info', delayRenderTimeoutInMilliseconds: delayRenderTimeoutInMilliseconds ?? BrowserSafeApis.DEFAULT_TIMEOUT, diff --git a/packages/cloudrun/src/api/render-still-on-cloudrun.ts b/packages/cloudrun/src/api/render-still-on-cloudrun.ts index 80cc3be6deb..f23c45d9c02 100644 --- a/packages/cloudrun/src/api/render-still-on-cloudrun.ts +++ b/packages/cloudrun/src/api/render-still-on-cloudrun.ts @@ -43,6 +43,8 @@ type OptionalParameters = { chromiumOptions: ChromiumOptions; forceWidth: number | null; forceHeight: number | null; + forceFps: number | null; + forceDurationInFrames: number | null; indent: boolean; downloadBehavior: DownloadBehavior; renderIdOverride: z.infer['renderIdOverride']; @@ -96,6 +98,8 @@ const internalRenderStillOnCloudRun = async ({ scale, forceWidth, forceHeight, + forceFps, + forceDurationInFrames, logLevel, delayRenderTimeoutInMilliseconds, offthreadVideoCacheSizeInBytes, @@ -138,6 +142,8 @@ const internalRenderStillOnCloudRun = async ({ scale, forceWidth, forceHeight, + forceFps, + forceDurationInFrames, frame, type: 'still', logLevel, @@ -239,6 +245,8 @@ export const renderStillOnCloudrun = (options: RenderStillOnCloudrunInput) => { forceBucketName: options.forceBucketName ?? null, forceHeight: options.forceHeight ?? null, forceWidth: options.forceWidth ?? null, + forceFps: options.forceFps ?? null, + forceDurationInFrames: options.forceDurationInFrames ?? null, frame: options.frame ?? 0, imageFormat: options.imageFormat, indent: options.indent ?? false, diff --git a/packages/cloudrun/src/cli/commands/render/index.ts b/packages/cloudrun/src/cli/commands/render/index.ts index 76290749993..7972396fa99 100644 --- a/packages/cloudrun/src/cli/commands/render/index.ts +++ b/packages/cloudrun/src/cli/commands/render/index.ts @@ -90,6 +90,8 @@ export const renderCommand = async ( inputProps, height, width, + fps, + durationInFrames, } = CliInternals.getCliOptions({ isStill: false, logLevel, @@ -191,13 +193,15 @@ export const renderCommand = async ( chromiumOptions, envVariables, height, + width, + fps, + durationInFrames, indent, port: ConfigInternals.getRendererPortFromConfigFileAndCliFlag(), puppeteerInstance: undefined, serveUrlOrWebpackUrl: serveUrl, timeoutInMilliseconds: puppeteerTimeout, logLevel, - width, server: await server, serializedInputPropsWithCustomSchema: NoReactInternals.serializeJSONWithSpecialTypes({ @@ -355,6 +359,8 @@ ${downloadName ? ` Downloaded File = ${downloadName}` : ''} muted, forceWidth: width, forceHeight: height, + forceFps: fps, + forceDurationInFrames: durationInFrames, logLevel, delayRenderTimeoutInMilliseconds: puppeteerTimeout, // Special case: Should not use default local concurrency, or from diff --git a/packages/cloudrun/src/cli/commands/still.ts b/packages/cloudrun/src/cli/commands/still.ts index cc152904e4b..78198a7a4b9 100644 --- a/packages/cloudrun/src/cli/commands/still.ts +++ b/packages/cloudrun/src/cli/commands/still.ts @@ -51,6 +51,8 @@ export const stillCommand = async ( stillFrame, height, width, + fps, + durationInFrames, } = CliInternals.getCliOptions({ isStill: false, logLevel, @@ -155,6 +157,8 @@ export const stillCommand = async ( timeoutInMilliseconds: puppeteerTimeout, height, width, + fps, + durationInFrames, server: await server, offthreadVideoCacheSizeInBytes, binariesDirectory, @@ -249,6 +253,8 @@ ${downloadName ? ` Downloaded File = ${downloadName}` : ''} scale, forceHeight: height, forceWidth: width, + forceFps: fps, + forceDurationInFrames: durationInFrames, forceBucketName, outName, logLevel, diff --git a/packages/cloudrun/src/functions/helpers/payloads.ts b/packages/cloudrun/src/functions/helpers/payloads.ts index 0f0b416dbe6..4a32ab986a4 100644 --- a/packages/cloudrun/src/functions/helpers/payloads.ts +++ b/packages/cloudrun/src/functions/helpers/payloads.ts @@ -37,6 +37,8 @@ export const CloudRunPayload = z.discriminatedUnion('type', [ composition: z.string(), forceHeight: z.number().optional().nullable(), forceWidth: z.number().optional().nullable(), + forceFps: z.number().optional().nullable(), + forceDurationInFrames: z.number().optional().nullable(), codec, serializedInputPropsWithCustomSchema: z.string(), jpegQuality: z.number().nullable(), @@ -92,6 +94,8 @@ export const CloudRunPayload = z.discriminatedUnion('type', [ composition: z.string(), forceHeight: z.number().optional().nullable(), forceWidth: z.number().optional().nullable(), + forceFps: z.number().optional().nullable(), + forceDurationInFrames: z.number().optional().nullable(), serializedInputPropsWithCustomSchema: z.string(), jpegQuality: z.number().optional(), imageFormat: stillImageFormat, diff --git a/packages/cloudrun/src/functions/render-media-single-thread.ts b/packages/cloudrun/src/functions/render-media-single-thread.ts index ce7b801dd60..42538dee64f 100644 --- a/packages/cloudrun/src/functions/render-media-single-thread.ts +++ b/packages/cloudrun/src/functions/render-media-single-thread.ts @@ -139,6 +139,8 @@ export const renderMediaSingleThread = async ( ...composition, height: body.forceHeight ?? composition.height, width: body.forceWidth ?? composition.width, + fps: body.forceFps ?? composition.fps, + durationInFrames: body.forceDurationInFrames ?? composition.durationInFrames, }, serveUrl: body.serveUrl, codec: body.codec, diff --git a/packages/cloudrun/src/functions/render-still-single-thread.ts b/packages/cloudrun/src/functions/render-still-single-thread.ts index 3cc6374d31b..c84864e7903 100644 --- a/packages/cloudrun/src/functions/render-still-single-thread.ts +++ b/packages/cloudrun/src/functions/render-still-single-thread.ts @@ -71,6 +71,8 @@ export const renderStillSingleThread = async ( ...composition, height: body.forceHeight ?? composition.height, width: body.forceWidth ?? composition.width, + fps: body.forceFps ?? composition.fps, + durationInFrames: body.forceDurationInFrames ?? composition.durationInFrames, }, serveUrl: body.serveUrl, output: tempFilePath, diff --git a/packages/docs/docs/cli/render.mdx b/packages/docs/docs/cli/render.mdx index ad2fb973bdd..6168d78a7ba 100644 --- a/packages/docs/docs/cli/render.mdx +++ b/packages/docs/docs/cli/render.mdx @@ -39,6 +39,14 @@ Inline JSON string isn't supported on Windows shells because it removes the `"` +### `--fps` + + + +### `--duration` + + + ### `--concurrency` diff --git a/packages/docs/docs/config.mdx b/packages/docs/docs/config.mdx index 13c836dc8f2..cb0394f61bf 100644 --- a/packages/docs/docs/config.mdx +++ b/packages/docs/docs/config.mdx @@ -632,6 +632,30 @@ Config.overrideWidth(900); The [command line flag](/docs/cli/render#--width) `--width` will take precedence over this option. +## `overrideFps()` + + + +```ts twoslash title="remotion.config.ts" +import {Config} from '@remotion/cli/config'; +// ---cut--- +Config.overrideFps(25); +``` + +The [command line flag](/docs/cli/render#--fps) `--fps` will take precedence over this option. + +## `overrideDuration()` + + + +```ts twoslash title="remotion.config.ts" +import {Config} from '@remotion/cli/config'; +// ---cut--- +Config.overrideDuration(300); +``` + +The [command line flag](/docs/cli/render#--duration) `--duration` will take precedence over this option. + ## `setCrf()` The "Constant Rate Factor" (CRF) of the output. [Use this setting to tell FFmpeg how to trade off size and quality.](/docs/encoding#controlling-quality-using-the-crf-setting) diff --git a/packages/docs/docs/lambda/cli/render.mdx b/packages/docs/docs/lambda/cli/render.mdx index 17c73bd607c..8b9c3947590 100644 --- a/packages/docs/docs/lambda/cli/render.mdx +++ b/packages/docs/docs/lambda/cli/render.mdx @@ -232,6 +232,14 @@ Sets a webhook secret for the webhook (see above). [`renderMediaOnLambda() -> we +### `--fps` + + + +### `--duration` + + + ### `--function-name` Specify the name of the function which should be used to invoke and orchestrate the render. You only need to pass it if there are multiple functions with different configurations. diff --git a/packages/lambda-client/src/make-lambda-payload.ts b/packages/lambda-client/src/make-lambda-payload.ts index b57c5040b0d..c98c2852ca1 100644 --- a/packages/lambda-client/src/make-lambda-payload.ts +++ b/packages/lambda-client/src/make-lambda-payload.ts @@ -78,6 +78,8 @@ export type InnerRenderMediaOnLambdaInput = { webhook: WebhookOption | null; forceWidth: number | null; forceHeight: number | null; + forceFps: number | null; + forceDurationInFrames: number | null; rendererFunctionName: string | null; forceBucketName: string | null; audioCodec: AudioCodec | null; @@ -124,6 +126,8 @@ export const makeLambdaRenderMediaPayload = async ({ audioCodec, forceHeight, forceWidth, + forceFps, + forceDurationInFrames, webhook, videoBitrate, encodingMaxRate, @@ -213,6 +217,8 @@ export const makeLambdaRenderMediaPayload = async ({ webhook: webhook ?? null, forceHeight: forceHeight ?? null, forceWidth: forceWidth ?? null, + forceFps: forceFps ?? null, + forceDurationInFrames: forceDurationInFrames ?? null, bucketName: bucketName ?? null, audioCodec: audioCodec ?? null, type: ServerlessRoutines.start, @@ -267,6 +273,8 @@ export const makeLambdaRenderStillPayload = async ({ downloadBehavior, forceHeight, forceWidth, + forceFps, + forceDurationInFrames, forceBucketName, offthreadVideoCacheSizeInBytes, deleteAfter, @@ -322,6 +330,8 @@ export const makeLambdaRenderStillPayload = async ({ version: VERSION, forceHeight, forceWidth, + forceFps, + forceDurationInFrames, bucketName: forceBucketName, offthreadVideoCacheSizeInBytes, deleteAfter, diff --git a/packages/lambda-client/src/render-media-on-lambda.ts b/packages/lambda-client/src/render-media-on-lambda.ts index 37c6b003843..8d4232f4a34 100644 --- a/packages/lambda-client/src/render-media-on-lambda.ts +++ b/packages/lambda-client/src/render-media-on-lambda.ts @@ -63,6 +63,8 @@ export type RenderMediaOnLambdaInput = { webhook?: WebhookOption | null; forceWidth?: number | null; forceHeight?: number | null; + forceFps?: number | null; + forceDurationInFrames?: number | null; rendererFunctionName?: string | null; forceBucketName?: string; audioCodec?: AudioCodec | null; @@ -174,6 +176,8 @@ export const renderMediaOnLambdaOptionalToRequired = ( forceBucketName: options.forceBucketName ?? null, forceHeight: options.forceHeight ?? null, forceWidth: options.forceWidth ?? null, + forceFps: options.forceFps ?? null, + forceDurationInFrames: options.forceDurationInFrames ?? null, frameRange: options.frameRange ?? null, framesPerLambda: options.framesPerLambda ?? null, functionName: options.functionName, diff --git a/packages/lambda-client/src/render-still-on-lambda.ts b/packages/lambda-client/src/render-still-on-lambda.ts index 49421559bd7..e5a411ef7ea 100644 --- a/packages/lambda-client/src/render-still-on-lambda.ts +++ b/packages/lambda-client/src/render-still-on-lambda.ts @@ -42,6 +42,8 @@ type OptionalParameters = { downloadBehavior: DownloadBehavior; forceWidth: number | null; forceHeight: number | null; + forceFps: number | null; + forceDurationInFrames: number | null; forceBucketName: string | null; onInit: (data: { renderId: string; @@ -187,6 +189,8 @@ export function renderStillOnLambda( forceBucketName: input.forceBucketName ?? null, forceHeight: input.forceHeight ?? null, forceWidth: input.forceWidth ?? null, + forceFps: input.forceFps ?? null, + forceDurationInFrames: input.forceDurationInFrames ?? null, frame: input.frame ?? 0, functionName: input.functionName, imageFormat: input.imageFormat, diff --git a/packages/lambda-client/src/test/concurrency-payload.test.ts b/packages/lambda-client/src/test/concurrency-payload.test.ts index 810a3d684db..cc720cd4e92 100644 --- a/packages/lambda-client/src/test/concurrency-payload.test.ts +++ b/packages/lambda-client/src/test/concurrency-payload.test.ts @@ -40,6 +40,8 @@ test('Should include concurrency field in payload', async () => { webhook: null, forceHeight: null, forceWidth: null, + forceFps: null, + forceDurationInFrames: null, rendererFunctionName: null, forceBucketName: null, audioCodec: null, @@ -100,6 +102,8 @@ test('Should handle null concurrency', async () => { webhook: null, forceHeight: null, forceWidth: null, + forceFps: null, + forceDurationInFrames: null, rendererFunctionName: null, forceBucketName: null, audioCodec: null, diff --git a/packages/lambda/src/cli/commands/render/render.ts b/packages/lambda/src/cli/commands/render/render.ts index f0f5e583736..95c85126dc9 100644 --- a/packages/lambda/src/cli/commands/render/render.ts +++ b/packages/lambda/src/cli/commands/render/render.ts @@ -93,7 +93,7 @@ export const renderCommand = async ({ const region = getAwsRegion(); - const {envVariables, frameRange, inputProps, height, width} = + const {envVariables, frameRange, inputProps, height, width, fps, durationInFrames} = CliInternals.getCliOptions({ isStill: false, logLevel, @@ -248,6 +248,9 @@ export const renderCommand = async ({ chromiumOptions, envVariables, height, + width, + fps, + durationInFrames, indent, serializedInputPropsWithCustomSchema: NoReactInternals.serializeJSONWithSpecialTypes({ @@ -260,7 +263,6 @@ export const renderCommand = async ({ serveUrlOrWebpackUrl: serveUrl, timeoutInMilliseconds, logLevel, - width, server, offthreadVideoCacheSizeInBytes, offthreadVideoThreads, @@ -346,6 +348,8 @@ export const renderCommand = async ({ encodingMaxRate, forceHeight: height, forceWidth: width, + forceFps: fps, + forceDurationInFrames: durationInFrames, webhook: parsedLambdaCli.webhook ? { url: parsedLambdaCli.webhook, diff --git a/packages/lambda/src/cli/commands/still.ts b/packages/lambda/src/cli/commands/still.ts index 103d45670d7..65d5dcee3da 100644 --- a/packages/lambda/src/cli/commands/still.ts +++ b/packages/lambda/src/cli/commands/still.ts @@ -79,7 +79,7 @@ export const stillCommand = async ({ quit(1); } - const {envVariables, inputProps, stillFrame, height, width} = getCliOptions({ + const {envVariables, inputProps, stillFrame, height, width, fps, durationInFrames} = getCliOptions({ isStill: true, logLevel, indent: false, @@ -184,6 +184,8 @@ export const stillCommand = async ({ timeoutInMilliseconds, height, width, + fps, + durationInFrames, server, offthreadVideoCacheSizeInBytes, offthreadVideoThreads, @@ -265,6 +267,8 @@ export const stillCommand = async ({ scale, forceHeight: height, forceWidth: width, + forceFps: fps, + forceDurationInFrames: durationInFrames, onInit: ({cloudWatchLogs, lambdaInsightsUrl}) => { Log.verbose( {indent: false, logLevel}, diff --git a/packages/renderer/src/options/index.tsx b/packages/renderer/src/options/index.tsx index c9eaaca2955..a669ff7611d 100644 --- a/packages/renderer/src/options/index.tsx +++ b/packages/renderer/src/options/index.tsx @@ -45,6 +45,8 @@ import {offthreadVideoCacheSizeInBytesOption} from './offthreadvideo-cache-size' import {offthreadVideoThreadsOption} from './offthreadvideo-threads'; import {onBrowserDownloadOption} from './on-browser-download'; import type {AnyRemotionOption} from './option'; +import {overrideDurationOption} from './override-duration'; +import {overrideFpsOption} from './override-fps'; import {overrideHeightOption} from './override-height'; import {overrideWidthOption} from './override-width'; import {overwriteOption} from './overwrite'; @@ -137,6 +139,8 @@ export const allOptions = { videoImageFormatOption, overrideHeightOption, overrideWidthOption, + overrideFpsOption, + overrideDurationOption, }; export type AvailableOptions = keyof typeof allOptions; diff --git a/packages/renderer/src/options/override-duration.tsx b/packages/renderer/src/options/override-duration.tsx new file mode 100644 index 00000000000..5544a779e8d --- /dev/null +++ b/packages/renderer/src/options/override-duration.tsx @@ -0,0 +1,53 @@ +import type {AnyRemotionOption} from './option'; + +let currentDuration: number | null = null; + +const cliFlag = 'duration' as const; + +export const overrideDurationOption = { + name: 'Override Duration', + cliFlag, + description: () => ( + <>Overrides the duration in frames of the composition. + ), + ssrName: null, + docLink: 'https://www.remotion.dev/docs/config#overrideduration', + type: null as number | null, + getValue: ({commandLine}) => { + if (commandLine[cliFlag] !== undefined) { + const value = commandLine[cliFlag] as number; + if (typeof value !== 'number') { + throw new TypeError( + `--duration must be a number, got ${JSON.stringify(value)}`, + ); + } + + return { + source: 'cli', + value, + }; + } + + if (currentDuration !== null) { + return { + source: 'config', + value: currentDuration, + }; + } + + return { + source: 'default', + value: null, + }; + }, + setConfig: (duration) => { + if (typeof duration !== 'number') { + throw new TypeError( + `overrideDuration() must receive a number, got ${JSON.stringify(duration)}`, + ); + } + + currentDuration = duration; + }, + id: cliFlag, +} satisfies AnyRemotionOption; diff --git a/packages/renderer/src/options/override-fps.tsx b/packages/renderer/src/options/override-fps.tsx new file mode 100644 index 00000000000..33e9c10ab5a --- /dev/null +++ b/packages/renderer/src/options/override-fps.tsx @@ -0,0 +1,51 @@ +import type {AnyRemotionOption} from './option'; + +let currentFps: number | null = null; + +const cliFlag = 'fps' as const; + +export const overrideFpsOption = { + name: 'Override FPS', + cliFlag, + description: () => <>Overrides the frames per second of the composition., + ssrName: null, + docLink: 'https://www.remotion.dev/docs/config#overridefps', + type: null as number | null, + getValue: ({commandLine}) => { + if (commandLine[cliFlag] !== undefined) { + const value = commandLine[cliFlag] as number; + if (typeof value !== 'number') { + throw new TypeError( + `--fps must be a number, got ${JSON.stringify(value)}`, + ); + } + + return { + source: 'cli', + value, + }; + } + + if (currentFps !== null) { + return { + source: 'config', + value: currentFps, + }; + } + + return { + source: 'default', + value: null, + }; + }, + setConfig: (fps) => { + if (typeof fps !== 'number') { + throw new TypeError( + `overrideFps() must receive a number, got ${JSON.stringify(fps)}`, + ); + } + + currentFps = fps; + }, + id: cliFlag, +} satisfies AnyRemotionOption; diff --git a/packages/serverless-client/src/constants.ts b/packages/serverless-client/src/constants.ts index 42333ef1f27..b0ac559b9e1 100644 --- a/packages/serverless-client/src/constants.ts +++ b/packages/serverless-client/src/constants.ts @@ -149,6 +149,8 @@ export type ServerlessStartPayload = { webhook: WebhookOption; forceHeight: number | null; forceWidth: number | null; + forceFps: number | null; + forceDurationInFrames: number | null; bucketName: string | null; offthreadVideoCacheSizeInBytes: number | null; offthreadVideoThreads: number | null; @@ -210,6 +212,8 @@ export type ServerlessPayloads = { webhook: WebhookOption; forceHeight: number | null; forceWidth: number | null; + forceFps: number | null; + forceDurationInFrames: number | null; offthreadVideoCacheSizeInBytes: number | null; offthreadVideoThreads: number | null; mediaCacheSizeInBytes: number | null; @@ -295,6 +299,8 @@ export type ServerlessPayloads = { version: string; forceHeight: number | null; forceWidth: number | null; + forceFps: number | null; + forceDurationInFrames: number | null; bucketName: string | null; offthreadVideoCacheSizeInBytes: number | null; offthreadVideoThreads: number | null; diff --git a/packages/serverless/src/handlers/launch.ts b/packages/serverless/src/handlers/launch.ts index 10516c18d99..994f47dc2e1 100644 --- a/packages/serverless/src/handlers/launch.ts +++ b/packages/serverless/src/handlers/launch.ts @@ -145,6 +145,8 @@ const innerLaunchHandler = async ({ port: null, forceHeight: params.forceHeight, forceWidth: params.forceWidth, + forceFps: params.forceFps ?? null, + forceDurationInFrames: params.forceDurationInFrames ?? null, logLevel: params.logLevel, server: undefined, offthreadVideoCacheSizeInBytes: params.offthreadVideoCacheSizeInBytes, diff --git a/packages/serverless/src/handlers/start.ts b/packages/serverless/src/handlers/start.ts index 78bac250010..43cbb0acf18 100644 --- a/packages/serverless/src/handlers/start.ts +++ b/packages/serverless/src/handlers/start.ts @@ -118,6 +118,8 @@ export const startHandler = async ({ encodingMaxRate: params.encodingMaxRate, forceHeight: params.forceHeight, forceWidth: params.forceWidth, + forceFps: params.forceFps ?? null, + forceDurationInFrames: params.forceDurationInFrames ?? null, rendererFunctionName: params.rendererFunctionName, audioCodec: params.audioCodec, offthreadVideoCacheSizeInBytes: params.offthreadVideoCacheSizeInBytes, diff --git a/packages/serverless/src/handlers/still.ts b/packages/serverless/src/handlers/still.ts index 3b0831a6269..f32e7594755 100644 --- a/packages/serverless/src/handlers/still.ts +++ b/packages/serverless/src/handlers/still.ts @@ -171,6 +171,8 @@ const innerStillHandler = async ( port: null, forceHeight: params.forceHeight, forceWidth: params.forceWidth, + forceFps: params.forceFps ?? null, + forceDurationInFrames: params.forceDurationInFrames ?? null, logLevel: params.logLevel, server, offthreadVideoCacheSizeInBytes: params.offthreadVideoCacheSizeInBytes, diff --git a/packages/serverless/src/validate-composition.ts b/packages/serverless/src/validate-composition.ts index 0553cfeff20..5d0334efdb3 100644 --- a/packages/serverless/src/validate-composition.ts +++ b/packages/serverless/src/validate-composition.ts @@ -29,6 +29,8 @@ type ValidateCompositionOptions = { port: number | null; forceHeight: number | null; forceWidth: number | null; + forceFps: number | null; + forceDurationInFrames: number | null; logLevel: LogLevel; server: RemotionServer | undefined; offthreadVideoCacheSizeInBytes: number | null; @@ -50,6 +52,8 @@ export const validateComposition = async ({ port, forceHeight, forceWidth, + forceFps, + forceDurationInFrames, logLevel, server, offthreadVideoCacheSizeInBytes, @@ -86,6 +90,8 @@ export const validateComposition = async ({ ...comp, height: forceHeight ?? comp.height, width: forceWidth ?? comp.width, + fps: forceFps ?? comp.fps, + durationInFrames: forceDurationInFrames ?? comp.durationInFrames, }; const reason = `of the "" component with the id "${composition}"`; From 42d4b5ce8a0c79e12ef50326ee2d47e2a34376f6 Mon Sep 17 00:00:00 2001 From: JonnyBurger Date: Wed, 18 Feb 2026 08:36:13 +0100 Subject: [PATCH 14/56] Add missing forceDurationInFrames and forceFps to Python and Ruby test payloads Co-Authored-By: Claude Opus 4.6 --- packages/it-tests/src/monorepo/python-package.test.ts | 4 ++++ packages/it-tests/src/monorepo/ruby-package.test.ts | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/packages/it-tests/src/monorepo/python-package.test.ts b/packages/it-tests/src/monorepo/python-package.test.ts index 1124662f82f..c31bb3a7921 100644 --- a/packages/it-tests/src/monorepo/python-package.test.ts +++ b/packages/it-tests/src/monorepo/python-package.test.ts @@ -75,6 +75,8 @@ test('Python package should create the same renderMedia payload as normal Lambda forceBucketName: null, forceHeight: null, forceWidth: null, + forceDurationInFrames: null, + forceFps: null, frameRange: null, framesPerLambda: null, imageFormat: 'jpeg', @@ -181,6 +183,8 @@ test('Python package should create the same renderStill payload as normal Lambda forceBucketName: null, forceHeight: null, forceWidth: null, + forceDurationInFrames: null, + forceFps: null, imageFormat: 'jpeg', jpegQuality: 80, logLevel: 'info', diff --git a/packages/it-tests/src/monorepo/ruby-package.test.ts b/packages/it-tests/src/monorepo/ruby-package.test.ts index d3ca9bfc866..38475b71617 100644 --- a/packages/it-tests/src/monorepo/ruby-package.test.ts +++ b/packages/it-tests/src/monorepo/ruby-package.test.ts @@ -100,6 +100,8 @@ test('Render Media payload', async () => { forceBucketName: null, forceHeight: null, forceWidth: null, + forceDurationInFrames: null, + forceFps: null, frameRange: null, framesPerLambda: null, imageFormat: 'jpeg', @@ -168,6 +170,8 @@ test('Render Still payload', async () => { forceBucketName: null, forceHeight: null, forceWidth: null, + forceDurationInFrames: null, + forceFps: null, imageFormat: 'jpeg', jpegQuality: 80, logLevel: 'info', From 669402cc4ac958602cf7d013bf954610d5532f8f Mon Sep 17 00:00:00 2001 From: JonnyBurger Date: Wed, 18 Feb 2026 08:37:11 +0100 Subject: [PATCH 15/56] ok --- ...get-composition-with-dimension-override.ts | 3 +-- packages/cli/src/still.ts | 10 +++++++++- .../it-tests/src/monorepo/go-package.test.ts | 2 ++ .../it-tests/src/monorepo/php-package.test.ts | 2 ++ .../lambda/src/cli/commands/render/render.ts | 19 +++++++++++++------ packages/lambda/src/cli/commands/still.ts | 10 +++++++++- .../src/options/override-duration.tsx | 4 +--- 7 files changed, 37 insertions(+), 13 deletions(-) diff --git a/packages/cli/src/get-composition-with-dimension-override.ts b/packages/cli/src/get-composition-with-dimension-override.ts index 5cd2adf6210..147e5a0b4e6 100644 --- a/packages/cli/src/get-composition-with-dimension-override.ts +++ b/packages/cli/src/get-composition-with-dimension-override.ts @@ -93,8 +93,7 @@ export const getCompositionWithDimensionOverride = async ({ height: height ?? returnValue.config.height, width: width ?? returnValue.config.width, fps: fps ?? returnValue.config.fps, - durationInFrames: - durationInFrames ?? returnValue.config.durationInFrames, + durationInFrames: durationInFrames ?? returnValue.config.durationInFrames, }, }; }; diff --git a/packages/cli/src/still.ts b/packages/cli/src/still.ts index ba51b8b1b2f..a6669ff9334 100644 --- a/packages/cli/src/still.ts +++ b/packages/cli/src/still.ts @@ -73,7 +73,15 @@ export const still = async ( process.exit(1); } - const {envVariables, height, inputProps, stillFrame, width, fps, durationInFrames} = getCliOptions({ + const { + envVariables, + height, + inputProps, + stillFrame, + width, + fps, + durationInFrames, + } = getCliOptions({ isStill: true, logLevel, indent: false, diff --git a/packages/it-tests/src/monorepo/go-package.test.ts b/packages/it-tests/src/monorepo/go-package.test.ts index 282b880b6ec..073c95c5e1e 100644 --- a/packages/it-tests/src/monorepo/go-package.test.ts +++ b/packages/it-tests/src/monorepo/go-package.test.ts @@ -51,6 +51,8 @@ test( forceBucketName: null, forceHeight: null, forceWidth: null, + forceDurationInFrames: null, + forceFps: null, frameRange: null, framesPerLambda: null, imageFormat: 'jpeg', diff --git a/packages/it-tests/src/monorepo/php-package.test.ts b/packages/it-tests/src/monorepo/php-package.test.ts index f3458f4629b..db318b002e7 100644 --- a/packages/it-tests/src/monorepo/php-package.test.ts +++ b/packages/it-tests/src/monorepo/php-package.test.ts @@ -97,6 +97,8 @@ class Semantic forceBucketName: null, forceHeight: null, forceWidth: null, + forceDurationInFrames: null, + forceFps: null, frameRange: null, framesPerLambda: null, imageFormat: 'jpeg', diff --git a/packages/lambda/src/cli/commands/render/render.ts b/packages/lambda/src/cli/commands/render/render.ts index 95c85126dc9..2ee5686bd68 100644 --- a/packages/lambda/src/cli/commands/render/render.ts +++ b/packages/lambda/src/cli/commands/render/render.ts @@ -93,12 +93,19 @@ export const renderCommand = async ({ const region = getAwsRegion(); - const {envVariables, frameRange, inputProps, height, width, fps, durationInFrames} = - CliInternals.getCliOptions({ - isStill: false, - logLevel, - indent: false, - }); + const { + envVariables, + frameRange, + inputProps, + height, + width, + fps, + durationInFrames, + } = CliInternals.getCliOptions({ + isStill: false, + logLevel, + indent: false, + }); const pixelFormat = pixelFormatOption.getValue({ commandLine: CliInternals.parsedCli, diff --git a/packages/lambda/src/cli/commands/still.ts b/packages/lambda/src/cli/commands/still.ts index 65d5dcee3da..e29ad658efb 100644 --- a/packages/lambda/src/cli/commands/still.ts +++ b/packages/lambda/src/cli/commands/still.ts @@ -79,7 +79,15 @@ export const stillCommand = async ({ quit(1); } - const {envVariables, inputProps, stillFrame, height, width, fps, durationInFrames} = getCliOptions({ + const { + envVariables, + inputProps, + stillFrame, + height, + width, + fps, + durationInFrames, + } = getCliOptions({ isStill: true, logLevel, indent: false, diff --git a/packages/renderer/src/options/override-duration.tsx b/packages/renderer/src/options/override-duration.tsx index 5544a779e8d..0fe8e30ea04 100644 --- a/packages/renderer/src/options/override-duration.tsx +++ b/packages/renderer/src/options/override-duration.tsx @@ -7,9 +7,7 @@ const cliFlag = 'duration' as const; export const overrideDurationOption = { name: 'Override Duration', cliFlag, - description: () => ( - <>Overrides the duration in frames of the composition. - ), + description: () => <>Overrides the duration in frames of the composition., ssrName: null, docLink: 'https://www.remotion.dev/docs/config#overrideduration', type: null as number | null, From aca2232daa0281fab1fa38a25b1d856c8c2ee8a4 Mon Sep 17 00:00:00 2001 From: Jonny Burger Date: Wed, 18 Feb 2026 09:13:16 +0100 Subject: [PATCH 16/56] Studio: Only disable selection on right side of timeline Co-Authored-By: Claude Opus 4.6 --- packages/studio/src/components/Timeline/Timeline.tsx | 2 +- packages/studio/src/components/Timeline/TimelineScrollable.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/studio/src/components/Timeline/Timeline.tsx b/packages/studio/src/components/Timeline/Timeline.tsx index abc55c00115..f8c44970783 100644 --- a/packages/studio/src/components/Timeline/Timeline.tsx +++ b/packages/studio/src/components/Timeline/Timeline.tsx @@ -98,7 +98,7 @@ export const Timeline: React.FC = () => {
diff --git a/packages/studio/src/components/Timeline/TimelineScrollable.tsx b/packages/studio/src/components/Timeline/TimelineScrollable.tsx index ccd994e3ef6..4a1e011627f 100644 --- a/packages/studio/src/components/Timeline/TimelineScrollable.tsx +++ b/packages/studio/src/components/Timeline/TimelineScrollable.tsx @@ -26,7 +26,7 @@ export const TimelineScrollable: React.FC<{
{children}
From 6b8601fb043fe3c30a2132d22f9cbf8ec14a8472 Mon Sep 17 00:00:00 2001 From: Jonny Burger Date: Wed, 18 Feb 2026 09:27:56 +0100 Subject: [PATCH 17/56] Studio: Disable selection only while dragging in timeline Co-Authored-By: Claude Opus 4.6 --- .../src/components/Timeline/TimelineDragHandler.tsx | 8 ++++++++ .../studio/src/components/Timeline/TimelineScrollable.tsx | 2 +- packages/studio/src/helpers/inject-css.ts | 7 ------- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/packages/studio/src/components/Timeline/TimelineDragHandler.tsx b/packages/studio/src/components/Timeline/TimelineDragHandler.tsx index d7a1eebc047..82ea7dcb2f4 100644 --- a/packages/studio/src/components/Timeline/TimelineDragHandler.tsx +++ b/packages/studio/src/components/Timeline/TimelineDragHandler.tsx @@ -175,6 +175,9 @@ const Inner: React.FC = () => { return; } + document.body.style.userSelect = 'none'; + document.body.style.webkitUserSelect = 'none'; + if ((e.target as Node) === inPointerHandle.current) { if (inFrame === null) { throw new Error('expected outframe'); @@ -399,6 +402,8 @@ const Inner: React.FC = () => { const onPointerUpScrubbing = useCallback( (e: PointerEvent) => { stopInterval(); + document.body.style.userSelect = ''; + document.body.style.webkitUserSelect = ''; if (!videoConfig) { return; @@ -434,6 +439,9 @@ const Inner: React.FC = () => { const onPointerUpInOut = useCallback( (e: PointerEvent) => { + document.body.style.userSelect = ''; + document.body.style.webkitUserSelect = ''; + if (!videoConfig) { return; } diff --git a/packages/studio/src/components/Timeline/TimelineScrollable.tsx b/packages/studio/src/components/Timeline/TimelineScrollable.tsx index 4a1e011627f..ccd994e3ef6 100644 --- a/packages/studio/src/components/Timeline/TimelineScrollable.tsx +++ b/packages/studio/src/components/Timeline/TimelineScrollable.tsx @@ -26,7 +26,7 @@ export const TimelineScrollable: React.FC<{
{children}
diff --git a/packages/studio/src/helpers/inject-css.ts b/packages/studio/src/helpers/inject-css.ts index 38b48773af6..0ab60c69ddd 100644 --- a/packages/studio/src/helpers/inject-css.ts +++ b/packages/studio/src/helpers/inject-css.ts @@ -22,13 +22,6 @@ const makeDefaultGlobalCSS = () => { position: static !important; } - - - .timeline-root { - user-select: none; - -webkit-user-select: none; - } - .remotion-splitter { user-select: none; -webkit-user-select: none; From 595c35928323801eab81e9bbeee60a563e0fbafc Mon Sep 17 00:00:00 2001 From: Jonny Burger Date: Wed, 18 Feb 2026 09:39:13 +0100 Subject: [PATCH 18/56] Studio: Remove explicit userSelect from TimeValue Co-Authored-By: Claude Opus 4.6 --- packages/studio/src/components/TimeValue.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/studio/src/components/TimeValue.tsx b/packages/studio/src/components/TimeValue.tsx index 13043d05230..2e09cb7cb9b 100644 --- a/packages/studio/src/components/TimeValue.tsx +++ b/packages/studio/src/components/TimeValue.tsx @@ -21,8 +21,6 @@ const text: React.CSSProperties = { fontVariantNumeric: 'tabular-nums', lineHeight: 1, width: '100%', - userSelect: 'none', - WebkitUserSelect: 'none', }; const time: React.CSSProperties = { From 8773bc36752870b349c4607aa85b1472d1fc1499 Mon Sep 17 00:00:00 2001 From: Jonny Burger Date: Wed, 18 Feb 2026 09:41:30 +0100 Subject: [PATCH 19/56] refine --- packages/studio/src/components/Tabs/index.tsx | 2 ++ packages/studio/src/components/Tabs/vertical.tsx | 2 ++ 2 files changed, 4 insertions(+) diff --git a/packages/studio/src/components/Tabs/index.tsx b/packages/studio/src/components/Tabs/index.tsx index d0aca36d71e..f8beec0003a 100644 --- a/packages/studio/src/components/Tabs/index.tsx +++ b/packages/studio/src/components/Tabs/index.tsx @@ -40,6 +40,8 @@ const selectorButton: React.CSSProperties = { color: 'inherit', alignItems: 'center', cursor: 'default', + userSelect: 'none', + WebkitUserSelect: 'none', }; export const Tab: React.FC<{ diff --git a/packages/studio/src/components/Tabs/vertical.tsx b/packages/studio/src/components/Tabs/vertical.tsx index cb3a5b7d284..cc654a6cd07 100644 --- a/packages/studio/src/components/Tabs/vertical.tsx +++ b/packages/studio/src/components/Tabs/vertical.tsx @@ -16,6 +16,8 @@ const selectorButton: React.CSSProperties = { fontSize: 14, color: 'inherit', alignItems: 'center', + userSelect: 'none', + WebkitUserSelect: 'none', }; export const VerticalTab: React.FC<{ From db531f9ce38b4200457f87fa0ffd502fc463bb00 Mon Sep 17 00:00:00 2001 From: Jonny Burger Date: Wed, 18 Feb 2026 09:16:28 +0100 Subject: [PATCH 20/56] `remotion`: Automatically premount Sequences behind v5 flag Co-Authored-By: Claude Opus 4.6 --- packages/core/src/Sequence.tsx | 22 +++++++++++++++++++--- packages/docs/docs/5-0-migration.mdx | 11 +++++++++-- packages/docs/docs/player/premounting.mdx | 6 ++++-- packages/docs/docs/sequence.mdx | 1 + 4 files changed, 33 insertions(+), 7 deletions(-) diff --git a/packages/core/src/Sequence.tsx b/packages/core/src/Sequence.tsx index 5c4d9442ff3..5b6243ea35e 100644 --- a/packages/core/src/Sequence.tsx +++ b/packages/core/src/Sequence.tsx @@ -22,6 +22,7 @@ import {useVideoConfig} from './use-video-config.js'; import {Freeze} from './freeze.js'; import {useCurrentFrame} from './use-current-frame'; import {useRemotionEnvironment} from './use-remotion-environment.js'; +import {ENABLE_V5_BREAKING_CHANGES} from './v5-flag.js'; export type AbsoluteFillLayout = { layout?: 'absolute-fill'; @@ -311,17 +312,24 @@ const PremountedPostmountedSequenceRefForwardingFunction: React.ForwardRefRender ); } + const {fps} = useVideoConfig(); + const { style: passedStyle, from = 0, durationInFrames = Infinity, - premountFor = 0, - postmountFor = 0, + premountFor: premountForProp, + postmountFor: postmountForProp, styleWhilePremounted, styleWhilePostmounted, ...otherProps } = props; + const premountFor = ENABLE_V5_BREAKING_CHANGES + ? (premountForProp ?? (postmountForProp === undefined ? fps : 0)) + : (premountForProp ?? 0); + const postmountFor = postmountForProp ?? 0; + const endThreshold = Math.ceil(from + durationInFrames - 1); const premountingActive = frame < from && frame >= from - premountFor; const postmountingActive = @@ -382,7 +390,15 @@ const SequenceRefForwardingFunction: React.ForwardRefRenderFunction< const env = useRemotionEnvironment(); if (props.layout !== 'none' && !env.isRendering) { if (props.premountFor || props.postmountFor) { - return ; + return ; + } + + if ( + ENABLE_V5_BREAKING_CHANGES && + props.premountFor === undefined && + props.postmountFor === undefined + ) { + return ; } } diff --git a/packages/docs/docs/5-0-migration.mdx b/packages/docs/docs/5-0-migration.mdx index 06fef8476a9..bf4979942c9 100644 --- a/packages/docs/docs/5-0-migration.mdx +++ b/packages/docs/docs/5-0-migration.mdx @@ -103,11 +103,18 @@ When using `loadFonts()` from `@remotion/google-fonts`, you must now specify whi import {loadFont} from '@remotion/google-fonts/Roboto'; loadFont('normal', { - weights: ['400', '700'], - subsets: ['latin'], + weights: ['400', '700'], + subsets: ['latin'], }); ``` +## Sequences are automatically premounted + +[``](/docs/sequence), [``](/docs/series) and [``](/docs/transitions/transitionseries) components now automatically [premount](/docs/player/premounting) for 1 second (`fps` frames) before they appear. +This prevents black frames that were commonly seen when a `` containing heavy content (e.g. videos, images) appeared. + +**Required action**: If you don't want this behavior for a specific Sequence, opt out by passing [`premountFor={0}`](/docs/sequence#premountfor). + ## License changes Remotion 5.0 has an updated license. View the [license](https://github.com/remotion-dev/remotion/blob/5-0-license/LICENSE.md) here or compare the [changes](https://github.com/remotion-dev/remotion/pull/3750). diff --git a/packages/docs/docs/player/premounting.mdx b/packages/docs/docs/player/premounting.mdx index ef5ed6bbba1..b4be657bcae 100644 --- a/packages/docs/docs/player/premounting.mdx +++ b/packages/docs/docs/player/premounting.mdx @@ -34,8 +34,10 @@ It's time defined by{' '}useCurrentFrame ## Preloading a `` -Use premounting sparingly since having more elements mounted will slow down the page. -Add the [`premountFor`](/docs/sequence#premountfor) prop to the [``](/docs/sequence) component to enable premounting. +From [v5.0](/docs/5-0-migration), all [``](/docs/sequence) components are premounted for 1 second (`fps` frames) by default. +You can opt out by passing `premountFor={0}`. + +In v4, add the [`premountFor`](/docs/sequence#premountfor) prop to the [``](/docs/sequence) component to enable premounting. The number you pass is the number in frames you premount the component for. diff --git a/packages/docs/docs/sequence.mdx b/packages/docs/docs/sequence.mdx index 2821b7d18e1..1382c1b1fe3 100644 --- a/packages/docs/docs/sequence.mdx +++ b/packages/docs/docs/sequence.mdx @@ -219,6 +219,7 @@ A class name to be applied to the container. If `layout` is set to `none`, there ### `premountFor?` [Premount](/docs/player/premounting) the sequence for a set number of frames. +From [v5.0](/docs/5-0-migration), the default value changes from `0` to `fps` (1 second). ### `postmountFor?` From 8b3cb78b695e5ca498acdcc689ded228c02847b0 Mon Sep 17 00:00:00 2001 From: JonnyBurger Date: Wed, 18 Feb 2026 09:44:23 +0100 Subject: [PATCH 21/56] looks good --- packages/docs/docs/cli/render.mdx | 4 ++-- .../docs/cloudrun/rendermediaoncloudrun.mdx | 8 ++++++++ .../docs/cloudrun/renderstilloncloudrun.mdx | 8 ++++++++ packages/docs/docs/config.mdx | 4 ++-- packages/docs/docs/lambda/cli/render.mdx | 4 ++-- .../docs/docs/lambda/rendermediaonlambda.mdx | 8 ++++++++ .../docs/docs/lambda/renderstillonlambda.mdx | 8 ++++++++ .../src/options/override-duration.tsx | 20 +++++++++---------- .../renderer/src/options/override-fps.tsx | 14 +++---------- .../renderer/src/options/override-height.tsx | 14 +++---------- .../renderer/src/options/override-width.tsx | 14 +++---------- 11 files changed, 56 insertions(+), 50 deletions(-) diff --git a/packages/docs/docs/cli/render.mdx b/packages/docs/docs/cli/render.mdx index 6168d78a7ba..c54eb2d826c 100644 --- a/packages/docs/docs/cli/render.mdx +++ b/packages/docs/docs/cli/render.mdx @@ -39,11 +39,11 @@ Inline JSON string isn't supported on Windows shells because it removes the `"` -### `--fps` +### `--fps` -### `--duration` +### `--duration` diff --git a/packages/docs/docs/cloudrun/rendermediaoncloudrun.mdx b/packages/docs/docs/cloudrun/rendermediaoncloudrun.mdx index 76d9ff38582..ab093aa739d 100644 --- a/packages/docs/docs/cloudrun/rendermediaoncloudrun.mdx +++ b/packages/docs/docs/cloudrun/rendermediaoncloudrun.mdx @@ -263,6 +263,14 @@ Overrides default composition width. Overrides default composition height. +### `forceFps?` + +Overrides the default composition FPS. + +### `forceDurationInFrames?` + +Overrides the default composition duration in frames. + ### `logLevel?` diff --git a/packages/docs/docs/cloudrun/renderstilloncloudrun.mdx b/packages/docs/docs/cloudrun/renderstilloncloudrun.mdx index c450960f739..45fa19a04c4 100644 --- a/packages/docs/docs/cloudrun/renderstilloncloudrun.mdx +++ b/packages/docs/docs/cloudrun/renderstilloncloudrun.mdx @@ -141,6 +141,14 @@ Overrides default composition width. Overrides default composition height. +### `forceFps?` + +Overrides the default composition FPS. + +### `forceDurationInFrames?` + +Overrides the default composition duration in frames. + ### `logLevel?` diff --git a/packages/docs/docs/config.mdx b/packages/docs/docs/config.mdx index cb0394f61bf..5782e446832 100644 --- a/packages/docs/docs/config.mdx +++ b/packages/docs/docs/config.mdx @@ -632,7 +632,7 @@ Config.overrideWidth(900); The [command line flag](/docs/cli/render#--width) `--width` will take precedence over this option. -## `overrideFps()` +## `overrideFps()` @@ -644,7 +644,7 @@ Config.overrideFps(25); The [command line flag](/docs/cli/render#--fps) `--fps` will take precedence over this option. -## `overrideDuration()` +## `overrideDuration()` diff --git a/packages/docs/docs/lambda/cli/render.mdx b/packages/docs/docs/lambda/cli/render.mdx index 8b9c3947590..5f625774841 100644 --- a/packages/docs/docs/lambda/cli/render.mdx +++ b/packages/docs/docs/lambda/cli/render.mdx @@ -232,11 +232,11 @@ Sets a webhook secret for the webhook (see above). [`renderMediaOnLambda() -> we -### `--fps` +### `--fps` -### `--duration` +### `--duration` diff --git a/packages/docs/docs/lambda/rendermediaonlambda.mdx b/packages/docs/docs/lambda/rendermediaonlambda.mdx index d51ff979783..4bf1d0d9348 100644 --- a/packages/docs/docs/lambda/rendermediaonlambda.mdx +++ b/packages/docs/docs/lambda/rendermediaonlambda.mdx @@ -135,6 +135,14 @@ Overrides default composition height. Overrides default composition width. +### `forceFps?` + +Overrides the default composition FPS. + +### `forceDurationInFrames?` + +Overrides the default composition duration in frames. + ### `muted?` Disables audio output. See also [`renderMedia() -> muted`](/docs/renderer/render-media#muted). diff --git a/packages/docs/docs/lambda/renderstillonlambda.mdx b/packages/docs/docs/lambda/renderstillonlambda.mdx index 7ee60707938..c0e39debe77 100644 --- a/packages/docs/docs/lambda/renderstillonlambda.mdx +++ b/packages/docs/docs/lambda/renderstillonlambda.mdx @@ -153,6 +153,14 @@ Overrides the default composition height. Overrides the default composition width. +### `forceFps?` + +Overrides the default composition FPS. + +### `forceDurationInFrames?` + +Overrides the default composition duration in frames. + ### `scale?` diff --git a/packages/renderer/src/options/override-duration.tsx b/packages/renderer/src/options/override-duration.tsx index 0fe8e30ea04..a5ab0311395 100644 --- a/packages/renderer/src/options/override-duration.tsx +++ b/packages/renderer/src/options/override-duration.tsx @@ -1,3 +1,4 @@ +import {validateDurationInFrames} from '../validate'; import type {AnyRemotionOption} from './option'; let currentDuration: number | null = null; @@ -14,11 +15,10 @@ export const overrideDurationOption = { getValue: ({commandLine}) => { if (commandLine[cliFlag] !== undefined) { const value = commandLine[cliFlag] as number; - if (typeof value !== 'number') { - throw new TypeError( - `--duration must be a number, got ${JSON.stringify(value)}`, - ); - } + validateDurationInFrames(value, { + component: 'in --duration flag', + allowFloats: false, + }); return { source: 'cli', @@ -39,12 +39,10 @@ export const overrideDurationOption = { }; }, setConfig: (duration) => { - if (typeof duration !== 'number') { - throw new TypeError( - `overrideDuration() must receive a number, got ${JSON.stringify(duration)}`, - ); - } - + validateDurationInFrames(duration, { + component: 'in Config.overrideDuration()', + allowFloats: false, + }); currentDuration = duration; }, id: cliFlag, diff --git a/packages/renderer/src/options/override-fps.tsx b/packages/renderer/src/options/override-fps.tsx index 33e9c10ab5a..927125f0b35 100644 --- a/packages/renderer/src/options/override-fps.tsx +++ b/packages/renderer/src/options/override-fps.tsx @@ -1,3 +1,4 @@ +import {validateFps} from '../validate'; import type {AnyRemotionOption} from './option'; let currentFps: number | null = null; @@ -14,11 +15,7 @@ export const overrideFpsOption = { getValue: ({commandLine}) => { if (commandLine[cliFlag] !== undefined) { const value = commandLine[cliFlag] as number; - if (typeof value !== 'number') { - throw new TypeError( - `--fps must be a number, got ${JSON.stringify(value)}`, - ); - } + validateFps(value, 'in --fps flag', false); return { source: 'cli', @@ -39,12 +36,7 @@ export const overrideFpsOption = { }; }, setConfig: (fps) => { - if (typeof fps !== 'number') { - throw new TypeError( - `overrideFps() must receive a number, got ${JSON.stringify(fps)}`, - ); - } - + validateFps(fps, 'in Config.overrideFps()', false); currentFps = fps; }, id: cliFlag, diff --git a/packages/renderer/src/options/override-height.tsx b/packages/renderer/src/options/override-height.tsx index 21bd3adaa43..b49be4230b4 100644 --- a/packages/renderer/src/options/override-height.tsx +++ b/packages/renderer/src/options/override-height.tsx @@ -1,3 +1,4 @@ +import {validateDimension} from '../validate'; import type {AnyRemotionOption} from './option'; let currentHeight: number | null = null; @@ -14,11 +15,7 @@ export const overrideHeightOption = { getValue: ({commandLine}) => { if (commandLine[cliFlag] !== undefined) { const value = commandLine[cliFlag] as number; - if (typeof value !== 'number') { - throw new TypeError( - `--height must be a number, got ${JSON.stringify(value)}`, - ); - } + validateDimension(value, 'height', 'in --height flag'); return { source: 'cli', @@ -39,12 +36,7 @@ export const overrideHeightOption = { }; }, setConfig: (height) => { - if (typeof height !== 'number') { - throw new TypeError( - `overrideHeight() must receive a number, got ${JSON.stringify(height)}`, - ); - } - + validateDimension(height, 'height', 'in Config.overrideHeight()'); currentHeight = height; }, id: cliFlag, diff --git a/packages/renderer/src/options/override-width.tsx b/packages/renderer/src/options/override-width.tsx index 52049870d17..d48d91a7dcb 100644 --- a/packages/renderer/src/options/override-width.tsx +++ b/packages/renderer/src/options/override-width.tsx @@ -1,3 +1,4 @@ +import {validateDimension} from '../validate'; import type {AnyRemotionOption} from './option'; let currentWidth: number | null = null; @@ -14,11 +15,7 @@ export const overrideWidthOption = { getValue: ({commandLine}) => { if (commandLine[cliFlag] !== undefined) { const value = commandLine[cliFlag] as number; - if (typeof value !== 'number') { - throw new TypeError( - `--width must be a number, got ${JSON.stringify(value)}`, - ); - } + validateDimension(value, 'width', 'in --width flag'); return { source: 'cli', @@ -39,12 +36,7 @@ export const overrideWidthOption = { }; }, setConfig: (width) => { - if (typeof width !== 'number') { - throw new TypeError( - `overrideWidth() must receive a number, got ${JSON.stringify(width)}`, - ); - } - + validateDimension(width, 'width', 'in Config.overrideWidth()'); currentWidth = width; }, id: cliFlag, From e79d8e4020ff8c62d057b30bb033df013a8543f1 Mon Sep 17 00:00:00 2001 From: JonnyBurger Date: Wed, 18 Feb 2026 09:46:02 +0100 Subject: [PATCH 22/56] Update prewarm-twoslash.ts --- packages/docs/prewarm-twoslash.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/docs/prewarm-twoslash.ts b/packages/docs/prewarm-twoslash.ts index 592bddba2ad..11e84fbfce2 100644 --- a/packages/docs/prewarm-twoslash.ts +++ b/packages/docs/prewarm-twoslash.ts @@ -104,7 +104,7 @@ function extractTwoslashBlocks( while ((match = codeBlockRegex.exec(content)) !== null) { const lang = match[1]; const meta = match[2].trim(); - const code = match[3]; + const code = match[3].replace(/\n$/, ''); if (lang === 'twoslash') { const includeMatch = meta.match(/include\s+(\S+)/); From 2d0b223ed19f2c03475ce2c0f54ac4af3a1f3467 Mon Sep 17 00:00:00 2001 From: JonnyBurger Date: Wed, 18 Feb 2026 09:46:14 +0100 Subject: [PATCH 23/56] Add AvailableFrom flags for fps/duration options, use proper validation - Add AvailableFrom v="4.0.424" to config, CLI, Lambda, and CloudRun docs - Add forceFps/forceDurationInFrames entries to Lambda and CloudRun API docs - Use validateFps, validateDurationInFrames, validateDimension in option setConfig/getValue - Add missing forceFps/forceDurationInFrames to Lambda test payloads Co-Authored-By: Claude Opus 4.6 --- .../lambda/src/test/integration/renders/old-version.test.ts | 2 ++ packages/lambda/src/test/integration/webhooks.test.ts | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/packages/lambda/src/test/integration/renders/old-version.test.ts b/packages/lambda/src/test/integration/renders/old-version.test.ts index c430f925e6d..1daad0951da 100644 --- a/packages/lambda/src/test/integration/renders/old-version.test.ts +++ b/packages/lambda/src/test/integration/renders/old-version.test.ts @@ -49,6 +49,8 @@ test( encodingMaxRate: null, forceHeight: null, forceWidth: null, + forceFps: null, + forceDurationInFrames: null, rendererFunctionName: null, bucketName: 'remotion-dev-render', audioCodec: null, diff --git a/packages/lambda/src/test/integration/webhooks.test.ts b/packages/lambda/src/test/integration/webhooks.test.ts index 330d4c283b2..3cdb4b697b9 100644 --- a/packages/lambda/src/test/integration/webhooks.test.ts +++ b/packages/lambda/src/test/integration/webhooks.test.ts @@ -84,6 +84,8 @@ test( encodingMaxRate: null, forceHeight: null, forceWidth: null, + forceFps: null, + forceDurationInFrames: null, rendererFunctionName: null, bucketName: null, audioCodec: null, @@ -210,6 +212,8 @@ test( renderId: 'abc', forceHeight: null, forceWidth: null, + forceFps: null, + forceDurationInFrames: null, rendererFunctionName: null, audioCodec: null, deleteAfter: null, From 0675b1bfb6ce4d33f9d4041b0d12d8ba0c2769e4 Mon Sep 17 00:00:00 2001 From: Jonny Burger Date: Wed, 18 Feb 2026 10:05:46 +0100 Subject: [PATCH 24/56] `remotion`: Add audio variant to tag switching snippet Co-Authored-By: Claude Opus 4.6 --- packages/docs/docs/video-tags.mdx | 40 ++++++++++++++++++++++++------- 1 file changed, 32 insertions(+), 8 deletions(-) diff --git a/packages/docs/docs/video-tags.mdx b/packages/docs/docs/video-tags.mdx index e72d138da2d..577f10a1b1e 100644 --- a/packages/docs/docs/video-tags.mdx +++ b/packages/docs/docs/video-tags.mdx @@ -35,19 +35,43 @@ This page offers a comparison to help you decide which tag to use. Use the [`useRemotionEnvironment()`](/docs/use-remotion-environment) hook to render a different component in preview and rendering. ```tsx twoslash title="OffthreadVideo during preview, @remotion/media during rendering" -import {useRemotionEnvironment, OffthreadVideo, RemotionOffthreadVideoProps} from 'remotion'; +import { + useRemotionEnvironment, + OffthreadVideo, + RemotionOffthreadVideoProps, +} from 'remotion'; import {Video, VideoProps} from '@remotion/media'; const OffthreadWhileRenderingRefForwardingFunction: React.FC<{ - offthreadVideoProps: RemotionOffthreadVideoProps; - videoProps: VideoProps; + offthreadVideoProps: RemotionOffthreadVideoProps; + videoProps: VideoProps; }> = ({offthreadVideoProps, videoProps}) => { - const env = useRemotionEnvironment(); + const env = useRemotionEnvironment(); - if (!env.isRendering) { - return ; - } + if (!env.isRendering) { + return ; + } - return