@deepnote/convert 2.2.0 → 2.3.1

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.
@@ -27,10 +27,10 @@ let node_path = require("node:path");
27
27
  node_path = __toESM(node_path);
28
28
  let __deepnote_blocks = require("@deepnote/blocks");
29
29
  __deepnote_blocks = __toESM(__deepnote_blocks);
30
- let node_crypto = require("node:crypto");
31
- node_crypto = __toESM(node_crypto);
32
30
  let yaml = require("yaml");
33
31
  yaml = __toESM(yaml);
32
+ let node_crypto = require("node:crypto");
33
+ node_crypto = __toESM(node_crypto);
34
34
 
35
35
  //#region src/utils.ts
36
36
  /**
@@ -671,11 +671,49 @@ function convertBlockToCell(block) {
671
671
  //#region src/format-detection.ts
672
672
  /** Check if file content is Marimo format */
673
673
  function isMarimoContent(content) {
674
- return /^import marimo\b/m.test(content) && /@app\.cell\b/.test(content) && !/^\s*['"]{3}[\s\S]*?import marimo/m.test(content);
674
+ const lines = content.split("\n");
675
+ let inDocstring = false;
676
+ let docstringQuote = "";
677
+ const firstLine = lines.find((line) => {
678
+ const t = line.trim();
679
+ if (t.length === 0) return false;
680
+ if (t.startsWith("#!") || /^#\s*(-\*-\s*)?(coding|encoding)[:=]/i.test(t)) return false;
681
+ for (const q of ["\"\"\"", "'''"]) {
682
+ if (!inDocstring && t.startsWith(q)) {
683
+ if (t.length > 3 && t.endsWith(q)) return false;
684
+ inDocstring = true;
685
+ docstringQuote = q;
686
+ return false;
687
+ }
688
+ if (inDocstring && docstringQuote === q && t.includes(q)) {
689
+ inDocstring = false;
690
+ return false;
691
+ }
692
+ }
693
+ if (inDocstring) return false;
694
+ return true;
695
+ }) ?? "";
696
+ return (/^import\s+marimo\b/.test(firstLine) || /^from\s+marimo\s+import\b/.test(firstLine)) && /@app\.cell\b/.test(content);
675
697
  }
676
698
  /** Check if file content is percent format */
677
699
  function isPercentContent(content) {
678
- return /^# %%/m.test(content) && !/^\s*['"]{3}[\s\S]*?# %%/m.test(content);
700
+ const lines = content.split("\n");
701
+ let inTripleQuote = false;
702
+ let quoteChar = "";
703
+ for (const line of lines) {
704
+ for (const q of ["\"\"\"", "'''"]) {
705
+ let idx = line.indexOf(q, 0);
706
+ while (idx !== -1) {
707
+ if (!inTripleQuote) {
708
+ inTripleQuote = true;
709
+ quoteChar = q;
710
+ } else if (quoteChar === q) inTripleQuote = false;
711
+ idx = line.indexOf(q, idx + 3);
712
+ }
713
+ }
714
+ if (!inTripleQuote && /^# %%/.test(line)) return true;
715
+ }
716
+ return false;
679
717
  }
680
718
  /**
681
719
  * Detects the notebook format from filename and optionally content.
@@ -777,9 +815,14 @@ function convertJupyterNotebooksToDeepnote(notebooks, options) {
777
815
  return deepnoteFile;
778
816
  }
779
817
  /**
780
- * Converts multiple Jupyter Notebook (.ipynb) files into a single Deepnote project file.
818
+ * Reads and converts multiple Jupyter Notebook (.ipynb) files into a DeepnoteFile.
819
+ * This function reads the files and returns the converted DeepnoteFile without writing to disk.
820
+ *
821
+ * @param inputFilePaths - Array of paths to .ipynb files
822
+ * @param options - Conversion options including project name
823
+ * @returns A DeepnoteFile object
781
824
  */
782
- async function convertIpynbFilesToDeepnoteFile(inputFilePaths, options) {
825
+ async function readAndConvertIpynbFiles(inputFilePaths, options) {
783
826
  const notebooks = [];
784
827
  for (const filePath of inputFilePaths) {
785
828
  const notebook = await parseIpynbFile(filePath);
@@ -788,7 +831,13 @@ async function convertIpynbFilesToDeepnoteFile(inputFilePaths, options) {
788
831
  notebook
789
832
  });
790
833
  }
791
- const yamlContent = (0, yaml.stringify)(convertJupyterNotebooksToDeepnote(notebooks, { projectName: options.projectName }));
834
+ return convertJupyterNotebooksToDeepnote(notebooks, { projectName: options.projectName });
835
+ }
836
+ /**
837
+ * Converts multiple Jupyter Notebook (.ipynb) files into a single Deepnote project file.
838
+ */
839
+ async function convertIpynbFilesToDeepnoteFile(inputFilePaths, options) {
840
+ const yamlContent = (0, yaml.stringify)(await readAndConvertIpynbFiles(inputFilePaths, { projectName: options.projectName }));
792
841
  const parentDir = (0, node_path.dirname)(options.outputPath);
793
842
  await node_fs_promises.default.mkdir(parentDir, { recursive: true });
794
843
  await node_fs_promises.default.writeFile(options.outputPath, yamlContent, "utf-8");
@@ -818,7 +867,10 @@ function convertCellToBlock$3(cell, index, idGenerator) {
818
867
  const executionFinishedAt = cell.metadata?.deepnote_execution_finished_at;
819
868
  const blockGroup = cell.metadata?.deepnote_block_group ?? cell.block_group ?? idGenerator();
820
869
  const deepnoteSource = cell.metadata?.deepnote_source;
821
- if (deepnoteSource !== void 0) source = deepnoteSource;
870
+ const isPlainCodeBlock = cell.cell_type === "code" && (!deepnoteCellType || deepnoteCellType === "code");
871
+ if (deepnoteSource !== void 0) if (isPlainCodeBlock) {
872
+ if (deepnoteSource === source) source = deepnoteSource;
873
+ } else source = deepnoteSource;
822
874
  const blockType = deepnoteCellType ?? (cell.cell_type === "code" ? "code" : "markdown");
823
875
  const originalMetadata = { ...cell.metadata };
824
876
  delete originalMetadata.cell_id;
@@ -832,7 +884,7 @@ function convertCellToBlock$3(cell, index, idGenerator) {
832
884
  delete cell.block_group;
833
885
  const executionCount = cell.execution_count ?? void 0;
834
886
  const hasExecutionCount = executionCount !== void 0;
835
- const hasOutputs = cell.cell_type === "code" && cell.outputs !== void 0;
887
+ const hasOutputs$1 = cell.cell_type === "code" && cell.outputs !== void 0;
836
888
  return sortKeysAlphabetically(__deepnote_blocks.deepnoteBlockSchema.parse({
837
889
  blockGroup,
838
890
  content: source,
@@ -842,15 +894,444 @@ function convertCellToBlock$3(cell, index, idGenerator) {
842
894
  ...executionStartedAt ? { executionStartedAt } : {},
843
895
  id: cellId ?? idGenerator(),
844
896
  metadata: originalMetadata,
845
- ...hasOutputs ? { outputs: cell.outputs } : {},
897
+ ...hasOutputs$1 ? { outputs: cell.outputs } : {},
846
898
  sortingKey: sortingKey ?? createSortingKey(index),
847
899
  type: blockType
848
900
  }));
849
901
  }
850
902
 
903
+ //#endregion
904
+ //#region src/snapshot/hash.ts
905
+ /**
906
+ * Computes a SHA-256 hash of the given content.
907
+ *
908
+ * @param content - The content to hash
909
+ * @returns Hash string in format 'sha256:{hex}'
910
+ */
911
+ function computeContentHash(content) {
912
+ return `sha256:${(0, node_crypto.createHash)("sha256").update(content, "utf-8").digest("hex")}`;
913
+ }
914
+ /**
915
+ * Computes a snapshot hash from the file's key properties.
916
+ * The hash is based on: version, environment.hash, integrations, and all block contentHashes.
917
+ *
918
+ * @param file - The DeepnoteFile to compute hash for
919
+ * @returns Hash string in format 'sha256:{hex}'
920
+ */
921
+ function computeSnapshotHash(file) {
922
+ const parts = [];
923
+ parts.push(`version:${file.version}`);
924
+ if (file.environment?.hash) parts.push(`env:${file.environment.hash}`);
925
+ const sortedIntegrations = [...file.project.integrations ?? []].sort((a, b) => a.id.localeCompare(b.id));
926
+ for (const integration of sortedIntegrations) parts.push(`integration:${integration.id}:${integration.type}`);
927
+ for (const notebook of file.project.notebooks) for (const block of notebook.blocks) if (block.contentHash) parts.push(`block:${block.id}:${block.contentHash}`);
928
+ const combined = parts.join("\n");
929
+ return `sha256:${(0, node_crypto.createHash)("sha256").update(combined, "utf-8").digest("hex")}`;
930
+ }
931
+ /**
932
+ * Adds content hashes to all blocks in a DeepnoteFile that don't already have them.
933
+ * Returns a new DeepnoteFile with hashes added (does not mutate the input).
934
+ *
935
+ * @param file - The DeepnoteFile to add hashes to
936
+ * @returns A new DeepnoteFile with content hashes added
937
+ */
938
+ function addContentHashes(file) {
939
+ const result = structuredClone(file);
940
+ for (const notebook of result.project.notebooks) for (const block of notebook.blocks) if (!block.contentHash && block.content) block.contentHash = computeContentHash(block.content);
941
+ return result;
942
+ }
943
+
944
+ //#endregion
945
+ //#region src/snapshot/lookup.ts
946
+ /** Default directory name for snapshots */
947
+ const DEFAULT_SNAPSHOT_DIR = "snapshots";
948
+ /** Regex pattern for snapshot filenames */
949
+ const SNAPSHOT_FILENAME_PATTERN = /^(.+)_([0-9a-f-]{36})_(latest|[\dT:-]+)\.snapshot\.deepnote$/;
950
+ /**
951
+ * Parses a snapshot filename into its components.
952
+ *
953
+ * @param filename - The snapshot filename to parse
954
+ * @returns Parsed components or null if filename doesn't match pattern
955
+ */
956
+ function parseSnapshotFilename(filename) {
957
+ const match = SNAPSHOT_FILENAME_PATTERN.exec(filename);
958
+ if (!match) return null;
959
+ return {
960
+ slug: match[1],
961
+ projectId: match[2],
962
+ timestamp: match[3]
963
+ };
964
+ }
965
+ /**
966
+ * Finds all snapshot files for a given project.
967
+ *
968
+ * @param projectDir - Directory containing the .deepnote file
969
+ * @param projectId - The project UUID to search for
970
+ * @param options - Snapshot options
971
+ * @returns Array of SnapshotInfo objects, sorted by timestamp (newest first)
972
+ */
973
+ async function findSnapshotsForProject(projectDir, projectId, options = {}) {
974
+ const snapshotsPath = (0, node_path.resolve)(projectDir, options.snapshotDir ?? DEFAULT_SNAPSHOT_DIR);
975
+ try {
976
+ const entries = await node_fs_promises.default.readdir(snapshotsPath, { withFileTypes: true });
977
+ const snapshots = [];
978
+ for (const entry of entries) {
979
+ if (!entry.isFile() || !entry.name.endsWith(".snapshot.deepnote")) continue;
980
+ const parsed = parseSnapshotFilename(entry.name);
981
+ if (parsed && parsed.projectId === projectId) snapshots.push({
982
+ path: (0, node_path.join)(snapshotsPath, entry.name),
983
+ slug: parsed.slug,
984
+ projectId: parsed.projectId,
985
+ timestamp: parsed.timestamp
986
+ });
987
+ }
988
+ snapshots.sort((a, b) => {
989
+ if (a.timestamp === "latest") return -1;
990
+ if (b.timestamp === "latest") return 1;
991
+ return b.timestamp.localeCompare(a.timestamp);
992
+ });
993
+ return snapshots;
994
+ } catch (err) {
995
+ if (err.code === "ENOENT") return [];
996
+ throw err;
997
+ }
998
+ }
999
+ /**
1000
+ * Loads the latest snapshot for a project.
1001
+ *
1002
+ * @param sourceFilePath - Path to the source .deepnote file
1003
+ * @param projectId - The project UUID
1004
+ * @param options - Snapshot options
1005
+ * @returns The parsed DeepnoteSnapshot or null if not found
1006
+ */
1007
+ async function loadLatestSnapshot(sourceFilePath, projectId, options = {}) {
1008
+ const snapshots = await findSnapshotsForProject((0, node_path.dirname)(sourceFilePath), projectId, options);
1009
+ if (snapshots.length === 0) return null;
1010
+ const snapshotInfo = snapshots[0];
1011
+ return loadSnapshotFile(snapshotInfo.path);
1012
+ }
1013
+ /**
1014
+ * Loads and parses a snapshot file.
1015
+ *
1016
+ * @param snapshotPath - Path to the snapshot file
1017
+ * @returns The parsed DeepnoteSnapshot
1018
+ */
1019
+ async function loadSnapshotFile(snapshotPath) {
1020
+ const parsed = (0, yaml.parse)(await node_fs_promises.default.readFile(snapshotPath, "utf-8"));
1021
+ return __deepnote_blocks.deepnoteSnapshotSchema.parse(parsed);
1022
+ }
1023
+ /**
1024
+ * Gets the snapshot directory path for a source file.
1025
+ *
1026
+ * @param sourceFilePath - Path to the source .deepnote file
1027
+ * @param options - Snapshot options
1028
+ * @returns The snapshot directory path
1029
+ */
1030
+ function getSnapshotDir(sourceFilePath, options = {}) {
1031
+ const snapshotDir = options.snapshotDir ?? DEFAULT_SNAPSHOT_DIR;
1032
+ return (0, node_path.resolve)((0, node_path.dirname)(sourceFilePath), snapshotDir);
1033
+ }
1034
+ /**
1035
+ * Checks if a snapshot exists for a project.
1036
+ *
1037
+ * @param sourceFilePath - Path to the source .deepnote file
1038
+ * @param projectId - The project UUID
1039
+ * @param options - Snapshot options
1040
+ * @returns True if at least one snapshot exists
1041
+ */
1042
+ async function snapshotExists(sourceFilePath, projectId, options = {}) {
1043
+ return (await findSnapshotsForProject((0, node_path.dirname)(sourceFilePath), projectId, options)).length > 0;
1044
+ }
1045
+ /**
1046
+ * Extracts project information from a source file path.
1047
+ *
1048
+ * @param sourceFilePath - Path to the source .deepnote file
1049
+ * @returns Object with directory and filename without extension
1050
+ */
1051
+ function parseSourceFilePath(sourceFilePath) {
1052
+ return {
1053
+ dir: (0, node_path.dirname)(sourceFilePath),
1054
+ name: (0, node_path.basename)(sourceFilePath).replace(/\.deepnote$/, "")
1055
+ };
1056
+ }
1057
+
1058
+ //#endregion
1059
+ //#region src/snapshot/marimo-outputs.ts
1060
+ /**
1061
+ * Finds the Marimo session cache file for a given .py file.
1062
+ * Looks in __marimo__/session/{filename}.json relative to the file's directory.
1063
+ *
1064
+ * @param marimoFilePath - Path to the .py Marimo file
1065
+ * @returns Path to the session cache file, or null if not found
1066
+ */
1067
+ async function findMarimoSessionCache(marimoFilePath) {
1068
+ const sessionPath = (0, node_path.join)((0, node_path.dirname)(marimoFilePath), "__marimo__", "session", `${(0, node_path.basename)(marimoFilePath)}.json`);
1069
+ try {
1070
+ await node_fs_promises.default.access(sessionPath);
1071
+ return sessionPath;
1072
+ } catch {
1073
+ return null;
1074
+ }
1075
+ }
1076
+ /**
1077
+ * Reads and parses a Marimo session cache file.
1078
+ *
1079
+ * @param sessionPath - Path to the session cache JSON file
1080
+ * @returns The parsed session cache, or null if reading fails
1081
+ */
1082
+ async function readMarimoSessionCache(sessionPath) {
1083
+ try {
1084
+ const content = await node_fs_promises.default.readFile(sessionPath, "utf-8");
1085
+ return JSON.parse(content);
1086
+ } catch (_error) {
1087
+ return null;
1088
+ }
1089
+ }
1090
+ /**
1091
+ * Converts a Marimo session output to Jupyter/Deepnote output format.
1092
+ *
1093
+ * @param output - The Marimo session output
1094
+ * @returns A Jupyter-compatible output object
1095
+ */
1096
+ function convertMarimoOutputToJupyter(output) {
1097
+ if (output.type === "error") return {
1098
+ output_type: "error",
1099
+ ename: output.ename || "Error",
1100
+ evalue: output.evalue || "",
1101
+ traceback: output.traceback || []
1102
+ };
1103
+ if (output.data) return {
1104
+ output_type: "execute_result",
1105
+ data: output.data,
1106
+ metadata: {},
1107
+ execution_count: null
1108
+ };
1109
+ return {
1110
+ output_type: "execute_result",
1111
+ data: { "text/plain": "" },
1112
+ metadata: {},
1113
+ execution_count: null
1114
+ };
1115
+ }
1116
+ /**
1117
+ * Converts Marimo console outputs to Jupyter stream outputs.
1118
+ *
1119
+ * @param consoleOutputs - Array of Marimo console outputs
1120
+ * @returns Array of Jupyter stream outputs
1121
+ */
1122
+ function convertMarimoConsoleToJupyter(consoleOutputs) {
1123
+ const outputs = [];
1124
+ for (const consoleOutput of consoleOutputs) outputs.push({
1125
+ output_type: "stream",
1126
+ name: consoleOutput.channel,
1127
+ text: consoleOutput.data
1128
+ });
1129
+ return outputs;
1130
+ }
1131
+ /**
1132
+ * Converts a full Marimo session cell to Jupyter outputs.
1133
+ *
1134
+ * @param cell - The Marimo session cell
1135
+ * @returns Array of Jupyter-compatible outputs
1136
+ */
1137
+ function convertMarimoSessionCellToOutputs(cell) {
1138
+ const outputs = [];
1139
+ outputs.push(...convertMarimoConsoleToJupyter(cell.console ?? []));
1140
+ for (const output of cell.outputs) outputs.push(convertMarimoOutputToJupyter(output));
1141
+ return outputs;
1142
+ }
1143
+ /**
1144
+ * Gets outputs from the Marimo session cache file.
1145
+ * This is the preferred method as it doesn't require the marimo CLI.
1146
+ * Outputs are keyed by code_hash for reliable matching even when cells are reordered.
1147
+ *
1148
+ * @param marimoFilePath - Path to the .py Marimo file
1149
+ * @returns Map of code_hash to outputs, or null if session cache is not found
1150
+ */
1151
+ async function getMarimoOutputsFromCache(marimoFilePath) {
1152
+ const sessionPath = await findMarimoSessionCache(marimoFilePath);
1153
+ if (!sessionPath) return null;
1154
+ const cache = await readMarimoSessionCache(sessionPath);
1155
+ if (!cache) return null;
1156
+ const outputMap = /* @__PURE__ */ new Map();
1157
+ for (const cell of cache.cells) {
1158
+ const outputs = convertMarimoSessionCellToOutputs(cell);
1159
+ if (outputs.length > 0) {
1160
+ const nonEmptyOutputs = outputs.filter((o) => {
1161
+ if (o.output_type === "execute_result" && o.data) {
1162
+ const data = o.data;
1163
+ return Object.values(data).some((v) => {
1164
+ if (typeof v === "string") return v.trim() !== "";
1165
+ return v != null;
1166
+ });
1167
+ }
1168
+ return true;
1169
+ });
1170
+ if (nonEmptyOutputs.length > 0) outputMap.set(cell.code_hash, nonEmptyOutputs);
1171
+ }
1172
+ }
1173
+ return outputMap;
1174
+ }
1175
+
1176
+ //#endregion
1177
+ //#region src/snapshot/merge.ts
1178
+ /**
1179
+ * Merges outputs from a snapshot into a source file.
1180
+ * Returns a new DeepnoteFile with outputs added from the snapshot.
1181
+ *
1182
+ * @param source - The source DeepnoteFile (without outputs)
1183
+ * @param snapshot - The snapshot containing outputs
1184
+ * @param options - Merge options
1185
+ * @returns A new DeepnoteFile with outputs merged in
1186
+ */
1187
+ function mergeSnapshotIntoSource(source, snapshot, options = {}) {
1188
+ const { skipMismatched = false } = options;
1189
+ const outputMap = /* @__PURE__ */ new Map();
1190
+ for (const notebook of snapshot.project.notebooks) for (const block of notebook.blocks) {
1191
+ const execBlock = block;
1192
+ if (execBlock.outputs && execBlock.outputs.length > 0) outputMap.set(block.id, {
1193
+ contentHash: block.contentHash,
1194
+ executionCount: execBlock.executionCount,
1195
+ executionStartedAt: execBlock.executionStartedAt,
1196
+ executionFinishedAt: execBlock.executionFinishedAt,
1197
+ outputs: execBlock.outputs
1198
+ });
1199
+ }
1200
+ return {
1201
+ ...source,
1202
+ environment: snapshot.environment ?? source.environment,
1203
+ execution: snapshot.execution ?? source.execution,
1204
+ project: {
1205
+ ...source.project,
1206
+ notebooks: source.project.notebooks.map((notebook) => ({
1207
+ ...notebook,
1208
+ blocks: notebook.blocks.map((block) => {
1209
+ const snapshotData = outputMap.get(block.id);
1210
+ if (!snapshotData) return block;
1211
+ if (skipMismatched && snapshotData.contentHash && block.contentHash) {
1212
+ if (snapshotData.contentHash !== block.contentHash) return block;
1213
+ }
1214
+ return {
1215
+ ...block,
1216
+ ...snapshotData.executionCount !== void 0 ? { executionCount: snapshotData.executionCount } : {},
1217
+ ...snapshotData.executionStartedAt ? { executionStartedAt: snapshotData.executionStartedAt } : {},
1218
+ ...snapshotData.executionFinishedAt ? { executionFinishedAt: snapshotData.executionFinishedAt } : {},
1219
+ ...snapshotData.outputs ? { outputs: snapshotData.outputs } : {}
1220
+ };
1221
+ })
1222
+ }))
1223
+ }
1224
+ };
1225
+ }
1226
+ /**
1227
+ * Counts blocks with outputs in a file.
1228
+ *
1229
+ * @param file - The DeepnoteFile to count outputs in
1230
+ * @returns Number of blocks that have outputs
1231
+ */
1232
+ function countBlocksWithOutputs(file) {
1233
+ let count = 0;
1234
+ for (const notebook of file.project.notebooks) for (const block of notebook.blocks) {
1235
+ const execBlock = block;
1236
+ if (execBlock.outputs && execBlock.outputs.length > 0) count++;
1237
+ }
1238
+ return count;
1239
+ }
1240
+
1241
+ //#endregion
1242
+ //#region src/snapshot/split.ts
1243
+ /**
1244
+ * Creates a slug from a project name.
1245
+ * Normalizes accented characters to ASCII equivalents (e.g., é → e),
1246
+ * converts to lowercase, replaces spaces and special chars with hyphens,
1247
+ * removes consecutive hyphens, and trims leading/trailing hyphens.
1248
+ *
1249
+ * @param name - The project name to slugify
1250
+ * @returns A URL-safe slug
1251
+ */
1252
+ function slugifyProjectName(name) {
1253
+ return name.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
1254
+ }
1255
+ /**
1256
+ * Generates a snapshot filename from project info.
1257
+ *
1258
+ * @param slug - The project name slug
1259
+ * @param projectId - The project UUID
1260
+ * @param timestamp - Timestamp string or 'latest'
1261
+ * @returns Filename in format '{slug}_{projectId}_{timestamp}.snapshot.deepnote'
1262
+ */
1263
+ function generateSnapshotFilename(slug, projectId, timestamp = "latest") {
1264
+ return `${slug}_${projectId}_${timestamp}.snapshot.deepnote`;
1265
+ }
1266
+ /**
1267
+ * Removes output-related fields from a block, returning a clean source block.
1268
+ */
1269
+ function stripOutputsFromBlock(block) {
1270
+ if (!(0, __deepnote_blocks.isExecutableBlockType)(block.type)) return block;
1271
+ const { executionCount: _executionCount, executionStartedAt: _executionStartedAt, executionFinishedAt: _executionFinishedAt, outputs: _outputs,...rest } = block;
1272
+ return rest;
1273
+ }
1274
+ /**
1275
+ * Splits a DeepnoteFile into a source file (no outputs) and a snapshot file (outputs only).
1276
+ *
1277
+ * @param file - The complete DeepnoteFile with outputs
1278
+ * @returns Object containing source and snapshot files
1279
+ */
1280
+ function splitDeepnoteFile(file) {
1281
+ const fileWithHashes = addContentHashes(file);
1282
+ const snapshotHash = computeSnapshotHash(fileWithHashes);
1283
+ const { snapshotHash: _snapshotHash,...sourceMetadata } = fileWithHashes.metadata ?? {};
1284
+ return {
1285
+ source: {
1286
+ ...fileWithHashes,
1287
+ metadata: sourceMetadata,
1288
+ project: {
1289
+ ...fileWithHashes.project,
1290
+ notebooks: fileWithHashes.project.notebooks.map((notebook) => ({
1291
+ ...notebook,
1292
+ blocks: notebook.blocks.map(stripOutputsFromBlock)
1293
+ }))
1294
+ }
1295
+ },
1296
+ snapshot: {
1297
+ ...fileWithHashes,
1298
+ environment: fileWithHashes.environment ?? {},
1299
+ execution: fileWithHashes.execution ?? {},
1300
+ metadata: {
1301
+ ...fileWithHashes.metadata,
1302
+ snapshotHash
1303
+ }
1304
+ }
1305
+ };
1306
+ }
1307
+ /**
1308
+ * Checks if a DeepnoteFile has any outputs.
1309
+ *
1310
+ * @param file - The DeepnoteFile to check
1311
+ * @returns True if any block has outputs
1312
+ */
1313
+ function hasOutputs(file) {
1314
+ for (const notebook of file.project.notebooks) for (const block of notebook.blocks) {
1315
+ if (!(0, __deepnote_blocks.isExecutableBlockType)(block.type)) continue;
1316
+ const execBlock = block;
1317
+ if (execBlock.outputs && execBlock.outputs.length > 0) return true;
1318
+ }
1319
+ return false;
1320
+ }
1321
+
851
1322
  //#endregion
852
1323
  //#region src/marimo-to-deepnote.ts
853
1324
  /**
1325
+ * Computes a code hash for a cell's content.
1326
+ * This matches how Marimo computes code_hash for the session cache.
1327
+ *
1328
+ * @param content - The cell's code content
1329
+ * @returns An MD5 hash string (32 hex chars)
1330
+ */
1331
+ function computeCodeHash(content) {
1332
+ return (0, node_crypto.createHash)("md5").update(content, "utf-8").digest("hex");
1333
+ }
1334
+ /**
854
1335
  * Splits a string on commas that are at the top level (not inside parentheses, brackets, braces, or string literals).
855
1336
  * This handles cases like "func(a, b), other" and 'return "a,b", x' correctly.
856
1337
  * Supports single quotes, double quotes, and backticks, with proper escape handling.
@@ -921,13 +1402,26 @@ function parseMarimoFormat(content) {
921
1402
  const generatedWith = /__generated_with\s*=\s*["']([^"']+)["']/.exec(content)?.[1];
922
1403
  const width = /(?:marimo|mo)\.App\([^)]*width\s*=\s*["']([^"']+)["']/.exec(content)?.[1];
923
1404
  const title = /(?:marimo|mo)\.App\([^)]*title\s*=\s*["']([^"']+)["']/.exec(content)?.[1];
924
- const cellRegex = /@app\.cell(?:\(([^)]*)\))?\s*\n\s*def\s+(\w+)\s*\(([^)]*)\)\s*(?:->.*?)?\s*:\s*\n([\s\S]*?)(?=@app\.cell|if\s+__name__|$)/g;
1405
+ const cellRegex = /@app\.(cell|function)(?:\(([^)]*)\))?\s*\n\s*def\s+(\w+)\s*\(([^)]*)\)\s*(?:->.*?)?\s*:\s*\n([\s\S]*?)(?=@app\.cell|@app\.function|if\s+__name__|$)/g;
925
1406
  let match = cellRegex.exec(content);
926
1407
  while (match !== null) {
927
- const decoratorArgs = match[1] || "";
928
- const functionName = match[2];
929
- const params = match[3].trim();
930
- let body = match[4];
1408
+ const decoratorType = match[1];
1409
+ const decoratorArgs = match[2] || "";
1410
+ const functionName = match[3];
1411
+ const params = match[4].trim();
1412
+ let body = match[5];
1413
+ if (decoratorType === "function") {
1414
+ const funcDef = `def ${functionName}(${params})${/@app\.function(?:\([^)]*\))?\s*\n\s*def\s+\w+\s*\([^)]*\)\s*(->.*?)?\s*:/.exec(content.slice(match.index))?.[1] || ""}:\n${body}`;
1415
+ cells.push({
1416
+ cellType: "code",
1417
+ content: funcDef.trim(),
1418
+ functionName,
1419
+ hidden: /hide_code\s*=\s*True/.test(decoratorArgs),
1420
+ disabled: /disabled\s*=\s*True/.test(decoratorArgs)
1421
+ });
1422
+ match = cellRegex.exec(content);
1423
+ continue;
1424
+ }
931
1425
  const dependencies = params ? params.split(",").map((p) => p.trim()).filter((p) => p.length > 0) : void 0;
932
1426
  const hidden = /hide_code\s*=\s*True/.test(decoratorArgs);
933
1427
  const disabled = /disabled\s*=\s*True/.test(decoratorArgs);
@@ -1015,41 +1509,70 @@ function parseMarimoFormat(content) {
1015
1509
  * This is the lowest-level conversion function.
1016
1510
  *
1017
1511
  * @param app - The Marimo app object to convert
1018
- * @param options - Optional conversion options including custom ID generator
1512
+ * @param options - Optional conversion options including custom ID generator and outputs
1019
1513
  * @returns Array of DeepnoteBlock objects
1020
1514
  */
1021
1515
  function convertMarimoAppToBlocks(app, options) {
1022
1516
  const idGenerator = options?.idGenerator ?? node_crypto.randomUUID;
1023
- return app.cells.map((cell, index) => convertCellToBlock$2(cell, index, idGenerator));
1517
+ const outputs = options?.outputs;
1518
+ return app.cells.map((cell, index) => {
1519
+ const codeHash = computeCodeHash(cell.content);
1520
+ const cellOutputs = outputs?.get(codeHash);
1521
+ return convertCellToBlock$2(cell, index, idGenerator, cellOutputs);
1522
+ });
1024
1523
  }
1025
1524
  /**
1026
- * Converts Marimo app objects into a Deepnote project file.
1027
- * This is a pure conversion function that doesn't perform any file I/O.
1028
- *
1029
- * @param apps - Array of Marimo apps with filenames
1030
- * @param options - Conversion options including project name and optional ID generator
1031
- * @returns A DeepnoteFile object
1525
+ * Creates a base DeepnoteFile structure with empty notebooks.
1526
+ * This is a helper to reduce duplication in conversion functions.
1032
1527
  */
1033
- function convertMarimoAppsToDeepnote(apps, options) {
1034
- const idGenerator = options.idGenerator ?? node_crypto.randomUUID;
1035
- const firstNotebookId = apps.length > 0 ? idGenerator() : void 0;
1036
- const deepnoteFile = {
1528
+ function createDeepnoteFileSkeleton(projectName, idGenerator, firstNotebookId) {
1529
+ return {
1037
1530
  metadata: { createdAt: (/* @__PURE__ */ new Date()).toISOString() },
1038
1531
  project: {
1039
1532
  id: idGenerator(),
1040
1533
  initNotebookId: firstNotebookId,
1041
1534
  integrations: [],
1042
- name: options.projectName,
1535
+ name: projectName,
1043
1536
  notebooks: [],
1044
1537
  settings: {}
1045
1538
  },
1046
1539
  version: "1.0.0"
1047
1540
  };
1541
+ }
1542
+ /**
1543
+ * Converts Marimo app objects into a Deepnote project file.
1544
+ * This is a pure conversion function that doesn't perform any file I/O.
1545
+ *
1546
+ * @param apps - Array of Marimo apps with filenames
1547
+ * @param options - Conversion options including project name and optional ID generator
1548
+ * @returns A DeepnoteFile object
1549
+ */
1550
+ function convertMarimoAppsToDeepnote(apps, options) {
1551
+ return convertMarimoAppsToDeepnoteFile(apps.map((app) => ({
1552
+ ...app,
1553
+ outputs: void 0
1554
+ })), options);
1555
+ }
1556
+ /**
1557
+ * Converts Marimo app objects with outputs into a Deepnote project file.
1558
+ * This variant includes outputs from the Marimo session cache.
1559
+ *
1560
+ * @param apps - Array of Marimo apps with filenames and optional outputs
1561
+ * @param options - Conversion options including project name and optional ID generator
1562
+ * @returns A DeepnoteFile object
1563
+ */
1564
+ function convertMarimoAppsToDeepnoteFile(apps, options) {
1565
+ const idGenerator = options.idGenerator ?? node_crypto.randomUUID;
1566
+ const firstNotebookId = apps.length > 0 ? idGenerator() : void 0;
1567
+ const deepnoteFile = createDeepnoteFileSkeleton(options.projectName, idGenerator, firstNotebookId);
1048
1568
  for (let i = 0; i < apps.length; i++) {
1049
- const { filename, app } = apps[i];
1569
+ const { filename, app, outputs } = apps[i];
1050
1570
  const filenameWithoutExt = (0, node_path.basename)(filename, (0, node_path.extname)(filename)) || "Untitled notebook";
1051
1571
  const notebookName = app.title || filenameWithoutExt;
1052
- const blocks = convertMarimoAppToBlocks(app, { idGenerator });
1572
+ const blocks = convertMarimoAppToBlocks(app, {
1573
+ idGenerator,
1574
+ outputs
1575
+ });
1053
1576
  const notebookId = i === 0 && firstNotebookId ? firstNotebookId : idGenerator();
1054
1577
  deepnoteFile.project.notebooks.push({
1055
1578
  blocks,
@@ -1062,15 +1585,45 @@ function convertMarimoAppsToDeepnote(apps, options) {
1062
1585
  return deepnoteFile;
1063
1586
  }
1064
1587
  /**
1588
+ * Reads and converts multiple Marimo (.py) files into a DeepnoteFile.
1589
+ * This function reads the files and returns the converted DeepnoteFile without writing to disk.
1590
+ *
1591
+ * @param inputFilePaths - Array of paths to Marimo .py files
1592
+ * @param options - Conversion options including project name
1593
+ * @returns A DeepnoteFile object
1594
+ */
1595
+ async function readAndConvertMarimoFiles(inputFilePaths, options) {
1596
+ const apps = [];
1597
+ for (const filePath of inputFilePaths) try {
1598
+ const app = parseMarimoFormat(await node_fs_promises.default.readFile(filePath, "utf-8"));
1599
+ const outputs = await getMarimoOutputsFromCache(filePath);
1600
+ apps.push({
1601
+ filename: (0, node_path.basename)(filePath),
1602
+ app,
1603
+ outputs: outputs ?? void 0
1604
+ });
1605
+ } catch (err) {
1606
+ const errorMessage = err instanceof Error ? err.message : String(err);
1607
+ const errorStack = err instanceof Error ? err.stack : void 0;
1608
+ throw new Error(`Failed to read or parse file ${(0, node_path.basename)(filePath)}: ${errorMessage}`, { cause: errorStack ? {
1609
+ originalError: err,
1610
+ stack: errorStack
1611
+ } : err });
1612
+ }
1613
+ return convertMarimoAppsToDeepnoteFile(apps, { projectName: options.projectName });
1614
+ }
1615
+ /**
1065
1616
  * Converts multiple Marimo (.py) files into a single Deepnote project file.
1066
1617
  */
1067
1618
  async function convertMarimoFilesToDeepnoteFile(inputFilePaths, options) {
1068
1619
  const apps = [];
1069
1620
  for (const filePath of inputFilePaths) try {
1070
1621
  const app = parseMarimoFormat(await node_fs_promises.default.readFile(filePath, "utf-8"));
1622
+ const outputs = await getMarimoOutputsFromCache(filePath);
1071
1623
  apps.push({
1072
1624
  filename: (0, node_path.basename)(filePath),
1073
- app
1625
+ app,
1626
+ outputs: outputs ?? void 0
1074
1627
  });
1075
1628
  } catch (err) {
1076
1629
  const errorMessage = err instanceof Error ? err.message : String(err);
@@ -1080,12 +1633,12 @@ async function convertMarimoFilesToDeepnoteFile(inputFilePaths, options) {
1080
1633
  stack: errorStack
1081
1634
  } : err });
1082
1635
  }
1083
- const yamlContent = (0, yaml.stringify)(convertMarimoAppsToDeepnote(apps, { projectName: options.projectName }));
1636
+ const yamlContent = (0, yaml.stringify)(convertMarimoAppsToDeepnoteFile(apps, { projectName: options.projectName }));
1084
1637
  const parentDir = (0, node_path.dirname)(options.outputPath);
1085
1638
  await node_fs_promises.default.mkdir(parentDir, { recursive: true });
1086
1639
  await node_fs_promises.default.writeFile(options.outputPath, yamlContent, "utf-8");
1087
1640
  }
1088
- function convertCellToBlock$2(cell, index, idGenerator) {
1641
+ function convertCellToBlock$2(cell, index, idGenerator, outputs) {
1089
1642
  let blockType;
1090
1643
  if (cell.cellType === "markdown") blockType = "markdown";
1091
1644
  else if (cell.cellType === "sql") blockType = "sql";
@@ -1103,7 +1656,8 @@ function convertCellToBlock$2(cell, index, idGenerator) {
1103
1656
  id: idGenerator(),
1104
1657
  metadata: Object.keys(metadata).length > 0 ? metadata : {},
1105
1658
  sortingKey: createSortingKey(index),
1106
- type: blockType
1659
+ type: blockType,
1660
+ ...outputs && outputs.length > 0 && (blockType === "code" || blockType === "sql") ? { outputs } : {}
1107
1661
  };
1108
1662
  }
1109
1663
 
@@ -1230,9 +1784,14 @@ function convertPercentNotebooksToDeepnote(notebooks, options) {
1230
1784
  return deepnoteFile;
1231
1785
  }
1232
1786
  /**
1233
- * Converts multiple percent format (.py) files into a single Deepnote project file.
1787
+ * Reads and converts multiple percent format (.py) files into a DeepnoteFile.
1788
+ * This function reads the files and returns the converted DeepnoteFile without writing to disk.
1789
+ *
1790
+ * @param inputFilePaths - Array of paths to percent format .py files
1791
+ * @param options - Conversion options including project name
1792
+ * @returns A DeepnoteFile object
1234
1793
  */
1235
- async function convertPercentFilesToDeepnoteFile(inputFilePaths, options) {
1794
+ async function readAndConvertPercentFiles(inputFilePaths, options) {
1236
1795
  const notebooks = [];
1237
1796
  for (const filePath of inputFilePaths) {
1238
1797
  const notebook = parsePercentFormat(await node_fs_promises.default.readFile(filePath, "utf-8"));
@@ -1241,7 +1800,13 @@ async function convertPercentFilesToDeepnoteFile(inputFilePaths, options) {
1241
1800
  notebook
1242
1801
  });
1243
1802
  }
1244
- const yamlContent = (0, yaml.stringify)(convertPercentNotebooksToDeepnote(notebooks, { projectName: options.projectName }));
1803
+ return convertPercentNotebooksToDeepnote(notebooks, { projectName: options.projectName });
1804
+ }
1805
+ /**
1806
+ * Converts multiple percent format (.py) files into a single Deepnote project file.
1807
+ */
1808
+ async function convertPercentFilesToDeepnoteFile(inputFilePaths, options) {
1809
+ const yamlContent = (0, yaml.stringify)(await readAndConvertPercentFiles(inputFilePaths, { projectName: options.projectName }));
1245
1810
  const parentDir = (0, node_path.dirname)(options.outputPath);
1246
1811
  await node_fs_promises.default.mkdir(parentDir, { recursive: true });
1247
1812
  await node_fs_promises.default.writeFile(options.outputPath, yamlContent, "utf-8");
@@ -1492,9 +2057,14 @@ function convertQuartoDocumentsToDeepnote(documents, options) {
1492
2057
  return deepnoteFile;
1493
2058
  }
1494
2059
  /**
1495
- * Converts multiple Quarto (.qmd) files into a single Deepnote project file.
2060
+ * Reads and converts multiple Quarto (.qmd) files into a DeepnoteFile.
2061
+ * This function reads the files and returns the converted DeepnoteFile without writing to disk.
2062
+ *
2063
+ * @param inputFilePaths - Array of paths to .qmd files
2064
+ * @param options - Conversion options including project name
2065
+ * @returns A DeepnoteFile object
1496
2066
  */
1497
- async function convertQuartoFilesToDeepnoteFile(inputFilePaths, options) {
2067
+ async function readAndConvertQuartoFiles(inputFilePaths, options) {
1498
2068
  const documents = [];
1499
2069
  for (const filePath of inputFilePaths) {
1500
2070
  const document = parseQuartoFormat(await node_fs_promises.default.readFile(filePath, "utf-8"));
@@ -1503,7 +2073,13 @@ async function convertQuartoFilesToDeepnoteFile(inputFilePaths, options) {
1503
2073
  document
1504
2074
  });
1505
2075
  }
1506
- const yamlContent = (0, yaml.stringify)(convertQuartoDocumentsToDeepnote(documents, { projectName: options.projectName }));
2076
+ return convertQuartoDocumentsToDeepnote(documents, { projectName: options.projectName });
2077
+ }
2078
+ /**
2079
+ * Converts multiple Quarto (.qmd) files into a single Deepnote project file.
2080
+ */
2081
+ async function convertQuartoFilesToDeepnoteFile(inputFilePaths, options) {
2082
+ const yamlContent = (0, yaml.stringify)(await readAndConvertQuartoFiles(inputFilePaths, { projectName: options.projectName }));
1507
2083
  const parentDir = (0, node_path.dirname)(options.outputPath);
1508
2084
  await node_fs_promises.default.mkdir(parentDir, { recursive: true });
1509
2085
  await node_fs_promises.default.writeFile(options.outputPath, yamlContent, "utf-8");
@@ -1529,6 +2105,43 @@ function convertCellToBlock(cell, index, idGenerator) {
1529
2105
  };
1530
2106
  }
1531
2107
 
2108
+ //#endregion
2109
+ //#region src/write-deepnote-file.ts
2110
+ /**
2111
+ * Writes a DeepnoteFile to disk, optionally splitting outputs into a snapshot file.
2112
+ *
2113
+ * When singleFile is false (default) and the file contains outputs:
2114
+ * - Splits the file in memory into source (no outputs) and snapshot (with outputs)
2115
+ * - Writes both files in parallel
2116
+ *
2117
+ * When singleFile is true or there are no outputs:
2118
+ * - Writes the complete file as-is
2119
+ *
2120
+ * @param options - Write options including the file, output path, and project name
2121
+ * @returns Object containing paths to the written files
2122
+ */
2123
+ async function writeDeepnoteFile(options) {
2124
+ const { file, outputPath, projectName, singleFile = false } = options;
2125
+ const parentDir = (0, node_path.dirname)(outputPath);
2126
+ await node_fs_promises.default.mkdir(parentDir, { recursive: true });
2127
+ if (singleFile || !hasOutputs(file)) {
2128
+ const yamlContent = (0, yaml.stringify)(file);
2129
+ await node_fs_promises.default.writeFile(outputPath, yamlContent, "utf-8");
2130
+ return { sourcePath: outputPath };
2131
+ }
2132
+ const { source, snapshot } = splitDeepnoteFile(file);
2133
+ const snapshotDir = getSnapshotDir(outputPath);
2134
+ const snapshotPath = (0, node_path.resolve)(snapshotDir, generateSnapshotFilename(slugifyProjectName(projectName) || "project", file.project.id));
2135
+ const sourceYaml = (0, yaml.stringify)(source);
2136
+ const snapshotYaml = (0, yaml.stringify)(snapshot);
2137
+ await node_fs_promises.default.mkdir(snapshotDir, { recursive: true });
2138
+ await Promise.all([node_fs_promises.default.writeFile(outputPath, sourceYaml, "utf-8"), node_fs_promises.default.writeFile(snapshotPath, snapshotYaml, "utf-8")]);
2139
+ return {
2140
+ sourcePath: outputPath,
2141
+ snapshotPath
2142
+ };
2143
+ }
2144
+
1532
2145
  //#endregion
1533
2146
  Object.defineProperty(exports, '__toESM', {
1534
2147
  enumerable: true,
@@ -1536,6 +2149,24 @@ Object.defineProperty(exports, '__toESM', {
1536
2149
  return __toESM;
1537
2150
  }
1538
2151
  });
2152
+ Object.defineProperty(exports, 'addContentHashes', {
2153
+ enumerable: true,
2154
+ get: function () {
2155
+ return addContentHashes;
2156
+ }
2157
+ });
2158
+ Object.defineProperty(exports, 'computeContentHash', {
2159
+ enumerable: true,
2160
+ get: function () {
2161
+ return computeContentHash;
2162
+ }
2163
+ });
2164
+ Object.defineProperty(exports, 'computeSnapshotHash', {
2165
+ enumerable: true,
2166
+ get: function () {
2167
+ return computeSnapshotHash;
2168
+ }
2169
+ });
1539
2170
  Object.defineProperty(exports, 'convertBlockToJupyterCell', {
1540
2171
  enumerable: true,
1541
2172
  get: function () {
@@ -1686,12 +2317,42 @@ Object.defineProperty(exports, 'convertQuartoFilesToDeepnoteFile', {
1686
2317
  return convertQuartoFilesToDeepnoteFile;
1687
2318
  }
1688
2319
  });
2320
+ Object.defineProperty(exports, 'countBlocksWithOutputs', {
2321
+ enumerable: true,
2322
+ get: function () {
2323
+ return countBlocksWithOutputs;
2324
+ }
2325
+ });
1689
2326
  Object.defineProperty(exports, 'detectFormat', {
1690
2327
  enumerable: true,
1691
2328
  get: function () {
1692
2329
  return detectFormat;
1693
2330
  }
1694
2331
  });
2332
+ Object.defineProperty(exports, 'findSnapshotsForProject', {
2333
+ enumerable: true,
2334
+ get: function () {
2335
+ return findSnapshotsForProject;
2336
+ }
2337
+ });
2338
+ Object.defineProperty(exports, 'generateSnapshotFilename', {
2339
+ enumerable: true,
2340
+ get: function () {
2341
+ return generateSnapshotFilename;
2342
+ }
2343
+ });
2344
+ Object.defineProperty(exports, 'getSnapshotDir', {
2345
+ enumerable: true,
2346
+ get: function () {
2347
+ return getSnapshotDir;
2348
+ }
2349
+ });
2350
+ Object.defineProperty(exports, 'hasOutputs', {
2351
+ enumerable: true,
2352
+ get: function () {
2353
+ return hasOutputs;
2354
+ }
2355
+ });
1695
2356
  Object.defineProperty(exports, 'isMarimoContent', {
1696
2357
  enumerable: true,
1697
2358
  get: function () {
@@ -1704,6 +2365,24 @@ Object.defineProperty(exports, 'isPercentContent', {
1704
2365
  return isPercentContent;
1705
2366
  }
1706
2367
  });
2368
+ Object.defineProperty(exports, 'loadLatestSnapshot', {
2369
+ enumerable: true,
2370
+ get: function () {
2371
+ return loadLatestSnapshot;
2372
+ }
2373
+ });
2374
+ Object.defineProperty(exports, 'loadSnapshotFile', {
2375
+ enumerable: true,
2376
+ get: function () {
2377
+ return loadSnapshotFile;
2378
+ }
2379
+ });
2380
+ Object.defineProperty(exports, 'mergeSnapshotIntoSource', {
2381
+ enumerable: true,
2382
+ get: function () {
2383
+ return mergeSnapshotIntoSource;
2384
+ }
2385
+ });
1707
2386
  Object.defineProperty(exports, 'parseMarimoFormat', {
1708
2387
  enumerable: true,
1709
2388
  get: function () {
@@ -1722,6 +2401,42 @@ Object.defineProperty(exports, 'parseQuartoFormat', {
1722
2401
  return parseQuartoFormat;
1723
2402
  }
1724
2403
  });
2404
+ Object.defineProperty(exports, 'parseSnapshotFilename', {
2405
+ enumerable: true,
2406
+ get: function () {
2407
+ return parseSnapshotFilename;
2408
+ }
2409
+ });
2410
+ Object.defineProperty(exports, 'parseSourceFilePath', {
2411
+ enumerable: true,
2412
+ get: function () {
2413
+ return parseSourceFilePath;
2414
+ }
2415
+ });
2416
+ Object.defineProperty(exports, 'readAndConvertIpynbFiles', {
2417
+ enumerable: true,
2418
+ get: function () {
2419
+ return readAndConvertIpynbFiles;
2420
+ }
2421
+ });
2422
+ Object.defineProperty(exports, 'readAndConvertMarimoFiles', {
2423
+ enumerable: true,
2424
+ get: function () {
2425
+ return readAndConvertMarimoFiles;
2426
+ }
2427
+ });
2428
+ Object.defineProperty(exports, 'readAndConvertPercentFiles', {
2429
+ enumerable: true,
2430
+ get: function () {
2431
+ return readAndConvertPercentFiles;
2432
+ }
2433
+ });
2434
+ Object.defineProperty(exports, 'readAndConvertQuartoFiles', {
2435
+ enumerable: true,
2436
+ get: function () {
2437
+ return readAndConvertQuartoFiles;
2438
+ }
2439
+ });
1725
2440
  Object.defineProperty(exports, 'serializeMarimoFormat', {
1726
2441
  enumerable: true,
1727
2442
  get: function () {
@@ -1739,4 +2454,28 @@ Object.defineProperty(exports, 'serializeQuartoFormat', {
1739
2454
  get: function () {
1740
2455
  return serializeQuartoFormat;
1741
2456
  }
2457
+ });
2458
+ Object.defineProperty(exports, 'slugifyProjectName', {
2459
+ enumerable: true,
2460
+ get: function () {
2461
+ return slugifyProjectName;
2462
+ }
2463
+ });
2464
+ Object.defineProperty(exports, 'snapshotExists', {
2465
+ enumerable: true,
2466
+ get: function () {
2467
+ return snapshotExists;
2468
+ }
2469
+ });
2470
+ Object.defineProperty(exports, 'splitDeepnoteFile', {
2471
+ enumerable: true,
2472
+ get: function () {
2473
+ return splitDeepnoteFile;
2474
+ }
2475
+ });
2476
+ Object.defineProperty(exports, 'writeDeepnoteFile', {
2477
+ enumerable: true,
2478
+ get: function () {
2479
+ return writeDeepnoteFile;
2480
+ }
1742
2481
  });