@remotion/studio: Save default props immediately#6826
Conversation
Replace polling pattern for default props updatability with server-push subscriptions (mirroring sequence props pattern), apply default props changes immediately on save with server-side HMR suppression, and extract current default props values from the source AST. - Add /api/subscribe-to-default-props and /api/unsubscribe-from-default-props - Remove /api/can-update-default-props polling endpoint - Push 'default-props-updatable-changed' SSE events on root file changes - Extract current defaultProps values from source AST via isStaticValue/extractStaticValue - Handle TSAsExpression nodes (e.g. 'a' as const) in isStaticValue and extractStaticValue - Suppress HMR in update-default-props handler (matching save-sequence-props) - Apply default props optimistically on save, revert on error - Remove client-side remotion_ignoreFastRefreshUpdate usage for default props - Clean up default props watchers on client disconnect Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Instead of installing a new file watcher per client, use one ref-counted global watcher for the root file. Different clients can subscribe to different compositionIds; on file change, each active compositionId is recomputed and pushed. Also removes debug console.log calls. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add sendEventToClientId() to LiveEventsServer for targeted delivery. The default props watcher now sends events only to clients that subscribed to the specific compositionId, not to all connected clients. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
CI fix pushed ✅ Root cause: Fix: Removed the unused -import {getInputProps, Internals} from 'remotion';
+import {getInputProps} from 'remotion'; |
…-dev/remotion into subscribe-default-props
CI FixFailure: Cause: A debug Fix: Removed the debug |
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The HMR suppression mechanism (suppressHmrForFile/shouldSuppressHmr) was a secondary layer that let webpack rebuild but suppressed the HMR notification. suppressBundlerUpdateForFile prevents webpack from seeing the file change entirely via WatchIgnoreNextChangePlugin, which is more efficient. The plugin previously failed for files nested deeper than one level under a watched directory (e.g. src/VisualControls/index.tsx) because watchpack reports ancestor directories (e.g. src/) as changed alongside the file. The plugin only suppressed the immediate parent directory, leaving ancestor directories in changedFiles which triggered a rebuild. Fixed by: - Suppressing callbackUndelayed for ancestor directories of ignored files - Removing ancestor directories from changedFiles in the aggregated callback - Snapshotting ancestor directory timestamps for proper restoration Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Fix navigateToVisualControls: use specific composition selector instead of getByText which matched both folder and composition - Fix subtitle test: exclude checkbox from input selector - Fix extra-rotation test: toggle optional checkbox before interacting with dragger, use regex for flexible file content matching - Clear visual control handles on webpack rebuild - Make extraPlugins required in webpack/rspack config Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Review Summary
This is a substantial PR implementing immediate saving of default props with full undo/redo support and cross-tab synchronization. The architecture is well-designed with proper separation of concerns.
What's Done Well
- Architecture: Clean separation between file watchers, undo stack, and live events
- Undo/Redo System: Full support for default props, visual controls, and sequence props with 100-entry stacks
- Cross-Tab Sync: Proper use of file watchers and SSE for synchronization
- HMR Suppression:
WatchIgnoreNextChangePluginelegantly prevents unnecessary rebuilds - API Migration:
updateDefaultProps()as deprecated alias maintains backward compatibility - Documentation: Clear migration path with info box about
unsavedDefaultPropschanges - Tests: Comprehensive e2e coverage for undo/redo and visual controls
Issues to Address
packages/studio-server/src/preview-server/default-props-watchers.ts:139-150 - releaseGlobalWatcher called in loop
The unsubscribeClientDefaultPropsWatchers function calls releaseGlobalWatcher() inside the loop for each composition. While this currently works (because releaseGlobalWatcher checks if (!globalWatcher) at the start), it's fragile:
for (const compositionId of Object.keys(subscriptions)) {
if (subscriptions[compositionId].has(clientId)) {
subscriptions[compositionId].delete(clientId);
// ...
releaseGlobalWatcher(); // Called multiple times in loop
}
}Consider counting the total subscriptions to release and calling releaseGlobalWatcher() once after the loop:
export const unsubscribeClientDefaultPropsWatchers = (clientId: string) => {
let shouldRelease = false;
for (const compositionId of Object.keys(subscriptions)) {
if (subscriptions[compositionId].has(clientId)) {
subscriptions[compositionId].delete(clientId);
if (subscriptions[compositionId].size === 0) {
delete subscriptions[compositionId];
}
shouldRelease = true;
}
}
if (shouldRelease) {
releaseGlobalWatcher();
}
};packages/studio-server/src/preview-server/default-props-watchers.ts:100-104 - Redundant delete-then-add
When subscribing the same client+composition pair, the code first deletes and releases, then immediately adds and ensures. While the net refCount is correct, this is unnecessary churn:
if (subscriptions[compositionId]?.has(clientId)) {
subscriptions[compositionId].delete(clientId);
releaseGlobalWatcher(); // Release first
}
// ...
subscriptions[compositionId].add(clientId);
ensureGlobalWatcher(rootFile); // Then acquireConsider skipping the delete+release if the root file hasn't changed.
packages/studio-server/src/preview-server/default-props-watchers.ts:53 - No error handling
The onChange callback in ensureGlobalWatcher lacks try/catch around computeCanUpdateDefaultPropsFromContent. If this throws, the error won't be caught.
Minor Observations
- The
WatchIgnoreNextChangePluginrelies heavily on webpack internals (watchpack,NodeWatchFileSystem). This could break with webpack version updates. Consider adding a comment noting this dependency. - The
watchIgnorePlugintrace is set up beforesetWatchIgnoreNextChangePluginis called, which is correct. Good design.
Verification
The TODO items are mostly checked off. The two unchecked items ("Should print good console messages" and "Should skip prettier and log") appear to have partial implementations but may need more work before merge.
| releaseGlobalWatcher(); | ||
| }; | ||
|
|
||
| export const unsubscribeClientDefaultPropsWatchers = (clientId: string) => { |
There was a problem hiding this comment.
Consider calling releaseGlobalWatcher() once after the loop instead of inside it for each composition. While the current code works (because releaseGlobalWatcher checks if (!globalWatcher)), it's fragile and could cause issues if the logic changes.
- Restart studio server before each test for full isolation - Wait for source map resolution before interacting with visual controls (the save endpoint was never called when originalFileName was still loading) - Clear visual control unsaved overrides when code value changes instead of wiping all handles on build completion (preserves originalFileName state) - Add trace logging to save endpoints for debugging - Make extraPlugins required in webpack/rspack config Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Run e2e tests via turborepo so upstream packages (CLI, studio, etc.) are built before the tests run. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The schema editor test was failing in CI because it edited props before the subscribe-to-default-props API call resolved, meaning saves to the source file were not yet active. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

TODO:
default-props-updatable-changedshould write updateProps.updateDefaultProps()API