From 506e5c0499fe5d6d0e88884359f9b08798d4f75a Mon Sep 17 00:00:00 2001 From: Jonny Burger Date: Thu, 19 Feb 2026 13:09:12 +0100 Subject: [PATCH 01/33] `@remotion/bundler`: Add `--rspack` flag for optional Rspack bundling Add Rspack as an optional alternative bundler controlled by `--rspack` flag (e.g., `npx remotion studio --rspack`). Webpack remains the default. - Add `@rspack/core` and `@rspack/plugin-react-refresh` dependencies - Create `rspack-config.ts` paralleling webpack-config.ts with SWC loader - Define `--rspack` CLI option and thread it through studio/bundle paths - Branch compiler creation in start-server.ts and bundle.ts based on flag Co-Authored-By: Claude Opus 4.6 --- bun.lock | 16 +- packages/bundler/package.json | 2 + packages/bundler/src/bundle.ts | 80 ++++-- packages/bundler/src/index.ts | 3 + packages/bundler/src/rspack-config.ts | 255 ++++++++++++++++++ packages/cli/src/parse-command-line.ts | 2 + packages/cli/src/parsed-cli.ts | 1 + packages/cli/src/setup-cache.ts | 1 + packages/cli/src/studio.ts | 4 + packages/cloudrun/src/api/deploy-site.ts | 1 + packages/lambda/src/api/deploy-site.ts | 1 + packages/renderer/src/options/index.tsx | 2 + packages/renderer/src/options/rspack.tsx | 33 +++ .../dev-middleware/setup-hooks.ts | 5 +- .../src/preview-server/start-server.ts | 18 +- packages/studio-server/src/start-studio.ts | 3 + 16 files changed, 405 insertions(+), 22 deletions(-) create mode 100644 packages/bundler/src/rspack-config.ts create mode 100644 packages/renderer/src/options/rspack.tsx diff --git a/bun.lock b/bun.lock index b1c4837e7ea..4490d8df732 100644 --- a/bun.lock +++ b/bun.lock @@ -97,6 +97,8 @@ "@remotion/media-parser": "workspace:*", "@remotion/studio": "workspace:*", "@remotion/studio-shared": "workspace:*", + "@rspack/core": "1.7.6", + "@rspack/plugin-react-refresh": "1.6.1", "css-loader": "5.2.7", "esbuild": "0.25.0", "react-refresh": "0.9.0", @@ -3849,6 +3851,8 @@ "@rspack/lite-tapable": ["@rspack/lite-tapable@1.1.0", "", {}, "sha512-E2B0JhYFmVAwdDiG14+DW0Di4Ze4Jg10Pc4/lILUrd5DRCaklduz2OvJ5HYQ6G+hd+WTzqQb3QnDNfK4yvAFYw=="], + "@rspack/plugin-react-refresh": ["@rspack/plugin-react-refresh@1.6.1", "", { "dependencies": { "error-stack-parser": "^2.1.4", "html-entities": "^2.6.0" }, "peerDependencies": { "react-refresh": ">=0.10.0 <1.0.0", "webpack-hot-middleware": "2.x" }, "optionalPeers": ["webpack-hot-middleware"] }, "sha512-eqqW5645VG3CzGzFgNg5HqNdHVXY+567PGjtDhhrM8t67caxmsSzRmT5qfoEIfBcGgFkH9vEg7kzXwmCYQdQDw=="], + "@rtsao/scc": ["@rtsao/scc@1.1.0", "", {}, "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g=="], "@rushstack/eslint-patch": ["@rushstack/eslint-patch@1.10.4", "", {}, "sha512-WJgX9nzTqknM393q1QJDJmoW28kUfEnybeTfVNcNAPnIx210RXm2DiXiHzfNPJNIUUb1tJnz/l4QGtJ30PgWmA=="], @@ -5387,6 +5391,8 @@ "error-ex": ["error-ex@1.3.2", "", { "dependencies": { "is-arrayish": "0.2.1" } }, "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g=="], + "error-stack-parser": ["error-stack-parser@2.1.4", "", { "dependencies": { "stackframe": "^1.3.4" } }, "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ=="], + "es-abstract": ["es-abstract@1.23.9", "", { "dependencies": { "array-buffer-byte-length": "1.0.2", "arraybuffer.prototype.slice": "1.0.4", "available-typed-arrays": "1.0.7", "call-bind": "1.0.8", "call-bound": "1.0.3", "data-view-buffer": "1.0.2", "data-view-byte-length": "1.0.2", "data-view-byte-offset": "1.0.1", "es-define-property": "1.0.1", "es-errors": "1.3.0", "es-object-atoms": "1.0.0", "es-set-tostringtag": "2.1.0", "es-to-primitive": "1.3.0", "function.prototype.name": "1.1.8", "get-intrinsic": "1.2.7", "get-proto": "1.0.1", "get-symbol-description": "1.1.0", "globalthis": "1.0.4", "gopd": "1.2.0", "has-property-descriptors": "1.0.2", "has-proto": "1.2.0", "has-symbols": "1.1.0", "hasown": "2.0.2", "internal-slot": "1.1.0", "is-array-buffer": "3.0.5", "is-callable": "1.2.7", "is-data-view": "1.0.2", "is-regex": "1.2.1", "is-shared-array-buffer": "1.0.4", "is-string": "1.1.1", "is-typed-array": "1.1.15", "is-weakref": "1.1.0", "math-intrinsics": "1.1.0", "object-inspect": "1.13.3", "object-keys": "1.1.1", "object.assign": "4.1.7", "own-keys": "1.0.1", "regexp.prototype.flags": "1.5.4", "safe-array-concat": "1.1.3", "safe-push-apply": "1.0.0", "safe-regex-test": "1.1.0", "set-proto": "1.0.0", "string.prototype.trim": "1.2.10", "string.prototype.trimend": "1.0.9", "string.prototype.trimstart": "1.0.8", "typed-array-buffer": "1.0.3", "typed-array-byte-length": "1.0.3", "typed-array-byte-offset": "1.0.4", "typed-array-length": "1.0.7", "unbox-primitive": "1.1.0", "which-typed-array": "1.1.18" } }, "sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA=="], "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], @@ -5785,7 +5791,7 @@ "hpack.js": ["hpack.js@2.1.6", "", { "dependencies": { "inherits": "2.0.4", "obuf": "1.1.2", "readable-stream": "2.3.8", "wbuf": "1.7.3" } }, "sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ=="], - "html-entities": ["html-entities@2.5.2", "", {}, "sha512-K//PSRMQk4FZ78Kyau+mZurHn3FH0Vwr+H36eE0rPbeYkRRi9YxceYPhuN60UwWorxyKHhqoAJl2OFKa4BVtaA=="], + "html-entities": ["html-entities@2.6.0", "", {}, "sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ=="], "html-escaper": ["html-escaper@3.0.3", "", {}, "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ=="], @@ -7237,6 +7243,8 @@ "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], + "stackframe": ["stackframe@1.3.4", "", {}, "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw=="], + "state-local": ["state-local@1.0.7", "", {}, "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w=="], "stats-gl": ["stats-gl@2.4.2", "", { "peerDependencies": { "@types/three": "0.170.0", "three": "0.178.0" } }, "sha512-g5O9B0hm9CvnM36+v7SFl39T7hmAlv541tU81ME8YeSb3i1CIP5/QdDeSB3A0la0bKNHpxpwxOVRo2wFTYEosQ=="], @@ -8451,6 +8459,8 @@ "@google-cloud/common/google-auth-library": ["google-auth-library@9.11.0", "", { "dependencies": { "base64-js": "1.5.1", "ecdsa-sig-formatter": "1.0.11", "gaxios": "6.6.0", "gcp-metadata": "6.1.0", "gtoken": "7.1.0", "jws": "4.0.0" } }, "sha512-epX3ww/mNnhl6tL45EQ/oixsY8JLEgUFoT4A5E/5iAR4esld9Kqv6IJGk7EmGuOgDvaarwF95hU2+v7Irql9lw=="], + "@google-cloud/common/html-entities": ["html-entities@2.5.2", "", {}, "sha512-K//PSRMQk4FZ78Kyau+mZurHn3FH0Vwr+H36eE0rPbeYkRRi9YxceYPhuN60UwWorxyKHhqoAJl2OFKa4BVtaA=="], + "@google-cloud/functions-framework/@types/express": ["@types/express@5.0.1", "", { "dependencies": { "@types/body-parser": "1.19.1", "@types/express-serve-static-core": "5.0.4", "@types/serve-static": "1.13.10" } }, "sha512-UZUw8vjpWFXuDnjFTh7/5c2TWDlQqeXHi6hcN7F2XSVT5P+WmUnnbFS3KA6Jnc6IsEqI2qCVu2bK0R0J4A8ZQQ=="], "@google-cloud/functions-framework/express": ["express@4.21.2", "", { "dependencies": { "accepts": "1.3.8", "array-flatten": "1.1.1", "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "1.0.5", "cookie": "0.7.1", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "2.0.0", "escape-html": "1.0.3", "etag": "1.8.1", "finalhandler": "1.3.1", "fresh": "0.5.2", "http-errors": "2.0.0", "merge-descriptors": "1.0.3", "methods": "1.1.2", "on-finished": "2.4.1", "parseurl": "1.3.3", "path-to-regexp": "0.1.12", "proxy-addr": "2.0.7", "qs": "6.13.0", "range-parser": "1.2.1", "safe-buffer": "5.2.1", "send": "0.19.0", "serve-static": "1.16.2", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "1.6.18", "utils-merge": "1.0.1", "vary": "1.1.2" } }, "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA=="], @@ -8465,6 +8475,8 @@ "@google-cloud/storage/google-auth-library": ["google-auth-library@9.11.0", "", { "dependencies": { "base64-js": "1.5.1", "ecdsa-sig-formatter": "1.0.11", "gaxios": "6.6.0", "gcp-metadata": "6.1.0", "gtoken": "7.1.0", "jws": "4.0.0" } }, "sha512-epX3ww/mNnhl6tL45EQ/oixsY8JLEgUFoT4A5E/5iAR4esld9Kqv6IJGk7EmGuOgDvaarwF95hU2+v7Irql9lw=="], + "@google-cloud/storage/html-entities": ["html-entities@2.5.2", "", {}, "sha512-K//PSRMQk4FZ78Kyau+mZurHn3FH0Vwr+H36eE0rPbeYkRRi9YxceYPhuN60UwWorxyKHhqoAJl2OFKa4BVtaA=="], + "@google-cloud/storage/p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], "@google-cloud/text-to-speech/google-gax": ["google-gax@3.6.1", "", { "dependencies": { "@grpc/grpc-js": "1.8.22", "@grpc/proto-loader": "0.7.13", "@types/long": "4.0.2", "@types/rimraf": "3.0.2", "abort-controller": "3.0.0", "duplexify": "4.1.3", "fast-text-encoding": "1.0.6", "google-auth-library": "8.7.0", "is-stream-ended": "0.1.4", "node-fetch": "2.6.9", "object-hash": "3.0.0", "proto3-json-serializer": "1.1.1", "protobufjs": "7.2.4", "protobufjs-cli": "1.1.1", "retry-request": "5.0.2" }, "bin": { "compileProtos": "build/tools/compileProtos.js", "minifyProtoJson": "build/tools/minify.js" } }, "sha512-g/lcUjGcB6DSw2HxgEmCDOrI/CByOwqRvsuUvNalHUK2iPPPlmAIpbMbl62u0YufGMr8zgE3JL7th6dCb1Ry+w=="], @@ -8939,6 +8951,8 @@ "@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], + "@rspack/plugin-react-refresh/react-refresh": ["react-refresh@0.14.2", "", {}, "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA=="], + "@shopify/react-native-skia/react": ["react@19.0.0", "", {}, "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ=="], "@smithy/abort-controller/@smithy/types": ["@smithy/types@4.2.0", "", { "dependencies": { "tslib": "2.8.1" } }, "sha512-7eMk09zQKCO+E/ivsjQv+fDlOupcFUCSC/L2YUPgwhvowVGWbPQHjEFcmjt7QQ4ra5lyowS92SV53Zc6XD4+fg=="], diff --git a/packages/bundler/package.json b/packages/bundler/package.json index 36a501f5b2a..1c84faebc67 100644 --- a/packages/bundler/package.json +++ b/packages/bundler/package.json @@ -19,6 +19,8 @@ "author": "Jonny Burger ", "license": "SEE LICENSE IN LICENSE.md", "dependencies": { + "@rspack/core": "1.7.6", + "@rspack/plugin-react-refresh": "1.6.1", "css-loader": "5.2.7", "esbuild": "0.25.0", "react-refresh": "0.9.0", diff --git a/packages/bundler/src/bundle.ts b/packages/bundler/src/bundle.ts index 84c7945b2d7..c2c520480ab 100644 --- a/packages/bundler/src/bundle.ts +++ b/packages/bundler/src/bundle.ts @@ -9,6 +9,7 @@ import webpack from 'webpack'; import {copyDir} from './copy-dir'; import {indexHtml} from './index-html'; import {readRecursively} from './read-recursively'; +import {rspackConfig} from './rspack-config'; import type {WebpackOverrideFn} from './webpack-config'; import {webpackConfig} from './webpack-config'; @@ -52,6 +53,7 @@ export type MandatoryLegacyBundleOptions = { onSymlinkDetected: (path: string) => void; keyboardShortcutsEnabled: boolean; askAIEnabled: boolean; + rspack: boolean; }; export type LegacyBundleOptions = Partial; @@ -75,7 +77,7 @@ export const getConfig = ({ onProgress?: (progress: number) => void; options?: LegacyBundleOptions; }) => { - return webpackConfig({ + const configArgs = { entry: path.join( require.resolve('@remotion/studio/renderEntry'), '..', @@ -84,9 +86,9 @@ export const getConfig = ({ ), userDefinedComponent: entryPoint, outDir, - environment: 'production', + environment: 'production' as const, webpackOverride: options?.webpackOverride ?? ((f) => f), - onProgress: (p) => { + onProgress: (p: number) => { onProgress?.(p); }, enableCaching: options?.enableCaching ?? true, @@ -97,7 +99,13 @@ export const getConfig = ({ poll: null, experimentalClientSideRenderingEnabled, askAIEnabled: options?.askAIEnabled ?? true, - }); + }; + + if (options?.rspack) { + return rspackConfig(configArgs); + } + + return webpackConfig(configArgs); }; type NewBundleOptions = { @@ -229,20 +237,59 @@ export const internalBundle = async ( actualArgs.experimentalClientSideRenderingEnabled, }); - const output = (await promisified([config])) as - | webpack.MultiStats - | undefined; - if (isMainThread) { - process.chdir(currentCwd); - } + if (actualArgs.rspack) { + const {rspack: rspackFn} = require('@rspack/core'); + const rspackCompiler = rspackFn(config); + const rspackOutput = await new Promise<{ + toJson: (opts: unknown) => { + errors?: Array<{message: string; details: string}>; + }; + }>((resolve, reject) => { + rspackCompiler.run( + ( + err: Error | null, + stats: { + toJson: (opts: unknown) => { + errors?: Array<{message: string; details: string}>; + }; + }, + ) => { + if (err) { + reject(err); + return; + } + + rspackCompiler.close(() => { + resolve(stats); + }); + }, + ); + }); - if (!output) { - throw new Error('Expected webpack output'); - } + if (isMainThread) { + process.chdir(currentCwd); + } + + const {errors} = rspackOutput.toJson({}); + if (errors !== undefined && errors.length > 0) { + throw new Error(errors[0].message + '\n' + errors[0].details); + } + } else { + const output = (await promisified([config as webpack.Configuration])) as + | webpack.MultiStats + | undefined; + if (isMainThread) { + process.chdir(currentCwd); + } + + if (!output) { + throw new Error('Expected webpack output'); + } - const {errors} = output.toJson(); - if (errors !== undefined && errors.length > 0) { - throw new Error(errors[0].message + '\n' + errors[0].details); + const {errors} = output.toJson(); + if (errors !== undefined && errors.length > 0) { + throw new Error(errors[0].message + '\n' + errors[0].details); + } } const publicPath = actualArgs?.publicPath ?? '/'; @@ -368,6 +415,7 @@ export async function bundle(...args: Arguments): Promise { renderDefaults: actualArgs.renderDefaults ?? null, askAIEnabled: actualArgs.askAIEnabled ?? true, keyboardShortcutsEnabled: actualArgs.keyboardShortcutsEnabled ?? true, + rspack: actualArgs.rspack ?? false, }); return result; } diff --git a/packages/bundler/src/index.ts b/packages/bundler/src/index.ts index e9e7286d870..7cbd0de2324 100644 --- a/packages/bundler/src/index.ts +++ b/packages/bundler/src/index.ts @@ -1,6 +1,7 @@ import {findClosestFolderWithItem, getConfig, internalBundle} from './bundle'; import {indexHtml} from './index-html'; import {readRecursively} from './read-recursively'; +import {createRspackCompiler, rspackConfig} from './rspack-config'; import {cacheExists, clearCache} from './webpack-cache'; import {webpackConfig} from './webpack-config'; import esbuild = require('esbuild'); @@ -9,6 +10,8 @@ import webpack = require('webpack'); export const BundlerInternals = { esbuild, webpackConfig, + rspackConfig, + createRspackCompiler, indexHtml, cacheExists, clearCache, diff --git a/packages/bundler/src/rspack-config.ts b/packages/bundler/src/rspack-config.ts new file mode 100644 index 00000000000..04847d3a97a --- /dev/null +++ b/packages/bundler/src/rspack-config.ts @@ -0,0 +1,255 @@ +import type {Configuration} from '@rspack/core'; +import {DefinePlugin, ProgressPlugin, rspack} from '@rspack/core'; +import ReactRefreshPlugin from '@rspack/plugin-react-refresh'; +import {createHash} from 'node:crypto'; +import path from 'node:path'; +import ReactDOM from 'react-dom'; +import {NoReactInternals} from 'remotion/no-react'; +import {jsonStringifyWithCircularReferences} from './stringify-with-circular-references'; +import {getWebpackCacheName} from './webpack-cache'; +import type {WebpackOverrideFn} from './webpack-config'; + +export type RspackConfiguration = Configuration; + +if (!ReactDOM?.version) { + throw new Error('Could not find "react-dom" package. Did you install it?'); +} + +const reactDomVersion = ReactDOM.version.split('.')[0]; +if (reactDomVersion === '0') { + throw new Error( + `Version ${reactDomVersion} of "react-dom" is not supported by Remotion`, + ); +} + +const shouldUseReactDomClient = NoReactInternals.ENABLE_V5_BREAKING_CHANGES + ? true + : parseInt(reactDomVersion, 10) >= 18; + +export const rspackConfig = async ({ + entry, + userDefinedComponent, + outDir, + environment, + webpackOverride = (f) => f, + onProgress, + enableCaching = true, + maxTimelineTracks, + remotionRoot, + keyboardShortcutsEnabled, + bufferStateDelayInMilliseconds, + poll, + experimentalClientSideRenderingEnabled, + askAIEnabled, +}: { + entry: string; + userDefinedComponent: string; + outDir: string | null; + environment: 'development' | 'production'; + webpackOverride: WebpackOverrideFn; + onProgress?: (f: number) => void; + enableCaching?: boolean; + maxTimelineTracks: number | null; + keyboardShortcutsEnabled: boolean; + bufferStateDelayInMilliseconds: number | null; + remotionRoot: string; + poll: number | null; + askAIEnabled: boolean; + experimentalClientSideRenderingEnabled: boolean; +}): Promise<[string, RspackConfiguration]> => { + let lastProgress = 0; + + const isBun = typeof Bun !== 'undefined'; + + const define = new DefinePlugin({ + 'process.env.MAX_TIMELINE_TRACKS': maxTimelineTracks as unknown as string, + 'process.env.ASK_AI_ENABLED': askAIEnabled as unknown as string, + 'process.env.KEYBOARD_SHORTCUTS_ENABLED': + keyboardShortcutsEnabled as unknown as string, + 'process.env.BUFFER_STATE_DELAY_IN_MILLISECONDS': + bufferStateDelayInMilliseconds as unknown as string, + 'process.env.EXPERIMENTAL_CLIENT_SIDE_RENDERING_ENABLED': + experimentalClientSideRenderingEnabled as unknown as string, + }); + + const swcLoaderRule = { + loader: 'builtin:swc-loader', + options: { + jsc: { + parser: {syntax: 'typescript' as const, tsx: true}, + transform: { + react: { + runtime: 'automatic' as const, + development: environment === 'development', + refresh: environment === 'development', + }, + }, + }, + env: {targets: 'Chrome >= 85'}, + }, + }; + + const swcLoaderRuleJsx = { + loader: 'builtin:swc-loader', + options: { + jsc: { + parser: {syntax: 'ecmascript' as const, jsx: true}, + transform: { + react: { + runtime: 'automatic' as const, + development: environment === 'development', + refresh: environment === 'development', + }, + }, + }, + env: {targets: 'Chrome >= 85'}, + }, + }; + + // Rspack config is structurally compatible with webpack config at runtime, + // but the TypeScript types differ. Cast through `any` for the override. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const conf = (await webpackOverride({ + optimization: { + minimize: false, + }, + experiments: { + lazyCompilation: isBun + ? false + : environment === 'production' + ? false + : { + entries: false, + }, + }, + watchOptions: { + poll: poll ?? undefined, + aggregateTimeout: 0, + ignored: ['**/.git/**', '**/.turbo/**', '**/node_modules/**'], + }, + devtool: + environment === 'development' ? 'source-map' : 'cheap-module-source-map', + entry: [ + require.resolve('./setup-environment'), + userDefinedComponent, + require.resolve('../react-shim.js'), + entry, + ].filter(Boolean) as [string, ...string[]], + mode: environment, + plugins: + environment === 'development' + ? [ + new ReactRefreshPlugin(), + new rspack.HotModuleReplacementPlugin(), + define, + ] + : [ + new ProgressPlugin((p: number) => { + if (onProgress) { + if ((p === 1 && p > lastProgress) || p - lastProgress > 0.05) { + lastProgress = p; + onProgress(Number((p * 100).toFixed(2))); + } + } + }), + define, + ], + output: { + hashFunction: 'xxhash64', + filename: NoReactInternals.bundleName, + devtoolModuleFilenameTemplate: '[resource-path]', + assetModuleFilename: + environment === 'development' ? '[path][name][ext]' : '[hash][ext]', + }, + resolve: { + extensions: ['.ts', '.tsx', '.web.js', '.js', '.jsx', '.mjs', '.cjs'], + alias: { + 'react/jsx-runtime': require.resolve('react/jsx-runtime'), + 'react/jsx-dev-runtime': require.resolve('react/jsx-dev-runtime'), + react: require.resolve('react'), + 'remotion/no-react': path.resolve( + require.resolve('remotion'), + '..', + '..', + 'esm', + 'no-react.mjs', + ), + 'remotion/version': path.resolve( + require.resolve('remotion'), + '..', + '..', + 'esm', + 'version.mjs', + ), + remotion: path.resolve( + require.resolve('remotion'), + '..', + '..', + 'esm', + 'index.mjs', + ), + '@remotion/media-parser/worker': path.resolve( + require.resolve('@remotion/media-parser'), + '..', + 'esm', + 'worker.mjs', + ), + '@remotion/studio': require.resolve('@remotion/studio'), + 'react-dom/client': shouldUseReactDomClient + ? require.resolve('react-dom/client') + : require.resolve('react-dom'), + }, + }, + module: { + rules: [ + { + test: /\.css$/i, + use: [require.resolve('style-loader'), require.resolve('css-loader')], + type: 'javascript/auto', + }, + { + test: /\.(png|svg|jpg|jpeg|webp|gif|bmp|webm|mp4|mov|mp3|m4a|wav|aac)$/, + type: 'asset/resource', + }, + { + test: /\.tsx?$/, + use: [swcLoaderRule], + }, + { + test: /\.(woff(2)?|otf|ttf|eot)(\?v=\d+\.\d+\.\d+)?$/, + type: 'asset/resource', + }, + { + test: /\.jsx?$/, + exclude: /node_modules/, + use: [swcLoaderRuleJsx], + }, + ], + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any)) as RspackConfiguration; + + const hash = createHash('md5') + .update(jsonStringifyWithCircularReferences(conf)) + .digest('hex'); + const finalConf: RspackConfiguration = { + ...conf, + cache: (enableCaching + ? { + type: 'filesystem', + name: getWebpackCacheName(environment, hash), + version: hash, + } + : false) as unknown as RspackConfiguration['cache'], + output: { + ...conf.output, + ...(outDir ? {path: outDir} : {}), + }, + context: remotionRoot, + }; + return [hash, finalConf]; +}; + +export const createRspackCompiler = (config: RspackConfiguration) => { + return rspack(config); +}; diff --git a/packages/cli/src/parse-command-line.ts b/packages/cli/src/parse-command-line.ts index a47faa45ce0..a295c9280cf 100644 --- a/packages/cli/src/parse-command-line.ts +++ b/packages/cli/src/parse-command-line.ts @@ -46,6 +46,7 @@ const { overrideWidthOption, overrideFpsOption, overrideDurationOption, + rspackOption, } = BrowserSafeApis.options; export type CommandLineOptions = { @@ -140,6 +141,7 @@ export type CommandLineOptions = { 'license-key': string; [publicLicenseKeyOption.cliFlag]: string; [forceNewStudioOption.cliFlag]: TypeOfOption; + [rspackOption.cliFlag]: TypeOfOption; }; export const parseCommandLine = () => { diff --git a/packages/cli/src/parsed-cli.ts b/packages/cli/src/parsed-cli.ts index 76a1675fcff..5d0a9c0474a 100644 --- a/packages/cli/src/parsed-cli.ts +++ b/packages/cli/src/parsed-cli.ts @@ -37,6 +37,7 @@ export const BooleanFlags = [ 'onlyAllocateCpuDuringRequestProcessing', BrowserSafeApis.options.isProductionOption.cliFlag, BrowserSafeApis.options.forceNewStudioOption.cliFlag, + BrowserSafeApis.options.rspackOption.cliFlag, ]; export const parsedCli = minimist(process.argv.slice(2), { diff --git a/packages/cli/src/setup-cache.ts b/packages/cli/src/setup-cache.ts index 781319f92d3..bf6b2d0878e 100644 --- a/packages/cli/src/setup-cache.ts +++ b/packages/cli/src/setup-cache.ts @@ -214,6 +214,7 @@ export const bundleOnCli = async ({ publicPath, askAIEnabled, keyboardShortcutsEnabled, + rspack: false, }; const [hash] = await BundlerInternals.getConfig({ diff --git a/packages/cli/src/studio.ts b/packages/cli/src/studio.ts index 1bf1f5aa377..7514fcc7154 100644 --- a/packages/cli/src/studio.ts +++ b/packages/cli/src/studio.ts @@ -42,6 +42,7 @@ const { numberOfSharedAudioTagsOption, audioLatencyHintOption, ipv4Option, + rspackOption, } = BrowserSafeApis.options; export const studioCommand = async ( @@ -142,6 +143,8 @@ export const studioCommand = async ( const gitSource = getGitSource({remotionRoot, disableGitSource, logLevel}); + const useRspack = rspackOption.getValue({commandLine: parsedCli}).value; + const result = await StudioServerInternals.startStudio({ previewEntry: require.resolve('@remotion/studio/previewEntry'), browserArgs: parsedCli['browser-args'], @@ -183,6 +186,7 @@ export const studioCommand = async ( enableCrossSiteIsolation, askAIEnabled, forceNew: forceNewStudioOption.getValue({commandLine: parsedCli}).value, + rspack: useRspack, }); if (result.type === 'already-running') { diff --git a/packages/cloudrun/src/api/deploy-site.ts b/packages/cloudrun/src/api/deploy-site.ts index bddb8316abf..83e9d7f6af6 100644 --- a/packages/cloudrun/src/api/deploy-site.ts +++ b/packages/cloudrun/src/api/deploy-site.ts @@ -110,6 +110,7 @@ export const internalDeploySiteRaw = async ({ renderDefaults: null, askAIEnabled: options?.askAIEnabled ?? true, keyboardShortcutsEnabled: options?.keyboardShortcutsEnabled ?? true, + rspack: false, }), ]); diff --git a/packages/lambda/src/api/deploy-site.ts b/packages/lambda/src/api/deploy-site.ts index 10822236d14..cbc156e3f56 100644 --- a/packages/lambda/src/api/deploy-site.ts +++ b/packages/lambda/src/api/deploy-site.ts @@ -137,6 +137,7 @@ const mandatoryDeploySite = async ({ options?.experimentalClientSideRenderingEnabled ?? false, keyboardShortcutsEnabled: options?.keyboardShortcutsEnabled ?? true, renderDefaults: null, + rspack: false, }), ]); diff --git a/packages/renderer/src/options/index.tsx b/packages/renderer/src/options/index.tsx index a669ff7611d..cf7fa62b44e 100644 --- a/packages/renderer/src/options/index.tsx +++ b/packages/renderer/src/options/index.tsx @@ -57,6 +57,7 @@ import {publicDirOption} from './public-dir'; import {publicLicenseKeyOption} from './public-license-key'; import {publicPathOption} from './public-path'; import {reproOption} from './repro'; +import {rspackOption} from './rspack'; import {scaleOption} from './scale'; import {separateAudioOption} from './separate-audio'; import {stillImageFormatOption} from './still-image-format'; @@ -141,6 +142,7 @@ export const allOptions = { overrideWidthOption, overrideFpsOption, overrideDurationOption, + rspackOption, }; export type AvailableOptions = keyof typeof allOptions; diff --git a/packages/renderer/src/options/rspack.tsx b/packages/renderer/src/options/rspack.tsx new file mode 100644 index 00000000000..52b61fa98d0 --- /dev/null +++ b/packages/renderer/src/options/rspack.tsx @@ -0,0 +1,33 @@ +import type {AnyRemotionOption} from './option'; + +let rspackEnabled = false; + +const cliFlag = 'rspack' as const; + +export const rspackOption = { + name: 'Rspack', + cliFlag, + description: () => ( + <>Uses Rspack instead of Webpack as the bundler for the Studio or bundle. + ), + ssrName: null, + docLink: null, + type: false as boolean, + getValue: ({commandLine}) => { + if (commandLine[cliFlag] !== undefined) { + return { + value: commandLine[cliFlag] as boolean, + source: 'cli', + }; + } + + return { + value: rspackEnabled, + source: 'config', + }; + }, + setConfig(value) { + rspackEnabled = value; + }, + id: cliFlag, +} satisfies AnyRemotionOption; diff --git a/packages/studio-server/src/preview-server/dev-middleware/setup-hooks.ts b/packages/studio-server/src/preview-server/dev-middleware/setup-hooks.ts index 9a41a2f534a..9c6ba8f7e06 100644 --- a/packages/studio-server/src/preview-server/dev-middleware/setup-hooks.ts +++ b/packages/studio-server/src/preview-server/dev-middleware/setup-hooks.ts @@ -38,7 +38,10 @@ export function setupHooks(context: DevMiddlewareContext, logLevel: LogLevel) { .map((a) => a.trimEnd()) .filter(NoReactInternals.truthy) .map((a) => { - if (a.startsWith('webpack compiled')) { + if ( + a.startsWith('webpack compiled') || + a.startsWith('rspack compiled') + ) { return `Built in ${stats.endTime - stats.startTime}ms`; } diff --git a/packages/studio-server/src/preview-server/start-server.ts b/packages/studio-server/src/preview-server/start-server.ts index 77c59e0dfe0..976578dd9bf 100644 --- a/packages/studio-server/src/preview-server/start-server.ts +++ b/packages/studio-server/src/preview-server/start-server.ts @@ -59,6 +59,7 @@ export const startServer = async (options: { enableCrossSiteIsolation: boolean; askAIEnabled: boolean; forceNew: boolean; + rspack: boolean; }): Promise => { const desiredPort = options?.port ?? @@ -78,11 +79,11 @@ export const startServer = async (options: { return detection.type === 'match' ? 'stop' : 'continue'; }; - const [, config] = await BundlerInternals.webpackConfig({ + const configArgs = { entry: options.entry, userDefinedComponent: options.userDefinedComponent, outDir: null, - environment: 'development', + environment: 'development' as const, webpackOverride: options?.webpackOverride, maxTimelineTracks: options?.maxTimelineTracks ?? null, remotionRoot: options.remotionRoot, @@ -92,9 +93,18 @@ export const startServer = async (options: { poll: options.poll, bufferStateDelayInMilliseconds: options.bufferStateDelayInMilliseconds, askAIEnabled: options.askAIEnabled, - }); + }; - const compiler = webpack(config); + let compiler: webpack.Compiler; + if (options.rspack) { + const [, rspackConf] = await BundlerInternals.rspackConfig(configArgs); + compiler = BundlerInternals.createRspackCompiler( + rspackConf, + ) as unknown as webpack.Compiler; + } else { + const [, webpackConf] = await BundlerInternals.webpackConfig(configArgs); + compiler = webpack(webpackConf); + } const wdmMiddleware = wdm(compiler, options.logLevel); const whm = webpackHotMiddleware(compiler, options.logLevel); diff --git a/packages/studio-server/src/start-studio.ts b/packages/studio-server/src/start-studio.ts index db65d833bca..6f1157a449a 100644 --- a/packages/studio-server/src/start-studio.ts +++ b/packages/studio-server/src/start-studio.ts @@ -55,6 +55,7 @@ export const startStudio = async ({ enableCrossSiteIsolation, askAIEnabled, forceNew, + rspack: useRspack, }: { browserArgs: string; browserFlag: string; @@ -85,6 +86,7 @@ export const startStudio = async ({ forceIPv4: boolean; askAIEnabled: boolean; forceNew: boolean; + rspack: boolean; }): Promise => { try { if (typeof Bun === 'undefined') { @@ -160,6 +162,7 @@ export const startStudio = async ({ enableCrossSiteIsolation, askAIEnabled, forceNew, + rspack: useRspack, }); if (result.type === 'already-running') { From cda35d15f29cf0220b44e99484f1b2e4de7aa7eb Mon Sep 17 00:00:00 2001 From: Jonny Burger Date: Thu, 19 Feb 2026 13:55:53 +0100 Subject: [PATCH 02/33] fix a couple of issues --- .../src/preview-server/dev-middleware/setup-hooks.ts | 2 +- packages/studio-server/src/start-studio.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/studio-server/src/preview-server/dev-middleware/setup-hooks.ts b/packages/studio-server/src/preview-server/dev-middleware/setup-hooks.ts index 9c6ba8f7e06..99287fa9bfe 100644 --- a/packages/studio-server/src/preview-server/dev-middleware/setup-hooks.ts +++ b/packages/studio-server/src/preview-server/dev-middleware/setup-hooks.ts @@ -40,7 +40,7 @@ export function setupHooks(context: DevMiddlewareContext, logLevel: LogLevel) { .map((a) => { if ( a.startsWith('webpack compiled') || - a.startsWith('rspack compiled') + a.startsWith('Rspack compiled') ) { return `Built in ${stats.endTime - stats.startTime}ms`; } diff --git a/packages/studio-server/src/start-studio.ts b/packages/studio-server/src/start-studio.ts index 6f1157a449a..ff03a62e967 100644 --- a/packages/studio-server/src/start-studio.ts +++ b/packages/studio-server/src/start-studio.ts @@ -55,7 +55,7 @@ export const startStudio = async ({ enableCrossSiteIsolation, askAIEnabled, forceNew, - rspack: useRspack, + rspack, }: { browserArgs: string; browserFlag: string; @@ -162,7 +162,7 @@ export const startStudio = async ({ enableCrossSiteIsolation, askAIEnabled, forceNew, - rspack: useRspack, + rspack, }); if (result.type === 'already-running') { From c54fa3b193d1f302b71fe029440fd669c6651405 Mon Sep 17 00:00:00 2001 From: Jonny Burger Date: Thu, 19 Feb 2026 14:02:55 +0100 Subject: [PATCH 03/33] make options mandatory --- packages/bundler/src/bundle.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/bundler/src/bundle.ts b/packages/bundler/src/bundle.ts index c2c520480ab..b13f0ff35da 100644 --- a/packages/bundler/src/bundle.ts +++ b/packages/bundler/src/bundle.ts @@ -74,8 +74,8 @@ export const getConfig = ({ bufferStateDelayInMilliseconds: number | null; experimentalClientSideRenderingEnabled: boolean; maxTimelineTracks: number | null; - onProgress?: (progress: number) => void; - options?: LegacyBundleOptions; + onProgress: (progress: number) => void; + options: MandatoryLegacyBundleOptions; }) => { const configArgs = { entry: path.join( @@ -101,7 +101,7 @@ export const getConfig = ({ askAIEnabled: options?.askAIEnabled ?? true, }; - if (options?.rspack) { + if (options.rspack) { return rspackConfig(configArgs); } From 5cebbf5b22968bc1ead2599468bb7139fc403cb0 Mon Sep 17 00:00:00 2001 From: Jonny Burger Date: Thu, 19 Feb 2026 14:02:59 +0100 Subject: [PATCH 04/33] ignore warnings --- packages/bundler/src/rspack-config.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/bundler/src/rspack-config.ts b/packages/bundler/src/rspack-config.ts index 04847d3a97a..99b1e37b671 100644 --- a/packages/bundler/src/rspack-config.ts +++ b/packages/bundler/src/rspack-config.ts @@ -113,6 +113,11 @@ export const rspackConfig = async ({ optimization: { minimize: false, }, + ignoreWarnings: [ + /Circular dependency between chunks with runtime/, + /Critical dependency: the request of a dependency is an expression/, + /"__dirname" is used and has been mocked/, + ], experiments: { lazyCompilation: isBun ? false From d3a0341e8136f69ecf48306539b03e6f99e30618 Mon Sep 17 00:00:00 2001 From: Jonny Burger Date: Thu, 19 Feb 2026 14:17:19 +0100 Subject: [PATCH 05/33] Rename `--rspack` to `--experimental-rspack` and add `Config.setExperimentalRspackEnabled()` Mark the Rspack bundler flag as experimental, add a config setter, warn when enabled, add template validation test, and document the new option. Co-Authored-By: Claude Opus 4.6 --- packages/cli/src/config/index.ts | 8 ++++++++ packages/cli/src/studio.ts | 7 +++++++ packages/docs/docs/config.mdx | 12 ++++++++++++ .../src/templates/validate-templates.test.ts | 9 +++++++++ packages/renderer/src/options/rspack.tsx | 4 ++-- 5 files changed, 38 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/config/index.ts b/packages/cli/src/config/index.ts index 2256a33a51b..38dd2a05e08 100644 --- a/packages/cli/src/config/index.ts +++ b/packages/cli/src/config/index.ts @@ -119,6 +119,7 @@ const { overrideWidthOption, overrideFpsOption, overrideDurationOption, + rspackOption, } = BrowserSafeApis.options; declare global { @@ -184,6 +185,12 @@ declare global { readonly setExperimentalClientSideRenderingEnabled: ( enabled: boolean, ) => void; + /** + * Enable experimental Rspack bundler instead of Webpack. + * @param enabled Boolean whether to enable the Rspack bundler + * @default false + */ + readonly setExperimentalRspackEnabled: (enabled: boolean) => void; /** * Set number of shared audio tags. https://www.remotion.dev/docs/player/autoplay#using-the-numberofsharedaudiotags-prop * @param numberOfAudioTags @@ -652,6 +659,7 @@ export const Config: FlatConfig = { setKeyboardShortcutsEnabled: keyboardShortcutsOption.setConfig, setExperimentalClientSideRenderingEnabled: experimentalClientSideRenderingOption.setConfig, + setExperimentalRspackEnabled: rspackOption.setConfig, setNumberOfSharedAudioTags: numberOfSharedAudioTagsOption.setConfig, setWebpackPollingInMilliseconds, setShouldOpenBrowser, diff --git a/packages/cli/src/studio.ts b/packages/cli/src/studio.ts index 7514fcc7154..7c5f7deca65 100644 --- a/packages/cli/src/studio.ts +++ b/packages/cli/src/studio.ts @@ -145,6 +145,13 @@ export const studioCommand = async ( const useRspack = rspackOption.getValue({commandLine: parsedCli}).value; + if (useRspack) { + Log.warn( + {indent: false, logLevel}, + 'Enabling experimental Rspack bundler.', + ); + } + const result = await StudioServerInternals.startStudio({ previewEntry: require.resolve('@remotion/studio/previewEntry'), browserArgs: parsedCli['browser-args'], diff --git a/packages/docs/docs/config.mdx b/packages/docs/docs/config.mdx index 5782e446832..f1e0c7bfdef 100644 --- a/packages/docs/docs/config.mdx +++ b/packages/docs/docs/config.mdx @@ -151,6 +151,18 @@ Config.setExperimentalClientSideRenderingEnabled(true); The [command line flag](/docs/cli/studio#--enable-experimental-client-side-rendering) `--enable-experimental-client-side-rendering` will take precedence over this option. +## `setExperimentalRspackEnabled()` + +Use [Rspack](https://rspack.dev) instead of Webpack as the bundler for the Studio. Default `false`. + +```ts twoslash title="remotion.config.ts" +import {Config} from '@remotion/cli/config'; +// ---cut--- +Config.setExperimentalRspackEnabled(true); +``` + +The [command line flag](/docs/cli/studio#--experimental-rspack) `--experimental-rspack` will take precedence over this option. + ## `setWebpackPollingInMilliseconds()` Enables Webpack polling instead of the file system event listeners for hot reloading. diff --git a/packages/it-tests/src/templates/validate-templates.test.ts b/packages/it-tests/src/templates/validate-templates.test.ts index ed1642b898b..eba23c12c1b 100644 --- a/packages/it-tests/src/templates/validate-templates.test.ts +++ b/packages/it-tests/src/templates/validate-templates.test.ts @@ -151,6 +151,15 @@ describe('Templates should be valid', () => { ); }); + it(`${template.shortName} should not use setExperimentalRspackEnabled`, async () => { + const {contents, entryPoint} = await findFile([ + getFileForTemplate(template, 'remotion.config.ts'), + getFileForTemplate(template, 'remotion.config.js'), + ]); + expect(entryPoint).toBeTruthy(); + expect(contents).not.toContain('setExperimentalRspackEnabled'); + }); + it(`${template.shortName} should use good tsconfig values`, async () => { if (template.shortName.includes('JavaScript')) { return; diff --git a/packages/renderer/src/options/rspack.tsx b/packages/renderer/src/options/rspack.tsx index 52b61fa98d0..5e3f0d4214c 100644 --- a/packages/renderer/src/options/rspack.tsx +++ b/packages/renderer/src/options/rspack.tsx @@ -2,10 +2,10 @@ import type {AnyRemotionOption} from './option'; let rspackEnabled = false; -const cliFlag = 'rspack' as const; +const cliFlag = 'experimental-rspack' as const; export const rspackOption = { - name: 'Rspack', + name: 'Experimental Rspack', cliFlag, description: () => ( <>Uses Rspack instead of Webpack as the bundler for the Studio or bundle. From 40cc637745df6e422beeb567f26d5fe2f57cc621 Mon Sep 17 00:00:00 2001 From: Jonny Burger Date: Thu, 19 Feb 2026 14:26:17 +0100 Subject: [PATCH 06/33] abstract --- .../bundler/src/define-plugin-definitions.ts | 21 +++++++++++++++++++ packages/bundler/src/rspack-config.ts | 20 +++++++++--------- packages/bundler/src/webpack-config.ts | 15 ++++++------- 3 files changed, 39 insertions(+), 17 deletions(-) create mode 100644 packages/bundler/src/define-plugin-definitions.ts diff --git a/packages/bundler/src/define-plugin-definitions.ts b/packages/bundler/src/define-plugin-definitions.ts new file mode 100644 index 00000000000..52c64775d00 --- /dev/null +++ b/packages/bundler/src/define-plugin-definitions.ts @@ -0,0 +1,21 @@ +export const getDefinePluginDefinitions = ({ + maxTimelineTracks, + askAIEnabled, + keyboardShortcutsEnabled, + bufferStateDelayInMilliseconds, + experimentalClientSideRenderingEnabled, +}: { + maxTimelineTracks: number | null; + askAIEnabled: boolean; + keyboardShortcutsEnabled: boolean; + bufferStateDelayInMilliseconds: number | null; + experimentalClientSideRenderingEnabled: boolean; +}) => ({ + 'process.env.MAX_TIMELINE_TRACKS': maxTimelineTracks, + 'process.env.ASK_AI_ENABLED': askAIEnabled, + 'process.env.KEYBOARD_SHORTCUTS_ENABLED': keyboardShortcutsEnabled, + 'process.env.BUFFER_STATE_DELAY_IN_MILLISECONDS': + bufferStateDelayInMilliseconds, + 'process.env.EXPERIMENTAL_CLIENT_SIDE_RENDERING_ENABLED': + experimentalClientSideRenderingEnabled, +}); diff --git a/packages/bundler/src/rspack-config.ts b/packages/bundler/src/rspack-config.ts index 99b1e37b671..c5497053522 100644 --- a/packages/bundler/src/rspack-config.ts +++ b/packages/bundler/src/rspack-config.ts @@ -5,6 +5,7 @@ import {createHash} from 'node:crypto'; import path from 'node:path'; import ReactDOM from 'react-dom'; import {NoReactInternals} from 'remotion/no-react'; +import {getDefinePluginDefinitions} from './define-plugin-definitions'; import {jsonStringifyWithCircularReferences} from './stringify-with-circular-references'; import {getWebpackCacheName} from './webpack-cache'; import type {WebpackOverrideFn} from './webpack-config'; @@ -61,16 +62,15 @@ export const rspackConfig = async ({ const isBun = typeof Bun !== 'undefined'; - const define = new DefinePlugin({ - 'process.env.MAX_TIMELINE_TRACKS': maxTimelineTracks as unknown as string, - 'process.env.ASK_AI_ENABLED': askAIEnabled as unknown as string, - 'process.env.KEYBOARD_SHORTCUTS_ENABLED': - keyboardShortcutsEnabled as unknown as string, - 'process.env.BUFFER_STATE_DELAY_IN_MILLISECONDS': - bufferStateDelayInMilliseconds as unknown as string, - 'process.env.EXPERIMENTAL_CLIENT_SIDE_RENDERING_ENABLED': - experimentalClientSideRenderingEnabled as unknown as string, - }); + const define = new DefinePlugin( + getDefinePluginDefinitions({ + maxTimelineTracks, + askAIEnabled, + keyboardShortcutsEnabled, + bufferStateDelayInMilliseconds, + experimentalClientSideRenderingEnabled, + }) as unknown as Record, + ); const swcLoaderRule = { loader: 'builtin:swc-loader', diff --git a/packages/bundler/src/webpack-config.ts b/packages/bundler/src/webpack-config.ts index 50cc8c96d95..dda350ad7e3 100644 --- a/packages/bundler/src/webpack-config.ts +++ b/packages/bundler/src/webpack-config.ts @@ -5,6 +5,7 @@ import {NoReactInternals} from 'remotion/no-react'; import type {Configuration} from 'webpack'; import webpack, {ProgressPlugin} from 'webpack'; import {CaseSensitivePathsPlugin} from './case-sensitive-paths'; +import {getDefinePluginDefinitions} from './define-plugin-definitions'; import type {LoaderOptions} from './esbuild-loader/interfaces'; import {ReactFreshWebpackPlugin} from './fast-refresh'; import {AllowDependencyExpressionPlugin} from './hide-expression-dependency'; @@ -82,15 +83,15 @@ export const webpackConfig = async ({ const isBun = typeof Bun !== 'undefined'; - const define = new webpack.DefinePlugin({ - 'process.env.MAX_TIMELINE_TRACKS': maxTimelineTracks, - 'process.env.ASK_AI_ENABLED': askAIEnabled, - 'process.env.KEYBOARD_SHORTCUTS_ENABLED': keyboardShortcutsEnabled, - 'process.env.BUFFER_STATE_DELAY_IN_MILLISECONDS': + const define = new webpack.DefinePlugin( + getDefinePluginDefinitions({ + maxTimelineTracks, + askAIEnabled, + keyboardShortcutsEnabled, bufferStateDelayInMilliseconds, - 'process.env.EXPERIMENTAL_CLIENT_SIDE_RENDERING_ENABLED': experimentalClientSideRenderingEnabled, - }); + }), + ); const conf: WebpackConfiguration = await webpackOverride({ optimization: { From 5e8965489fbf1ef29f5a183d8d39efe484813e3c Mon Sep 17 00:00:00 2001 From: Jonny Burger Date: Thu, 19 Feb 2026 14:26:21 +0100 Subject: [PATCH 07/33] simplify --- packages/example/remotion.config.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/example/remotion.config.ts b/packages/example/remotion.config.ts index 912bffef8bb..3a9b1118e64 100644 --- a/packages/example/remotion.config.ts +++ b/packages/example/remotion.config.ts @@ -10,3 +10,4 @@ Config.overrideWebpackConfig(async (config) => { }); Config.setExperimentalClientSideRenderingEnabled(true); +Config.setExperimentalRspackEnabled(true); From 376c3d32e401d5e4440a3fd5a768be2ca4f8d594 Mon Sep 17 00:00:00 2001 From: Jonny Burger Date: Thu, 19 Feb 2026 14:47:48 +0100 Subject: [PATCH 08/33] Add `rspack` option to CLI bundle/render pipeline, Lambda/CloudRun `deploySite` + docs Thread the `--experimental-rspack` CLI flag through the entire bundle/render pipeline instead of hardcoding `false`. Add `rspack?: boolean` to Lambda and CloudRun `deploySite()` options. Document the new option across all relevant CLI commands and Node.js APIs. Co-Authored-By: Claude Opus 4.6 --- packages/cli/src/benchmark.ts | 3 +++ packages/cli/src/bundle.ts | 3 +++ packages/cli/src/compositions.ts | 3 +++ packages/cli/src/render-flows/render.ts | 3 +++ packages/cli/src/render-flows/still.ts | 3 +++ packages/cli/src/render-queue/process-still.ts | 2 ++ packages/cli/src/render-queue/process-video.ts | 2 ++ packages/cli/src/render.tsx | 3 +++ packages/cli/src/setup-cache.ts | 7 ++++++- packages/cli/src/still.ts | 3 +++ packages/cloudrun/src/api/deploy-site.ts | 3 ++- packages/docs/docs/bundle.mdx | 4 ++++ packages/docs/docs/cli/benchmark.mdx | 4 ++++ packages/docs/docs/cli/bundle.mdx | 4 ++++ packages/docs/docs/cli/render.mdx | 4 ++++ packages/docs/docs/cli/still.mdx | 4 ++++ packages/docs/docs/cli/studio.mdx | 4 ++++ packages/docs/docs/cloudrun/deploysite.mdx | 6 ++++++ packages/docs/docs/lambda/deploysite.mdx | 6 ++++++ packages/lambda/src/api/deploy-site.ts | 3 ++- 20 files changed, 71 insertions(+), 3 deletions(-) diff --git a/packages/cli/src/benchmark.ts b/packages/cli/src/benchmark.ts index 1cb4c77a617..7236667f1e1 100644 --- a/packages/cli/src/benchmark.ts +++ b/packages/cli/src/benchmark.ts @@ -60,6 +60,7 @@ const { askAIOption, experimentalClientSideRenderingOption, keyboardShortcutsOption, + rspackOption, pixelFormatOption, browserExecutableOption, everyNthFrameOption, @@ -266,6 +267,7 @@ export const benchmarkCommand = async ( const keyboardShortcutsEnabled = keyboardShortcutsOption.getValue({ commandLine: parsedCli, }).value; + const rspack = rspackOption.getValue({commandLine: parsedCli}).value; if (experimentalClientSideRenderingEnabled) { Log.warn( @@ -338,6 +340,7 @@ export const benchmarkCommand = async ( experimentalClientSideRenderingEnabled, askAIEnabled, keyboardShortcutsEnabled, + rspack, }); registerCleanupJob(`Deleting bundle`, () => cleanupBundle()); diff --git a/packages/cli/src/bundle.ts b/packages/cli/src/bundle.ts index 71b99ea3a48..680006c0226 100644 --- a/packages/cli/src/bundle.ts +++ b/packages/cli/src/bundle.ts @@ -21,6 +21,7 @@ const { askAIOption, experimentalClientSideRenderingOption, keyboardShortcutsOption, + rspackOption, } = BrowserSafeApis.options; export const bundleCommand = async ( @@ -73,6 +74,7 @@ export const bundleCommand = async ( const keyboardShortcutsEnabled = keyboardShortcutsOption.getValue({ commandLine: parsedCli, }).value; + const rspack = rspackOption.getValue({commandLine: parsedCli}).value; if (experimentalClientSideRenderingEnabled) { Log.warn( @@ -160,6 +162,7 @@ export const bundleCommand = async ( experimentalClientSideRenderingEnabled, askAIEnabled, keyboardShortcutsEnabled, + rspack, }); Log.info( diff --git a/packages/cli/src/compositions.ts b/packages/cli/src/compositions.ts index 0f0deb39d8b..0582043c418 100644 --- a/packages/cli/src/compositions.ts +++ b/packages/cli/src/compositions.ts @@ -29,6 +29,7 @@ const { askAIOption, experimentalClientSideRenderingOption, keyboardShortcutsOption, + rspackOption, browserExecutableOption, userAgentOption, disableWebSecurityOption, @@ -135,6 +136,7 @@ export const listCompositionsCommand = async ( const keyboardShortcutsEnabled = keyboardShortcutsOption.getValue({ commandLine: parsedCli, }).value; + const rspack = rspackOption.getValue({commandLine: parsedCli}).value; if (experimentalClientSideRenderingEnabled) { Log.warn( @@ -168,6 +170,7 @@ export const listCompositionsCommand = async ( experimentalClientSideRenderingEnabled, askAIEnabled, keyboardShortcutsEnabled, + rspack, }); registerCleanupJob(`Cleanup bundle`, () => cleanupBundle()); diff --git a/packages/cli/src/render-flows/render.ts b/packages/cli/src/render-flows/render.ts index 2aca245daf2..868ce2ec527 100644 --- a/packages/cli/src/render-flows/render.ts +++ b/packages/cli/src/render-flows/render.ts @@ -126,6 +126,7 @@ export const renderVideoFlow = async ({ audioLatencyHint, imageSequencePattern, mediaCacheSizeInBytes, + rspack, askAIEnabled, experimentalClientSideRenderingEnabled, keyboardShortcutsEnabled, @@ -191,6 +192,7 @@ export const renderVideoFlow = async ({ audioLatencyHint: AudioContextLatencyCategory | null; imageSequencePattern: string | null; mediaCacheSizeInBytes: number | null; + rspack: boolean; askAIEnabled: boolean; experimentalClientSideRenderingEnabled: boolean; keyboardShortcutsEnabled: boolean; @@ -337,6 +339,7 @@ export const renderVideoFlow = async ({ experimentalClientSideRenderingEnabled, askAIEnabled, keyboardShortcutsEnabled, + rspack, }, ); diff --git a/packages/cli/src/render-flows/still.ts b/packages/cli/src/render-flows/still.ts index 7521da8cfd9..948c44ac7d8 100644 --- a/packages/cli/src/render-flows/still.ts +++ b/packages/cli/src/render-flows/still.ts @@ -84,6 +84,7 @@ export const renderStillFlow = async ({ offthreadVideoThreads, audioLatencyHint, mediaCacheSizeInBytes, + rspack, askAIEnabled, experimentalClientSideRenderingEnabled, keyboardShortcutsEnabled, @@ -123,6 +124,7 @@ export const renderStillFlow = async ({ chromeMode: ChromeMode; audioLatencyHint: AudioContextLatencyCategory | null; mediaCacheSizeInBytes: number | null; + rspack: boolean; askAIEnabled: boolean; experimentalClientSideRenderingEnabled: boolean; keyboardShortcutsEnabled: boolean; @@ -229,6 +231,7 @@ export const renderStillFlow = async ({ experimentalClientSideRenderingEnabled, askAIEnabled, keyboardShortcutsEnabled, + rspack, }, ); diff --git a/packages/cli/src/render-queue/process-still.ts b/packages/cli/src/render-queue/process-still.ts index 904e47ee740..2a55d04b18f 100644 --- a/packages/cli/src/render-queue/process-still.ts +++ b/packages/cli/src/render-queue/process-still.ts @@ -10,6 +10,7 @@ const { askAIOption, experimentalClientSideRenderingOption, keyboardShortcutsOption, + rspackOption, browserExecutableOption, } = BrowserSafeApis.options; @@ -88,5 +89,6 @@ export const processStill = async ({ askAIEnabled, experimentalClientSideRenderingEnabled, keyboardShortcutsEnabled, + rspack: rspackOption.getValue({commandLine: parsedCli}).value, }); }; diff --git a/packages/cli/src/render-queue/process-video.ts b/packages/cli/src/render-queue/process-video.ts index eda077a1cc8..b8001602c2a 100644 --- a/packages/cli/src/render-queue/process-video.ts +++ b/packages/cli/src/render-queue/process-video.ts @@ -12,6 +12,7 @@ const { askAIOption, experimentalClientSideRenderingOption, keyboardShortcutsOption, + rspackOption, browserExecutableOption, } = BrowserSafeApis.options; @@ -123,5 +124,6 @@ export const processVideoJob = async ({ experimentalClientSideRenderingOption.getValue({commandLine: parsedCli}) .value, keyboardShortcutsEnabled, + rspack: rspackOption.getValue({commandLine: parsedCli}).value, }); }; diff --git a/packages/cli/src/render.tsx b/packages/cli/src/render.tsx index 940cccd5881..0a6c9a48ac4 100644 --- a/packages/cli/src/render.tsx +++ b/packages/cli/src/render.tsx @@ -48,6 +48,7 @@ const { askAIOption, experimentalClientSideRenderingOption, keyboardShortcutsOption, + rspackOption, pixelFormatOption, browserExecutableOption, everyNthFrameOption, @@ -207,6 +208,7 @@ export const render = async ( const keyboardShortcutsEnabled = keyboardShortcutsOption.getValue({ commandLine: parsedCli, }).value; + const rspack = rspackOption.getValue({commandLine: parsedCli}).value; const chromiumOptions: Required = { disableWebSecurity, @@ -307,5 +309,6 @@ export const render = async ( experimentalClientSideRenderingOption.getValue({commandLine: parsedCli}) .value, keyboardShortcutsEnabled, + rspack, }); }; diff --git a/packages/cli/src/setup-cache.ts b/packages/cli/src/setup-cache.ts index bf6b2d0878e..01b45469f1c 100644 --- a/packages/cli/src/setup-cache.ts +++ b/packages/cli/src/setup-cache.ts @@ -35,6 +35,7 @@ export const bundleOnCliOrTakeServeUrl = async ({ experimentalClientSideRenderingEnabled, askAIEnabled, keyboardShortcutsEnabled, + rspack, }: { fullPath: string; remotionRoot: string; @@ -57,6 +58,7 @@ export const bundleOnCliOrTakeServeUrl = async ({ experimentalClientSideRenderingEnabled: boolean; askAIEnabled: boolean; keyboardShortcutsEnabled: boolean; + rspack: boolean; }): Promise<{ urlOrBundle: string; cleanup: () => void; @@ -100,6 +102,7 @@ export const bundleOnCliOrTakeServeUrl = async ({ experimentalClientSideRenderingEnabled, askAIEnabled, keyboardShortcutsEnabled, + rspack, }); return { @@ -127,6 +130,7 @@ export const bundleOnCli = async ({ experimentalClientSideRenderingEnabled, askAIEnabled, keyboardShortcutsEnabled, + rspack, }: { fullPath: string; remotionRoot: string; @@ -149,6 +153,7 @@ export const bundleOnCli = async ({ experimentalClientSideRenderingEnabled: boolean; keyboardShortcutsEnabled: boolean; askAIEnabled: boolean; + rspack: boolean; }) => { const shouldCache = ConfigInternals.getWebpackCaching(); @@ -214,7 +219,7 @@ export const bundleOnCli = async ({ publicPath, askAIEnabled, keyboardShortcutsEnabled, - rspack: false, + rspack, }; const [hash] = await BundlerInternals.getConfig({ diff --git a/packages/cli/src/still.ts b/packages/cli/src/still.ts index a6669ff9334..3cd247510f9 100644 --- a/packages/cli/src/still.ts +++ b/packages/cli/src/still.ts @@ -30,6 +30,7 @@ const { askAIOption, experimentalClientSideRenderingOption, keyboardShortcutsOption, + rspackOption, browserExecutableOption, userAgentOption, disableWebSecurityOption, @@ -147,6 +148,7 @@ export const still = async ( const keyboardShortcutsEnabled = keyboardShortcutsOption.getValue({ commandLine: parsedCli, }).value; + const rspack = rspackOption.getValue({commandLine: parsedCli}).value; const chromiumOptions: Required = { disableWebSecurity, @@ -206,5 +208,6 @@ export const still = async ( experimentalClientSideRenderingOption.getValue({commandLine: parsedCli}) .value, keyboardShortcutsEnabled, + rspack, }); }; diff --git a/packages/cloudrun/src/api/deploy-site.ts b/packages/cloudrun/src/api/deploy-site.ts index 83e9d7f6af6..149f2c8b337 100644 --- a/packages/cloudrun/src/api/deploy-site.ts +++ b/packages/cloudrun/src/api/deploy-site.ts @@ -31,6 +31,7 @@ type Options = { keyboardShortcutsEnabled?: boolean; askAIEnabled?: boolean; experimentalClientSideRenderingEnabled?: boolean; + rspack?: boolean; }; type OptionalParameters = { @@ -110,7 +111,7 @@ export const internalDeploySiteRaw = async ({ renderDefaults: null, askAIEnabled: options?.askAIEnabled ?? true, keyboardShortcutsEnabled: options?.keyboardShortcutsEnabled ?? true, - rspack: false, + rspack: options?.rspack ?? false, }), ]); diff --git a/packages/docs/docs/bundle.mdx b/packages/docs/docs/bundle.mdx index 9e669b158ce..7d069241295 100644 --- a/packages/docs/docs/bundle.mdx +++ b/packages/docs/docs/bundle.mdx @@ -66,6 +66,10 @@ Specify a desired output directory. If no passed, the webpack bundle will be cre +### `rspack?` + +Whether to use [Rspack](https://rspack.dev) instead of Webpack as the bundler. Default `false`. + ### `keyboardShortcutsEnabled?` A `boolean` specifying whether the Studio responds to the predefined keyboard shortcuts.. Default `true`. diff --git a/packages/docs/docs/cli/benchmark.mdx b/packages/docs/docs/cli/benchmark.mdx index 5ea0e292528..e12922a9d23 100644 --- a/packages/docs/docs/cli/benchmark.mdx +++ b/packages/docs/docs/cli/benchmark.mdx @@ -179,6 +179,10 @@ Inherited from [`npx remotion render`](/docs/cli/render#--audio-bitrate) +### `--experimental-rspack` + + + ### ~~`--ffmpeg-executable`~~ Removed in v4.0. Inherited from [`npx remotion render`](/docs/cli/render#--ffmpeg-executable) diff --git a/packages/docs/docs/cli/bundle.mdx b/packages/docs/docs/cli/bundle.mdx index 98b5de3c550..fb0469522a6 100644 --- a/packages/docs/docs/cli/bundle.mdx +++ b/packages/docs/docs/cli/bundle.mdx @@ -41,3 +41,7 @@ Define the location of the resulting bundle. By default it is a folder called `. ### `--disable-git-source` + +### `--experimental-rspack` + + diff --git a/packages/docs/docs/cli/render.mdx b/packages/docs/docs/cli/render.mdx index c54eb2d826c..8f70acb9535 100644 --- a/packages/docs/docs/cli/render.mdx +++ b/packages/docs/docs/cli/render.mdx @@ -256,6 +256,10 @@ Not to be confused with the [`--timeout` flag when deploying a Lambda function]( +### `--experimental-rspack` + + + ### `--for-seamless-aac-concatenation` diff --git a/packages/docs/docs/cli/still.mdx b/packages/docs/docs/cli/still.mdx index 7d8504149d3..56b6e2881fc 100644 --- a/packages/docs/docs/cli/still.mdx +++ b/packages/docs/docs/cli/still.mdx @@ -144,6 +144,10 @@ Not to be confused with the [`--timeout` flag when deploying a Lambda function]( +### `--experimental-rspack` + + + ### ~~`--ffmpeg-executable`~~ _removed in v4.0_ diff --git a/packages/docs/docs/cli/studio.mdx b/packages/docs/docs/cli/studio.mdx index 81907c98eba..2cb6de52a6f 100644 --- a/packages/docs/docs/cli/studio.mdx +++ b/packages/docs/docs/cli/studio.mdx @@ -58,6 +58,10 @@ Specify a location for a dotenv file - Default `.env`. [Read about how environme [Enables experimental client-side rendering in the Studio](/docs/config#setexperimentalclientsiderenderingenabled). Default is `false`. +### `--experimental-rspack` + + + ### `--webpack-poll` [Enables Webpack polling](/docs/config#setwebpackpollinginmilliseconds) instead of the file system event listeners for hot reloading. This is useful if you are inside a virtual machine or have a remote file system. diff --git a/packages/docs/docs/cloudrun/deploysite.mdx b/packages/docs/docs/cloudrun/deploysite.mdx index 09908d52862..fabe302c4a7 100644 --- a/packages/docs/docs/cloudrun/deploysite.mdx +++ b/packages/docs/docs/cloudrun/deploysite.mdx @@ -115,6 +115,12 @@ _default: `false`_ Whether experimental client-side rendering should be enabled in the Studio. See [Config.setExperimentalClientSideRenderingEnabled()](/docs/config#setexperimentalclientsiderenderingenabled) for more information. +#### `rspack?` + +_default: `false`_ + +Whether to use [Rspack](https://rspack.dev) instead of Webpack as the bundler. See [Config.setExperimentalRspackEnabled()](/docs/config#setexperimentalrspackenabled) for more information. + ## Return value An object with the following values: diff --git a/packages/docs/docs/lambda/deploysite.mdx b/packages/docs/docs/lambda/deploysite.mdx index efae1ff3574..0ec4051222a 100644 --- a/packages/docs/docs/lambda/deploysite.mdx +++ b/packages/docs/docs/lambda/deploysite.mdx @@ -124,6 +124,12 @@ _default: `false`_ Whether experimental client-side rendering should be enabled in the Studio. See [Config.setExperimentalClientSideRenderingEnabled()](/docs/config#setexperimentalclientsiderenderingenabled) for more information. +#### `rspack?` + +_default: `false`_ + +Whether to use [Rspack](https://rspack.dev) instead of Webpack as the bundler. See [Config.setExperimentalRspackEnabled()](/docs/config#setexperimentalrspackenabled) for more information. + ### `privacy?` _available from v3.3.97_ diff --git a/packages/lambda/src/api/deploy-site.ts b/packages/lambda/src/api/deploy-site.ts index cbc156e3f56..7da8cd89c05 100644 --- a/packages/lambda/src/api/deploy-site.ts +++ b/packages/lambda/src/api/deploy-site.ts @@ -40,6 +40,7 @@ type OptionalParameters = { keyboardShortcutsEnabled?: boolean; askAIEnabled?: boolean; experimentalClientSideRenderingEnabled?: boolean; + rspack?: boolean; }; privacy: 'public' | 'no-acl'; gitSource: GitSource | null; @@ -137,7 +138,7 @@ const mandatoryDeploySite = async ({ options?.experimentalClientSideRenderingEnabled ?? false, keyboardShortcutsEnabled: options?.keyboardShortcutsEnabled ?? true, renderDefaults: null, - rspack: false, + rspack: options?.rspack ?? false, }), ]); From 1ee8634fd1e213d34148a7280801707a63a17fac Mon Sep 17 00:00:00 2001 From: Jonny Burger Date: Thu, 19 Feb 2026 15:16:03 +0100 Subject: [PATCH 09/33] Extract shared config between rspack and webpack, replace 4.0.TBD with 4.0.426 Extract duplicated resolve, output, base config, module rules, and hash/cache logic into shared-bundler-config.ts to reduce duplication between webpack-config.ts and rspack-config.ts. Update version placeholders in docs. Co-Authored-By: Claude Opus 4.6 --- packages/bundler/src/rspack-config.ts | 138 ++------------- packages/bundler/src/shared-bundler-config.ts | 157 ++++++++++++++++++ packages/bundler/src/webpack-config.ts | 145 ++-------------- packages/docs/docs/cli/benchmark.mdx | 2 +- packages/docs/docs/cli/bundle.mdx | 2 +- packages/docs/docs/cli/render.mdx | 2 +- packages/docs/docs/cli/still.mdx | 2 +- packages/docs/docs/cli/studio.mdx | 2 +- packages/docs/docs/cloudrun/deploysite.mdx | 2 +- packages/docs/docs/config.mdx | 2 +- packages/docs/docs/lambda/deploysite.mdx | 2 +- 11 files changed, 201 insertions(+), 255 deletions(-) create mode 100644 packages/bundler/src/shared-bundler-config.ts diff --git a/packages/bundler/src/rspack-config.ts b/packages/bundler/src/rspack-config.ts index c5497053522..303d1a3896a 100644 --- a/packages/bundler/src/rspack-config.ts +++ b/packages/bundler/src/rspack-config.ts @@ -1,32 +1,18 @@ import type {Configuration} from '@rspack/core'; import {DefinePlugin, ProgressPlugin, rspack} from '@rspack/core'; import ReactRefreshPlugin from '@rspack/plugin-react-refresh'; -import {createHash} from 'node:crypto'; -import path from 'node:path'; -import ReactDOM from 'react-dom'; -import {NoReactInternals} from 'remotion/no-react'; import {getDefinePluginDefinitions} from './define-plugin-definitions'; -import {jsonStringifyWithCircularReferences} from './stringify-with-circular-references'; -import {getWebpackCacheName} from './webpack-cache'; +import { + computeHashAndFinalConfig, + getBaseConfig, + getOutputConfig, + getResolveConfig, + getSharedModuleRules, +} from './shared-bundler-config'; import type {WebpackOverrideFn} from './webpack-config'; export type RspackConfiguration = Configuration; -if (!ReactDOM?.version) { - throw new Error('Could not find "react-dom" package. Did you install it?'); -} - -const reactDomVersion = ReactDOM.version.split('.')[0]; -if (reactDomVersion === '0') { - throw new Error( - `Version ${reactDomVersion} of "react-dom" is not supported by Remotion`, - ); -} - -const shouldUseReactDomClient = NoReactInternals.ENABLE_V5_BREAKING_CHANGES - ? true - : parseInt(reactDomVersion, 10) >= 18; - export const rspackConfig = async ({ entry, userDefinedComponent, @@ -60,8 +46,6 @@ export const rspackConfig = async ({ }): Promise<[string, RspackConfiguration]> => { let lastProgress = 0; - const isBun = typeof Bun !== 'undefined'; - const define = new DefinePlugin( getDefinePluginDefinitions({ maxTimelineTracks, @@ -110,30 +94,12 @@ export const rspackConfig = async ({ // but the TypeScript types differ. Cast through `any` for the override. // eslint-disable-next-line @typescript-eslint/no-explicit-any const conf = (await webpackOverride({ - optimization: { - minimize: false, - }, + ...getBaseConfig(environment, poll), ignoreWarnings: [ /Circular dependency between chunks with runtime/, /Critical dependency: the request of a dependency is an expression/, /"__dirname" is used and has been mocked/, ], - experiments: { - lazyCompilation: isBun - ? false - : environment === 'production' - ? false - : { - entries: false, - }, - }, - watchOptions: { - poll: poll ?? undefined, - aggregateTimeout: 0, - ignored: ['**/.git/**', '**/.turbo/**', '**/node_modules/**'], - }, - devtool: - environment === 'development' ? 'source-map' : 'cheap-module-source-map', entry: [ require.resolve('./setup-environment'), userDefinedComponent, @@ -159,71 +125,15 @@ export const rspackConfig = async ({ }), define, ], - output: { - hashFunction: 'xxhash64', - filename: NoReactInternals.bundleName, - devtoolModuleFilenameTemplate: '[resource-path]', - assetModuleFilename: - environment === 'development' ? '[path][name][ext]' : '[hash][ext]', - }, - resolve: { - extensions: ['.ts', '.tsx', '.web.js', '.js', '.jsx', '.mjs', '.cjs'], - alias: { - 'react/jsx-runtime': require.resolve('react/jsx-runtime'), - 'react/jsx-dev-runtime': require.resolve('react/jsx-dev-runtime'), - react: require.resolve('react'), - 'remotion/no-react': path.resolve( - require.resolve('remotion'), - '..', - '..', - 'esm', - 'no-react.mjs', - ), - 'remotion/version': path.resolve( - require.resolve('remotion'), - '..', - '..', - 'esm', - 'version.mjs', - ), - remotion: path.resolve( - require.resolve('remotion'), - '..', - '..', - 'esm', - 'index.mjs', - ), - '@remotion/media-parser/worker': path.resolve( - require.resolve('@remotion/media-parser'), - '..', - 'esm', - 'worker.mjs', - ), - '@remotion/studio': require.resolve('@remotion/studio'), - 'react-dom/client': shouldUseReactDomClient - ? require.resolve('react-dom/client') - : require.resolve('react-dom'), - }, - }, + output: getOutputConfig(environment), + resolve: getResolveConfig(), module: { rules: [ - { - test: /\.css$/i, - use: [require.resolve('style-loader'), require.resolve('css-loader')], - type: 'javascript/auto', - }, - { - test: /\.(png|svg|jpg|jpeg|webp|gif|bmp|webm|mp4|mov|mp3|m4a|wav|aac)$/, - type: 'asset/resource', - }, + ...getSharedModuleRules(), { test: /\.tsx?$/, use: [swcLoaderRule], }, - { - test: /\.(woff(2)?|otf|ttf|eot)(\?v=\d+\.\d+\.\d+)?$/, - type: 'asset/resource', - }, { test: /\.jsx?$/, exclude: /node_modules/, @@ -234,25 +144,13 @@ export const rspackConfig = async ({ // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any)) as RspackConfiguration; - const hash = createHash('md5') - .update(jsonStringifyWithCircularReferences(conf)) - .digest('hex'); - const finalConf: RspackConfiguration = { - ...conf, - cache: (enableCaching - ? { - type: 'filesystem', - name: getWebpackCacheName(environment, hash), - version: hash, - } - : false) as unknown as RspackConfiguration['cache'], - output: { - ...conf.output, - ...(outDir ? {path: outDir} : {}), - }, - context: remotionRoot, - }; - return [hash, finalConf]; + const [hash, finalConf] = computeHashAndFinalConfig(conf, { + enableCaching, + environment, + outDir, + remotionRoot, + }); + return [hash, finalConf as unknown as RspackConfiguration]; }; export const createRspackCompiler = (config: RspackConfiguration) => { diff --git a/packages/bundler/src/shared-bundler-config.ts b/packages/bundler/src/shared-bundler-config.ts new file mode 100644 index 00000000000..756f98065ee --- /dev/null +++ b/packages/bundler/src/shared-bundler-config.ts @@ -0,0 +1,157 @@ +import {createHash} from 'node:crypto'; +import path from 'node:path'; +import ReactDOM from 'react-dom'; +import {NoReactInternals} from 'remotion/no-react'; +import {jsonStringifyWithCircularReferences} from './stringify-with-circular-references'; +import {getWebpackCacheName} from './webpack-cache'; + +if (!ReactDOM?.version) { + throw new Error('Could not find "react-dom" package. Did you install it?'); +} + +const reactDomVersion = ReactDOM.version.split('.')[0]; +if (reactDomVersion === '0') { + throw new Error( + `Version ${reactDomVersion} of "react-dom" is not supported by Remotion`, + ); +} + +export const shouldUseReactDomClient = + NoReactInternals.ENABLE_V5_BREAKING_CHANGES + ? true + : parseInt(reactDomVersion, 10) >= 18; + +export const getResolveConfig = () => ({ + extensions: ['.ts', '.tsx', '.web.js', '.js', '.jsx', '.mjs', '.cjs'], + alias: { + // Only one version of react + 'react/jsx-runtime': require.resolve('react/jsx-runtime'), + 'react/jsx-dev-runtime': require.resolve('react/jsx-dev-runtime'), + react: require.resolve('react'), + // Needed to not fail on this: https://github.com/remotion-dev/remotion/issues/5045 + 'remotion/no-react': path.resolve( + require.resolve('remotion'), + '..', + '..', + 'esm', + 'no-react.mjs', + ), + 'remotion/version': path.resolve( + require.resolve('remotion'), + '..', + '..', + 'esm', + 'version.mjs', + ), + remotion: path.resolve( + require.resolve('remotion'), + '..', + '..', + 'esm', + 'index.mjs', + ), + + '@remotion/media-parser/worker': path.resolve( + require.resolve('@remotion/media-parser'), + '..', + 'esm', + 'worker.mjs', + ), + // test visual controls before removing this + '@remotion/studio': require.resolve('@remotion/studio'), + 'react-dom/client': shouldUseReactDomClient + ? require.resolve('react-dom/client') + : require.resolve('react-dom'), + }, +}); + +export const getOutputConfig = ( + environment: 'development' | 'production', +) => ({ + hashFunction: 'xxhash64' as const, + filename: NoReactInternals.bundleName, + devtoolModuleFilenameTemplate: '[resource-path]', + assetModuleFilename: + environment === 'development' ? '[path][name][ext]' : '[hash][ext]', +}); + +export const getBaseConfig = ( + environment: 'development' | 'production', + poll: number | null, +) => { + const isBun = typeof Bun !== 'undefined'; + + return { + optimization: { + minimize: false, + }, + experiments: { + lazyCompilation: isBun + ? false + : environment === 'production' + ? false + : { + entries: false, + }, + }, + watchOptions: { + poll: poll ?? undefined, + aggregateTimeout: 0, + ignored: ['**/.git/**', '**/.turbo/**', '**/node_modules/**'], + }, + // Higher source map quality in development to power line numbers for stack traces + devtool: + environment === 'development' + ? ('source-map' as const) + : ('cheap-module-source-map' as const), + }; +}; + +export const getSharedModuleRules = () => [ + { + test: /\.css$/i, + use: [require.resolve('style-loader'), require.resolve('css-loader')], + type: 'javascript/auto' as const, + }, + { + test: /\.(png|svg|jpg|jpeg|webp|gif|bmp|webm|mp4|mov|mp3|m4a|wav|aac)$/, + type: 'asset/resource' as const, + }, + { + test: /\.(woff(2)?|otf|ttf|eot)(\?v=\d+\.\d+\.\d+)?$/, + type: 'asset/resource' as const, + }, +]; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const computeHashAndFinalConfig = ( + conf: T, + options: { + enableCaching: boolean; + environment: 'development' | 'production'; + outDir: string | null; + remotionRoot: string; + }, +): [string, T] => { + const hash = createHash('md5') + .update(jsonStringifyWithCircularReferences(conf)) + .digest('hex'); + return [ + hash, + { + ...conf, + cache: options.enableCaching + ? { + type: 'filesystem', + name: getWebpackCacheName(options.environment, hash), + version: hash, + } + : false, + output: { + ...conf.output, + ...(options.outDir ? {path: options.outDir} : {}), + }, + context: options.remotionRoot, + }, + ]; +}; diff --git a/packages/bundler/src/webpack-config.ts b/packages/bundler/src/webpack-config.ts index dda350ad7e3..6324b54373a 100644 --- a/packages/bundler/src/webpack-config.ts +++ b/packages/bundler/src/webpack-config.ts @@ -1,7 +1,3 @@ -import {createHash} from 'node:crypto'; -import path from 'node:path'; -import ReactDOM from 'react-dom'; -import {NoReactInternals} from 'remotion/no-react'; import type {Configuration} from 'webpack'; import webpack, {ProgressPlugin} from 'webpack'; import {CaseSensitivePathsPlugin} from './case-sensitive-paths'; @@ -11,8 +7,13 @@ import {ReactFreshWebpackPlugin} from './fast-refresh'; import {AllowDependencyExpressionPlugin} from './hide-expression-dependency'; import {IgnorePackFileCacheWarningsPlugin} from './ignore-packfilecache-warnings'; import {AllowOptionalDependenciesPlugin} from './optional-dependencies'; -import {jsonStringifyWithCircularReferences} from './stringify-with-circular-references'; -import {getWebpackCacheName} from './webpack-cache'; +import { + computeHashAndFinalConfig, + getBaseConfig, + getOutputConfig, + getResolveConfig, + getSharedModuleRules, +} from './shared-bundler-config'; import esbuild = require('esbuild'); export type WebpackConfiguration = Configuration; @@ -20,21 +21,6 @@ export type WebpackOverrideFn = ( currentConfiguration: WebpackConfiguration, ) => WebpackConfiguration | Promise; -if (!ReactDOM?.version) { - throw new Error('Could not find "react-dom" package. Did you install it?'); -} - -const reactDomVersion = ReactDOM.version.split('.')[0]; -if (reactDomVersion === '0') { - throw new Error( - `Version ${reactDomVersion} of "react-dom" is not supported by Remotion`, - ); -} - -const shouldUseReactDomClient = NoReactInternals.ENABLE_V5_BREAKING_CHANGES - ? true - : parseInt(reactDomVersion, 10) >= 18; - type Truthy = T extends false | '' | 0 | null | undefined ? never : T; function truthy(value: T): value is Truthy { @@ -81,8 +67,6 @@ export const webpackConfig = async ({ let lastProgress = 0; - const isBun = typeof Bun !== 'undefined'; - const define = new webpack.DefinePlugin( getDefinePluginDefinitions({ maxTimelineTracks, @@ -94,26 +78,7 @@ export const webpackConfig = async ({ ); const conf: WebpackConfiguration = await webpackOverride({ - optimization: { - minimize: false, - }, - experiments: { - lazyCompilation: isBun - ? false - : environment === 'production' - ? false - : { - entries: false, - }, - }, - watchOptions: { - poll: poll ?? undefined, - aggregateTimeout: 0, - ignored: ['**/.git/**', '**/.turbo/**', '**/node_modules/**'], - }, - // Higher source map quality in development to power line numbers for stack traces - devtool: - environment === 'development' ? 'source-map' : 'cheap-module-source-map', + ...getBaseConfig(environment, poll), entry: [ // Fast Refresh must come first, // because setup-environment imports ReactDOM. @@ -152,67 +117,11 @@ export const webpackConfig = async ({ new AllowDependencyExpressionPlugin(), new IgnorePackFileCacheWarningsPlugin(), ], - output: { - hashFunction: 'xxhash64', - filename: NoReactInternals.bundleName, - devtoolModuleFilenameTemplate: '[resource-path]', - assetModuleFilename: - environment === 'development' ? '[path][name][ext]' : '[hash][ext]', - }, - resolve: { - extensions: ['.ts', '.tsx', '.web.js', '.js', '.jsx', '.mjs', '.cjs'], - alias: { - // Only one version of react - 'react/jsx-runtime': require.resolve('react/jsx-runtime'), - 'react/jsx-dev-runtime': require.resolve('react/jsx-dev-runtime'), - react: require.resolve('react'), - // Needed to not fail on this: https://github.com/remotion-dev/remotion/issues/5045 - 'remotion/no-react': path.resolve( - require.resolve('remotion'), - '..', - '..', - 'esm', - 'no-react.mjs', - ), - 'remotion/version': path.resolve( - require.resolve('remotion'), - '..', - '..', - 'esm', - 'version.mjs', - ), - remotion: path.resolve( - require.resolve('remotion'), - '..', - '..', - 'esm', - 'index.mjs', - ), - - '@remotion/media-parser/worker': path.resolve( - require.resolve('@remotion/media-parser'), - '..', - 'esm', - 'worker.mjs', - ), - // test visual controls before removing this - '@remotion/studio': require.resolve('@remotion/studio'), - 'react-dom/client': shouldUseReactDomClient - ? require.resolve('react-dom/client') - : require.resolve('react-dom'), - }, - }, + output: getOutputConfig(environment), + resolve: getResolveConfig(), module: { rules: [ - { - test: /\.css$/i, - use: [require.resolve('style-loader'), require.resolve('css-loader')], - type: 'javascript/auto', - }, - { - test: /\.(png|svg|jpg|jpeg|webp|gif|bmp|webm|mp4|mov|mp3|m4a|wav|aac)$/, - type: 'asset/resource', - }, + ...getSharedModuleRules(), { test: /\.tsx?$/, use: [ @@ -228,10 +137,6 @@ export const webpackConfig = async ({ : null, ].filter(truthy), }, - { - test: /\.(woff(2)?|otf|ttf|eot)(\?v=\d+\.\d+\.\d+)?$/, - type: 'asset/resource', - }, { test: /\.jsx?$/, exclude: /node_modules/, @@ -250,25 +155,11 @@ export const webpackConfig = async ({ ], }, }); - const hash = createHash('md5') - .update(jsonStringifyWithCircularReferences(conf)) - .digest('hex'); - return [ - hash, - { - ...conf, - cache: enableCaching - ? { - type: 'filesystem', - name: getWebpackCacheName(environment, hash), - version: hash, - } - : false, - output: { - ...conf.output, - ...(outDir ? {path: outDir} : {}), - }, - context: remotionRoot, - }, - ]; + + return computeHashAndFinalConfig(conf, { + enableCaching, + environment, + outDir, + remotionRoot, + }); }; diff --git a/packages/docs/docs/cli/benchmark.mdx b/packages/docs/docs/cli/benchmark.mdx index e12922a9d23..6a1b252aa31 100644 --- a/packages/docs/docs/cli/benchmark.mdx +++ b/packages/docs/docs/cli/benchmark.mdx @@ -179,7 +179,7 @@ Inherited from [`npx remotion render`](/docs/cli/render#--audio-bitrate) -### `--experimental-rspack` +### `--experimental-rspack` diff --git a/packages/docs/docs/cli/bundle.mdx b/packages/docs/docs/cli/bundle.mdx index fb0469522a6..ab798dba5bf 100644 --- a/packages/docs/docs/cli/bundle.mdx +++ b/packages/docs/docs/cli/bundle.mdx @@ -42,6 +42,6 @@ Define the location of the resulting bundle. By default it is a folder called `. -### `--experimental-rspack` +### `--experimental-rspack` diff --git a/packages/docs/docs/cli/render.mdx b/packages/docs/docs/cli/render.mdx index 8f70acb9535..4ffeebbe76e 100644 --- a/packages/docs/docs/cli/render.mdx +++ b/packages/docs/docs/cli/render.mdx @@ -256,7 +256,7 @@ Not to be confused with the [`--timeout` flag when deploying a Lambda function]( -### `--experimental-rspack` +### `--experimental-rspack` diff --git a/packages/docs/docs/cli/still.mdx b/packages/docs/docs/cli/still.mdx index 56b6e2881fc..d8412bc6bb2 100644 --- a/packages/docs/docs/cli/still.mdx +++ b/packages/docs/docs/cli/still.mdx @@ -144,7 +144,7 @@ Not to be confused with the [`--timeout` flag when deploying a Lambda function]( -### `--experimental-rspack` +### `--experimental-rspack` diff --git a/packages/docs/docs/cli/studio.mdx b/packages/docs/docs/cli/studio.mdx index 2cb6de52a6f..0b47dd39d76 100644 --- a/packages/docs/docs/cli/studio.mdx +++ b/packages/docs/docs/cli/studio.mdx @@ -58,7 +58,7 @@ Specify a location for a dotenv file - Default `.env`. [Read about how environme [Enables experimental client-side rendering in the Studio](/docs/config#setexperimentalclientsiderenderingenabled). Default is `false`. -### `--experimental-rspack` +### `--experimental-rspack` diff --git a/packages/docs/docs/cloudrun/deploysite.mdx b/packages/docs/docs/cloudrun/deploysite.mdx index fabe302c4a7..29908ddf165 100644 --- a/packages/docs/docs/cloudrun/deploysite.mdx +++ b/packages/docs/docs/cloudrun/deploysite.mdx @@ -115,7 +115,7 @@ _default: `false`_ Whether experimental client-side rendering should be enabled in the Studio. See [Config.setExperimentalClientSideRenderingEnabled()](/docs/config#setexperimentalclientsiderenderingenabled) for more information. -#### `rspack?` +#### `rspack?` _default: `false`_ diff --git a/packages/docs/docs/config.mdx b/packages/docs/docs/config.mdx index f1e0c7bfdef..50d65110464 100644 --- a/packages/docs/docs/config.mdx +++ b/packages/docs/docs/config.mdx @@ -151,7 +151,7 @@ Config.setExperimentalClientSideRenderingEnabled(true); The [command line flag](/docs/cli/studio#--enable-experimental-client-side-rendering) `--enable-experimental-client-side-rendering` will take precedence over this option. -## `setExperimentalRspackEnabled()` +## `setExperimentalRspackEnabled()` Use [Rspack](https://rspack.dev) instead of Webpack as the bundler for the Studio. Default `false`. diff --git a/packages/docs/docs/lambda/deploysite.mdx b/packages/docs/docs/lambda/deploysite.mdx index 0ec4051222a..f2d663a9fa7 100644 --- a/packages/docs/docs/lambda/deploysite.mdx +++ b/packages/docs/docs/lambda/deploysite.mdx @@ -124,7 +124,7 @@ _default: `false`_ Whether experimental client-side rendering should be enabled in the Studio. See [Config.setExperimentalClientSideRenderingEnabled()](/docs/config#setexperimentalclientsiderenderingenabled) for more information. -#### `rspack?` +#### `rspack?` _default: `false`_ From bf9f8d35aefafbd0320b438925a0bc9442d111ab Mon Sep 17 00:00:00 2001 From: Jonny Burger Date: Thu, 19 Feb 2026 15:45:44 +0100 Subject: [PATCH 10/33] fix --- packages/bundler/src/shared-bundler-config.ts | 4 +--- packages/cli/src/parsed-cli.ts | 1 - packages/renderer/src/options/rspack.tsx | 1 + 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/bundler/src/shared-bundler-config.ts b/packages/bundler/src/shared-bundler-config.ts index 756f98065ee..e4f4e5ee9c0 100644 --- a/packages/bundler/src/shared-bundler-config.ts +++ b/packages/bundler/src/shared-bundler-config.ts @@ -65,9 +65,7 @@ export const getResolveConfig = () => ({ }, }); -export const getOutputConfig = ( - environment: 'development' | 'production', -) => ({ +export const getOutputConfig = (environment: 'development' | 'production') => ({ hashFunction: 'xxhash64' as const, filename: NoReactInternals.bundleName, devtoolModuleFilenameTemplate: '[resource-path]', diff --git a/packages/cli/src/parsed-cli.ts b/packages/cli/src/parsed-cli.ts index 5d0a9c0474a..76a1675fcff 100644 --- a/packages/cli/src/parsed-cli.ts +++ b/packages/cli/src/parsed-cli.ts @@ -37,7 +37,6 @@ export const BooleanFlags = [ 'onlyAllocateCpuDuringRequestProcessing', BrowserSafeApis.options.isProductionOption.cliFlag, BrowserSafeApis.options.forceNewStudioOption.cliFlag, - BrowserSafeApis.options.rspackOption.cliFlag, ]; export const parsedCli = minimist(process.argv.slice(2), { diff --git a/packages/renderer/src/options/rspack.tsx b/packages/renderer/src/options/rspack.tsx index 5e3f0d4214c..9f8addfeda8 100644 --- a/packages/renderer/src/options/rspack.tsx +++ b/packages/renderer/src/options/rspack.tsx @@ -15,6 +15,7 @@ export const rspackOption = { type: false as boolean, getValue: ({commandLine}) => { if (commandLine[cliFlag] !== undefined) { + rspackEnabled = true; return { value: commandLine[cliFlag] as boolean, source: 'cli', From 600c05f80ff1c54966d919461b1a0b3018ae1f1d Mon Sep 17 00:00:00 2001 From: Jonny Burger Date: Thu, 19 Feb 2026 17:35:17 +0100 Subject: [PATCH 11/33] Remove process-update.ts HMR client, add disk fallback for rspack hot-update files - Delete process-update.ts which had complex recursive HMR check/apply logic that interfered with rspack's native React Fast Refresh - Replace with minimal inline module.hot.check(true) in client.ts with hash comparison and idle status guards - Add disk fallback in dev middleware for .hot-update. files that rspack's native compiler may write to disk instead of memfs - Disable ReactRefreshPlugin overlay for rspack Co-Authored-By: Claude Opus 4.6 --- packages/bundler/src/rspack-config.ts | 3 +- .../dev-middleware/middleware.ts | 72 +++++++ .../src/hot-middleware-client/client.ts | 16 +- .../hot-middleware-client/process-update.ts | 202 ------------------ 4 files changed, 87 insertions(+), 206 deletions(-) delete mode 100644 packages/studio/src/hot-middleware-client/process-update.ts diff --git a/packages/bundler/src/rspack-config.ts b/packages/bundler/src/rspack-config.ts index 303d1a3896a..2c66a297368 100644 --- a/packages/bundler/src/rspack-config.ts +++ b/packages/bundler/src/rspack-config.ts @@ -92,7 +92,6 @@ export const rspackConfig = async ({ // Rspack config is structurally compatible with webpack config at runtime, // but the TypeScript types differ. Cast through `any` for the override. - // eslint-disable-next-line @typescript-eslint/no-explicit-any const conf = (await webpackOverride({ ...getBaseConfig(environment, poll), ignoreWarnings: [ @@ -110,7 +109,7 @@ export const rspackConfig = async ({ plugins: environment === 'development' ? [ - new ReactRefreshPlugin(), + new ReactRefreshPlugin({overlay: false}), new rspack.HotModuleReplacementPlugin(), define, ] diff --git a/packages/studio-server/src/preview-server/dev-middleware/middleware.ts b/packages/studio-server/src/preview-server/dev-middleware/middleware.ts index a9236992090..107039048bc 100644 --- a/packages/studio-server/src/preview-server/dev-middleware/middleware.ts +++ b/packages/studio-server/src/preview-server/dev-middleware/middleware.ts @@ -1,4 +1,5 @@ import {RenderInternals} from '@remotion/renderer'; +import fs from 'node:fs'; import type {ReadStream} from 'node:fs'; import type {IncomingMessage, ServerResponse} from 'node:http'; import path from 'node:path'; @@ -117,6 +118,58 @@ function getFilenameFromUrl( return foundFilename; } +// Rspack's native compiler may write HMR update files to disk +// rather than the in-memory outputFileSystem. Check the real filesystem +// as a fallback for .hot-update. files. +function getFilenameFromUrlDiskFallback( + context: DevMiddlewareContext, + url: string | undefined, +): {filename: string; fromDisk: true} | undefined { + const paths = getPaths(context); + + let urlObject; + try { + urlObject = memoizedParse(url, false, true); + } catch { + return; + } + + const pathname = urlObject.pathname; + if (!pathname || !pathname.includes('.hot-update.')) { + return; + } + + for (const {publicPath, outputPath} of paths) { + let publicPathObject; + try { + publicPathObject = memoizedParse( + publicPath !== 'auto' && publicPath ? publicPath : '/', + false, + true, + ); + } catch { + continue; + } + + if (pathname.startsWith(publicPathObject.pathname)) { + const stripped = pathname.substr(publicPathObject.pathname.length); + const filename = stripped + ? path.join(outputPath, querystring.unescape(stripped)) + : outputPath; + + try { + if (fs.statSync(filename).isFile()) { + return {filename, fromDisk: true}; + } + } catch { + continue; + } + } + } + + return undefined; +} + export function getValueContentRangeHeader( type: string, size: number, @@ -177,6 +230,25 @@ export function middleware(context: DevMiddlewareContext) { const filename = getFilenameFromUrl(context, req.url); if (!filename) { + // Rspack may write HMR update files to disk instead of memfs. + // Try serving from the real filesystem as a fallback. + const diskResult = getFilenameFromUrlDiskFallback( + context, + req.url, + ); + if (diskResult) { + const contentType = RenderInternals.mimeContentType( + path.extname(diskResult.filename), + ); + if (contentType) { + setHeaderForResponse(res, 'Content-Type', contentType); + } + + const content = fs.readFileSync(diskResult.filename); + send(req, res, content, content.byteLength); + return; + } + goNext(); return; diff --git a/packages/studio/src/hot-middleware-client/client.ts b/packages/studio/src/hot-middleware-client/client.ts index 82d84a95ed4..f399ac44ff6 100644 --- a/packages/studio/src/hot-middleware-client/client.ts +++ b/packages/studio/src/hot-middleware-client/client.ts @@ -9,7 +9,6 @@ */ import type {HotMiddlewareMessage} from '@remotion/studio-shared'; import {hotMiddlewareOptions, stripAnsi} from '@remotion/studio-shared'; -import {processUpdate} from './process-update'; function eventSourceWrapper() { let source: EventSource; @@ -180,7 +179,20 @@ function processMessage(obj: HotMiddlewareMessage) { if (applyUpdate) { window.remotion_finishedBuilding?.(); - processUpdate(obj.hash, obj.modules, hotMiddlewareOptions); + if ( + obj.hash && + obj.hash !== __webpack_hash__ && + __webpack_module__.hot?.status() === 'idle' + ) { + __webpack_module__.hot + ?.check(true) + .catch((err: Error) => { + console.warn( + '[Fast refresh] Update check failed: ' + + (err.stack || err.message), + ); + }); + } } break; diff --git a/packages/studio/src/hot-middleware-client/process-update.ts b/packages/studio/src/hot-middleware-client/process-update.ts deleted file mode 100644 index 1aad3a0b287..00000000000 --- a/packages/studio/src/hot-middleware-client/process-update.ts +++ /dev/null @@ -1,202 +0,0 @@ -/* eslint-disable no-console */ -/** - * Source code is adapted from - * https://github.com/webpack-contrib/webpack-hot-middleware#readme - * and rewritten in TypeScript. This file is MIT licensed - */ - -/** - * Based heavily on https://github.com/webpack/webpack/blob/ - * c0afdf9c6abc1dd70707c594e473802a566f7b6e/hot/only-dev-server.js - * Original copyright Tobias Koppers @sokra (MIT license) - */ - -import type {HotMiddlewareOptions, ModuleMap} from '@remotion/studio-shared'; -import {showNotification} from '../components/Notifications/NotificationCenter'; -import {reloadUrl} from '../helpers/url-state'; - -if (!__webpack_module__.hot) { - throw new Error('[Fast refresh] Hot Module Replacement is disabled.'); -} - -const hmrDocsUrl = 'https://webpack.js.org/concepts/hot-module-replacement/'; - -let lastHash: string | undefined; -const failureStatuses = {abort: 1, fail: 1}; -const applyOptions: AcceptOptions = { - ignoreUnaccepted: true, - ignoreDeclined: true, - ignoreErrored: true, - onUnaccepted(data) { - console.warn( - 'Ignored an update to unaccepted module ' + - (data.chain ?? []).join(' -> '), - ); - - // Case: - // 1. Import a CSS file with a bad filename in Root.tsx - // 2. Fix the import and save it - if (!window.remotion_isStudio) { - reloadUrl(); - } - }, - onDeclined(data) { - console.warn( - 'Ignored an update to declined module ' + (data.chain ?? []).join(' -> '), - ); - }, - onErrored(data) { - console.error(data.error); - console.warn( - 'Ignored an error while updating module ' + - data.moduleId + - ' (' + - data.type + - ')', - ); - }, -}; - -function upToDate(hash?: string) { - if (hash) lastHash = hash; - return lastHash === __webpack_hash__; -} - -export const processUpdate = function ( - hash: string | undefined, - moduleMap: ModuleMap, - options: HotMiddlewareOptions, -) { - const {reload} = options; - if (!upToDate(hash) && __webpack_module__.hot?.status() === 'idle') { - check(); - } - - async function check() { - const cb = function (err: Error | null, updatedModules: ModuleId[] | null) { - if (err) return handleError(err); - - if (!updatedModules) { - if (options.warn) { - console.warn( - '[Fast refresh] Cannot find update (Full reload needed)', - ); - console.warn( - '[Fast refresh] (Probably because of restarting the server)', - ); - } - - performReload(); - return null; - } - - const applyCallback = function ( - applyErr: Error | null, - renewedModules: ModuleId[], - ) { - if (applyErr) return handleError(applyErr); - - if (!upToDate()) { - check(); - } - - logUpdates(updatedModules, renewedModules); - }; - - const applyResult = __webpack_module__.hot?.apply(applyOptions); - if ((applyResult as unknown as Promise)?.then) { - // HotModuleReplacement.runtime.js refers to the result as `outdatedModules` - (applyResult as unknown as Promise) - .then((outdatedModules) => { - applyCallback(null, outdatedModules); - }) - .catch((_err: Error) => applyCallback(_err, [])); - } - }; - - try { - const result = await __webpack_module__.hot?.check(false); - cb(null, result); - } catch (err) { - cb(err as Error, []); - } - } - - function logUpdates(updatedModules: ModuleId[], renewedModules: ModuleId[]) { - const unacceptedModules = - updatedModules?.filter((moduleId) => { - return renewedModules && renewedModules.indexOf(moduleId) < 0; - }) ?? []; - - if (unacceptedModules.length > 0) { - if (options.warn) { - console.warn( - "[Fast refresh] The following modules couldn't be hot updated: " + - '(Full reload needed)\n' + - 'This is usually because the modules which have changed ' + - '(and their parents) do not know how to hot reload themselves. ' + - 'See ' + - hmrDocsUrl + - ' for more details.', - ); - unacceptedModules.forEach((moduleId) => { - console.warn( - '[Fast refresh] - ' + (moduleMap[moduleId] || moduleId), - ); - }); - } - - performReload(); - return; - } - - if (!renewedModules || renewedModules.length === 0) { - console.log('[Fast refresh] Nothing hot updated.'); - } else { - renewedModules.forEach((moduleId) => { - console.log( - `[Fast refresh] ${moduleMap[moduleId] || moduleId} fast refreshed.`, - ); - }); - } - } - - function handleError(err: Error) { - if ((__webpack_module__.hot?.status() ?? 'nope') in failureStatuses) { - if (options.warn) { - console.warn( - '[Fast refresh] Cannot check for update (Full reload needed)', - ); - console.warn('[Fast refresh] ' + (err.stack || err.message)); - } - - performReload(); - return; - } - - if (options.warn) { - console.warn( - '[Fast refresh] Update check failed: ' + (err.stack || err.message), - ); - if (!window.remotion_unsavedProps) { - reloadUrl(); - } - } - } - - function performReload() { - if (!reload) { - return; - } - - if (options.warn) console.warn('[Fast refresh] Reloading page'); - if (window.remotion_unsavedProps) { - showNotification( - 'Fast refresh needs to reload the page, but you have unsaved props. Save then reload the page to apply changes.', - 1000, - ); - } else { - reloadUrl(); - } - } -}; From f812af81075515c8b213e7ad447df3ebf9b2171c Mon Sep 17 00:00:00 2001 From: Jonny Burger Date: Fri, 20 Feb 2026 08:22:17 +0100 Subject: [PATCH 12/33] Add `pullfrog.yml` workflow --- .github/workflows/pullfrog.yml | 46 ++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 .github/workflows/pullfrog.yml diff --git a/.github/workflows/pullfrog.yml b/.github/workflows/pullfrog.yml new file mode 100644 index 00000000000..d8647943214 --- /dev/null +++ b/.github/workflows/pullfrog.yml @@ -0,0 +1,46 @@ +# PULLFROG ACTION — DO NOT EDIT EXCEPT WHERE INDICATED +name: Pullfrog +run-name: ${{ inputs.name || github.workflow }} +on: + workflow_dispatch: + inputs: + prompt: + type: string + description: Agent prompt + name: + type: string + description: Run name + +permissions: + id-token: write + contents: write + pull-requests: write + issues: write + actions: read + checks: read + +jobs: + pullfrog: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v6 + with: + fetch-depth: 1 + - name: Run agent + uses: pullfrog/pullfrog@v0 + with: + prompt: ${{ inputs.prompt }} + env: + # add any additional keys your agent(s) need + # optionally, comment out any you won't use + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }} + GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} + CURSOR_API_KEY: ${{ secrets.CURSOR_API_KEY }} + MISTRAL_API_KEY: ${{ secrets.MISTRAL_API_KEY }} + GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }} + DEEPSEEK_API_KEY: ${{ secrets.DEEPSEEK_API_KEY }} + OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }} + From 69652cb3258f9e21ca75f610a29f10b2167f217a Mon Sep 17 00:00:00 2001 From: JonnyBurger Date: Fri, 20 Feb 2026 08:38:09 +0100 Subject: [PATCH 13/33] Migrate --port, --props, --config, --browser to options system Move these CLI flags from direct `parsedCli` access to proper `AnyRemotionOption` definitions with `getValue()`/`setConfig()`. Update all docs to use `` component. Co-Authored-By: Claude Opus 4.6 --- packages/cli/src/config/preview-server.ts | 7 ++- packages/cli/src/get-config-file-name.ts | 16 +++---- packages/cli/src/get-input-props.ts | 18 ++++---- packages/cli/src/parsed-cli.ts | 12 ++++-- packages/cli/src/studio.ts | 22 +++------- packages/docs/docs/cli/bundle.mdx | 2 +- packages/docs/docs/cli/compositions.mdx | 9 ++-- packages/docs/docs/cli/render.mdx | 9 ++-- packages/docs/docs/cli/still.mdx | 9 ++-- packages/docs/docs/cli/studio.mdx | 17 ++------ packages/docs/docs/cloudrun/cli/render.mdx | 5 +-- packages/docs/docs/cloudrun/cli/still.mdx | 5 +-- packages/docs/docs/config.mdx | 24 +++++------ .../docs/docs/lambda/cli/compositions.mdx | 7 +-- packages/docs/docs/lambda/cli/render.mdx | 5 +-- packages/docs/docs/lambda/cli/still.mdx | 5 +-- packages/renderer/src/options/browser.tsx | 39 +++++++++++++++++ packages/renderer/src/options/config.tsx | 31 +++++++++++++ packages/renderer/src/options/index.tsx | 8 ++++ packages/renderer/src/options/port.tsx | 43 +++++++++++++++++++ packages/renderer/src/options/props.tsx | 38 ++++++++++++++++ 21 files changed, 229 insertions(+), 102 deletions(-) create mode 100644 packages/renderer/src/options/browser.tsx create mode 100644 packages/renderer/src/options/config.tsx create mode 100644 packages/renderer/src/options/port.tsx create mode 100644 packages/renderer/src/options/props.tsx diff --git a/packages/cli/src/config/preview-server.ts b/packages/cli/src/config/preview-server.ts index dcced81201d..6b0c42e00bf 100644 --- a/packages/cli/src/config/preview-server.ts +++ b/packages/cli/src/config/preview-server.ts @@ -1,5 +1,8 @@ +import {BrowserSafeApis} from '@remotion/renderer/client'; import {parsedCli} from '../parsed-cli'; +const {portOption} = BrowserSafeApis.options; + let studioPort: number | undefined; let rendererPort: number | undefined; @@ -53,5 +56,7 @@ export const getRendererPortFromConfigFile = () => { }; export const getRendererPortFromConfigFileAndCliFlag = (): number | null => { - return parsedCli.port ?? rendererPort ?? null; + return ( + portOption.getValue({commandLine: parsedCli}).value ?? rendererPort ?? null + ); }; diff --git a/packages/cli/src/get-config-file-name.ts b/packages/cli/src/get-config-file-name.ts index d95eb05f8f1..789fc6640ac 100644 --- a/packages/cli/src/get-config-file-name.ts +++ b/packages/cli/src/get-config-file-name.ts @@ -1,28 +1,28 @@ +import {BrowserSafeApis} from '@remotion/renderer/client'; import {existsSync} from 'node:fs'; import path from 'node:path'; import {loadConfigFile} from './load-config'; import {Log} from './log'; import {parsedCli} from './parsed-cli'; +const {configOption} = BrowserSafeApis.options; + const defaultConfigFileJavascript = 'remotion.config.js'; const defaultConfigFileTypescript = 'remotion.config.ts'; export const loadConfig = (remotionRoot: string): Promise => { - if (parsedCli.config) { - const fullPath = path.resolve(process.cwd(), parsedCli.config); + const configFile = configOption.getValue({commandLine: parsedCli}).value; + if (configFile) { + const fullPath = path.resolve(process.cwd(), configFile); if (!existsSync(fullPath)) { Log.error( {indent: false, logLevel: 'error'}, - `You specified a config file location of "${parsedCli.config}" but no file under ${fullPath} was found.`, + `You specified a config file location of "${configFile}" but no file under ${fullPath} was found.`, ); process.exit(1); } - return loadConfigFile( - remotionRoot, - parsedCli.config, - fullPath.endsWith('.js'), - ); + return loadConfigFile(remotionRoot, configFile, fullPath.endsWith('.js')); } if (remotionRoot === null) { diff --git a/packages/cli/src/get-input-props.ts b/packages/cli/src/get-input-props.ts index 2953eb51b13..0297e229be7 100644 --- a/packages/cli/src/get-input-props.ts +++ b/packages/cli/src/get-input-props.ts @@ -1,19 +1,23 @@ import type {LogLevel, LogOptions} from '@remotion/renderer'; +import {BrowserSafeApis} from '@remotion/renderer/client'; import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import {Log} from './log'; import {parsedCli} from './parsed-cli'; +const {propsOption} = BrowserSafeApis.options; + export const getInputProps = ( onUpdate: ((newProps: Record) => void) | null, logLevel: LogLevel, ): Record => { - if (!parsedCli.props) { + const props = propsOption.getValue({commandLine: parsedCli}).value; + if (!props) { return {}; } - const jsonFile = path.resolve(process.cwd(), parsedCli.props); + const jsonFile = path.resolve(process.cwd(), props); try { if (fs.existsSync(jsonFile)) { const rawJsonData = fs.readFileSync(jsonFile, 'utf-8'); @@ -38,18 +42,14 @@ export const getInputProps = ( return JSON.parse(rawJsonData); } - return JSON.parse(parsedCli.props); + return JSON.parse(props); } catch { Log.error( {indent: false, logLevel}, 'You passed --props but it was neither valid JSON nor a file path to a valid JSON file. Provided value: ' + - parsedCli.props, - ); - Log.info( - {indent: false, logLevel}, - 'Got the following value:', - parsedCli.props, + props, ); + Log.info({indent: false, logLevel}, 'Got the following value:', props); Log.error( {indent: false, logLevel}, 'Check that your input is parseable using `JSON.parse` and try again.', diff --git a/packages/cli/src/parsed-cli.ts b/packages/cli/src/parsed-cli.ts index c78f8a1f1ae..7d3b6ca3133 100644 --- a/packages/cli/src/parsed-cli.ts +++ b/packages/cli/src/parsed-cli.ts @@ -68,6 +68,10 @@ const { forSeamlessAacConcatenationOption, isProductionOption, noOpenOption, + portOption, + propsOption, + configOption, + browserOption, } = BrowserSafeApis.options; export type CommandLineOptions = { @@ -104,7 +108,7 @@ export type CommandLineOptions = { [videoCodecOption.cliFlag]: TypeOfOption; [concurrencyOption.cliFlag]: TypeOfOption; timeout: number; - config: string; + [configOption.cliFlag]: TypeOfOption; ['public-dir']: string; [audioBitrateOption.cliFlag]: TypeOfOption; [videoBitrateOption.cliFlag]: TypeOfOption; @@ -119,7 +123,7 @@ export type CommandLineOptions = { output: string | undefined; [overwriteOption.cliFlag]: TypeOfOption; png: boolean; - props: string; + [propsOption.cliFlag]: TypeOfOption; quality: number; [jpegQualityOption.cliFlag]: TypeOfOption; frames: string | number; @@ -129,7 +133,7 @@ export type CommandLineOptions = { q: boolean; [logLevelOption.cliFlag]: TypeOfOption; help: boolean; - port: number; + [portOption.cliFlag]: TypeOfOption; [stillFrameOption.cliFlag]: TypeOfOption; [headlessOption.cliFlag]: TypeOfOption; [keyboardShortcutsOption.cliFlag]: TypeOfOption< @@ -150,7 +154,7 @@ export type CommandLineOptions = { [packageManagerOption.cliFlag]: TypeOfOption; [webpackPollOption.cliFlag]: TypeOfOption; [noOpenOption.cliFlag]: TypeOfOption; - ['browser']: string; + [browserOption.cliFlag]: TypeOfOption; ['browser-args']: string; [userAgentOption.cliFlag]: TypeOfOption; [outDirOption.cliFlag]: TypeOfOption; diff --git a/packages/cli/src/studio.ts b/packages/cli/src/studio.ts index 69d2b337695..b658b9cd8ba 100644 --- a/packages/cli/src/studio.ts +++ b/packages/cli/src/studio.ts @@ -17,19 +17,6 @@ import { removeJob, } from './render-queue/queue'; -const getPort = () => { - if (parsedCli.port) { - return parsedCli.port; - } - - const serverPort = ConfigInternals.getStudioPort(); - if (serverPort) { - return serverPort; - } - - return null; -}; - const { binariesDirectoryOption, publicDirOption, @@ -44,6 +31,8 @@ const { ipv4Option, webpackPollOption, noOpenOption, + portOption, + browserOption, } = BrowserSafeApis.options; export const studioCommand = async ( @@ -79,7 +68,10 @@ export const studioCommand = async ( process.exit(1); } - const desiredPort = getPort(); + const desiredPort = + portOption.getValue({commandLine: parsedCli}).value ?? + ConfigInternals.getStudioPort() ?? + null; const fullEntryPath = convertEntryPointToServeUrl(file); @@ -147,7 +139,7 @@ export const studioCommand = async ( const result = await StudioServerInternals.startStudio({ previewEntry: require.resolve('@remotion/studio/previewEntry'), browserArgs: parsedCli['browser-args'], - browserFlag: parsedCli.browser, + browserFlag: browserOption.getValue({commandLine: parsedCli}).value ?? '', logLevel, shouldOpenBrowser: !noOpenOption.getValue({commandLine: parsedCli}).value, fullEntryPath, diff --git a/packages/docs/docs/cli/bundle.mdx b/packages/docs/docs/cli/bundle.mdx index 9693cf2d54b..7380185a8de 100644 --- a/packages/docs/docs/cli/bundle.mdx +++ b/packages/docs/docs/cli/bundle.mdx @@ -20,7 +20,7 @@ You may pass a [Serve URL](/docs/terminology/serve-url) or an [entry point](/doc ### `--config` -Specify a location for the Remotion config file. + ### `--log` diff --git a/packages/docs/docs/cli/compositions.mdx b/packages/docs/docs/cli/compositions.mdx index fe2a9d6d5d9..cc37695aba8 100644 --- a/packages/docs/docs/cli/compositions.mdx +++ b/packages/docs/docs/cli/compositions.mdx @@ -19,10 +19,7 @@ You may pass a [Serve URL](/docs/terminology/serve-url) or an [entry point](/doc ### `--props` -[Input Props to pass to the selected composition of your video.](/docs/passing-props#passing-input-props-in-the-cli). -Must be a serialized JSON string (`--props='{"hello": "world"}'`) or a path to a JSON file (`./path/to/props.json`). -From the root component the props can be read using [`getInputProps()`](/docs/get-input-props). -You may transform input props using [`calculateMetadata()`](/docs/calculate-metadata). + :::note Inline JSON string isn't supported on Windows shells because it removes the `"` character, use a file name instead. @@ -30,7 +27,7 @@ Inline JSON string isn't supported on Windows shells because it removes the `"` ### `--config` -Specify a location for the Remotion config file. + ### `--env-file` @@ -50,7 +47,7 @@ If you don't feel like passing command line flags every time, consider creating ### `--port` -[Set a custom HTTP server port to host the Webpack bundle](/docs/config#setport). If not defined, Remotion will try to find a free port. + ### `--public-dir` diff --git a/packages/docs/docs/cli/render.mdx b/packages/docs/docs/cli/render.mdx index 03ce7785b00..3cc4bb45da8 100644 --- a/packages/docs/docs/cli/render.mdx +++ b/packages/docs/docs/cli/render.mdx @@ -22,10 +22,7 @@ Besides choosing a video and output location with the command line arguments, th ### `--props` -[Input Props to pass to the selected composition of your video.](/docs/passing-props#passing-input-props-in-the-cli). -Must be a serialized JSON string (`--props='{"hello": "world"}'`) or a path to a JSON file (`./path/to/props.json`). -From the root component the props can be read using [`getInputProps()`](/docs/get-input-props). -You may transform input props using [`calculateMetadata()`](/docs/calculate-metadata). + :::note Inline JSON string isn't supported on Windows shells because it removes the `"` character, use a file name instead. @@ -65,7 +62,7 @@ Inline JSON string isn't supported on Windows shells because it removes the `"` ### `--config` -Specify a location for the Remotion config file. + ### `--env-file` @@ -187,7 +184,7 @@ Disallows the renderer from doing rendering frames and encoding at the same time ### `--port` -[Set a custom HTTP server port that will be used to host the Webpack bundle](/docs/config#setrendererport). If not defined, Remotion will try to find a free port. + ### `--public-dir` diff --git a/packages/docs/docs/cli/still.mdx b/packages/docs/docs/cli/still.mdx index b745899b208..bde769f5f47 100644 --- a/packages/docs/docs/cli/still.mdx +++ b/packages/docs/docs/cli/still.mdx @@ -22,10 +22,7 @@ If `composition-id` is also not passed, Remotion will let you select a compositi ### `--props` -[Input Props to pass to the selected composition of your video.](/docs/passing-props#passing-input-props-in-the-cli). -Must be a serialized JSON string (`--props='{"hello": "world"}'`) or a path to a JSON file (`./path/to/props.json`). -From the root component the props can be read using [`getInputProps()`](/docs/get-input-props). -You may transform input props using [`calculateMetadata()`](/docs/calculate-metadata). + :::note Inline JSON string isn't supported on Windows shells because it removes the `"` character, use a file name instead. @@ -37,7 +34,7 @@ Inline JSON string isn't supported on Windows shells because it removes the `"` ### `--config` -Specify a location for the Remotion config file. + ### `--env-file` @@ -81,7 +78,7 @@ Sets the output file path, as an alternative to the `output-location` positional ### `--port` -[Set a custom HTTP server port to serve the Webpack bundle](/docs/config#setport). If not defined, Remotion will try to find a free port. + ### `--public-dir` diff --git a/packages/docs/docs/cli/studio.mdx b/packages/docs/docs/cli/studio.mdx index 82a0fc0dd81..c3e943c9136 100644 --- a/packages/docs/docs/cli/studio.mdx +++ b/packages/docs/docs/cli/studio.mdx @@ -19,12 +19,7 @@ You may pass an [entry point](/docs/terminology/entry-point) as an argument, oth ### `--props` -[Input Props to pass to the selected composition of your video.](/docs/passing-props#passing-input-props-in-the-cli). -We don't recommend passing this flag when using the Studio - use [`defaultProps`](/docs/composition#defaultprops) instead. - -Must be a serialized JSON string (`--props='{"hello": "world"}'`) or a path to a JSON file (`./path/to/props.json`). -From the root component the props can be read using [`getInputProps()`](/docs/get-input-props). -You may transform input props using [`calculateMetadata()`](/docs/calculate-metadata). + :::note Inline JSON string isn't supported on Windows shells because it removes the `"` character, use a file name instead. @@ -32,7 +27,7 @@ Inline JSON string isn't supported on Windows shells because it removes the `"` ### `--config` -Specify a location for the Remotion config file. + ### `--env-file` @@ -44,7 +39,7 @@ Specify a location for the Remotion config file. ### `--port` -[Set a custom HTTP server port to start the server on](/docs/config#setstudioport). If not defined, Remotion will try to find a free port. + ### `--public-dir` @@ -68,11 +63,7 @@ Specify a location for the Remotion config file. ### `--browser` -Specify the browser which should be used for opening tab - using the default browser by default. -Pass an absolute string or `"chrome"` to use Chrome. -If Chrome is selected as the browser and you are on macOS, Remotion will try to reuse an existing tab - -For backwards compatibility, the `BROWSER` environment variable is also supported. + ### `--browser-args` diff --git a/packages/docs/docs/cloudrun/cli/render.mdx b/packages/docs/docs/cloudrun/cli/render.mdx index b769e5d6319..c0f83e4ea85 100644 --- a/packages/docs/docs/cloudrun/cli/render.mdx +++ b/packages/docs/docs/cloudrun/cli/render.mdx @@ -53,10 +53,7 @@ The [GCP region](/docs/cloudrun/region-selection) to select. For lowest latency, ### `--props` -[Input Props to pass to the selected composition of your video.](/docs/passing-props#passing-input-props-in-the-cli). -Must be a serialized JSON string (`--props='{"hello": "world"}'`) or a path to a JSON file (`./path/to/props.json`). -From the root component the props can be read using [`getInputProps()`](/docs/get-input-props). -You may transform input props using [`calculateMetadata()`](/docs/calculate-metadata). + :::note Inline JSON string isn't supported on Windows shells because it removes the `"` character, use a file name instead. diff --git a/packages/docs/docs/cloudrun/cli/still.mdx b/packages/docs/docs/cloudrun/cli/still.mdx index e394caf6491..14240113033 100644 --- a/packages/docs/docs/cloudrun/cli/still.mdx +++ b/packages/docs/docs/cloudrun/cli/still.mdx @@ -53,10 +53,7 @@ The [GCP region](/docs/cloudrun/region-selection) to select. For lowest latency, ### `--props` -[Input Props to pass to the selected composition of your video.](/docs/passing-props#passing-input-props-in-the-cli). -Must be a serialized JSON string (`--props='{"hello": "world"}'`) or a path to a JSON file (`./path/to/props.json`). -From the root component the props can be read using [`getInputProps()`](/docs/get-input-props). -You may transform input props using [`calculateMetadata()`](/docs/calculate-metadata). + :::note Inline JSON string isn't supported on Windows shells because it removes the `"` character, use a file name instead. diff --git a/packages/docs/docs/config.mdx b/packages/docs/docs/config.mdx index ce493f274cf..2c05c27e0ef 100644 --- a/packages/docs/docs/config.mdx +++ b/packages/docs/docs/config.mdx @@ -980,6 +980,18 @@ Config.setPublicLicenseKey('your-license-key'); The [command line flag](/docs/cli/studio#--public-license-key) `--public-license-key` will take precedence over this option. +## `setImageSequencePattern()` + + + +```ts twoslash title="remotion.config.ts" +import {Config} from '@remotion/cli/config'; +// ---cut--- +Config.setImageSequencePattern('frame_[frame]_custom.[ext]'); +``` + +The [command line flag](/docs/cli/render#--image-sequence-pattern) `--image-sequence-pattern` will take precedence over this option. + ## ~~`setQuality()`~~ Renamed to `setJpegQuality` in `v4.0.0`. @@ -1094,18 +1106,6 @@ Config.overrideWebpackConfig(async (currentConfiguration) => { }); ``` -### `setImageSequencePattern()` - - - -```ts twoslash title="remotion.config.ts" -import {Config} from '@remotion/cli/config'; -// ---cut--- -Config.setImageSequencePattern('frame_[frame]_custom.[ext]'); -``` - -The [command line flag](/docs/cli/render#--image-sequence-pattern) `--image-sequence-pattern` will take precedence over this option. - ## Old config file format In v3.3.39, a new config file format was introduced which flattens the options so they can more easily be discovered using TypeScript autocompletion. diff --git a/packages/docs/docs/lambda/cli/compositions.mdx b/packages/docs/docs/lambda/cli/compositions.mdx index 0faefd61943..4290f50b74b 100644 --- a/packages/docs/docs/lambda/cli/compositions.mdx +++ b/packages/docs/docs/lambda/cli/compositions.mdx @@ -66,10 +66,7 @@ You should use `npx remotion lambda compositions` if you cannot use [`npx remoti ### `--props` -[Input Props to pass to the selected composition of your video.](/docs/passing-props#passing-input-props-in-the-cli). -Must be a serialized JSON string (`--props='{"hello": "world"}'`) or a path to a JSON file (`./path/to/props.json`). -From the root component the props can be read using [`getInputProps()`](/docs/get-input-props). -You may transform input props using [`calculateMetadata()`](/docs/calculate-metadata). + :::note Inline JSON string isn't supported on Windows shells because it removes the `"` character, use a file name instead. @@ -77,7 +74,7 @@ Inline JSON string isn't supported on Windows shells because it removes the `"` ### `--config` -Specify a location for the Remotion config file. + ### `--env-file` diff --git a/packages/docs/docs/lambda/cli/render.mdx b/packages/docs/docs/lambda/cli/render.mdx index 987ef428455..9331f5f3803 100644 --- a/packages/docs/docs/lambda/cli/render.mdx +++ b/packages/docs/docs/lambda/cli/render.mdx @@ -79,10 +79,7 @@ The [AWS region](/docs/lambda/region-selection) to select. Both project and func ### `--props` -[Input Props to pass to the selected composition of your video.](/docs/passing-props#passing-input-props-in-the-cli). -Must be a serialized JSON string (`--props='{"hello": "world"}'`) or a path to a JSON file (`./path/to/props.json`). -From the root component the props can be read using [`getInputProps()`](/docs/get-input-props). -You may transform input props using [`calculateMetadata()`](/docs/calculate-metadata). + :::note Inline JSON string isn't supported on Windows shells because it removes the `"` character, use a file name instead. diff --git a/packages/docs/docs/lambda/cli/still.mdx b/packages/docs/docs/lambda/cli/still.mdx index 67bfe43a050..52d8ce834b8 100644 --- a/packages/docs/docs/lambda/cli/still.mdx +++ b/packages/docs/docs/lambda/cli/still.mdx @@ -61,10 +61,7 @@ The [AWS region](/docs/lambda/region-selection) to select. Both project and func ### `--props` -[Input Props to pass to the selected composition of your video.](/docs/passing-props#passing-input-props-in-the-cli). -Must be a serialized JSON string (`--props='{"hello": "world"}'`) or a path to a JSON file (`./path/to/props.json`). -From the root component the props can be read using [`getInputProps()`](/docs/get-input-props). -You may transform input props using [`calculateMetadata()`](/docs/calculate-metadata). + :::note Inline JSON string isn't supported on Windows shells because it removes the `"` character, use a file name instead. diff --git a/packages/renderer/src/options/browser.tsx b/packages/renderer/src/options/browser.tsx new file mode 100644 index 00000000000..2ebdefa20a8 --- /dev/null +++ b/packages/renderer/src/options/browser.tsx @@ -0,0 +1,39 @@ +import type {AnyRemotionOption} from './option'; + +const cliFlag = 'browser' as const; + +export const browserOption = { + name: 'Browser', + cliFlag, + description: () => ( + <> + Specify the browser which should be used for opening a tab. The + default browser will be used by default. Pass an absolute path or{' '} + "chrome" to use Chrome. If Chrome is selected as + the browser and you are on macOS, Remotion will try to reuse an + existing tab. + + ), + ssrName: null, + docLink: 'https://www.remotion.dev/docs/cli/studio#--browser', + getValue: ({commandLine}) => { + if (commandLine[cliFlag] !== undefined) { + return { + source: 'cli', + value: commandLine[cliFlag] as string, + }; + } + + return { + source: 'default', + value: null, + }; + }, + setConfig: () => { + throw new Error( + 'setBrowser is not supported. Pass --browser via the CLI instead.', + ); + }, + type: '' as string | null, + id: cliFlag, +} satisfies AnyRemotionOption; diff --git a/packages/renderer/src/options/config.tsx b/packages/renderer/src/options/config.tsx new file mode 100644 index 00000000000..178041a958f --- /dev/null +++ b/packages/renderer/src/options/config.tsx @@ -0,0 +1,31 @@ +import type {AnyRemotionOption} from './option'; + +const cliFlag = 'config' as const; + +export const configOption = { + name: 'Config file', + cliFlag, + description: () => <>Specify a location for the Remotion config file., + ssrName: null, + docLink: 'https://www.remotion.dev/docs/config', + getValue: ({commandLine}) => { + if (commandLine[cliFlag] !== undefined) { + return { + source: 'cli', + value: commandLine[cliFlag] as string, + }; + } + + return { + source: 'default', + value: null, + }; + }, + setConfig: () => { + throw new Error( + 'setConfig is not supported. Pass --config via the CLI instead.', + ); + }, + type: '' as string | null, + id: cliFlag, +} satisfies AnyRemotionOption; diff --git a/packages/renderer/src/options/index.tsx b/packages/renderer/src/options/index.tsx index 4697db766ca..a1b96f2e709 100644 --- a/packages/renderer/src/options/index.tsx +++ b/packages/renderer/src/options/index.tsx @@ -4,11 +4,13 @@ import {audioBitrateOption} from './audio-bitrate'; import {audioCodecOption} from './audio-codec'; import {beepOnFinishOption} from './beep-on-finish'; import {binariesDirectoryOption} from './binaries-directory'; +import {browserOption} from './browser'; import {browserExecutableOption} from './browser-executable'; import {bundleCacheOption} from './bundle-cache'; import {chromeModeOption} from './chrome-mode'; import {colorSpaceOption} from './color-space'; import {concurrencyOption} from './concurrency'; +import {configOption} from './config'; import {crfOption} from './crf'; import {enableCrossSiteIsolationOption} from './cross-site-isolation'; import {darkModeOption} from './dark-mode'; @@ -57,6 +59,8 @@ import {overrideWidthOption} from './override-width'; import {overwriteOption} from './overwrite'; import {packageManagerOption} from './package-manager'; import {pixelFormatOption} from './pixel-format'; +import {portOption} from './port'; +import {propsOption} from './props'; import {preferLosslessAudioOption} from './prefer-lossless'; import {proResProfileOption} from './prores-profile'; import {publicDirOption} from './public-dir'; @@ -161,6 +165,10 @@ export const allOptions = { versionFlagOption, bundleCacheOption, envFileOption, + portOption, + propsOption, + configOption, + browserOption, }; export type AvailableOptions = keyof typeof allOptions; diff --git a/packages/renderer/src/options/port.tsx b/packages/renderer/src/options/port.tsx new file mode 100644 index 00000000000..7da52ec9390 --- /dev/null +++ b/packages/renderer/src/options/port.tsx @@ -0,0 +1,43 @@ +import type {AnyRemotionOption} from './option'; + +const cliFlag = 'port' as const; + +let currentPort: number | null = null; + +export const portOption = { + name: 'Port', + cliFlag, + description: () => ( + <> + Set a custom HTTP server port for the Studio or the render process. If + not defined, Remotion will try to find a free port. + + ), + ssrName: null, + docLink: 'https://www.remotion.dev/docs/config#setstudioport', + getValue: ({commandLine}) => { + if (commandLine[cliFlag] !== undefined) { + return { + source: 'cli', + value: commandLine[cliFlag] as number, + }; + } + + if (currentPort !== null) { + return { + source: 'config', + value: currentPort, + }; + } + + return { + source: 'default', + value: null, + }; + }, + setConfig: (value: number | null) => { + currentPort = value; + }, + type: 0 as number | null, + id: cliFlag, +} satisfies AnyRemotionOption; diff --git a/packages/renderer/src/options/props.tsx b/packages/renderer/src/options/props.tsx new file mode 100644 index 00000000000..0a624934b5f --- /dev/null +++ b/packages/renderer/src/options/props.tsx @@ -0,0 +1,38 @@ +import type {AnyRemotionOption} from './option'; + +const cliFlag = 'props' as const; + +export const propsOption = { + name: 'Input Props', + cliFlag, + description: () => ( + <> + Input Props to pass to the selected composition of your video. Must be + a serialized JSON string (--props='{'{'}"hello": + "world"{'}'}') or a path to a JSON file ( + ./path/to/props.json). + + ), + ssrName: null, + docLink: 'https://www.remotion.dev/docs/passing-props#passing-input-props-in-the-cli', + getValue: ({commandLine}) => { + if (commandLine[cliFlag] !== undefined) { + return { + source: 'cli', + value: commandLine[cliFlag] as string, + }; + } + + return { + source: 'default', + value: null, + }; + }, + setConfig: () => { + throw new Error( + 'setProps is not supported. Pass --props via the CLI instead.', + ); + }, + type: '' as string | null, + id: cliFlag, +} satisfies AnyRemotionOption; From 29913c94e696d86ecb9937bcc8ea6430a7aabb13 Mon Sep 17 00:00:00 2001 From: JonnyBurger Date: Fri, 20 Feb 2026 08:40:04 +0100 Subject: [PATCH 14/33] Update proxy.mdx --- packages/docs/docs/lambda/proxy.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/docs/docs/lambda/proxy.mdx b/packages/docs/docs/lambda/proxy.mdx index 161e5ab7827..eee54039c15 100644 --- a/packages/docs/docs/lambda/proxy.mdx +++ b/packages/docs/docs/lambda/proxy.mdx @@ -6,7 +6,7 @@ sidebar_label: Using a proxy crumb: 'Lambda' --- - +#Using a proxy with Remotion Lambda Remotion Lambda supports using HTTP/HTTPS proxies for all AWS API calls by accepting a `requestHandler` option that allows you to pass a custom AWS SDK request handler. From a81dea4f61030f956eefc33a78c90436d3e55185 Mon Sep 17 00:00:00 2001 From: Jonny Burger Date: Fri, 20 Feb 2026 08:49:18 +0100 Subject: [PATCH 15/33] Update packages/docs/docs/cli/studio.mdx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- packages/docs/docs/cli/studio.mdx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/docs/docs/cli/studio.mdx b/packages/docs/docs/cli/studio.mdx index c3e943c9136..98e221069e9 100644 --- a/packages/docs/docs/cli/studio.mdx +++ b/packages/docs/docs/cli/studio.mdx @@ -65,6 +65,11 @@ Inline JSON string isn't supported on Windows shells because it removes the `"` +You can also configure the browser via environment variables for backwards compatibility: + +- `BROWSER` behaves like the `--browser` flag. +- `BROWSER_ARGS` behaves like the `--browser-args` flag. +- Setting `BROWSER=none` disables auto-opening of the browser (equivalent to `--no-open`). ### `--browser-args` A set of command line flags that should be passed to the browser. Pass them like this: From cc69cbe9e7b0dfa8e3c577a9f32d4b6ac55f0458 Mon Sep 17 00:00:00 2001 From: "pullfrog[bot]" <226033991+pullfrog[bot]@users.noreply.github.com> Date: Fri, 20 Feb 2026 08:04:23 +0000 Subject: [PATCH 16/33] style: Fix formatting --- packages/docs/docs/cli/browser/index.mdx | 6 ++-- .../docs/cli/browser/table-of-contents.tsx | 28 +++++++++---------- packages/docs/docs/cli/studio.mdx | 1 + packages/docs/docs/lambda/cli/functions.mdx | 2 +- .../docs/docs/lambda/cli/functions/ls.mdx | 2 +- .../docs/docs/lambda/cli/functions/rm.mdx | 2 +- .../docs/docs/lambda/cli/functions/rmall.mdx | 2 +- packages/docs/docs/lambda/cli/regions.mdx | 7 +---- packages/docs/docs/lambda/cli/sites.mdx | 2 +- .../docs/docs/lambda/cli/sites/create.mdx | 2 +- packages/docs/docs/lambda/cli/sites/ls.mdx | 3 +- packages/docs/docs/lambda/cli/sites/rm.mdx | 2 +- packages/docs/docs/lambda/cli/sites/rmall.mdx | 2 +- packages/renderer/src/options/browser.tsx | 8 +++--- packages/renderer/src/options/index.tsx | 2 +- packages/renderer/src/options/port.tsx | 4 +-- packages/renderer/src/options/props.tsx | 13 +++++---- 17 files changed, 43 insertions(+), 45 deletions(-) diff --git a/packages/docs/docs/cli/browser/index.mdx b/packages/docs/docs/cli/browser/index.mdx index f2ed162601d..61a55282b63 100644 --- a/packages/docs/docs/cli/browser/index.mdx +++ b/packages/docs/docs/cli/browser/index.mdx @@ -1,11 +1,11 @@ --- image: /generated/articles-docs-cli-browser-index.png title: npx remotion browser -crumb: "@remotion/cli" -sidebar_label: "browser" +crumb: '@remotion/cli' +sidebar_label: 'browser' --- -import { TableOfContents } from "./table-of-contents"; +import {TableOfContents} from './table-of-contents'; # npx remotion browser diff --git a/packages/docs/docs/cli/browser/table-of-contents.tsx b/packages/docs/docs/cli/browser/table-of-contents.tsx index 59fd4770e1a..fc6a53ec1e0 100644 --- a/packages/docs/docs/cli/browser/table-of-contents.tsx +++ b/packages/docs/docs/cli/browser/table-of-contents.tsx @@ -1,16 +1,16 @@ -import React from "react"; -import { Grid } from "../../../components/TableOfContents/Grid"; -import { TOCItem } from "../../../components/TableOfContents/TOCItem"; +import React from 'react'; +import {Grid} from '../../../components/TableOfContents/Grid'; +import {TOCItem} from '../../../components/TableOfContents/TOCItem'; export const TableOfContents: React.FC = () => { - return ( -
- - - browser ensure -
Ensure Remotion has a browser to render
-
-
-
- ); -}; \ No newline at end of file + return ( +
+ + + browser ensure +
Ensure Remotion has a browser to render
+
+
+
+ ); +}; diff --git a/packages/docs/docs/cli/studio.mdx b/packages/docs/docs/cli/studio.mdx index 98e221069e9..880e7b03bd4 100644 --- a/packages/docs/docs/cli/studio.mdx +++ b/packages/docs/docs/cli/studio.mdx @@ -70,6 +70,7 @@ You can also configure the browser via environment variables for backwards compa - `BROWSER` behaves like the `--browser` flag. - `BROWSER_ARGS` behaves like the `--browser-args` flag. - Setting `BROWSER=none` disables auto-opening of the browser (equivalent to `--no-open`). + ### `--browser-args` A set of command line flags that should be passed to the browser. Pass them like this: diff --git a/packages/docs/docs/lambda/cli/functions.mdx b/packages/docs/docs/lambda/cli/functions.mdx index 9bee56febc1..669a537bd4b 100644 --- a/packages/docs/docs/lambda/cli/functions.mdx +++ b/packages/docs/docs/lambda/cli/functions.mdx @@ -17,4 +17,4 @@ You only need one function per AWS region and Remotion version. Suggested readin ## See also - [Do I need to deploy a function for each render?](/docs/lambda/faq#do-i-need-to-deploy-a-function-for-each-render) -- [Setup guide](/docs/lambda/setup) \ No newline at end of file +- [Setup guide](/docs/lambda/setup) diff --git a/packages/docs/docs/lambda/cli/functions/ls.mdx b/packages/docs/docs/lambda/cli/functions/ls.mdx index 095d423df47..5c5f83993ef 100644 --- a/packages/docs/docs/lambda/cli/functions/ls.mdx +++ b/packages/docs/docs/lambda/cli/functions/ls.mdx @@ -37,4 +37,4 @@ The [AWS region](/docs/lambda/region-selection) to select. ## `--quiet`, `-q` -Prints only the function names in a space-separated list. If no functions exist, prints `()` \ No newline at end of file +Prints only the function names in a space-separated list. If no functions exist, prints `()` diff --git a/packages/docs/docs/lambda/cli/functions/rm.mdx b/packages/docs/docs/lambda/cli/functions/rm.mdx index 670dea74452..f64b75f0a9a 100644 --- a/packages/docs/docs/lambda/cli/functions/rm.mdx +++ b/packages/docs/docs/lambda/cli/functions/rm.mdx @@ -35,4 +35,4 @@ The [AWS region](/docs/lambda/region-selection) to select. ## `--yes`, `-y` -Skips confirmation. \ No newline at end of file +Skips confirmation. diff --git a/packages/docs/docs/lambda/cli/functions/rmall.mdx b/packages/docs/docs/lambda/cli/functions/rmall.mdx index 26e4bbaab7b..1cc4d763c29 100644 --- a/packages/docs/docs/lambda/cli/functions/rmall.mdx +++ b/packages/docs/docs/lambda/cli/functions/rmall.mdx @@ -42,4 +42,4 @@ The [AWS region](/docs/lambda/region-selection) to select. ## `--yes`, `-y` -Skips confirmation. \ No newline at end of file +Skips confirmation. diff --git a/packages/docs/docs/lambda/cli/regions.mdx b/packages/docs/docs/lambda/cli/regions.mdx index 044338ace8e..bae3310a16f 100644 --- a/packages/docs/docs/lambda/cli/regions.mdx +++ b/packages/docs/docs/lambda/cli/regions.mdx @@ -14,12 +14,7 @@ npx remotion lambda regions
Show output -
-    eu-central-1 eu-west-1 eu-west-2 eu-west-3 eu-south-1 eu-north-1 us-east-1
-    us-east-2 us-west-1 us-west-2 af-south-1 ap-south-1 ap-east-1 ap-southeast-1
-    ap-southeast-2 ap-northeast-3 ap-northeast-1 ap-northeast-2 ca-central-1
-    me-south-1 sa-east-1
-  
+
eu-central-1 eu-west-1 eu-west-2 eu-west-3 eu-south-1 eu-north-1 us-east-1 us-east-2 us-west-1 us-west-2 af-south-1 ap-south-1 ap-east-1 ap-southeast-1 ap-southeast-2 ap-northeast-3 ap-northeast-1 ap-northeast-2 ca-central-1 me-south-1 sa-east-1
## Flags diff --git a/packages/docs/docs/lambda/cli/sites.mdx b/packages/docs/docs/lambda/cli/sites.mdx index dabc986f465..16195e67781 100644 --- a/packages/docs/docs/lambda/cli/sites.mdx +++ b/packages/docs/docs/lambda/cli/sites.mdx @@ -10,4 +10,4 @@ The `npx remotion lambda sites` command allows to create, view and delete Remoti - [`create`](/docs/lambda/cli/sites/create) - [`ls`](/docs/lambda/cli/sites/ls) - [`rm`](/docs/lambda/cli/sites/rm) -- [`rmall`](/docs/lambda/cli/sites/rmall) \ No newline at end of file +- [`rmall`](/docs/lambda/cli/sites/rmall) diff --git a/packages/docs/docs/lambda/cli/sites/create.mdx b/packages/docs/docs/lambda/cli/sites/create.mdx index 77d3ca15992..e9250554507 100644 --- a/packages/docs/docs/lambda/cli/sites/create.mdx +++ b/packages/docs/docs/lambda/cli/sites/create.mdx @@ -86,4 +86,4 @@ Either `public` (default) or `no-acl` if you are not using ACL. Sites must have ## `--force-path-style` -Passes `forcePathStyle` to the AWS S3 client. If you don't know what this is, you probably don't need it. \ No newline at end of file +Passes `forcePathStyle` to the AWS S3 client. If you don't know what this is, you probably don't need it. diff --git a/packages/docs/docs/lambda/cli/sites/ls.mdx b/packages/docs/docs/lambda/cli/sites/ls.mdx index 7969edf8bd8..0c452b78512 100644 --- a/packages/docs/docs/lambda/cli/sites/ls.mdx +++ b/packages/docs/docs/lambda/cli/sites/ls.mdx @@ -28,7 +28,6 @@ Get a list of sites. The URL that is printed can be passed to the `render` comma - ## `--region` The [AWS region](/docs/lambda/region-selection) to select. Both project and function should be in this region. @@ -51,4 +50,4 @@ npx remotion lambda sites ls -q ## `--force-path-style` -Passes `forcePathStyle` to the AWS S3 client. If you don't know what this is, you probably don't need it. \ No newline at end of file +Passes `forcePathStyle` to the AWS S3 client. If you don't know what this is, you probably don't need it. diff --git a/packages/docs/docs/lambda/cli/sites/rm.mdx b/packages/docs/docs/lambda/cli/sites/rm.mdx index 36ad5059a9a..39ab89a1fb0 100644 --- a/packages/docs/docs/lambda/cli/sites/rm.mdx +++ b/packages/docs/docs/lambda/cli/sites/rm.mdx @@ -127,4 +127,4 @@ Specify a specific bucket name to be used. [This is not recommended](/docs/lambd ## `--force-path-style` -Passes `forcePathStyle` to the AWS S3 client. If you don't know what this is, you probably don't need it. \ No newline at end of file +Passes `forcePathStyle` to the AWS S3 client. If you don't know what this is, you probably don't need it. diff --git a/packages/docs/docs/lambda/cli/sites/rmall.mdx b/packages/docs/docs/lambda/cli/sites/rmall.mdx index b739a1970b1..f1ed906f290 100644 --- a/packages/docs/docs/lambda/cli/sites/rmall.mdx +++ b/packages/docs/docs/lambda/cli/sites/rmall.mdx @@ -126,4 +126,4 @@ Specify a specific bucket name to be used. [This is not recommended](/docs/lambd ## `--force-path-style` -Passes `forcePathStyle` to the AWS S3 client. If you don't know what this is, you probably don't need it. \ No newline at end of file +Passes `forcePathStyle` to the AWS S3 client. If you don't know what this is, you probably don't need it. diff --git a/packages/renderer/src/options/browser.tsx b/packages/renderer/src/options/browser.tsx index 2ebdefa20a8..9202418cab4 100644 --- a/packages/renderer/src/options/browser.tsx +++ b/packages/renderer/src/options/browser.tsx @@ -7,11 +7,11 @@ export const browserOption = { cliFlag, description: () => ( <> - Specify the browser which should be used for opening a tab. The - default browser will be used by default. Pass an absolute path or{' '} + Specify the browser which should be used for opening a tab. The default + browser will be used by default. Pass an absolute path or{' '} "chrome" to use Chrome. If Chrome is selected as - the browser and you are on macOS, Remotion will try to reuse an - existing tab. + the browser and you are on macOS, Remotion will try to reuse an existing + tab. ), ssrName: null, diff --git a/packages/renderer/src/options/index.tsx b/packages/renderer/src/options/index.tsx index a1b96f2e709..2440e3f923b 100644 --- a/packages/renderer/src/options/index.tsx +++ b/packages/renderer/src/options/index.tsx @@ -60,8 +60,8 @@ import {overwriteOption} from './overwrite'; import {packageManagerOption} from './package-manager'; import {pixelFormatOption} from './pixel-format'; import {portOption} from './port'; -import {propsOption} from './props'; import {preferLosslessAudioOption} from './prefer-lossless'; +import {propsOption} from './props'; import {proResProfileOption} from './prores-profile'; import {publicDirOption} from './public-dir'; import {publicLicenseKeyOption} from './public-license-key'; diff --git a/packages/renderer/src/options/port.tsx b/packages/renderer/src/options/port.tsx index 7da52ec9390..7a5fefa6515 100644 --- a/packages/renderer/src/options/port.tsx +++ b/packages/renderer/src/options/port.tsx @@ -9,8 +9,8 @@ export const portOption = { cliFlag, description: () => ( <> - Set a custom HTTP server port for the Studio or the render process. If - not defined, Remotion will try to find a free port. + Set a custom HTTP server port for the Studio or the render process. If not + defined, Remotion will try to find a free port. ), ssrName: null, diff --git a/packages/renderer/src/options/props.tsx b/packages/renderer/src/options/props.tsx index 0a624934b5f..11826d8843a 100644 --- a/packages/renderer/src/options/props.tsx +++ b/packages/renderer/src/options/props.tsx @@ -7,14 +7,17 @@ export const propsOption = { cliFlag, description: () => ( <> - Input Props to pass to the selected composition of your video. Must be - a serialized JSON string (--props='{'{'}"hello": - "world"{'}'}') or a path to a JSON file ( - ./path/to/props.json). + Input Props to pass to the selected composition of your video. Must be a + serialized JSON string ( + + --props='{'{'}"hello": "world"{'}'}' + + ) or a path to a JSON file (./path/to/props.json). ), ssrName: null, - docLink: 'https://www.remotion.dev/docs/passing-props#passing-input-props-in-the-cli', + docLink: + 'https://www.remotion.dev/docs/passing-props#passing-input-props-in-the-cli', getValue: ({commandLine}) => { if (commandLine[cliFlag] !== undefined) { return { From a999134ddb30c5d093b497cb35afe11849332c04 Mon Sep 17 00:00:00 2001 From: Jonny Burger Date: Fri, 20 Feb 2026 10:04:59 +0100 Subject: [PATCH 17/33] Update remotion.config.ts --- packages/example/remotion.config.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/example/remotion.config.ts b/packages/example/remotion.config.ts index 3a9b1118e64..912bffef8bb 100644 --- a/packages/example/remotion.config.ts +++ b/packages/example/remotion.config.ts @@ -10,4 +10,3 @@ Config.overrideWebpackConfig(async (config) => { }); Config.setExperimentalClientSideRenderingEnabled(true); -Config.setExperimentalRspackEnabled(true); From c1e72093ab47a0eaccddba31cabec2c49ca942e4 Mon Sep 17 00:00:00 2001 From: Jonny Burger Date: Fri, 20 Feb 2026 10:08:19 +0100 Subject: [PATCH 18/33] `@remotion/web-renderer`: Add support for text shadows Closes #6606 Co-Authored-By: Claude Opus 4.6 --- .../src/drawing/text/draw-text.ts | 30 +++++-- .../src/drawing/text/parse-text-shadow.ts | 59 ++++++++++++ packages/web-renderer/src/test/Root.tsx | 2 + .../text-shadow-chromium-darwin.png | Bin 0 -> 25949 bytes .../text-shadow-firefox-darwin.png | Bin 0 -> 24556 bytes .../text-shadow-webkit-darwin.png | Bin 0 -> 22371 bytes .../src/test/fixtures/text/text-shadow.tsx | 85 ++++++++++++++++++ .../src/test/text-shadow.test.tsx | 17 ++++ 8 files changed, 188 insertions(+), 5 deletions(-) create mode 100644 packages/web-renderer/src/drawing/text/parse-text-shadow.ts create mode 100644 packages/web-renderer/src/test/__screenshots__/text-shadow.test.tsx/text-shadow-chromium-darwin.png create mode 100644 packages/web-renderer/src/test/__screenshots__/text-shadow.test.tsx/text-shadow-firefox-darwin.png create mode 100644 packages/web-renderer/src/test/__screenshots__/text-shadow.test.tsx/text-shadow-webkit-darwin.png create mode 100644 packages/web-renderer/src/test/fixtures/text/text-shadow.tsx create mode 100644 packages/web-renderer/src/test/text-shadow.test.tsx diff --git a/packages/web-renderer/src/drawing/text/draw-text.ts b/packages/web-renderer/src/drawing/text/draw-text.ts index 7c75734bb30..5939ec7605a 100644 --- a/packages/web-renderer/src/drawing/text/draw-text.ts +++ b/packages/web-renderer/src/drawing/text/draw-text.ts @@ -3,6 +3,7 @@ import {Internals} from 'remotion'; import type {DrawFn} from '../drawn-fn'; import {applyTextTransform} from './apply-text-transform'; import {findWords} from './find-line-breaks.text'; +import {parseTextShadow} from './parse-text-shadow'; export const drawText = ({ span, @@ -25,6 +26,7 @@ export const drawText = ({ letterSpacing, textTransform, webkitTextFillColor, + textShadow: textShadowValue, } = computedStyle; const isVertical = writingMode !== 'horizontal-tb'; if (isVertical) { @@ -63,6 +65,8 @@ export const drawText = ({ const tokens = findWords(span); + const textShadows = parseTextShadow(textShadowValue); + for (const token of tokens) { const measurements = contextToDraw.measureText(originalText); const {fontBoundingBoxDescent, fontBoundingBoxAscent} = measurements; @@ -72,11 +76,27 @@ export const drawText = ({ const leading = token.rect.height - fontHeight; const halfLeading = leading / 2; - contextToDraw.fillText( - token.text, - (isRTL ? token.rect.right : token.rect.left) - parentRect.x, - token.rect.top + fontBoundingBoxAscent + halfLeading - parentRect.y, - ); + const x = (isRTL ? token.rect.right : token.rect.left) - parentRect.x; + const y = + token.rect.top + fontBoundingBoxAscent + halfLeading - parentRect.y; + + // Draw text shadows from last to first (so first shadow appears on top) + for (let i = textShadows.length - 1; i >= 0; i--) { + const shadow = textShadows[i]; + contextToDraw.shadowColor = shadow.color; + contextToDraw.shadowBlur = shadow.blurRadius; + contextToDraw.shadowOffsetX = shadow.offsetX; + contextToDraw.shadowOffsetY = shadow.offsetY; + contextToDraw.fillText(token.text, x, y); + } + + // Reset shadow and draw the actual text on top + contextToDraw.shadowColor = 'transparent'; + contextToDraw.shadowBlur = 0; + contextToDraw.shadowOffsetX = 0; + contextToDraw.shadowOffsetY = 0; + + contextToDraw.fillText(token.text, x, y); } span.textContent = originalText; diff --git a/packages/web-renderer/src/drawing/text/parse-text-shadow.ts b/packages/web-renderer/src/drawing/text/parse-text-shadow.ts new file mode 100644 index 00000000000..e4e16cbea98 --- /dev/null +++ b/packages/web-renderer/src/drawing/text/parse-text-shadow.ts @@ -0,0 +1,59 @@ +export interface TextShadow { + offsetX: number; + offsetY: number; + blurRadius: number; + color: string; +} + +export const parseTextShadow = (textShadowValue: string): TextShadow[] => { + if (!textShadowValue || textShadowValue === 'none') { + return []; + } + + const shadows: TextShadow[] = []; + + // Split by comma, but respect rgba() colors + const shadowStrings = textShadowValue.split(/,(?![^(]*\))/); + + for (const shadowStr of shadowStrings) { + const trimmed = shadowStr.trim(); + if (!trimmed || trimmed === 'none') { + continue; + } + + const shadow: TextShadow = { + offsetX: 0, + offsetY: 0, + blurRadius: 0, + color: 'rgba(0, 0, 0, 0.5)', + }; + + let remaining = trimmed; + + // Extract color (can be rgb(), rgba(), hsl(), hsla(), hex, or named color) + const colorMatch = remaining.match( + /(rgba?\([^)]+\)|hsla?\([^)]+\)|#[0-9a-f]{3,8}|[a-z]+)/i, + ); + if (colorMatch) { + shadow.color = colorMatch[0]; + remaining = remaining.replace(colorMatch[0], '').trim(); + } + + // Parse remaining numeric values (offset-x offset-y blur-radius) + const numbers = remaining.match(/[+-]?\d*\.?\d+(?:px|em|rem|%)?/gi) || []; + const values = numbers.map((n) => parseFloat(n) || 0); + + if (values.length >= 2) { + shadow.offsetX = values[0]; + shadow.offsetY = values[1]; + + if (values.length >= 3) { + shadow.blurRadius = Math.max(0, values[2]); + } + } + + shadows.push(shadow); + } + + return shadows; +}; diff --git a/packages/web-renderer/src/test/Root.tsx b/packages/web-renderer/src/test/Root.tsx index 1702dca8705..e231ed07174 100644 --- a/packages/web-renderer/src/test/Root.tsx +++ b/packages/web-renderer/src/test/Root.tsx @@ -54,6 +54,7 @@ import {backgroundClipText3dTransform} from './fixtures/text/background-clip-tex import {letterSpacing} from './fixtures/text/letter-spacing'; import {paragraphs} from './fixtures/text/paragraphs'; import {textFixture} from './fixtures/text/text'; +import {textShadow} from './fixtures/text/text-shadow'; import {textTransform} from './fixtures/text/text-transform'; import {webkitTextFillColor} from './fixtures/text/webkit-text-fill-color'; import {threeDoverflow} from './fixtures/three-d-overflow'; @@ -135,6 +136,7 @@ export const Root: React.FC = () => { + diff --git a/packages/web-renderer/src/test/__screenshots__/text-shadow.test.tsx/text-shadow-chromium-darwin.png b/packages/web-renderer/src/test/__screenshots__/text-shadow.test.tsx/text-shadow-chromium-darwin.png new file mode 100644 index 0000000000000000000000000000000000000000..1c897cd715274759b978a891bf50b86629374339 GIT binary patch literal 25949 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 literal 0 HcmV?d00001 diff --git a/packages/web-renderer/src/test/fixtures/text/text-shadow.tsx b/packages/web-renderer/src/test/fixtures/text/text-shadow.tsx new file mode 100644 index 00000000000..0b7c4800fc7 --- /dev/null +++ b/packages/web-renderer/src/test/fixtures/text/text-shadow.tsx @@ -0,0 +1,85 @@ +import React from 'react'; +import {AbsoluteFill} from 'remotion'; + +const Component: React.FC = () => { + return ( + + {/* Simple text shadow */} +
+ Shadow +
+ + {/* Colored text shadow */} +
+ Color +
+ + {/* Multiple text shadows */} +
+ Multi +
+ + {/* Text shadow with no blur */} +
+ Hard +
+ + {/* Text shadow with glow effect */} +
+ Glow +
+
+ ); +}; + +export const textShadow = { + component: Component, + id: 'text-shadow', + width: 300, + height: 400, + fps: 25, + durationInFrames: 1, +} as const; diff --git a/packages/web-renderer/src/test/text-shadow.test.tsx b/packages/web-renderer/src/test/text-shadow.test.tsx new file mode 100644 index 00000000000..554a4c5f3ac --- /dev/null +++ b/packages/web-renderer/src/test/text-shadow.test.tsx @@ -0,0 +1,17 @@ +import {test} from 'vitest'; +import {renderStillOnWeb} from '../render-still-on-web'; +import '../symbol-dispose'; +import {textShadow} from './fixtures/text/text-shadow'; +import {testImage} from './utils'; + +test('should render text-shadow', async () => { + const {blob} = await renderStillOnWeb({ + licenseKey: 'free-license', + composition: textShadow, + frame: 0, + inputProps: {}, + imageFormat: 'png', + }); + + await testImage({blob, testId: 'text-shadow', threshold: 0.01}); +}); From 2f0f4224191f54bdb84a68bb7f801f7a10c119f3 Mon Sep 17 00:00:00 2001 From: Jonny Burger Date: Fri, 20 Feb 2026 10:21:16 +0100 Subject: [PATCH 19/33] Allow changelog workflow to post for a specific release tag Co-Authored-By: Claude Opus 4.6 --- .github/workflows/changelog.yml | 6 +++++- packages/discord-poster/post.ts | 19 ++++++++++++------- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml index 6fc7855c914..05b8c72d783 100644 --- a/.github/workflows/changelog.yml +++ b/.github/workflows/changelog.yml @@ -2,6 +2,10 @@ on: release: types: [published] workflow_dispatch: + inputs: + tag: + description: 'Release tag to post (e.g. v4.0.422). Leave empty for latest.' + required: false name: Publish latest release jobs: @@ -18,4 +22,4 @@ jobs: DISCORD_TOKEN: ${{ secrets.DISCORD_TOKEN }} run: | cd packages/discord-poster - bun post.ts + bun post.ts ${{ github.event.inputs.tag }} diff --git a/packages/discord-poster/post.ts b/packages/discord-poster/post.ts index 1d8ba2346fe..2efc659bab3 100644 --- a/packages/discord-poster/post.ts +++ b/packages/discord-poster/post.ts @@ -1,15 +1,20 @@ const DISCORD_MAX_LENGTH = 2000; -const latestRelease = await fetch( - 'https://api.github.com/repos/remotion-dev/remotion/releases?per_page=1', -); +const tag = process.argv[2]; -const json = await latestRelease.json(); +const url = tag + ? `https://api.github.com/repos/remotion-dev/remotion/releases/tags/${tag}` + : 'https://api.github.com/repos/remotion-dev/remotion/releases?per_page=1'; + +const latestRelease = await fetch(url); + +const response = await latestRelease.json(); +const release = tag ? response : response[0]; const markdown = [ - `${json[0].tag_name} has been released!`, - `<:merge:909914451447259177> ${json[0].html_url}`, - ...json[0].body.split('\n').map((s: string) => { + `${release.tag_name} has been released!`, + `<:merge:909914451447259177> ${release.html_url}`, + ...release.body.split('\n').map((s: string) => { if (s.startsWith('## ')) { return s.replace('## ', '**<:love:989990489824559104> ') + '**'; } From e2f0d3a6cb5111159e24a282eb6a15682ef8030e Mon Sep 17 00:00:00 2001 From: Jonny Burger Date: Fri, 20 Feb 2026 10:24:11 +0100 Subject: [PATCH 20/33] docs: Use component for shared option descriptions Replace inline option descriptions with the component in deploySite docs (cloudrun, lambda) and bundle.mdx for consistency. Also adds missing logLevel option to cloudrun/deploySite. Co-Authored-By: Claude Opus 4.6 --- packages/docs/docs/bundle.mdx | 6 +++--- packages/docs/docs/cloudrun/deploysite.mdx | 20 +++++++++----------- packages/docs/docs/lambda/deploysite.mdx | 16 +++++----------- 3 files changed, 17 insertions(+), 25 deletions(-) diff --git a/packages/docs/docs/bundle.mdx b/packages/docs/docs/bundle.mdx index 9e669b158ce..14407c304bc 100644 --- a/packages/docs/docs/bundle.mdx +++ b/packages/docs/docs/bundle.mdx @@ -68,11 +68,11 @@ Specify a desired output directory. If no passed, the webpack bundle will be cre ### `keyboardShortcutsEnabled?` -A `boolean` specifying whether the Studio responds to the predefined keyboard shortcuts.. Default `true`. + ### `enableCaching?` -A `boolean` specifying whether Webpack caching should be enabled. Default `true`, it is recommended to leave caching enabled at all times since file changes should be recognized by Webpack nonetheless. + ### `publicPath?` @@ -88,7 +88,7 @@ The current working directory is the directory from which your program gets exec ### `publicDir?` -Set the directory in which the files that can be loaded using [`staticFile()`](/docs/staticfile) are located. By default it is the folder `public/` located in the [Remotion Root](/docs/terminology/remotion-root). If you pass a relative path, it will be resolved against the [Remotion Root](/docs/terminology/remotion-root). + ### `onPublicDirCopyProgress?` diff --git a/packages/docs/docs/cloudrun/deploysite.mdx b/packages/docs/docs/cloudrun/deploysite.mdx index 09908d52862..ae05b88b908 100644 --- a/packages/docs/docs/cloudrun/deploysite.mdx +++ b/packages/docs/docs/cloudrun/deploysite.mdx @@ -56,6 +56,10 @@ The bucket to where the website will be deployed. The bucket must have been crea Specify the subfolder in your Cloud Storage bucket that you want the site to deploy to. If you omit this property, a new subfolder with a random name will be created. If a site already exists with the name you passed, it will be overwritten. Can only contain the following characters: `0-9`, `a-z`, `A-Z`, `-`, `!`, `_`, `.`, `*`, `'`, `(`, `)` +### `logLevel?` + + + ### `options?` An object with the following properties: @@ -79,11 +83,11 @@ Allows to pass a custom webpack override. See [`bundle()` -> webpackOverride](/d #### `enableCaching?` -Whether webpack caching should be enabled. See [`bundle()` -> enableCaching](/docs/bundle#enablecaching) for more information. + #### `publicDir?` -Set the directory in which the files that can be loaded using [`staticFile()`](/docs/staticfile) are located. By default it is the folder `public/` located in the [Remotion Root](/docs/terminology/remotion-root) folder. If you pass a relative path, it will be resolved against the [Remotion Root](/docs/terminology/remotion-root). + #### `rootDir?` @@ -99,21 +103,15 @@ Ignore an error that gets thrown if you pass an entry point file which does not #### `keyboardShortcutsEnabled?` -_default: `true`_ - -Whether keyboard shortcuts should be enabled in the Studio. See [Config.setKeyboardShortcutsEnabled()](/docs/config#setkeyboardshortcutsenabled) for more information. + #### `askAIEnabled?` -_default: `true`_ - -Whether the Ask AI option should be enabled in the Studio. See [Config.setAskAIEnabled()](/docs/config#setaskaienabled) for more information. + #### `experimentalClientSideRenderingEnabled?` -_default: `false`_ - -Whether experimental client-side rendering should be enabled in the Studio. See [Config.setExperimentalClientSideRenderingEnabled()](/docs/config#setexperimentalclientsiderenderingenabled) for more information. + ## Return value diff --git a/packages/docs/docs/lambda/deploysite.mdx b/packages/docs/docs/lambda/deploysite.mdx index efae1ff3574..44d3e0faecb 100644 --- a/packages/docs/docs/lambda/deploysite.mdx +++ b/packages/docs/docs/lambda/deploysite.mdx @@ -82,13 +82,13 @@ Allows to pass a custom webpack override. See [`bundle()` -> webpackOverride](/d #### `enableCaching?` -Whether webpack caching should be enabled. Default true. See [`bundle()` -> enableCaching](/docs/bundle#enablecaching) for more information. + #### `publicDir?` _available from v3.2.17_ -Set the directory in which the files that can be loaded using [`staticFile()`](/docs/staticfile) are located. By default it is the folder `public/` located in the [Remotion Root](/docs/terminology/remotion-root) folder. If you pass a relative path, it will be resolved against the [Remotion Root](/docs/terminology/remotion-root). + #### `rootDir?` @@ -108,21 +108,15 @@ Ignore an error that gets thrown if you pass an entry point file which does not #### `keyboardShortcutsEnabled?` -_default: `true`_ - -Whether keyboard shortcuts should be enabled in the Studio. See [Config.setKeyboardShortcutsEnabled()](/docs/config#setkeyboardshortcutsenabled) for more information. + #### `askAIEnabled?` -_default: `true`_ - -Whether the Ask AI option should be enabled in the Studio. See [Config.setAskAIEnabled()](/docs/config#setaskaienabled) for more information. + #### `experimentalClientSideRenderingEnabled?` -_default: `false`_ - -Whether experimental client-side rendering should be enabled in the Studio. See [Config.setExperimentalClientSideRenderingEnabled()](/docs/config#setexperimentalclientsiderenderingenabled) for more information. + ### `privacy?` From 78c8da62a39932a4d2c954950861e3b1f0359fc5 Mon Sep 17 00:00:00 2001 From: Jonny Burger Date: Fri, 20 Feb 2026 10:31:59 +0100 Subject: [PATCH 21/33] Revert debugging changes to HMR middleware files Co-Authored-By: Claude Opus 4.6 --- .../dev-middleware/middleware.ts | 72 ------- .../src/hot-middleware-client/client.ts | 16 +- .../hot-middleware-client/process-update.ts | 202 ++++++++++++++++++ 3 files changed, 204 insertions(+), 86 deletions(-) create mode 100644 packages/studio/src/hot-middleware-client/process-update.ts diff --git a/packages/studio-server/src/preview-server/dev-middleware/middleware.ts b/packages/studio-server/src/preview-server/dev-middleware/middleware.ts index 107039048bc..a9236992090 100644 --- a/packages/studio-server/src/preview-server/dev-middleware/middleware.ts +++ b/packages/studio-server/src/preview-server/dev-middleware/middleware.ts @@ -1,5 +1,4 @@ import {RenderInternals} from '@remotion/renderer'; -import fs from 'node:fs'; import type {ReadStream} from 'node:fs'; import type {IncomingMessage, ServerResponse} from 'node:http'; import path from 'node:path'; @@ -118,58 +117,6 @@ function getFilenameFromUrl( return foundFilename; } -// Rspack's native compiler may write HMR update files to disk -// rather than the in-memory outputFileSystem. Check the real filesystem -// as a fallback for .hot-update. files. -function getFilenameFromUrlDiskFallback( - context: DevMiddlewareContext, - url: string | undefined, -): {filename: string; fromDisk: true} | undefined { - const paths = getPaths(context); - - let urlObject; - try { - urlObject = memoizedParse(url, false, true); - } catch { - return; - } - - const pathname = urlObject.pathname; - if (!pathname || !pathname.includes('.hot-update.')) { - return; - } - - for (const {publicPath, outputPath} of paths) { - let publicPathObject; - try { - publicPathObject = memoizedParse( - publicPath !== 'auto' && publicPath ? publicPath : '/', - false, - true, - ); - } catch { - continue; - } - - if (pathname.startsWith(publicPathObject.pathname)) { - const stripped = pathname.substr(publicPathObject.pathname.length); - const filename = stripped - ? path.join(outputPath, querystring.unescape(stripped)) - : outputPath; - - try { - if (fs.statSync(filename).isFile()) { - return {filename, fromDisk: true}; - } - } catch { - continue; - } - } - } - - return undefined; -} - export function getValueContentRangeHeader( type: string, size: number, @@ -230,25 +177,6 @@ export function middleware(context: DevMiddlewareContext) { const filename = getFilenameFromUrl(context, req.url); if (!filename) { - // Rspack may write HMR update files to disk instead of memfs. - // Try serving from the real filesystem as a fallback. - const diskResult = getFilenameFromUrlDiskFallback( - context, - req.url, - ); - if (diskResult) { - const contentType = RenderInternals.mimeContentType( - path.extname(diskResult.filename), - ); - if (contentType) { - setHeaderForResponse(res, 'Content-Type', contentType); - } - - const content = fs.readFileSync(diskResult.filename); - send(req, res, content, content.byteLength); - return; - } - goNext(); return; diff --git a/packages/studio/src/hot-middleware-client/client.ts b/packages/studio/src/hot-middleware-client/client.ts index f399ac44ff6..82d84a95ed4 100644 --- a/packages/studio/src/hot-middleware-client/client.ts +++ b/packages/studio/src/hot-middleware-client/client.ts @@ -9,6 +9,7 @@ */ import type {HotMiddlewareMessage} from '@remotion/studio-shared'; import {hotMiddlewareOptions, stripAnsi} from '@remotion/studio-shared'; +import {processUpdate} from './process-update'; function eventSourceWrapper() { let source: EventSource; @@ -179,20 +180,7 @@ function processMessage(obj: HotMiddlewareMessage) { if (applyUpdate) { window.remotion_finishedBuilding?.(); - if ( - obj.hash && - obj.hash !== __webpack_hash__ && - __webpack_module__.hot?.status() === 'idle' - ) { - __webpack_module__.hot - ?.check(true) - .catch((err: Error) => { - console.warn( - '[Fast refresh] Update check failed: ' + - (err.stack || err.message), - ); - }); - } + processUpdate(obj.hash, obj.modules, hotMiddlewareOptions); } break; diff --git a/packages/studio/src/hot-middleware-client/process-update.ts b/packages/studio/src/hot-middleware-client/process-update.ts new file mode 100644 index 00000000000..1aad3a0b287 --- /dev/null +++ b/packages/studio/src/hot-middleware-client/process-update.ts @@ -0,0 +1,202 @@ +/* eslint-disable no-console */ +/** + * Source code is adapted from + * https://github.com/webpack-contrib/webpack-hot-middleware#readme + * and rewritten in TypeScript. This file is MIT licensed + */ + +/** + * Based heavily on https://github.com/webpack/webpack/blob/ + * c0afdf9c6abc1dd70707c594e473802a566f7b6e/hot/only-dev-server.js + * Original copyright Tobias Koppers @sokra (MIT license) + */ + +import type {HotMiddlewareOptions, ModuleMap} from '@remotion/studio-shared'; +import {showNotification} from '../components/Notifications/NotificationCenter'; +import {reloadUrl} from '../helpers/url-state'; + +if (!__webpack_module__.hot) { + throw new Error('[Fast refresh] Hot Module Replacement is disabled.'); +} + +const hmrDocsUrl = 'https://webpack.js.org/concepts/hot-module-replacement/'; + +let lastHash: string | undefined; +const failureStatuses = {abort: 1, fail: 1}; +const applyOptions: AcceptOptions = { + ignoreUnaccepted: true, + ignoreDeclined: true, + ignoreErrored: true, + onUnaccepted(data) { + console.warn( + 'Ignored an update to unaccepted module ' + + (data.chain ?? []).join(' -> '), + ); + + // Case: + // 1. Import a CSS file with a bad filename in Root.tsx + // 2. Fix the import and save it + if (!window.remotion_isStudio) { + reloadUrl(); + } + }, + onDeclined(data) { + console.warn( + 'Ignored an update to declined module ' + (data.chain ?? []).join(' -> '), + ); + }, + onErrored(data) { + console.error(data.error); + console.warn( + 'Ignored an error while updating module ' + + data.moduleId + + ' (' + + data.type + + ')', + ); + }, +}; + +function upToDate(hash?: string) { + if (hash) lastHash = hash; + return lastHash === __webpack_hash__; +} + +export const processUpdate = function ( + hash: string | undefined, + moduleMap: ModuleMap, + options: HotMiddlewareOptions, +) { + const {reload} = options; + if (!upToDate(hash) && __webpack_module__.hot?.status() === 'idle') { + check(); + } + + async function check() { + const cb = function (err: Error | null, updatedModules: ModuleId[] | null) { + if (err) return handleError(err); + + if (!updatedModules) { + if (options.warn) { + console.warn( + '[Fast refresh] Cannot find update (Full reload needed)', + ); + console.warn( + '[Fast refresh] (Probably because of restarting the server)', + ); + } + + performReload(); + return null; + } + + const applyCallback = function ( + applyErr: Error | null, + renewedModules: ModuleId[], + ) { + if (applyErr) return handleError(applyErr); + + if (!upToDate()) { + check(); + } + + logUpdates(updatedModules, renewedModules); + }; + + const applyResult = __webpack_module__.hot?.apply(applyOptions); + if ((applyResult as unknown as Promise)?.then) { + // HotModuleReplacement.runtime.js refers to the result as `outdatedModules` + (applyResult as unknown as Promise) + .then((outdatedModules) => { + applyCallback(null, outdatedModules); + }) + .catch((_err: Error) => applyCallback(_err, [])); + } + }; + + try { + const result = await __webpack_module__.hot?.check(false); + cb(null, result); + } catch (err) { + cb(err as Error, []); + } + } + + function logUpdates(updatedModules: ModuleId[], renewedModules: ModuleId[]) { + const unacceptedModules = + updatedModules?.filter((moduleId) => { + return renewedModules && renewedModules.indexOf(moduleId) < 0; + }) ?? []; + + if (unacceptedModules.length > 0) { + if (options.warn) { + console.warn( + "[Fast refresh] The following modules couldn't be hot updated: " + + '(Full reload needed)\n' + + 'This is usually because the modules which have changed ' + + '(and their parents) do not know how to hot reload themselves. ' + + 'See ' + + hmrDocsUrl + + ' for more details.', + ); + unacceptedModules.forEach((moduleId) => { + console.warn( + '[Fast refresh] - ' + (moduleMap[moduleId] || moduleId), + ); + }); + } + + performReload(); + return; + } + + if (!renewedModules || renewedModules.length === 0) { + console.log('[Fast refresh] Nothing hot updated.'); + } else { + renewedModules.forEach((moduleId) => { + console.log( + `[Fast refresh] ${moduleMap[moduleId] || moduleId} fast refreshed.`, + ); + }); + } + } + + function handleError(err: Error) { + if ((__webpack_module__.hot?.status() ?? 'nope') in failureStatuses) { + if (options.warn) { + console.warn( + '[Fast refresh] Cannot check for update (Full reload needed)', + ); + console.warn('[Fast refresh] ' + (err.stack || err.message)); + } + + performReload(); + return; + } + + if (options.warn) { + console.warn( + '[Fast refresh] Update check failed: ' + (err.stack || err.message), + ); + if (!window.remotion_unsavedProps) { + reloadUrl(); + } + } + } + + function performReload() { + if (!reload) { + return; + } + + if (options.warn) console.warn('[Fast refresh] Reloading page'); + if (window.remotion_unsavedProps) { + showNotification( + 'Fast refresh needs to reload the page, but you have unsaved props. Save then reload the page to apply changes.', + 1000, + ); + } else { + reloadUrl(); + } + } +}; From b53f3fd06453c8144f63ea50f3acb2a1d6236821 Mon Sep 17 00:00:00 2001 From: Jonny Burger Date: Fri, 20 Feb 2026 10:32:05 +0100 Subject: [PATCH 22/33] Extract shared shadow parser and update docs - Extract shared `parseShadowValues` from box-shadow and text-shadow parsers - Update limitations.mdx to mark text-shadow as supported - Update web-renderer-test skill to mention limitations.mdx Co-Authored-By: Claude Opus 4.6 --- .claude/skills/web-renderer-test/SKILL.md | 1 + .../client-side-rendering/limitations.mdx | 2 +- .../src/drawing/draw-box-shadow.ts | 65 ++++--------------- .../web-renderer/src/drawing/parse-shadow.ts | 61 +++++++++++++++++ .../src/drawing/text/parse-text-shadow.ts | 61 ++--------------- 5 files changed, 80 insertions(+), 110 deletions(-) create mode 100644 packages/web-renderer/src/drawing/parse-shadow.ts diff --git a/.claude/skills/web-renderer-test/SKILL.md b/.claude/skills/web-renderer-test/SKILL.md index ea67b729c20..c493f445ba3 100644 --- a/.claude/skills/web-renderer-test/SKILL.md +++ b/.claude/skills/web-renderer-test/SKILL.md @@ -76,3 +76,4 @@ test('should render background-color', async () => { 2. **Important**: Add the fixture to `packages/web-renderer/src/test/Root.tsx` to add a way to preview it. 3. Add a new test in `packages/web-renderer/src/test`. 4. Run `bunx vitest src/test/video.test.tsx` to execute the test. +5. **Important**: Update `packages/docs/docs/client-side-rendering/limitations.mdx` to reflect the newly supported property. diff --git a/packages/docs/docs/client-side-rendering/limitations.mdx b/packages/docs/docs/client-side-rendering/limitations.mdx index 581dff16b68..d8be452cf7c 100644 --- a/packages/docs/docs/client-side-rendering/limitations.mdx +++ b/packages/docs/docs/client-side-rendering/limitations.mdx @@ -64,7 +64,7 @@ The `text-transform` property is supported. The `direction` HTML attribute is supported. The `writing-mode` property is not supported. The `text-decoration` property is not supported. -The `text-shadow` property is not supported. +The `text-shadow` property is supported. The `-webkit-text-stroke` property is not supported. ## Shadows diff --git a/packages/web-renderer/src/drawing/draw-box-shadow.ts b/packages/web-renderer/src/drawing/draw-box-shadow.ts index 4ee1d28148c..61ab93cc903 100644 --- a/packages/web-renderer/src/drawing/draw-box-shadow.ts +++ b/packages/web-renderer/src/drawing/draw-box-shadow.ts @@ -2,12 +2,10 @@ import type {LogLevel} from 'remotion'; import {Internals} from 'remotion'; import type {BorderRadiusCorners} from './border-radius'; import {drawRoundedRectPath} from './draw-rounded'; +import type {ShadowBase} from './parse-shadow'; +import {parseShadowValues} from './parse-shadow'; -interface BoxShadow { - offsetX: number; - offsetY: number; - blurRadius: number; - color: string; +interface BoxShadow extends ShadowBase { inset: boolean; } @@ -16,57 +14,18 @@ export const parseBoxShadow = (boxShadowValue: string): BoxShadow[] => { return []; } - const shadows: BoxShadow[] = []; + const baseShadows = parseShadowValues( + // Remove 'inset' before parsing shared values + boxShadowValue, + ); - // Split by comma, but respect rgba() colors + // Split by comma to check for inset on each shadow const shadowStrings = boxShadowValue.split(/,(?![^(]*\))/); - for (const shadowStr of shadowStrings) { - const trimmed = shadowStr.trim(); - if (!trimmed || trimmed === 'none') { - continue; - } - - const shadow: BoxShadow = { - offsetX: 0, - offsetY: 0, - blurRadius: 0, - color: 'rgba(0, 0, 0, 0.5)', - inset: false, - }; - - // Check for inset - shadow.inset = /\binset\b/i.test(trimmed); - - // Remove 'inset' keyword - let remaining = trimmed.replace(/\binset\b/gi, '').trim(); - - // Extract color (can be rgb(), rgba(), hsl(), hsla(), hex, or named color) - const colorMatch = remaining.match( - /(rgba?\([^)]+\)|hsla?\([^)]+\)|#[0-9a-f]{3,8}|[a-z]+)/i, - ); - if (colorMatch) { - shadow.color = colorMatch[0]; - remaining = remaining.replace(colorMatch[0], '').trim(); - } - - // Parse remaining numeric values (offset-x offset-y blur spread) - const numbers = remaining.match(/[+-]?\d*\.?\d+(?:px|em|rem|%)?/gi) || []; - const values = numbers.map((n) => parseFloat(n) || 0); - - if (values.length >= 2) { - shadow.offsetX = values[0]; - shadow.offsetY = values[1]; - - if (values.length >= 3) { - shadow.blurRadius = Math.max(0, values[2]); // Blur cannot be negative - } - } - - shadows.push(shadow); - } - - return shadows; + return baseShadows.map((base, i) => ({ + ...base, + inset: /\binset\b/i.test(shadowStrings[i] || ''), + })); }; export const drawBorderRadius = ({ diff --git a/packages/web-renderer/src/drawing/parse-shadow.ts b/packages/web-renderer/src/drawing/parse-shadow.ts new file mode 100644 index 00000000000..8799447b38b --- /dev/null +++ b/packages/web-renderer/src/drawing/parse-shadow.ts @@ -0,0 +1,61 @@ +export interface ShadowBase { + offsetX: number; + offsetY: number; + blurRadius: number; + color: string; +} + +export const parseShadowValues = (shadowValue: string): ShadowBase[] => { + if (!shadowValue || shadowValue === 'none') { + return []; + } + + const shadows: ShadowBase[] = []; + + // Split by comma, but respect rgba() colors + const shadowStrings = shadowValue.split(/,(?![^(]*\))/); + + for (const shadowStr of shadowStrings) { + const trimmed = shadowStr.trim(); + if (!trimmed || trimmed === 'none') { + continue; + } + + const shadow: ShadowBase = { + offsetX: 0, + offsetY: 0, + blurRadius: 0, + color: 'rgba(0, 0, 0, 0.5)', + }; + + // Remove 'inset' keyword (only relevant for box-shadow, but strip it + // so it doesn't interfere with color matching) + let remaining = trimmed.replace(/\binset\b/gi, '').trim(); + + // Extract color (can be rgb(), rgba(), hsl(), hsla(), hex, or named color) + const colorMatch = remaining.match( + /(rgba?\([^)]+\)|hsla?\([^)]+\)|#[0-9a-f]{3,8}|[a-z]+)/i, + ); + if (colorMatch) { + shadow.color = colorMatch[0]; + remaining = remaining.replace(colorMatch[0], '').trim(); + } + + // Parse remaining numeric values (offset-x offset-y blur-radius [spread]) + const numbers = remaining.match(/[+-]?\d*\.?\d+(?:px|em|rem|%)?/gi) || []; + const values = numbers.map((n) => parseFloat(n) || 0); + + if (values.length >= 2) { + shadow.offsetX = values[0]; + shadow.offsetY = values[1]; + + if (values.length >= 3) { + shadow.blurRadius = Math.max(0, values[2]); + } + } + + shadows.push(shadow); + } + + return shadows; +}; diff --git a/packages/web-renderer/src/drawing/text/parse-text-shadow.ts b/packages/web-renderer/src/drawing/text/parse-text-shadow.ts index e4e16cbea98..e1463d2c681 100644 --- a/packages/web-renderer/src/drawing/text/parse-text-shadow.ts +++ b/packages/web-renderer/src/drawing/text/parse-text-shadow.ts @@ -1,59 +1,8 @@ -export interface TextShadow { - offsetX: number; - offsetY: number; - blurRadius: number; - color: string; -} +import type {ShadowBase} from '../parse-shadow'; +import {parseShadowValues} from '../parse-shadow'; -export const parseTextShadow = (textShadowValue: string): TextShadow[] => { - if (!textShadowValue || textShadowValue === 'none') { - return []; - } - - const shadows: TextShadow[] = []; - - // Split by comma, but respect rgba() colors - const shadowStrings = textShadowValue.split(/,(?![^(]*\))/); - - for (const shadowStr of shadowStrings) { - const trimmed = shadowStr.trim(); - if (!trimmed || trimmed === 'none') { - continue; - } - - const shadow: TextShadow = { - offsetX: 0, - offsetY: 0, - blurRadius: 0, - color: 'rgba(0, 0, 0, 0.5)', - }; - - let remaining = trimmed; +export type TextShadow = ShadowBase; - // Extract color (can be rgb(), rgba(), hsl(), hsla(), hex, or named color) - const colorMatch = remaining.match( - /(rgba?\([^)]+\)|hsla?\([^)]+\)|#[0-9a-f]{3,8}|[a-z]+)/i, - ); - if (colorMatch) { - shadow.color = colorMatch[0]; - remaining = remaining.replace(colorMatch[0], '').trim(); - } - - // Parse remaining numeric values (offset-x offset-y blur-radius) - const numbers = remaining.match(/[+-]?\d*\.?\d+(?:px|em|rem|%)?/gi) || []; - const values = numbers.map((n) => parseFloat(n) || 0); - - if (values.length >= 2) { - shadow.offsetX = values[0]; - shadow.offsetY = values[1]; - - if (values.length >= 3) { - shadow.blurRadius = Math.max(0, values[2]); - } - } - - shadows.push(shadow); - } - - return shadows; +export const parseTextShadow = (textShadowValue: string): TextShadow[] => { + return parseShadowValues(textShadowValue); }; From c11c7380898eb6bf7a3aedf46c4d600f2147bc1a Mon Sep 17 00:00:00 2001 From: Jonny Burger Date: Fri, 20 Feb 2026 10:35:35 +0100 Subject: [PATCH 23/33] use options --- packages/docs/docs/cloudrun/deploysite.mdx | 4 +--- packages/docs/docs/lambda/deploysite.mdx | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/docs/docs/cloudrun/deploysite.mdx b/packages/docs/docs/cloudrun/deploysite.mdx index 8159d68b43d..b3e9e8a7c76 100644 --- a/packages/docs/docs/cloudrun/deploysite.mdx +++ b/packages/docs/docs/cloudrun/deploysite.mdx @@ -115,9 +115,7 @@ Ignore an error that gets thrown if you pass an entry point file which does not #### `rspack?` -_default: `false`_ - -Whether to use [Rspack](https://rspack.dev) instead of Webpack as the bundler. See [Config.setExperimentalRspackEnabled()](/docs/config#setexperimentalrspackenabled) for more information. + ## Return value diff --git a/packages/docs/docs/lambda/deploysite.mdx b/packages/docs/docs/lambda/deploysite.mdx index 81e40a1b734..3f01735ba8c 100644 --- a/packages/docs/docs/lambda/deploysite.mdx +++ b/packages/docs/docs/lambda/deploysite.mdx @@ -120,9 +120,7 @@ Ignore an error that gets thrown if you pass an entry point file which does not #### `rspack?` -_default: `false`_ - -Whether to use [Rspack](https://rspack.dev) instead of Webpack as the bundler. See [Config.setExperimentalRspackEnabled()](/docs/config#setexperimentalrspackenabled) for more information. + ### `privacy?` From f788a5ea2ec699ac7fdcf8efb99c04503275e124 Mon Sep 17 00:00:00 2001 From: Jonny Burger Date: Fri, 20 Feb 2026 10:57:53 +0100 Subject: [PATCH 24/33] `docs`: Add CompatibilityTable and fix backticks in renderer docs Co-Authored-By: Claude Opus 4.6 --- packages/docs/docs/renderer/combine-chunks.mdx | 6 +++++- packages/docs/docs/renderer/ensure-browser.mdx | 4 ++++ packages/docs/docs/renderer/extract-audio.mdx | 6 +++++- packages/docs/docs/renderer/get-compositions.mdx | 4 ++++ packages/docs/docs/renderer/get-silent-parts.mdx | 6 +++++- packages/docs/docs/renderer/get-video-metadata.mdx | 10 +++++++--- packages/docs/docs/renderer/make-cancel-signal.mdx | 4 ++++ packages/docs/docs/renderer/open-browser.mdx | 4 ++++ packages/docs/docs/renderer/render-frames.mdx | 14 +++++++++----- packages/docs/docs/renderer/render-media.mdx | 10 +++++++--- packages/docs/docs/renderer/render-still.mdx | 4 ++++ packages/docs/docs/renderer/select-composition.mdx | 4 ++++ .../docs/docs/renderer/stitch-frames-to-video.mdx | 12 ++++++++---- 13 files changed, 70 insertions(+), 18 deletions(-) diff --git a/packages/docs/docs/renderer/combine-chunks.mdx b/packages/docs/docs/renderer/combine-chunks.mdx index ce79c59f545..d2038c2a1b8 100644 --- a/packages/docs/docs/renderer/combine-chunks.mdx +++ b/packages/docs/docs/renderer/combine-chunks.mdx @@ -165,7 +165,11 @@ Metadata to add to the output file, in the format of key-value pairs. The function returns a Promise that resolves when the combining process is complete. +## Compatibility + + + ## See also - [Source code for this function](https://github.com/remotion-dev/remotion/blob/main/packages/renderer/src/combine-chunks.ts) -- [renderMedia()](/docs/renderer/render-media) +- [`renderMedia()`](/docs/renderer/render-media) diff --git a/packages/docs/docs/renderer/ensure-browser.mdx b/packages/docs/docs/renderer/ensure-browser.mdx index f9e7d91de16..9a394068dea 100644 --- a/packages/docs/docs/renderer/ensure-browser.mdx +++ b/packages/docs/docs/renderer/ensure-browser.mdx @@ -81,6 +81,10 @@ await ensureBrowser({ A promise with no value. +## Compatibility + + + ## See also - [Source code for this function](https://github.com/remotion-dev/remotion/blob/main/packages/renderer/src/ensure-browser.ts) diff --git a/packages/docs/docs/renderer/extract-audio.mdx b/packages/docs/docs/renderer/extract-audio.mdx index 74f83835077..9c0b497eb6c 100644 --- a/packages/docs/docs/renderer/extract-audio.mdx +++ b/packages/docs/docs/renderer/extract-audio.mdx @@ -62,7 +62,11 @@ The path where the extracted audio will be saved. The file extension must match The function returns a `Promise`, which resolves once the audio extraction is complete. +## Compatibility + + + ## See also - [Source code for this function](https://github.com/remotion-dev/remotion/blob/main/packages/renderer/src/extract-audio.ts) -- [getVideoMetadata()](/docs/renderer/get-video-metadata) +- [`getVideoMetadata()`](/docs/renderer/get-video-metadata) diff --git a/packages/docs/docs/renderer/get-compositions.mdx b/packages/docs/docs/renderer/get-compositions.mdx index 01c56fea606..c0f321fc987 100644 --- a/packages/docs/docs/renderer/get-compositions.mdx +++ b/packages/docs/docs/renderer/get-compositions.mdx @@ -177,6 +177,10 @@ Returns a promise that resolves to an array of available compositions. Example v The `defaultProps` only get returned since v2.5.7. +## Compatibility + + + ## See also - [Source code for this function](https://github.com/remotion-dev/remotion/blob/main/packages/renderer/src/get-compositions.ts) diff --git a/packages/docs/docs/renderer/get-silent-parts.mdx b/packages/docs/docs/renderer/get-silent-parts.mdx index f24f14b8c24..4030502f6c8 100644 --- a/packages/docs/docs/renderer/get-silent-parts.mdx +++ b/packages/docs/docs/renderer/get-silent-parts.mdx @@ -86,7 +86,11 @@ An array of objects with the following properties: The time length of the media in seconds. +## Compatibility + + + ## See also - [Source code for this function](https://github.com/remotion-dev/remotion/blob/main/packages/renderer/src/get-silent-parts.ts) -- [getVideoMetadata()](/docs/renderer/get-video-metadata) +- [`getVideoMetadata()`](/docs/renderer/get-video-metadata) diff --git a/packages/docs/docs/renderer/get-video-metadata.mdx b/packages/docs/docs/renderer/get-video-metadata.mdx index c134144a71a..1785cc713b5 100644 --- a/packages/docs/docs/renderer/get-video-metadata.mdx +++ b/packages/docs/docs/renderer/get-video-metadata.mdx @@ -120,10 +120,14 @@ If the video has no audio track or is unknown to Remotion, it is `null`. Otherwi One of `yuv420p`, `yuyv422`, `rgb24`, `bgr24`, `yuv422p`, `yuv444p`, `yuv410p`, `yuv411p`, `yuvj420p`, `yuvj422p`, `yuvj444p`, `argb`, `rgba`, `abgr`, `bgra`, `yuv440p`, `yuvj440p`, `yuva420p`, `yuv420p16le`, `yuv420p16be`, `yuv422p16le`, `yuv422p16be`, `yuv444p16le`, `yuv444p16be`, `yuv420p9be`, `yuv420p9le`, `yuv420p10be`, `yuv420p10le`, `yuv422p10be`, `yuv422p10le`, `yuv444p9be`, `yuv444p9le`, `yuv444p10be`, `yuv444p10le`, `yuv422p9be`, `yuv422p9le`, `yuva420p9be`, `yuva420p9le`, `yuva422p9be`, `yuva422p9le`, `yuva444p9be`, `yuva444p9le`, `yuva420p10be`, `yuva420p10le`, `yuva422p10be`, `yuva422p10le`, `yuva444p10be`, `yuva444p10le`, `yuva420p16be`, `yuva420p16le`, `yuva422p16be`, `yuva422p16le`, `yuva444p16be`, `yuva444p16le`, `yuva444p`, `yuva422p`, `yuv420p12be`, `yuv420p12le`, `yuv420p14be`, `yuv420p14le`, `yuv422p12be`, `yuv422p12le`, `yuv422p14be`, `yuv422p14le`, `yuv444p12be`, `yuv444p12le`, `yuv444p14be`, `yuv444p14le`, `yuvj411p`, `yuv440p10le`, `yuv440p10be`, `yuv440p12le`, `yuv440p12be`, `yuv420p9`, `yuv422p9`, `yuv444p9`, `yuv420p10`, `yuv422p10`, `yuv440p10`, `yuv444p10`, `yuv420p12`, `yuv422p12`, `yuv440p12`, `yuv444p12`, `yuv420p14`, `yuv422p14`, `yuv444p14`, `yuv420p16`, `yuv422p16`, `yuv444p16`, `yuva420p9`, `yuva422p9`, `yuva444p9`, `yuva420p10`, `yuva422p10`, `yuva444p10`, `yuva420p16`, `yuva422p16`, `yuva444p16`, `yuva422p12be`, `yuva422p12le`, `yuva444p12be`, `yuva444p12le`, `unknown`. +## Compatibility + + + ## See also - [Source code for this function](https://github.com/remotion-dev/remotion/blob/main/packages/renderer/src/get-video-metadata.ts) - [Server-Side rendering](/docs/ssr) -- [getCompositions()](/docs/renderer/get-compositions) -- [renderStill()](/docs/renderer/stitch-frames-to-video) -- [renderMediaOnLambda()](/docs/lambda/rendermediaonlambda) +- [`getCompositions()`](/docs/renderer/get-compositions) +- [`renderStill()`](/docs/renderer/stitch-frames-to-video) +- [`renderMediaOnLambda()`](/docs/lambda/rendermediaonlambda) diff --git a/packages/docs/docs/renderer/make-cancel-signal.mdx b/packages/docs/docs/renderer/make-cancel-signal.mdx index 758591bb298..91487afc8b1 100644 --- a/packages/docs/docs/renderer/make-cancel-signal.mdx +++ b/packages/docs/docs/renderer/make-cancel-signal.mdx @@ -61,6 +61,10 @@ Calling `makeCancelSignal` returns an object with two properties: - `cancelSignal`: An object to be passed to one of the above mentioned render functions - `cancel`: A function you should call when you want to cancel the render. +## Compatibility + + + ## See also - [Source code for this component](https://github.com/remotion-dev/remotion/blob/main/packages/renderer/src/make-cancel-signal.ts) diff --git a/packages/docs/docs/renderer/open-browser.mdx b/packages/docs/docs/renderer/open-browser.mdx index 6e135b04ea7..cbb13590a61 100644 --- a/packages/docs/docs/renderer/open-browser.mdx +++ b/packages/docs/docs/renderer/open-browser.mdx @@ -77,6 +77,10 @@ browser.close({silent: true}); If already closed or an operation is interrupted, an error is thrown. Setting the `silent` option to `true` will close the browser without generating an error. +## Compatibility + + + ## See also - [Source code for this function](https://github.com/remotion-dev/remotion/blob/main/packages/renderer/src/open-browser.ts) diff --git a/packages/docs/docs/renderer/render-frames.mdx b/packages/docs/docs/renderer/render-frames.mdx index aa540513aba..70b5dd8d9d7 100644 --- a/packages/docs/docs/renderer/render-frames.mdx +++ b/packages/docs/docs/renderer/render-frames.mdx @@ -326,12 +326,16 @@ A promise resolving to an object containing the following properties: - `frameCount`: `number` - describing how many frames got rendered. - `assetsInfo`: `RenderAssetInfo` - information that can be passed to `stitchFramesToVideo()` to mix audio. The shape of this object should be considered as Remotion internals and may change across Remotion versions. +## Compatibility + + + ## See also - [Source code for this function](https://github.com/remotion-dev/remotion/blob/main/packages/renderer/src/render-frames.ts) -- [renderMedia()](/docs/renderer/render-media) -- [bundle()](/docs/bundle) +- [`renderMedia()`](/docs/renderer/render-media) +- [`bundle()`](/docs/bundle) - [Server-Side rendering](/docs/ssr) -- [getCompositions()](/docs/renderer/get-compositions) -- [stitchFramesToVideo()](/docs/renderer/stitch-frames-to-video) -- [renderStill()](/docs/renderer/render-still) +- [`getCompositions()`](/docs/renderer/get-compositions) +- [`stitchFramesToVideo()`](/docs/renderer/stitch-frames-to-video) +- [`renderStill()`](/docs/renderer/render-still) diff --git a/packages/docs/docs/renderer/render-media.mdx b/packages/docs/docs/renderer/render-media.mdx index 30269d41bee..d371edb32be 100644 --- a/packages/docs/docs/renderer/render-media.mdx +++ b/packages/docs/docs/renderer/render-media.mdx @@ -554,10 +554,14 @@ _**from v3.0.26**:_ If `outputLocation` is not specified or `null`, the return value is a Promise that resolves a `Buffer`. If an output location is specified, the return value is a Promise that resolves no value. +## Compatibility + + + ## See also - [Source code for this function](https://github.com/remotion-dev/remotion/blob/main/packages/renderer/src/render-media.ts) - [Server-Side rendering](/docs/ssr) -- [getCompositions()](/docs/renderer/get-compositions) -- [renderStill()](/docs/renderer/stitch-frames-to-video) -- [renderMediaOnLambda()](/docs/lambda/rendermediaonlambda) +- [`getCompositions()`](/docs/renderer/get-compositions) +- [`renderStill()`](/docs/renderer/stitch-frames-to-video) +- [`renderMediaOnLambda()`](/docs/lambda/rendermediaonlambda) diff --git a/packages/docs/docs/renderer/render-still.mdx b/packages/docs/docs/renderer/render-still.mdx index 523e9b90f17..b76e7b11c0a 100644 --- a/packages/docs/docs/renderer/render-still.mdx +++ b/packages/docs/docs/renderer/render-still.mdx @@ -247,6 +247,10 @@ The return value is a promise that resolves to an object with the following keys - `buffer`: (_available from v3.3.9_) A `Buffer` that only exists if no `output` option was provided. Otherwise null. +## Compatibility + + + ## See also - [Source code for this function](https://github.com/remotion-dev/remotion/blob/main/packages/renderer/src/render-still.ts) diff --git a/packages/docs/docs/renderer/select-composition.mdx b/packages/docs/docs/renderer/select-composition.mdx index 643c7d01449..269836e4bab 100644 --- a/packages/docs/docs/renderer/select-composition.mdx +++ b/packages/docs/docs/renderer/select-composition.mdx @@ -113,6 +113,10 @@ See: [Environment variables](/docs/env-variables/) +## Compatibility + + + ## See also - [Source code for this function](https://github.com/remotion-dev/remotion/blob/main/packages/renderer/src/select-composition.ts) diff --git a/packages/docs/docs/renderer/stitch-frames-to-video.mdx b/packages/docs/docs/renderer/stitch-frames-to-video.mdx index 737b3c097b9..7668edf30d3 100644 --- a/packages/docs/docs/renderer/stitch-frames-to-video.mdx +++ b/packages/docs/docs/renderer/stitch-frames-to-video.mdx @@ -205,11 +205,15 @@ An absolute path overriding the `ffprobe` executable to use. `stitchFramesToVideo()` returns a promise which resolves to nothing. If everything goes well, the output will be placed in `outputLocation`. +## Compatibility + + + ## See also - [Source code for this function](https://github.com/remotion-dev/remotion/blob/main/packages/renderer/src/stitch-frames-to-video.ts) -- [bundle()](/docs/bundle) +- [`bundle()`](/docs/bundle) - [Server-Side rendering](/docs/ssr) -- [getCompositions()](/docs/renderer/get-compositions) -- [renderFrames()](/docs/renderer/render-frames) -- [renderMedia()](/docs/renderer/render-media) +- [`getCompositions()`](/docs/renderer/get-compositions) +- [`renderFrames()`](/docs/renderer/render-frames) +- [`renderMedia()`](/docs/renderer/render-media) From 0f7352aac21c7db9cb03d407219e911a26936d88 Mon Sep 17 00:00:00 2001 From: Jonny Burger Date: Fri, 20 Feb 2026 11:01:24 +0100 Subject: [PATCH 25/33] `@remotion/renderer`: Add mimeType field to renderMedia() and renderStill() return values Co-Authored-By: Claude Opus 4.6 --- packages/docs/docs/renderer/render-media.mdx | 1 + packages/docs/docs/renderer/render-still.mdx | 1 + packages/renderer/src/render-media.ts | 5 +++++ packages/renderer/src/render-still.ts | 8 ++++++-- 4 files changed, 13 insertions(+), 2 deletions(-) diff --git a/packages/docs/docs/renderer/render-media.mdx b/packages/docs/docs/renderer/render-media.mdx index 30269d41bee..4140db9cc25 100644 --- a/packages/docs/docs/renderer/render-media.mdx +++ b/packages/docs/docs/renderer/render-media.mdx @@ -549,6 +549,7 @@ The return value is an object with the following properties: - `buffer`: If `outputLocation` is not specified or `null`, contains a buffer, otherwise `null`. - `slowestFrames`: An array of the 10 slowest frames in the shape of `{frame:, time: