Skip to content

@remotion/studio: Save default props immediately#6826

Merged
JonnyBurger merged 83 commits intomainfrom
subscribe-default-props
Mar 24, 2026
Merged

@remotion/studio: Save default props immediately#6826
JonnyBurger merged 83 commits intomainfrom
subscribe-default-props

Conversation

@JonnyBurger
Copy link
Copy Markdown
Member

@JonnyBurger JonnyBurger commented Mar 6, 2026

TODO:

  • In it's place, there should be a good undo + redo system
  • Debounce InputDraggers, colors, text fields
  • Should print good console messages
  • Need to deprecate updateDefaultProps().. I think?
  • There should only be 1 state of truth, should synchronize across tabs. default-props-updatable-changed should write updateProps.
  • Should be able to change in code and get the newest value immediately
  • In read-only mode, there should still be a way to reset the values to the original.
  • Visual controls should work just as well
  • Indicate that it is saving? Or no?
  • Should not interfere updateDefaultProps() API
  • Should skip prettier and log
  • Should log a hint that it can be undone with Cmd+Z
  • Text input, textarea and color input save on blur does not yet work properly
  • Can we make Prettier update faster by only formatting part of the file?
  • Do changes apply in Studio, if the composition is not focused? If we just switch to it afterwards?
  • External changes need to be applied
  • Deleting 1 character from the color input leads it to become invalid, and props are uneditable
  • Editing the JSON should also save it
  • Undo stack should be immediately visible on refresh
  • Update default props should also integrate with the undo / redo stack.
  • What if we spam Cmd+Z? There is a latency between it
  • Build prevention: When subscribing with a new tab, it should return the current values and apply it

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>
@vercel
Copy link
Copy Markdown
Contributor

vercel bot commented Mar 6, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
bugs Ready Ready Preview, Comment Mar 24, 2026 4:17pm
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
remotion Skipped Skipped Mar 24, 2026 4:17pm

Request Review

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>
@pullfrog
Copy link
Copy Markdown
Contributor

pullfrog bot commented Mar 6, 2026

CI fix pushed

Root cause: DataEditor.tsx imported Internals from 'remotion' but never used it, causing TS6133: 'Internals' is declared but its value is never read — which failed the @remotion/studio#make build step (tsgo -d). This cascaded across 7 CI jobs (Linting + Formatting, SSR integration, Lambda integration, Next.js SSR build, and others).

Fix: Removed the unused Internals import from packages/studio/src/components/RenderModal/DataEditor.tsx (line 9).

-import {getInputProps, Internals} from 'remotion';
+import {getInputProps} from 'remotion';

Pullfrog  | View workflow run | Using Cursor CLI | Triggered by Pullfrogpullfrog.com𝕏

@pullfrog
Copy link
Copy Markdown
Contributor

pullfrog bot commented Mar 6, 2026

CI Fix

Failure: @remotion/studio#lint — eslint no-console error at packages/studio/src/components/DefaultPropsEditor.tsx:221

Cause: A debug console.log('onSave', onSave, saving) statement was left in the code.

Fix: Removed the debug console.log statement. Pushed as 91a33c3.

Pullfrog  | View workflow run | Using Cursor CLI | Triggered by Pullfrogpullfrog.com𝕏

JonnyBurger and others added 13 commits March 23, 2026 11:03
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>
Copy link
Copy Markdown
Contributor

@pullfrog pullfrog bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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: WatchIgnoreNextChangePlugin elegantly prevents unnecessary rebuilds
  • API Migration: updateDefaultProps() as deprecated alias maintains backward compatibility
  • Documentation: Clear migration path with info box about unsavedDefaultProps changes
  • 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 acquire

Consider 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 WatchIgnoreNextChangePlugin relies heavily on webpack internals (watchpack, NodeWatchFileSystem). This could break with webpack version updates. Consider adding a comment noting this dependency.
  • The watchIgnorePlugin trace is set up before setWatchIgnoreNextChangePlugin is 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.

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow runpullfrog.com𝕏

releaseGlobalWatcher();
};

export const unsubscribeClientDefaultPropsWatchers = (clientId: string) => {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

JonnyBurger and others added 10 commits March 24, 2026 15:34
- 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Visual Mode: Implement undo and redo Visual mode: Handle concurrent code edits to the same value

1 participant