@remotion/studio-server 4.0.476 → 4.0.478

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,26 @@
1
+ import type { SequenceNodePath } from 'remotion';
2
+ export type EffectDuplicationTarget = {
3
+ sequenceNodePath: SequenceNodePath;
4
+ effectIndex: number;
5
+ };
6
+ export declare const duplicateEffects: ({ input, effects, prettierConfigOverride, }: {
7
+ input: string;
8
+ effects: EffectDuplicationTarget[];
9
+ prettierConfigOverride?: Record<string, unknown> | null | undefined;
10
+ }) => Promise<{
11
+ output: string;
12
+ formatted: boolean;
13
+ effectLabels: string[];
14
+ logLines: number[];
15
+ }>;
16
+ export declare const duplicateEffect: ({ input, sequenceNodePath, effectIndex, prettierConfigOverride, }: {
17
+ input: string;
18
+ sequenceNodePath: SequenceNodePath;
19
+ effectIndex: number;
20
+ prettierConfigOverride?: Record<string, unknown> | null | undefined;
21
+ }) => Promise<{
22
+ output: string;
23
+ formatted: boolean;
24
+ effectLabel: string;
25
+ logLine: number;
26
+ }>;
@@ -0,0 +1,92 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.duplicateEffect = exports.duplicateEffects = void 0;
4
+ const types_1 = require("@babel/types");
5
+ const can_update_sequence_props_1 = require("../preview-server/routes/can-update-sequence-props");
6
+ const format_file_content_1 = require("./format-file-content");
7
+ const parse_ast_1 = require("./parse-ast");
8
+ const update_effect_props_1 = require("./update-effect-props/update-effect-props");
9
+ const getEffectsArray = (attr) => {
10
+ if (!attr.value || attr.value.type !== 'JSXExpressionContainer') {
11
+ throw new Error('Cannot duplicate effect: effects prop is not an array');
12
+ }
13
+ const expr = attr.value.expression;
14
+ if (expr.type !== 'ArrayExpression') {
15
+ throw new Error('Cannot duplicate effect: effects prop is not an array');
16
+ }
17
+ return expr;
18
+ };
19
+ const duplicateEffects = async ({ input, effects, prettierConfigOverride, }) => {
20
+ var _a, _b;
21
+ var _c, _d, _e;
22
+ if (effects.length === 0) {
23
+ throw new Error('No effects were specified for duplication');
24
+ }
25
+ const ast = (0, parse_ast_1.parseAst)(input);
26
+ const duplicationsByAttr = new Map();
27
+ for (const effect of effects) {
28
+ const { sequenceNodePath, effectIndex } = effect;
29
+ const jsx = (0, can_update_sequence_props_1.findJsxElementAtNodePath)(ast, sequenceNodePath);
30
+ if (!jsx) {
31
+ throw new Error('Could not find a JSX element at the specified location to duplicate effect');
32
+ }
33
+ const attr = (0, update_effect_props_1.findEffectsAttr)((_c = jsx.attributes) !== null && _c !== void 0 ? _c : []);
34
+ if (!attr) {
35
+ throw new Error('Could not find effects on the target JSX element');
36
+ }
37
+ const effectsArray = getEffectsArray(attr);
38
+ const existingDuplication = duplicationsByAttr.get(attr);
39
+ const duplication = existingDuplication !== null && existingDuplication !== void 0 ? existingDuplication : {
40
+ effectsArray,
41
+ effectIndices: new Set(),
42
+ effectLabels: [],
43
+ logLines: [],
44
+ };
45
+ duplicationsByAttr.set(attr, duplication);
46
+ if (duplication.effectIndices.has(effectIndex)) {
47
+ continue;
48
+ }
49
+ const found = (0, update_effect_props_1.findEffectCallExpression)({ attr, effectIndex });
50
+ if (found.kind === 'error') {
51
+ throw new Error(`Cannot duplicate effect: ${found.reason}`);
52
+ }
53
+ duplication.effectIndices.add(effectIndex);
54
+ duplication.effectLabels.push(`${found.callee}()`);
55
+ duplication.logLines.push((_e = (_d = (_a = found.call.loc) === null || _a === void 0 ? void 0 : _a.start.line) !== null && _d !== void 0 ? _d : (_b = jsx.loc) === null || _b === void 0 ? void 0 : _b.start.line) !== null && _e !== void 0 ? _e : 1);
56
+ }
57
+ for (const duplication of duplicationsByAttr.values()) {
58
+ for (const effectIndex of [...duplication.effectIndices].sort((a, b) => b - a)) {
59
+ const effect = duplication.effectsArray.elements[effectIndex];
60
+ if (!effect || effect.type !== 'CallExpression') {
61
+ throw new Error('Cannot duplicate effect: not-call-expression');
62
+ }
63
+ duplication.effectsArray.elements.splice(effectIndex + 1, 0, (0, types_1.cloneNode)(effect, true));
64
+ }
65
+ }
66
+ const finalFile = (0, parse_ast_1.serializeAst)(ast);
67
+ const { output, formatted } = await (0, format_file_content_1.formatFileContent)({
68
+ input: finalFile,
69
+ prettierConfigOverride,
70
+ });
71
+ return {
72
+ output,
73
+ formatted,
74
+ effectLabels: [...duplicationsByAttr.values()].flatMap((duplication) => duplication.effectLabels),
75
+ logLines: [...duplicationsByAttr.values()].flatMap((duplication) => duplication.logLines),
76
+ };
77
+ };
78
+ exports.duplicateEffects = duplicateEffects;
79
+ const duplicateEffect = async ({ input, sequenceNodePath, effectIndex, prettierConfigOverride, }) => {
80
+ const { output, formatted, effectLabels, logLines } = await (0, exports.duplicateEffects)({
81
+ input,
82
+ effects: [{ sequenceNodePath, effectIndex }],
83
+ prettierConfigOverride,
84
+ });
85
+ return {
86
+ output,
87
+ formatted,
88
+ effectLabel: effectLabels[0],
89
+ logLine: logLines[0],
90
+ };
91
+ };
92
+ exports.duplicateEffect = duplicateEffect;
@@ -515,10 +515,15 @@ const createStringAttribute = (name, value) => {
515
515
  const createBooleanAttribute = (name, value) => {
516
516
  return recast.types.builders.jsxAttribute(recast.types.builders.jsxIdentifier(name), recast.types.builders.jsxExpressionContainer(recast.types.builders.booleanLiteral(value)));
517
517
  };
518
- const createPositionAbsoluteStyleAttribute = () => {
519
- return recast.types.builders.jsxAttribute(recast.types.builders.jsxIdentifier('style'), recast.types.builders.jsxExpressionContainer(recast.types.builders.objectExpression([
518
+ const formatTranslateValue = ({ x, y }) => `${x}px ${y}px`;
519
+ const createPositionAbsoluteStyleAttribute = (position) => {
520
+ const properties = [
520
521
  recast.types.builders.objectProperty(recast.types.builders.identifier('position'), recast.types.builders.stringLiteral('absolute')),
521
- ])));
522
+ ];
523
+ if (position) {
524
+ properties.push(recast.types.builders.objectProperty(recast.types.builders.identifier('translate'), recast.types.builders.stringLiteral(formatTranslateValue(position))));
525
+ }
526
+ return recast.types.builders.jsxAttribute(recast.types.builders.jsxIdentifier('style'), recast.types.builders.jsxExpressionContainer(recast.types.builders.objectExpression(properties)));
522
527
  };
523
528
  const createStaticFileSrcAttribute = ({ staticFileLocalName, src, }) => {
524
529
  return recast.types.builders.jsxAttribute(recast.types.builders.jsxIdentifier('src'), recast.types.builders.jsxExpressionContainer(recast.types.builders.callExpression(recast.types.builders.identifier(staticFileLocalName), [recast.types.builders.stringLiteral(src)])));
@@ -535,25 +540,27 @@ const createComponentProp = ({ name, value, }) => {
535
540
  const createStringSrcAttribute = (src) => {
536
541
  return recast.types.builders.jsxAttribute(recast.types.builders.jsxIdentifier('src'), recast.types.builders.stringLiteral(src));
537
542
  };
538
- const createSolidElement = ({ localName, width, height, }) => {
543
+ const createSolidElement = ({ localName, width, height, position, }) => {
539
544
  return recast.types.builders.jsxElement(recast.types.builders.jsxOpeningElement(recast.types.builders.jsxIdentifier(localName), [
540
545
  createNumberAttribute('width', width),
541
546
  createNumberAttribute('height', height),
542
- createPositionAbsoluteStyleAttribute(),
547
+ createPositionAbsoluteStyleAttribute(position),
543
548
  ], true), null, []);
544
549
  };
545
- const createComponentElement = ({ localName, props, }) => {
550
+ const createComponentElement = ({ localName, props, position, }) => {
546
551
  return recast.types.builders.jsxElement(recast.types.builders.jsxOpeningElement(recast.types.builders.jsxIdentifier(localName), [
547
552
  ...props.map(createComponentProp),
548
- createPositionAbsoluteStyleAttribute(),
553
+ createPositionAbsoluteStyleAttribute(position),
549
554
  ], true), null, []);
550
555
  };
551
- const createAssetElement = ({ addPositionStyle, localName, staticFileLocalName, src, dimensions, }) => {
556
+ const createAssetElement = ({ addPositionStyle, localName, staticFileLocalName, src, dimensions, position, }) => {
552
557
  return recast.types.builders.jsxElement(recast.types.builders.jsxOpeningElement(recast.types.builders.jsxIdentifier(localName), [
553
558
  staticFileLocalName === null
554
559
  ? createStringSrcAttribute(src)
555
560
  : createStaticFileSrcAttribute({ staticFileLocalName, src }),
556
- ...(addPositionStyle ? [createPositionAbsoluteStyleAttribute()] : []),
561
+ ...(addPositionStyle
562
+ ? [createPositionAbsoluteStyleAttribute(position)]
563
+ : []),
557
564
  ...(dimensions
558
565
  ? [
559
566
  createNumberAttribute('width', dimensions.width),
@@ -728,6 +735,14 @@ const ensureImgImport = (ast) => {
728
735
  label: '<Img>',
729
736
  });
730
737
  };
738
+ const ensureAnimatedImageImport = (ast) => {
739
+ return ensureOfficialNamedImport({
740
+ ast,
741
+ importedName: 'AnimatedImage',
742
+ sourcePath: 'remotion',
743
+ label: '<AnimatedImage>',
744
+ });
745
+ };
731
746
  const ensureVideoImport = (ast) => {
732
747
  return ensureOfficialNamedImport({
733
748
  ast,
@@ -1007,6 +1022,7 @@ const createInsertableJsxElement = ({ ast, element, }) => {
1007
1022
  localName: solidLocalName,
1008
1023
  width: element.width,
1009
1024
  height: element.height,
1025
+ position: element.position,
1010
1026
  });
1011
1027
  }
1012
1028
  if (element.type === 'component') {
@@ -1019,6 +1035,7 @@ const createInsertableJsxElement = ({ ast, element, }) => {
1019
1035
  return createComponentElement({
1020
1036
  localName: componentLocalName,
1021
1037
  props: element.props,
1038
+ position: element.position,
1022
1039
  });
1023
1040
  }
1024
1041
  if (element.type === 'asset') {
@@ -1036,6 +1053,9 @@ const createInsertableJsxElement = ({ ast, element, }) => {
1036
1053
  else if (element.assetType === 'gif') {
1037
1054
  localName = ensureGifImport(ast);
1038
1055
  }
1056
+ else if (element.assetType === 'animated-image') {
1057
+ localName = ensureAnimatedImageImport(ast);
1058
+ }
1039
1059
  else if (element.assetType === 'audio') {
1040
1060
  localName = ensureAudioImport(ast);
1041
1061
  }
@@ -1048,6 +1068,7 @@ const createInsertableJsxElement = ({ ast, element, }) => {
1048
1068
  staticFileLocalName,
1049
1069
  src: element.src,
1050
1070
  dimensions: element.dimensions,
1071
+ position: element.position,
1051
1072
  });
1052
1073
  }
1053
1074
  throw new Error('Unsupported element type');
@@ -15,6 +15,7 @@ const delete_jsx_node_1 = require("./routes/delete-jsx-node");
15
15
  const delete_keyframes_1 = require("./routes/delete-keyframes");
16
16
  const delete_static_file_1 = require("./routes/delete-static-file");
17
17
  const download_remote_asset_1 = require("./routes/download-remote-asset");
18
+ const duplicate_effect_1 = require("./routes/duplicate-effect");
18
19
  const duplicate_jsx_node_1 = require("./routes/duplicate-jsx-node");
19
20
  const insert_jsx_element_1 = require("./routes/insert-jsx-element");
20
21
  const install_dependency_1 = require("./routes/install-dependency");
@@ -67,6 +68,7 @@ exports.allApiRoutes = {
67
68
  '/api/save-effect-props': save_effect_props_1.saveEffectPropsHandler,
68
69
  '/api/add-effect': add_effect_1.addEffectHandler,
69
70
  '/api/reorder-effect': reorder_effect_1.reorderEffectHandler,
71
+ '/api/duplicate-effect': duplicate_effect_1.duplicateEffectHandler,
70
72
  '/api/reorder-sequence': reorder_sequence_1.reorderSequenceHandler,
71
73
  '/api/delete-keyframes': delete_keyframes_1.deleteKeyframesHandler,
72
74
  '/api/move-keyframes': move_keyframes_1.moveKeyframesHandler,
@@ -0,0 +1,12 @@
1
+ import type { File, JSXOpeningElement } from '@babel/types';
2
+ export declare class JsxElementIdentityMismatchError extends Error {
3
+ constructor();
4
+ }
5
+ export declare const getJsxComponentIdentity: ({ ast, jsxElement, }: {
6
+ ast: File;
7
+ jsxElement: JSXOpeningElement;
8
+ }) => string | null;
9
+ export declare const jsxComponentIdentitiesMatch: ({ expected, actual, }: {
10
+ expected: string | null;
11
+ actual: string | null;
12
+ }) => boolean;
@@ -0,0 +1,126 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.jsxComponentIdentitiesMatch = exports.getJsxComponentIdentity = exports.JsxElementIdentityMismatchError = void 0;
4
+ const imports_1 = require("../helpers/imports");
5
+ class JsxElementIdentityMismatchError extends Error {
6
+ constructor() {
7
+ super('JSX element identity changed');
8
+ }
9
+ }
10
+ exports.JsxElementIdentityMismatchError = JsxElementIdentityMismatchError;
11
+ const jsxMemberToString = (expr) => {
12
+ if (expr.type === 'JSXIdentifier') {
13
+ return expr.name;
14
+ }
15
+ return `${jsxMemberToString(expr.object)}.${expr.property.name}`;
16
+ };
17
+ const getJsxRootIdentifier = (expr) => {
18
+ if (expr.type === 'JSXIdentifier') {
19
+ return expr;
20
+ }
21
+ return getJsxRootIdentifier(expr.object);
22
+ };
23
+ const getTagName = (node) => {
24
+ if (node.name.type === 'JSXIdentifier') {
25
+ return node.name.name;
26
+ }
27
+ if (node.name.type === 'JSXMemberExpression') {
28
+ return jsxMemberToString(node.name);
29
+ }
30
+ return null;
31
+ };
32
+ const getMemberPath = (tagName) => {
33
+ const dotIndex = tagName.indexOf('.');
34
+ return dotIndex === -1 ? '' : tagName.slice(dotIndex + 1);
35
+ };
36
+ const getMemberPathAfterImportName = ({ tagName, importName, }) => {
37
+ const parts = tagName.split('.');
38
+ const importNameIndex = parts.indexOf(importName);
39
+ if (importNameIndex === -1) {
40
+ return getMemberPath(tagName);
41
+ }
42
+ return parts.slice(importNameIndex + 1).join('.');
43
+ };
44
+ const sourcePathToIdentityPrefix = (sourcePath) => {
45
+ if (sourcePath === 'remotion') {
46
+ return 'dev.remotion.remotion';
47
+ }
48
+ if (sourcePath.startsWith('@remotion/')) {
49
+ const packageName = sourcePath
50
+ .slice('@remotion/'.length)
51
+ .replace(/-([a-z])/g, (_, char) => char.toUpperCase());
52
+ return `dev.remotion.${packageName}`;
53
+ }
54
+ return null;
55
+ };
56
+ const makeImportedIdentity = ({ sourcePath, importName, memberPath, }) => {
57
+ const prefix = sourcePathToIdentityPrefix(sourcePath);
58
+ if (prefix === null) {
59
+ return null;
60
+ }
61
+ return [prefix, importName, memberPath].filter(Boolean).join('.');
62
+ };
63
+ const getJsxComponentIdentity = ({ ast, jsxElement, }) => {
64
+ var _a;
65
+ const tagName = getTagName(jsxElement);
66
+ if (tagName === null) {
67
+ return null;
68
+ }
69
+ const rootIdentifier = jsxElement.name.type === 'JSXIdentifier'
70
+ ? jsxElement.name
71
+ : jsxElement.name.type === 'JSXMemberExpression'
72
+ ? getJsxRootIdentifier(jsxElement.name)
73
+ : null;
74
+ if (rootIdentifier === null) {
75
+ return tagName;
76
+ }
77
+ for (const node of ast.program.body) {
78
+ if (node.type !== 'ImportDeclaration') {
79
+ continue;
80
+ }
81
+ const sourcePath = String(node.source.value);
82
+ for (const specifier of node.specifiers) {
83
+ if (specifier.type === 'ImportSpecifier' &&
84
+ ((_a = specifier.local) === null || _a === void 0 ? void 0 : _a.name) === rootIdentifier.name) {
85
+ const importName = (0, imports_1.getImportedName)(specifier);
86
+ return makeImportedIdentity({
87
+ sourcePath,
88
+ importName,
89
+ memberPath: getMemberPath(tagName),
90
+ });
91
+ }
92
+ if (specifier.type === 'ImportNamespaceSpecifier' &&
93
+ specifier.local.name === rootIdentifier.name) {
94
+ const [importName, ...members] = getMemberPath(tagName).split('.');
95
+ if (!importName) {
96
+ return null;
97
+ }
98
+ return makeImportedIdentity({
99
+ sourcePath,
100
+ importName,
101
+ memberPath: members.join('.'),
102
+ });
103
+ }
104
+ if (specifier.type === 'ImportDefaultSpecifier' &&
105
+ specifier.local.name === rootIdentifier.name) {
106
+ return makeImportedIdentity({
107
+ sourcePath,
108
+ importName: 'default',
109
+ memberPath: getMemberPathAfterImportName({
110
+ tagName,
111
+ importName: rootIdentifier.name,
112
+ }),
113
+ });
114
+ }
115
+ }
116
+ }
117
+ return tagName;
118
+ };
119
+ exports.getJsxComponentIdentity = getJsxComponentIdentity;
120
+ const jsxComponentIdentitiesMatch = ({ expected, actual, }) => {
121
+ if (expected === null) {
122
+ return true;
123
+ }
124
+ return expected === actual;
125
+ };
126
+ exports.jsxComponentIdentitiesMatch = jsxComponentIdentitiesMatch;
@@ -74,6 +74,7 @@ const addSequenceKeyframeHandler = ({ input: { fileName, nodePath, key, frame, v
74
74
  fileContents: output,
75
75
  keys: (0, studio_shared_1.getAllSchemaKeys)(schema),
76
76
  nodePath: updatedNodePath,
77
+ componentIdentity: null,
77
78
  effects: [],
78
79
  });
79
80
  const updatedSubscriptionKey = { ...nodePath, nodePath: updatedNodePath };
@@ -10,22 +10,25 @@ export declare const getComputedStatus: (node: Expression, ast: File) => CanUpda
10
10
  export declare const findJsxElementAtNodePath: (ast: File, nodePath: SequenceNodePath) => JSXOpeningElement | null;
11
11
  export declare const findNodePathForJsxElement: (ast: File, target: JSXOpeningElement) => SequenceNodePath | null;
12
12
  export declare const lineColumnToNodePath: (ast: File, targetLine: number) => SequenceNodePath | null;
13
- export declare const computeSequencePropsStatusFromContent: ({ fileContents, nodePath, keys, effects, }: {
13
+ export declare const computeSequencePropsStatusFromContent: ({ fileContents, nodePath, componentIdentity, keys, effects, }: {
14
14
  fileContents: string;
15
15
  nodePath: SequenceNodePath;
16
+ componentIdentity: string | null;
16
17
  keys: string[];
17
18
  effects: string[][];
18
19
  }) => CanUpdateSequencePropsResponseTrue;
19
- export declare const computeSequencePropsStatus: ({ fileName, nodePath, keys, effects, remotionRoot, }: {
20
+ export declare const computeSequencePropsStatus: ({ fileName, nodePath, componentIdentity, keys, effects, remotionRoot, }: {
20
21
  fileName: string;
21
22
  nodePath: SequenceNodePath;
23
+ componentIdentity: string | null;
22
24
  keys: string[];
23
25
  effects: string[][];
24
26
  remotionRoot: string;
25
27
  }) => CanUpdateSequencePropsResponseTrue;
26
- export declare const computeSequencePropsStatusFromFilenameByLine: ({ fileName, line, keys, effects, remotionRoot, logLevel, }: {
28
+ export declare const computeSequencePropsStatusFromFilenameByLine: ({ fileName, line, componentIdentity, keys, effects, remotionRoot, logLevel, }: {
27
29
  fileName: string;
28
30
  line: number;
31
+ componentIdentity: string | null;
29
32
  keys: string[];
30
33
  effects: string[][];
31
34
  remotionRoot: string;
@@ -43,6 +43,7 @@ const parse_ast_1 = require("../../codemods/parse-ast");
43
43
  const get_ast_node_path_1 = require("../../helpers/get-ast-node-path");
44
44
  const import_agnostic_node_path_1 = require("../../helpers/import-agnostic-node-path");
45
45
  const resolve_file_inside_project_1 = require("../../helpers/resolve-file-inside-project");
46
+ const jsx_component_identity_1 = require("../jsx-component-identity");
46
47
  const jsx_element_not_found_at_location_error_1 = require("../jsx-element-not-found-at-location-error");
47
48
  const can_update_effect_props_1 = require("./can-update-effect-props");
48
49
  const staticStatus = (codeValue) => ({
@@ -353,6 +354,25 @@ const getInterpolationKeyframes = (node, ast) => {
353
354
  if (node.type === 'TSAsExpression') {
354
355
  return getInterpolationKeyframes(node.expression, ast);
355
356
  }
357
+ if (node.type === 'CallExpression' &&
358
+ node.callee.type === 'Identifier' &&
359
+ node.callee.name === 'String' &&
360
+ node.arguments.length === 1 &&
361
+ node.arguments[0].type !== 'ArgumentPlaceholder' &&
362
+ node.arguments[0].type !== 'JSXNamespacedName' &&
363
+ node.arguments[0].type !== 'SpreadElement') {
364
+ const interpolation = getInterpolationKeyframes(node.arguments[0], ast);
365
+ if (!interpolation) {
366
+ return undefined;
367
+ }
368
+ return {
369
+ ...interpolation,
370
+ keyframes: interpolation.keyframes.map((keyframe) => ({
371
+ ...keyframe,
372
+ value: String(keyframe.value),
373
+ })),
374
+ };
375
+ }
356
376
  if (node.type !== 'CallExpression') {
357
377
  return undefined;
358
378
  }
@@ -627,12 +647,18 @@ const computeSequenceOnlyPropsRecord = ({ jsxElement, ast, keys, }) => {
627
647
  }
628
648
  return filteredProps;
629
649
  };
630
- const computeSequencePropsStatusFromContent = ({ fileContents, nodePath, keys, effects, }) => {
650
+ const computeSequencePropsStatusFromContent = ({ fileContents, nodePath, componentIdentity, keys, effects, }) => {
631
651
  const ast = (0, parse_ast_1.parseAst)(fileContents);
632
652
  const jsxElement = (0, exports.findJsxElementAtNodePath)(ast, nodePath);
633
653
  if (!jsxElement) {
634
654
  throw new jsx_element_not_found_at_location_error_1.JsxElementNotFoundAtLocationError();
635
655
  }
656
+ if (!(0, jsx_component_identity_1.jsxComponentIdentitiesMatch)({
657
+ expected: componentIdentity,
658
+ actual: (0, jsx_component_identity_1.getJsxComponentIdentity)({ ast, jsxElement }),
659
+ })) {
660
+ throw new jsx_component_identity_1.JsxElementIdentityMismatchError();
661
+ }
636
662
  const filteredProps = computeSequenceOnlyPropsRecord({ jsxElement, ast, keys });
637
663
  const effectsStatuses = computeEffectsForJsx({ ast, jsxElement, effects });
638
664
  return {
@@ -642,7 +668,7 @@ const computeSequencePropsStatusFromContent = ({ fileContents, nodePath, keys, e
642
668
  };
643
669
  };
644
670
  exports.computeSequencePropsStatusFromContent = computeSequencePropsStatusFromContent;
645
- const computeSequencePropsStatus = ({ fileName, nodePath, keys, effects, remotionRoot, }) => {
671
+ const computeSequencePropsStatus = ({ fileName, nodePath, componentIdentity, keys, effects, remotionRoot, }) => {
646
672
  const { absolutePath } = (0, resolve_file_inside_project_1.resolveFileInsideProject)({
647
673
  remotionRoot,
648
674
  fileName,
@@ -652,12 +678,13 @@ const computeSequencePropsStatus = ({ fileName, nodePath, keys, effects, remotio
652
678
  return (0, exports.computeSequencePropsStatusFromContent)({
653
679
  fileContents,
654
680
  nodePath,
681
+ componentIdentity,
655
682
  keys,
656
683
  effects,
657
684
  });
658
685
  };
659
686
  exports.computeSequencePropsStatus = computeSequencePropsStatus;
660
- const computeSequencePropsStatusFromFilenameByLine = ({ fileName, line, keys, effects, remotionRoot, logLevel, }) => {
687
+ const computeSequencePropsStatusFromFilenameByLine = ({ fileName, line, componentIdentity, keys, effects, remotionRoot, logLevel, }) => {
661
688
  try {
662
689
  const { absolutePath } = (0, resolve_file_inside_project_1.resolveFileInsideProject)({
663
690
  remotionRoot,
@@ -680,6 +707,7 @@ const computeSequencePropsStatusFromFilenameByLine = ({ fileName, line, keys, ef
680
707
  status: (0, exports.computeSequencePropsStatus)({
681
708
  fileName,
682
709
  nodePath: resolvedNodePath,
710
+ componentIdentity,
683
711
  keys,
684
712
  effects,
685
713
  remotionRoot,
@@ -215,6 +215,7 @@ const deleteKeyframes = async ({ sequenceKeyframes, effectKeyframes, clientId, r
215
215
  fileContents: output,
216
216
  keys: (0, studio_shared_1.getAllSchemaKeys)(keyframe.schema),
217
217
  nodePath: keyframe.nodePath.nodePath,
218
+ componentIdentity: null,
218
219
  effects: [],
219
220
  });
220
221
  return {
@@ -13,9 +13,10 @@ const validate_same_origin_1 = require("../validate-same-origin");
13
13
  const maxRemoteAssetSize = 50 * 1024 * 1024;
14
14
  const remoteAssetDownloadTimeout = 15000;
15
15
  const maxRemoteAssetRedirects = 5;
16
- const remoteAssetAcceptHeader = 'image/png,image/jpeg,image/webp,image/bmp,image/gif';
16
+ const remoteAssetAcceptHeader = 'image/png,image/apng,image/jpeg,image/webp,image/bmp,image/gif';
17
17
  const extensionsForFileType = {
18
18
  png: ['png'],
19
+ apng: ['png', 'apng'],
19
20
  jpeg: ['jpg', 'jpeg'],
20
21
  webp: ['webp'],
21
22
  bmp: ['bmp'],
@@ -253,10 +254,15 @@ const downloadRemoteAssetHandler = async ({ input, publicDir, request }) => {
253
254
  }
254
255
  const element = {
255
256
  type: 'asset',
256
- assetType: fileType.type === 'gif' ? 'gif' : 'image',
257
+ assetType: fileType.type === 'gif'
258
+ ? 'gif'
259
+ : fileType.type === 'apng'
260
+ ? 'animated-image'
261
+ : 'image',
257
262
  src: assetPath,
258
263
  srcType: 'static',
259
264
  dimensions: fileType.dimensions,
265
+ position: null,
260
266
  };
261
267
  return {
262
268
  assetPath,
@@ -0,0 +1,3 @@
1
+ import type { DuplicateEffectRequest, DuplicateEffectResponse } from '@remotion/studio-shared';
2
+ import type { ApiHandler } from '../api-types';
3
+ export declare const duplicateEffectHandler: ApiHandler<DuplicateEffectRequest, DuplicateEffectResponse>;
@@ -0,0 +1,98 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.duplicateEffectHandler = void 0;
4
+ const node_fs_1 = require("node:fs");
5
+ const renderer_1 = require("@remotion/renderer");
6
+ const duplicate_effect_1 = require("../../codemods/duplicate-effect");
7
+ const file_watcher_1 = require("../../file-watcher");
8
+ const resolve_file_inside_project_1 = require("../../helpers/resolve-file-inside-project");
9
+ const format_log_file_location_1 = require("../format-log-file-location");
10
+ const undo_stack_1 = require("../undo-stack");
11
+ const formatting_1 = require("./log-updates/formatting");
12
+ const log_update_1 = require("./log-updates/log-update");
13
+ const getDuplicatedEffectDescription = (effectLabels) => {
14
+ if (effectLabels.length === 1) {
15
+ return effectLabels[0];
16
+ }
17
+ return `${effectLabels.length} effects`;
18
+ };
19
+ const duplicateEffectHandler = async ({ input: effects, remotionRoot, logLevel }) => {
20
+ var _a;
21
+ try {
22
+ if (effects.length === 0) {
23
+ throw new Error('No effects were specified for duplication');
24
+ }
25
+ renderer_1.RenderInternals.Log.trace({ indent: false, logLevel }, `[duplicate-effect] Received request to duplicate ${effects.length} effect target${effects.length === 1 ? '' : 's'}`);
26
+ const itemsByFileName = new Map();
27
+ for (const item of effects) {
28
+ const fileItems = (_a = itemsByFileName.get(item.fileName)) !== null && _a !== void 0 ? _a : [];
29
+ fileItems.push(item);
30
+ itemsByFileName.set(item.fileName, fileItems);
31
+ }
32
+ const updates = await Promise.all([...itemsByFileName.entries()].map(async ([fileName, fileItems]) => {
33
+ const { absolutePath, fileRelativeToRoot } = (0, resolve_file_inside_project_1.resolveFileInsideProject)({
34
+ remotionRoot,
35
+ fileName,
36
+ action: 'modify',
37
+ });
38
+ const fileContents = (0, node_fs_1.readFileSync)(absolutePath, 'utf-8');
39
+ const { output, formatted, effectLabels, logLines } = await (0, duplicate_effect_1.duplicateEffects)({
40
+ input: fileContents,
41
+ effects: fileItems.map((item) => ({
42
+ sequenceNodePath: item.sequenceNodePath.nodePath,
43
+ effectIndex: item.effectIndex,
44
+ })),
45
+ });
46
+ return {
47
+ absolutePath,
48
+ fileRelativeToRoot,
49
+ fileContents,
50
+ output,
51
+ formatted,
52
+ effectLabels,
53
+ logLine: Math.min(...logLines),
54
+ };
55
+ }));
56
+ for (const update of updates) {
57
+ const duplicatedEffectDescription = getDuplicatedEffectDescription(update.effectLabels);
58
+ (0, undo_stack_1.pushToUndoStack)({
59
+ filePath: update.absolutePath,
60
+ oldContents: update.fileContents,
61
+ newContents: null,
62
+ logLevel,
63
+ remotionRoot,
64
+ logLine: update.logLine,
65
+ description: {
66
+ undoMessage: `↩️ Duplication of ${duplicatedEffectDescription}`,
67
+ redoMessage: `↪️ Duplication of ${duplicatedEffectDescription}`,
68
+ },
69
+ entryType: 'duplicate-effect',
70
+ suppressHmrOnFileRestore: false,
71
+ });
72
+ (0, undo_stack_1.suppressUndoStackInvalidation)(update.absolutePath);
73
+ (0, file_watcher_1.writeFileAndNotifyFileWatchers)(update.absolutePath, update.output, undefined);
74
+ const locationLabel = (0, format_log_file_location_1.formatLogFileLocation)({
75
+ remotionRoot,
76
+ absolutePath: update.absolutePath,
77
+ line: update.logLine,
78
+ });
79
+ renderer_1.RenderInternals.Log.info({ indent: false, logLevel }, `${renderer_1.RenderInternals.chalk.blueBright(`${locationLabel}`)} Duplicated ${(0, formatting_1.attrName)(duplicatedEffectDescription)}`);
80
+ if (!update.formatted) {
81
+ (0, log_update_1.warnAboutPrettierOnce)(logLevel);
82
+ }
83
+ renderer_1.RenderInternals.Log.verbose({ indent: false, logLevel }, `[duplicate-effect] Wrote ${update.fileRelativeToRoot}${update.formatted ? ' (formatted)' : ''}`);
84
+ }
85
+ (0, undo_stack_1.printUndoHint)(logLevel);
86
+ return {
87
+ success: true,
88
+ };
89
+ }
90
+ catch (err) {
91
+ return {
92
+ success: false,
93
+ reason: err.message,
94
+ stack: err.stack,
95
+ };
96
+ }
97
+ };
98
+ exports.duplicateEffectHandler = duplicateEffectHandler;
@@ -14,7 +14,16 @@ const validateDimension = (name, value) => {
14
14
  throw new Error(`${name} must be a positive number`);
15
15
  }
16
16
  };
17
+ const validatePosition = (position) => {
18
+ if (position === null) {
19
+ return;
20
+ }
21
+ if (!Number.isFinite(position.x) || !Number.isFinite(position.y)) {
22
+ throw new Error('Position must be finite');
23
+ }
24
+ };
17
25
  const validateElement = (element) => {
26
+ validatePosition(element.position);
18
27
  if (element.type === 'solid') {
19
28
  validateDimension('width', element.width);
20
29
  validateDimension('height', element.height);
@@ -66,6 +75,9 @@ const getElementLabel = (element) => {
66
75
  if (element.assetType === 'gif') {
67
76
  return '<Gif>';
68
77
  }
78
+ if (element.assetType === 'animated-image') {
79
+ return '<AnimatedImage>';
80
+ }
69
81
  if (element.assetType === 'audio') {
70
82
  return '<Audio>';
71
83
  }
@@ -145,6 +145,7 @@ const saveSequencePropsHandler = ({ input: { edits, clientId, undoLabel, redoLab
145
145
  fileContents: output,
146
146
  keys: (0, studio_shared_1.getAllSchemaKeys)(edit.schema),
147
147
  nodePath: edit.nodePath.nodePath,
148
+ componentIdentity: null,
148
149
  effects: [],
149
150
  });
150
151
  return {
@@ -2,12 +2,13 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.subscribeToSequenceProps = void 0;
4
4
  const sequence_props_watchers_1 = require("../sequence-props-watchers");
5
- const subscribeToSequenceProps = ({ input: { fileName, line, column, nodePath, keys, effects, clientId }, remotionRoot, logLevel, }) => {
5
+ const subscribeToSequenceProps = ({ input: { fileName, line, column, nodePath, componentIdentity, keys, effects, clientId, }, remotionRoot, logLevel, }) => {
6
6
  const result = (0, sequence_props_watchers_1.subscribeToSequencePropsWatchers)({
7
7
  fileName,
8
8
  line,
9
9
  column,
10
10
  nodePath,
11
+ componentIdentity,
11
12
  keys,
12
13
  effects,
13
14
  remotionRoot,
@@ -77,6 +77,7 @@ const updateSequenceKeyframeSettingsHandler = ({ input: { fileName, nodePath, ke
77
77
  fileContents: output,
78
78
  keys: (0, studio_shared_1.getAllSchemaKeys)(schema),
79
79
  nodePath: updatedNodePath,
80
+ componentIdentity: null,
80
81
  effects: [],
81
82
  });
82
83
  const updatedSubscriptionKey = { ...nodePath, nodePath: updatedNodePath };
@@ -1,10 +1,11 @@
1
1
  import { type SubscribeToSequencePropsResponse } from '@remotion/studio-shared';
2
2
  import type { SequenceNodePath } from 'remotion';
3
- export declare const subscribeToSequencePropsWatchers: ({ fileName, line, column, nodePath: preferredNodePath, keys, effects, remotionRoot, clientId, logLevel, }: {
3
+ export declare const subscribeToSequencePropsWatchers: ({ fileName, line, column, nodePath: preferredNodePath, componentIdentity, keys, effects, remotionRoot, clientId, logLevel, }: {
4
4
  fileName: string;
5
5
  line: number;
6
6
  column: number;
7
7
  nodePath: SequenceNodePath | null;
8
+ componentIdentity: string | null;
8
9
  keys: string[];
9
10
  effects: string[][];
10
11
  remotionRoot: string;
@@ -8,42 +8,64 @@ const node_path_1 = __importDefault(require("node:path"));
8
8
  const renderer_1 = require("@remotion/renderer");
9
9
  const studio_shared_1 = require("@remotion/studio-shared");
10
10
  const file_watcher_1 = require("../file-watcher");
11
+ const jsx_component_identity_1 = require("./jsx-component-identity");
11
12
  const jsx_element_not_found_at_location_error_1 = require("./jsx-element-not-found-at-location-error");
12
13
  const live_events_1 = require("./live-events");
13
14
  const node_path_cache_1 = require("./node-path-cache");
14
15
  const can_update_sequence_props_1 = require("./routes/can-update-sequence-props");
15
16
  const sequencePropsWatchers = {};
16
- const getSequencePropsStatus = ({ fileName, line, column, preferredNodePath, keys, effects, remotionRoot, logLevel, }) => {
17
+ const getSequencePropsStatus = ({ fileName, line, column, preferredNodePath, componentIdentity, keys, effects, remotionRoot, logLevel, }) => {
17
18
  if (preferredNodePath) {
18
- const fromNodePath = (0, can_update_sequence_props_1.computeSequencePropsStatus)({
19
- fileName,
20
- nodePath: preferredNodePath,
21
- keys,
22
- effects,
23
- remotionRoot,
24
- });
25
- return {
26
- status: fromNodePath,
27
- nodePath: {
28
- absolutePath: node_path_1.default.resolve(remotionRoot, fileName),
19
+ try {
20
+ const fromNodePath = (0, can_update_sequence_props_1.computeSequencePropsStatus)({
21
+ fileName,
29
22
  nodePath: preferredNodePath,
30
- sequenceKeys: keys,
31
- effectKeys: effects,
32
- },
33
- success: true,
34
- };
23
+ componentIdentity,
24
+ keys,
25
+ effects,
26
+ remotionRoot,
27
+ });
28
+ return {
29
+ status: fromNodePath,
30
+ nodePath: {
31
+ absolutePath: node_path_1.default.resolve(remotionRoot, fileName),
32
+ nodePath: preferredNodePath,
33
+ sequenceKeys: keys,
34
+ effectKeys: effects,
35
+ },
36
+ success: true,
37
+ };
38
+ }
39
+ catch (error) {
40
+ if (!(error instanceof jsx_component_identity_1.JsxElementIdentityMismatchError ||
41
+ error instanceof jsx_element_not_found_at_location_error_1.JsxElementNotFoundAtLocationError)) {
42
+ throw error;
43
+ }
44
+ }
35
45
  }
36
46
  // Try cached nodePath first (handles stale source maps after suppressed rebuilds)
37
47
  const cachedNodePath = (0, node_path_cache_1.getCachedNodePath)(fileName, line, column);
38
48
  if (cachedNodePath) {
39
- const cachedResult = (0, can_update_sequence_props_1.computeSequencePropsStatus)({
40
- fileName,
41
- nodePath: cachedNodePath,
42
- keys,
43
- effects,
44
- remotionRoot,
45
- });
46
- if (cachedResult.canUpdate) {
49
+ const cachedResult = (() => {
50
+ try {
51
+ return (0, can_update_sequence_props_1.computeSequencePropsStatus)({
52
+ fileName,
53
+ nodePath: cachedNodePath,
54
+ componentIdentity,
55
+ keys,
56
+ effects,
57
+ remotionRoot,
58
+ });
59
+ }
60
+ catch (error) {
61
+ if (error instanceof jsx_component_identity_1.JsxElementIdentityMismatchError ||
62
+ error instanceof jsx_element_not_found_at_location_error_1.JsxElementNotFoundAtLocationError) {
63
+ return null;
64
+ }
65
+ throw error;
66
+ }
67
+ })();
68
+ if (cachedResult === null || cachedResult === void 0 ? void 0 : cachedResult.canUpdate) {
47
69
  return {
48
70
  status: cachedResult,
49
71
  nodePath: {
@@ -59,6 +81,7 @@ const getSequencePropsStatus = ({ fileName, line, column, preferredNodePath, key
59
81
  const status = (0, can_update_sequence_props_1.computeSequencePropsStatusFromFilenameByLine)({
60
82
  fileName,
61
83
  line,
84
+ componentIdentity,
62
85
  keys,
63
86
  effects,
64
87
  remotionRoot,
@@ -66,13 +89,14 @@ const getSequencePropsStatus = ({ fileName, line, column, preferredNodePath, key
66
89
  });
67
90
  return status;
68
91
  };
69
- const subscribeToSequencePropsWatchers = ({ fileName, line, column, nodePath: preferredNodePath, keys, effects, remotionRoot, clientId, logLevel, }) => {
92
+ const subscribeToSequencePropsWatchers = ({ fileName, line, column, nodePath: preferredNodePath, componentIdentity, keys, effects, remotionRoot, clientId, logLevel, }) => {
70
93
  var _a;
71
94
  const initialResult = getSequencePropsStatus({
72
95
  fileName,
73
96
  line,
74
97
  column,
75
98
  preferredNodePath,
99
+ componentIdentity,
76
100
  keys,
77
101
  effects,
78
102
  remotionRoot,
@@ -105,6 +129,7 @@ const subscribeToSequencePropsWatchers = ({ fileName, line, column, nodePath: pr
105
129
  const result = (0, can_update_sequence_props_1.computeSequencePropsStatusFromContent)({
106
130
  fileContents: event.content,
107
131
  nodePath: nodePath.nodePath,
132
+ componentIdentity,
108
133
  keys,
109
134
  effects,
110
135
  });
@@ -125,7 +150,8 @@ const subscribeToSequencePropsWatchers = ({ fileName, line, column, nodePath: pr
125
150
  });
126
151
  }
127
152
  catch (error) {
128
- if (error instanceof jsx_element_not_found_at_location_error_1.JsxElementNotFoundAtLocationError) {
153
+ if (error instanceof jsx_element_not_found_at_location_error_1.JsxElementNotFoundAtLocationError ||
154
+ error instanceof jsx_component_identity_1.JsxElementIdentityMismatchError) {
129
155
  (0, live_events_1.waitForLiveEventsListener)().then((listener) => {
130
156
  listener.sendEventToClientId(clientId, {
131
157
  type: 'lost-node-path',
@@ -3,7 +3,7 @@ export interface UndoEntryDescription {
3
3
  undoMessage: string;
4
4
  redoMessage: string;
5
5
  }
6
- type UndoEntryType = 'visual-control' | 'default-props' | 'sequence-props' | 'effect-props' | 'keyframe-add' | 'keyframe-delete' | 'add-effect' | 'delete-effect' | 'paste-effects' | 'reorder-effect' | 'reorder-sequence' | 'delete-jsx-node' | 'duplicate-jsx-node' | 'insert-jsx-element' | 'delete-composition' | 'rename-composition' | 'duplicate-composition' | 'delete-folder' | 'rename-folder';
6
+ type UndoEntryType = 'visual-control' | 'default-props' | 'sequence-props' | 'effect-props' | 'keyframe-add' | 'keyframe-delete' | 'add-effect' | 'delete-effect' | 'duplicate-effect' | 'paste-effects' | 'reorder-effect' | 'reorder-sequence' | 'delete-jsx-node' | 'duplicate-jsx-node' | 'insert-jsx-element' | 'delete-composition' | 'rename-composition' | 'duplicate-composition' | 'delete-folder' | 'rename-folder';
7
7
  type UndoEntrySnapshot = {
8
8
  filePath: string;
9
9
  oldContents: string;
@@ -35,6 +35,8 @@ type UndoEntry = {
35
35
  entryType: 'add-effect';
36
36
  } | {
37
37
  entryType: 'delete-effect';
38
+ } | {
39
+ entryType: 'duplicate-effect';
38
40
  } | {
39
41
  entryType: 'paste-effects';
40
42
  } | {
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "url": "https://github.com/remotion-dev/remotion/tree/main/packages/studio-server"
4
4
  },
5
5
  "name": "@remotion/studio-server",
6
- "version": "4.0.476",
6
+ "version": "4.0.478",
7
7
  "description": "Run a Remotion Studio with a server backend",
8
8
  "main": "dist",
9
9
  "scripts": {
@@ -27,11 +27,11 @@
27
27
  "@babel/parser": "7.24.1",
28
28
  "semver": "7.5.3",
29
29
  "prettier": "3.8.1",
30
- "remotion": "4.0.476",
30
+ "remotion": "4.0.478",
31
31
  "recast": "0.23.11",
32
- "@remotion/bundler": "4.0.476",
33
- "@remotion/renderer": "4.0.476",
34
- "@remotion/studio-shared": "4.0.476",
32
+ "@remotion/bundler": "4.0.478",
33
+ "@remotion/renderer": "4.0.478",
34
+ "@remotion/studio-shared": "4.0.478",
35
35
  "memfs": "3.4.3",
36
36
  "open": "8.4.2"
37
37
  },
@@ -39,7 +39,7 @@
39
39
  "ast-types": "0.16.1",
40
40
  "react": "19.2.3",
41
41
  "@types/semver": "7.5.3",
42
- "@remotion/eslint-config-internal": "4.0.476",
42
+ "@remotion/eslint-config-internal": "4.0.478",
43
43
  "eslint": "9.19.0",
44
44
  "@types/node": "20.12.14",
45
45
  "@typescript/native-preview": "7.0.0-dev.20260217.1"