@remotion/studio-server 4.0.485 → 4.0.486

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.
@@ -49,6 +49,26 @@ const getUseCurrentFrameLocalNames = (ast) => {
49
49
  }
50
50
  return names;
51
51
  };
52
+ const getGoogleFontLoadFontLocalNames = (ast) => {
53
+ var _a;
54
+ var _b;
55
+ const names = new Set();
56
+ for (const statement of ast.program.body) {
57
+ if (statement.type !== 'ImportDeclaration' ||
58
+ typeof statement.source.value !== 'string' ||
59
+ !statement.source.value.startsWith('@remotion/google-fonts/')) {
60
+ continue;
61
+ }
62
+ for (const specifier of statement.specifiers) {
63
+ if (specifier.type === 'ImportSpecifier' &&
64
+ specifier.imported.type === 'Identifier' &&
65
+ specifier.imported.name === 'loadFont') {
66
+ names.add((_b = (_a = specifier.local) === null || _a === void 0 ? void 0 : _a.name) !== null && _b !== void 0 ? _b : 'loadFont');
67
+ }
68
+ }
69
+ }
70
+ return names;
71
+ };
52
72
  const isUseCurrentFrameCall = (node, useCurrentFrameLocalNames) => {
53
73
  return ((node === null || node === void 0 ? void 0 : node.type) === 'CallExpression' &&
54
74
  node.callee.type === 'Identifier' &&
@@ -63,13 +83,19 @@ const isUseCurrentFrameStatement = (node, useCurrentFrameLocalNames) => {
63
83
  }
64
84
  return isUseCurrentFrameCall(node.declarations[0].init, useCurrentFrameLocalNames);
65
85
  };
86
+ const isGoogleFontLoadFontStatement = (node, googleFontLoadFontLocalNames) => {
87
+ return (node.type === 'ExpressionStatement' &&
88
+ node.expression.type === 'CallExpression' &&
89
+ node.expression.callee.type === 'Identifier' &&
90
+ googleFontLoadFontLocalNames.has(node.expression.callee.name));
91
+ };
66
92
  const getIgnoredNodePredicate = (ast) => {
67
93
  const useCurrentFrameLocalNames = getUseCurrentFrameLocalNames(ast);
94
+ const googleFontLoadFontLocalNames = getGoogleFontLoadFontLocalNames(ast);
68
95
  return (node, { parent, property }) => {
69
- if (parent.type === 'Program' &&
70
- property === 'body' &&
71
- node.type === 'ImportDeclaration') {
72
- return true;
96
+ if (parent.type === 'Program' && property === 'body') {
97
+ return (node.type === 'ImportDeclaration' ||
98
+ isGoogleFontLoadFontStatement(node, googleFontLoadFontLocalNames));
73
99
  }
74
100
  if (parent.type === 'BlockStatement' && property === 'body') {
75
101
  return isUseCurrentFrameStatement(node, useCurrentFrameLocalNames);
@@ -30,6 +30,7 @@ export declare const insertJsxElementIntoComposition: ({ remotionRoot, compositi
30
30
  width: number;
31
31
  height: number;
32
32
  };
33
+ durationInFrames?: number | null | undefined;
33
34
  name: string | null;
34
35
  position: InsertableCompositionElementPosition | null;
35
36
  } | null | undefined;
@@ -41,8 +41,10 @@ const node_fs_1 = __importDefault(require("node:fs"));
41
41
  const node_path_1 = __importDefault(require("node:path"));
42
42
  const studio_shared_1 = require("@remotion/studio-shared");
43
43
  const recast = __importStar(require("recast"));
44
+ const no_react_1 = require("remotion/no-react");
44
45
  const format_file_content_1 = require("../codemods/format-file-content");
45
46
  const parse_ast_1 = require("../codemods/parse-ast");
47
+ const update_nested_prop_1 = require("../codemods/update-nested-prop");
46
48
  const imports_1 = require("./imports");
47
49
  const allowedFileExtensions = new Set(['.tsx', '.ts', '.jsx', '.js']);
48
50
  const extensionsToProbe = ['.tsx', '.ts', '.jsx', '.js'];
@@ -119,7 +121,7 @@ const getComponentIdentifier = (element) => {
119
121
  return attribute.value.expression.name;
120
122
  };
121
123
  const isRecord = (value) => {
122
- return typeof value === 'object' && value !== null;
124
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
123
125
  };
124
126
  const findDynamicImportPath = (value) => {
125
127
  if (!isRecord(value)) {
@@ -578,11 +580,14 @@ const createComponentElement = ({ addPositionStyle, localName, props, position,
578
580
  : []),
579
581
  ], true), null, []);
580
582
  };
581
- const createSequenceWrappedElement = ({ child, dimensions, name, position, sequenceLocalName, }) => {
583
+ const createSequenceWrappedElement = ({ child, dimensions, durationInFrames, name, position, sequenceLocalName, }) => {
582
584
  return recast.types.builders.jsxElement(recast.types.builders.jsxOpeningElement(recast.types.builders.jsxIdentifier(sequenceLocalName), [
583
585
  ...(name === null ? [] : [createStringAttribute('name', name)]),
584
586
  createNumberAttribute('width', dimensions.width),
585
587
  createNumberAttribute('height', dimensions.height),
588
+ ...(durationInFrames === null
589
+ ? []
590
+ : [createNumberAttribute('durationInFrames', durationInFrames)]),
586
591
  createPositionAbsoluteStyleAttribute(position),
587
592
  ], false), recast.types.builders.jsxClosingElement(recast.types.builders.jsxIdentifier(sequenceLocalName)), [child]);
588
593
  };
@@ -847,6 +852,167 @@ const ensureComponentImport = ({ ast, componentName, importName, importPath, })
847
852
  localName: componentName,
848
853
  });
849
854
  };
855
+ const identifierRegex = /^[A-Za-z_$][0-9A-Za-z_$]*$/;
856
+ const toPascalCaseIdentifier = (value) => {
857
+ var _a;
858
+ const words = (_a = value.match(/[a-zA-Z0-9]+/g)) !== null && _a !== void 0 ? _a : [];
859
+ const candidate = words
860
+ .map((word) => `${word.charAt(0).toUpperCase()}${word.slice(1)}`)
861
+ .join('');
862
+ if (!candidate) {
863
+ return 'CompositionComponent';
864
+ }
865
+ if (/^[0-9]/.test(candidate)) {
866
+ return `Composition${candidate}`;
867
+ }
868
+ return identifierRegex.test(candidate) ? candidate : 'CompositionComponent';
869
+ };
870
+ const getAvailableLocalName = ({ ast, baseName, }) => {
871
+ if (!hasTopLevelBinding({ ast, name: baseName })) {
872
+ return baseName;
873
+ }
874
+ const suffixed = `${baseName}Composition`;
875
+ if (!hasTopLevelBinding({ ast, name: suffixed })) {
876
+ return suffixed;
877
+ }
878
+ for (let i = 2; i < 100; i++) {
879
+ const candidate = `${suffixed}${i}`;
880
+ if (!hasTopLevelBinding({ ast, name: candidate })) {
881
+ return candidate;
882
+ }
883
+ }
884
+ throw new Error(`Cannot find a local name for ${baseName}`);
885
+ };
886
+ const getImportPathBetweenFiles = ({ fromFile, toFile, }) => {
887
+ let relativeImport = node_path_1.default
888
+ .relative(node_path_1.default.dirname(fromFile), toFile)
889
+ .replaceAll(node_path_1.default.sep, '/')
890
+ .replace(/\.(tsx|ts|jsx|js)$/, '');
891
+ if (!relativeImport.startsWith('.')) {
892
+ relativeImport = `./${relativeImport}`;
893
+ }
894
+ return relativeImport;
895
+ };
896
+ const ensureDefaultImport = ({ ast, localName, sourcePath, }) => {
897
+ var _a, _b;
898
+ var _c;
899
+ for (const declaration of getImportDeclarations({ ast, sourcePath })) {
900
+ const defaultSpecifier = (_a = declaration.specifiers) === null || _a === void 0 ? void 0 : _a.find((specifier) => specifier.type === 'ImportDefaultSpecifier');
901
+ if ((_b = defaultSpecifier === null || defaultSpecifier === void 0 ? void 0 : defaultSpecifier.local) === null || _b === void 0 ? void 0 : _b.name) {
902
+ return defaultSpecifier.local.name;
903
+ }
904
+ }
905
+ const importSpecifier = recast.types.builders.importDefaultSpecifier(recast.types.builders.identifier(localName));
906
+ const existingImport = getImportDeclarations({ ast, sourcePath }).find((declaration) => {
907
+ var _a;
908
+ return !((_a = declaration.specifiers) === null || _a === void 0 ? void 0 : _a.some((specifier) => specifier.type === 'ImportNamespaceSpecifier'));
909
+ });
910
+ if (existingImport) {
911
+ existingImport.specifiers = [
912
+ importSpecifier,
913
+ ...((_c = existingImport.specifiers) !== null && _c !== void 0 ? _c : []),
914
+ ];
915
+ return localName;
916
+ }
917
+ const importDeclaration = recast.types.builders.importDeclaration([importSpecifier], recast.types.builders.stringLiteral(sourcePath));
918
+ (0, imports_1.insertImportDeclaration)(ast, importDeclaration);
919
+ return localName;
920
+ };
921
+ const ensureCompositionComponentImport = async ({ ast, compositionFile, compositionId, destinationFileName, remotionRoot, }) => {
922
+ const sourceLocation = await (0, exports.resolveCompositionComponentWithFile)({
923
+ remotionRoot,
924
+ compositionFile,
925
+ compositionId,
926
+ });
927
+ if (sourceLocation.fileName === destinationFileName) {
928
+ if (sourceLocation.exportName === 'default') {
929
+ throw new Error('Cannot insert a composition whose component is a default export in the same file');
930
+ }
931
+ if (!hasTopLevelBinding({ ast, name: sourceLocation.exportName })) {
932
+ throw new Error(`Cannot find component "${sourceLocation.exportName}" in this file`);
933
+ }
934
+ return sourceLocation.exportName;
935
+ }
936
+ const sourcePath = getImportPathBetweenFiles({
937
+ fromFile: destinationFileName,
938
+ toFile: sourceLocation.fileName,
939
+ });
940
+ if (sourceLocation.exportName === 'default') {
941
+ return ensureDefaultImport({
942
+ ast,
943
+ localName: getAvailableLocalName({
944
+ ast,
945
+ baseName: toPascalCaseIdentifier(compositionId),
946
+ }),
947
+ sourcePath,
948
+ });
949
+ }
950
+ return (0, imports_1.ensureNamedImport)({
951
+ ast,
952
+ importedName: sourceLocation.exportName,
953
+ sourcePath,
954
+ localName: getAvailableLocalName({
955
+ ast,
956
+ baseName: sourceLocation.exportName,
957
+ }),
958
+ });
959
+ };
960
+ const parseSerializedCompositionProps = (serializedResolvedPropsWithCustomSchema) => {
961
+ const parsed = JSON.parse(serializedResolvedPropsWithCustomSchema);
962
+ if (!isRecord(parsed)) {
963
+ throw new Error('Resolved composition props must be an object');
964
+ }
965
+ return parsed;
966
+ };
967
+ const containsFileToken = (value) => {
968
+ if (typeof value === 'string') {
969
+ return value.startsWith(no_react_1.NoReactInternals.FILE_TOKEN);
970
+ }
971
+ if (Array.isArray(value)) {
972
+ return value.some(containsFileToken);
973
+ }
974
+ if (isRecord(value)) {
975
+ return Object.values(value).some(containsFileToken);
976
+ }
977
+ return false;
978
+ };
979
+ const createExpressionAttribute = (name, value) => {
980
+ return recast.types.builders.jsxAttribute(recast.types.builders.jsxIdentifier(name), recast.types.builders.jsxExpressionContainer((0, update_nested_prop_1.parseValueExpression)(value)));
981
+ };
982
+ const createCompositionPropAttribute = ({ name, value, }) => {
983
+ if (typeof value === 'string' &&
984
+ !value.startsWith(no_react_1.NoReactInternals.FILE_TOKEN) &&
985
+ !value.startsWith(no_react_1.NoReactInternals.DATE_TOKEN)) {
986
+ return createStringAttribute(name, value);
987
+ }
988
+ return createExpressionAttribute(name, value);
989
+ };
990
+ const createCompositionObjectProperty = ({ name, value, }) => {
991
+ return recast.types.builders.objectProperty(identifierRegex.test(name)
992
+ ? recast.types.builders.identifier(name)
993
+ : recast.types.builders.stringLiteral(name), (0, update_nested_prop_1.parseValueExpression)(value));
994
+ };
995
+ const createCompositionComponentElement = ({ localName, props, }) => {
996
+ const directAttributes = [];
997
+ const spreadProperties = [];
998
+ for (const [name, value] of Object.entries(props)) {
999
+ if (identifierRegex.test(name)) {
1000
+ directAttributes.push(createCompositionPropAttribute({ name, value }));
1001
+ }
1002
+ else {
1003
+ spreadProperties.push(createCompositionObjectProperty({ name, value }));
1004
+ }
1005
+ }
1006
+ const attributes = [
1007
+ ...directAttributes,
1008
+ ...(spreadProperties.length === 0
1009
+ ? []
1010
+ : [
1011
+ recast.types.builders.jsxSpreadAttribute(recast.types.builders.objectExpression(spreadProperties)),
1012
+ ]),
1013
+ ];
1014
+ return recast.types.builders.jsxElement(recast.types.builders.jsxOpeningElement(recast.types.builders.jsxIdentifier(localName), attributes, true), null, []);
1015
+ };
850
1016
  const addElementToComponentRoot = ({ ast, exportName, element, }) => {
851
1017
  var _a;
852
1018
  var _b;
@@ -1061,7 +1227,7 @@ const resolveCompositionComponent = async ({ remotionRoot, compositionFile, comp
1061
1227
  };
1062
1228
  };
1063
1229
  exports.resolveCompositionComponent = resolveCompositionComponent;
1064
- const createInsertableJsxElement = ({ addPositionStyleToComponent, ast, element, }) => {
1230
+ const createInsertableJsxElement = ({ addPositionStyleToComponent, ast, destinationFileName, element, remotionRoot, }) => {
1065
1231
  if (element.type === 'solid') {
1066
1232
  const solidLocalName = ensureSolidImport(ast);
1067
1233
  return createSolidElement({
@@ -1085,6 +1251,21 @@ const createInsertableJsxElement = ({ addPositionStyleToComponent, ast, element,
1085
1251
  position: element.position,
1086
1252
  });
1087
1253
  }
1254
+ if (element.type === 'composition') {
1255
+ return Promise.resolve(ensureCompositionComponentImport({
1256
+ ast,
1257
+ compositionFile: element.compositionFile,
1258
+ compositionId: element.compositionId,
1259
+ destinationFileName,
1260
+ remotionRoot,
1261
+ })).then((localName) => {
1262
+ const props = parseSerializedCompositionProps(element.serializedResolvedPropsWithCustomSchema);
1263
+ if (containsFileToken(props)) {
1264
+ ensureStaticFileImport(ast);
1265
+ }
1266
+ return createCompositionComponentElement({ localName, props });
1267
+ });
1268
+ }
1088
1269
  if (element.type === 'asset') {
1089
1270
  if (element.srcType === 'remote' && !(0, studio_shared_1.isUrl)(element.src)) {
1090
1271
  throw new Error('Remote asset source must be a URL');
@@ -1121,6 +1302,7 @@ const createInsertableJsxElement = ({ addPositionStyleToComponent, ast, element,
1121
1302
  throw new Error('Unsupported element type');
1122
1303
  };
1123
1304
  const insertJsxElementIntoComposition = async ({ remotionRoot, compositionFile, compositionId, element, prettierConfigOverride, wrapInSequence = null, }) => {
1305
+ var _a;
1124
1306
  const location = await (0, exports.resolveCompositionComponentWithFile)({
1125
1307
  remotionRoot,
1126
1308
  compositionFile,
@@ -1134,17 +1316,32 @@ const insertJsxElementIntoComposition = async ({ remotionRoot, compositionFile,
1134
1316
  fileName: location.fileName,
1135
1317
  });
1136
1318
  const ast = (0, parse_ast_1.parseAst)(input);
1137
- const elementToInsert = createInsertableJsxElement({
1138
- addPositionStyleToComponent: wrapInSequence === null,
1319
+ if (element.type === 'composition' &&
1320
+ element.compositionId === compositionId) {
1321
+ throw new Error('Cannot insert a composition into itself');
1322
+ }
1323
+ const sequenceWrapper = element.type === 'composition'
1324
+ ? {
1325
+ dimensions: { width: element.width, height: element.height },
1326
+ durationInFrames: element.durationInFrames,
1327
+ name: element.compositionId,
1328
+ position: element.position,
1329
+ }
1330
+ : wrapInSequence;
1331
+ const elementToInsert = await createInsertableJsxElement({
1332
+ addPositionStyleToComponent: sequenceWrapper === null,
1139
1333
  ast,
1334
+ destinationFileName: location.fileName,
1140
1335
  element,
1336
+ remotionRoot,
1141
1337
  });
1142
- const finalElementToInsert = wrapInSequence
1338
+ const finalElementToInsert = sequenceWrapper
1143
1339
  ? createSequenceWrappedElement({
1144
1340
  child: elementToInsert,
1145
- dimensions: wrapInSequence.dimensions,
1146
- name: wrapInSequence.name,
1147
- position: wrapInSequence.position,
1341
+ dimensions: sequenceWrapper.dimensions,
1342
+ durationInFrames: (_a = sequenceWrapper.durationInFrames) !== null && _a !== void 0 ? _a : null,
1343
+ name: sequenceWrapper.name,
1344
+ position: sequenceWrapper.position,
1148
1345
  sequenceLocalName: ensureSequenceImport(ast),
1149
1346
  })
1150
1347
  : elementToInsert;
@@ -47,6 +47,17 @@ const getCodemodUndoDescription = (codemod) => {
47
47
  entryType: codemod.type,
48
48
  };
49
49
  }
50
+ if (codemod.type === 'move-composition-to-folder') {
51
+ const destination = codemod.folderName === null
52
+ ? 'to root'
53
+ : `into folder "${codemod.parentName ? `${codemod.parentName}/` : ''}${codemod.folderName}"`;
54
+ const label = `composition "${codemod.idToMove}" ${destination}`;
55
+ return {
56
+ undoMessage: `↩️ Move of ${label}`,
57
+ redoMessage: `↪️ Move of ${label}`,
58
+ entryType: codemod.type,
59
+ };
60
+ }
50
61
  if (codemod.type === 'new-composition') {
51
62
  return {
52
63
  undoMessage: `↩️ Creation of composition "${codemod.newId}"`,
@@ -62,6 +73,14 @@ const getCodemodUndoDescription = (codemod) => {
62
73
  entryType: codemod.type,
63
74
  };
64
75
  }
76
+ if (codemod.type === 'new-folder') {
77
+ const label = `folder "${codemod.parentName ? `${codemod.parentName}/` : ''}${codemod.folderName}"`;
78
+ return {
79
+ undoMessage: `↩️ Creation of ${label}`,
80
+ redoMessage: `↪️ Creation of ${label}`,
81
+ entryType: codemod.type,
82
+ };
83
+ }
65
84
  if (codemod.type === 'rename-folder') {
66
85
  const oldName = `${codemod.parentName ? `${codemod.parentName}/` : ''}${codemod.folderName}`;
67
86
  const newName = `${codemod.parentName ? `${codemod.parentName}/` : ''}${codemod.newName}`;
@@ -1,6 +1,10 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  exports.insertJsxElementHandler = void 0;
7
+ const node_path_1 = __importDefault(require("node:path"));
4
8
  const renderer_1 = require("@remotion/renderer");
5
9
  const studio_shared_1 = require("@remotion/studio-shared");
6
10
  const file_watcher_1 = require("../../file-watcher");
@@ -22,7 +26,27 @@ const validatePosition = (position) => {
22
26
  throw new Error('Position must be finite');
23
27
  }
24
28
  };
25
- const validateElement = (element) => {
29
+ const isInsideRemotionRoot = ({ fileName, remotionRoot, }) => {
30
+ const relativePath = node_path_1.default.relative(remotionRoot, fileName);
31
+ return !relativePath.startsWith('..') && !node_path_1.default.isAbsolute(relativePath);
32
+ };
33
+ const hasParentDirectorySegment = (fileName) => {
34
+ return fileName.split('/').includes('..');
35
+ };
36
+ const validateCompositionFile = ({ compositionFile, remotionRoot, }) => {
37
+ if (!compositionFile ||
38
+ compositionFile.includes('\0') ||
39
+ compositionFile.includes('\\') ||
40
+ compositionFile.startsWith('/') ||
41
+ hasParentDirectorySegment(compositionFile)) {
42
+ throw new Error('Unsupported composition file');
43
+ }
44
+ const resolved = node_path_1.default.resolve(remotionRoot, compositionFile);
45
+ if (!isInsideRemotionRoot({ fileName: resolved, remotionRoot })) {
46
+ throw new Error('Unsupported composition file');
47
+ }
48
+ };
49
+ const validateElement = (element, remotionRoot) => {
26
50
  validatePosition(element.position);
27
51
  if (element.type === 'solid') {
28
52
  validateDimension('width', element.width);
@@ -59,6 +83,28 @@ const validateElement = (element) => {
59
83
  }
60
84
  return;
61
85
  }
86
+ if (element.type === 'composition') {
87
+ if (typeof element.compositionId !== 'string' || !element.compositionId) {
88
+ throw new Error('Unsupported composition ID');
89
+ }
90
+ if (typeof element.compositionFile !== 'string') {
91
+ throw new Error('Unsupported composition file');
92
+ }
93
+ validateCompositionFile({
94
+ compositionFile: element.compositionFile,
95
+ remotionRoot,
96
+ });
97
+ validateDimension('width', element.width);
98
+ validateDimension('height', element.height);
99
+ validateDimension('durationInFrames', element.durationInFrames);
100
+ const parsedProps = JSON.parse(element.serializedResolvedPropsWithCustomSchema);
101
+ if (typeof parsedProps !== 'object' ||
102
+ parsedProps === null ||
103
+ Array.isArray(parsedProps)) {
104
+ throw new Error('Resolved composition props must be an object');
105
+ }
106
+ return;
107
+ }
62
108
  throw new Error('Unsupported element type');
63
109
  };
64
110
  const getElementLabel = (element) => {
@@ -85,11 +131,14 @@ const getElementLabel = (element) => {
85
131
  if (element.type === 'component') {
86
132
  return `<${element.componentName}>`;
87
133
  }
134
+ if (element.type === 'composition') {
135
+ return `composition "${element.compositionId}"`;
136
+ }
88
137
  throw new Error('Unsupported element type');
89
138
  };
90
139
  const insertJsxElementHandler = ({ input: { compositionFile, compositionId, element }, remotionRoot, logLevel, }) => (0, source_file_write_queue_1.withSourceFileWriteQueue)(async () => {
91
140
  try {
92
- validateElement(element);
141
+ validateElement(element, remotionRoot);
93
142
  const elementLabel = getElementLabel(element);
94
143
  renderer_1.RenderInternals.Log.trace({ indent: false, logLevel }, `[insert-jsx-element] Received request for compositionFile="${compositionFile}" compositionId="${compositionId}" element="${element.type}"`);
95
144
  const { fileName, source, oldContents, output, formatted, logLine } = await (0, resolve_composition_component_1.insertJsxElementIntoComposition)({
@@ -11,10 +11,12 @@ type ResolvedSequencePropEdit = {
11
11
  defaultValue: unknown | null;
12
12
  defaultValueString: string | null;
13
13
  schema: SaveSequencePropEdit['schema'];
14
+ sourceEdit: SaveSequencePropEdit['sourceEdit'];
14
15
  };
15
- export declare const convertSequencePropEditToCodemodChange: (edit: Pick<ResolvedSequencePropEdit, "defaultValue" | "key" | "nodePath" | "schema" | "value">) => SequencePropsNodeUpdate;
16
+ export declare const convertSequencePropEditToCodemodChange: (edit: Pick<ResolvedSequencePropEdit, "defaultValue" | "key" | "nodePath" | "schema" | "sourceEdit" | "value">) => SequencePropsNodeUpdate;
16
17
  export declare const shouldSuppressHmrForSequencePropEdits: (edits: readonly {
17
18
  key: string;
19
+ sourceEdit?: import("@remotion/studio-shared").SaveSequencePropSourceEdit | null | undefined;
18
20
  }[]) => boolean;
19
21
  export declare const saveSequencePropsHandler: ApiHandler<SaveSequencePropsRequest, SaveSequencePropsResponse>;
20
22
  export {};