storyblok 4.16.1 → 4.16.3

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.
package/dist/index.mjs CHANGED
@@ -681,6 +681,7 @@ const API_ACTIONS = {
681
681
  push_datasource: "Failed to push datasource",
682
682
  update_datasource: "Failed to update datasource",
683
683
  delete_datasource: "Failed to delete datasource",
684
+ delete_datasource_entry: "Failed to delete datasource entry",
684
685
  create_space: "Failed to create space",
685
686
  pull_spaces: "Failed to pull spaces",
686
687
  fetch_blueprints: "Failed to fetch blueprints from GitHub"
@@ -3995,6 +3996,8 @@ generateCmd$1.action(async (componentName, options, command) => {
3995
3996
  }
3996
3997
  });
3997
3998
 
3999
+ const normalizeFullSlug = (slug) => slug.replace(/\/$/, "");
4000
+
3998
4001
  const fetchStories = async (spaceId, params) => {
3999
4002
  try {
4000
4003
  const client = getMapiClient();
@@ -4096,9 +4099,15 @@ const prefetchTargetStories = async (spaceId, options) => {
4096
4099
  options?.onTotal?.(total);
4097
4100
  }
4098
4101
  for (const story of response.stories) {
4099
- const ref = { id: story.id, uuid: story.uuid };
4102
+ const ref = { id: story.id, uuid: story.uuid, is_folder: story.is_folder };
4100
4103
  if (story.full_slug) {
4101
- result.bySlug.set(story.full_slug, ref);
4104
+ const key = normalizeFullSlug(story.full_slug);
4105
+ const existing = result.bySlug.get(key);
4106
+ if (existing) {
4107
+ existing.push(ref);
4108
+ } else {
4109
+ result.bySlug.set(key, [ref]);
4110
+ }
4102
4111
  }
4103
4112
  result.byId.set(story.id, ref);
4104
4113
  }
@@ -5874,7 +5883,7 @@ const upsertDatasource = async (space, datasource, existingId) => {
5874
5883
  return await pushDatasource(space, datasource);
5875
5884
  }
5876
5885
  };
5877
- const pushDatasourceEntry = async (spaceId, datasourceId, entry) => {
5886
+ const pushDatasourceEntry = async (spaceId, datasourceId, entry, position) => {
5878
5887
  try {
5879
5888
  const client = getMapiClient();
5880
5889
  const { data } = await client.datasourceEntries.create({
@@ -5884,7 +5893,8 @@ const pushDatasourceEntry = async (spaceId, datasourceId, entry) => {
5884
5893
  body: {
5885
5894
  datasource_entry: {
5886
5895
  ...entry,
5887
- datasource_id: datasourceId
5896
+ datasource_id: datasourceId,
5897
+ ...position != null && { position }
5888
5898
  }
5889
5899
  },
5890
5900
  throwOnError: true
@@ -5894,7 +5904,7 @@ const pushDatasourceEntry = async (spaceId, datasourceId, entry) => {
5894
5904
  handleAPIError("push_datasource", error, `Failed to push datasource entry ${entry.name}`);
5895
5905
  }
5896
5906
  };
5897
- const updateDatasourceEntry = async (spaceId, entryId, entry) => {
5907
+ const updateDatasourceEntry = async (spaceId, entryId, entry, position) => {
5898
5908
  try {
5899
5909
  const client = getMapiClient();
5900
5910
  await client.datasourceEntries.updateDatasourceEntry({
@@ -5903,7 +5913,10 @@ const updateDatasourceEntry = async (spaceId, entryId, entry) => {
5903
5913
  datasource_entry_id: entryId
5904
5914
  },
5905
5915
  body: {
5906
- datasource_entry: entry
5916
+ datasource_entry: {
5917
+ ...entry,
5918
+ ...position != null && { position }
5919
+ }
5907
5920
  },
5908
5921
  throwOnError: true
5909
5922
  });
@@ -5911,12 +5924,26 @@ const updateDatasourceEntry = async (spaceId, entryId, entry) => {
5911
5924
  handleAPIError("update_datasource", error, `Failed to update datasource entry ${entry.name}`);
5912
5925
  }
5913
5926
  };
5914
- const upsertDatasourceEntry = async (space, datasourceId, entry, existingId) => {
5927
+ const upsertDatasourceEntry = async (space, datasourceId, entry, existingId, position) => {
5915
5928
  if (existingId) {
5916
- await updateDatasourceEntry(space, existingId, entry);
5929
+ await updateDatasourceEntry(space, existingId, entry, position);
5917
5930
  return void 0;
5918
5931
  } else {
5919
- return await pushDatasourceEntry(space, datasourceId, entry);
5932
+ return await pushDatasourceEntry(space, datasourceId, entry, position);
5933
+ }
5934
+ };
5935
+ const deleteDatasourceEntry = async (spaceId, entryId) => {
5936
+ try {
5937
+ const client = getMapiClient();
5938
+ await client.datasourceEntries.delete({
5939
+ path: {
5940
+ space_id: spaceId,
5941
+ datasource_entry_id: entryId
5942
+ },
5943
+ throwOnError: true
5944
+ });
5945
+ } catch (error) {
5946
+ handleAPIError("delete_datasource_entry", error, `Failed to delete datasource entry ${entryId}`);
5920
5947
  }
5921
5948
  };
5922
5949
  const readDatasourcesFiles = async (options) => {
@@ -5994,16 +6021,17 @@ const generateCmd = typesCommand.command("generate").description("Generate types
5994
6021
  ).option("--sf, --separate-files", "Generate one .d.ts file per component instead of a single combined file").option("--strict", "strict mode, no loose typing").option("--type-prefix <prefix>", "prefix to be prepended to all generated component type names").option("--type-suffix <suffix>", "suffix to be appended to all generated component type names").option("--suffix <suffix>", "Components suffix").option("--custom-fields-parser <path>", "Path to the parser file for Custom Field Types").option("--compiler-options <options>", "path to the compiler options from json-schema-to-typescript").option("-s, --space <space>", "space ID").option("-p, --path <path>", "path for file storage");
5995
6022
  generateCmd.action(async (options, command) => {
5996
6023
  konsola.title(`${commands.TYPES}`, colorPalette.TYPES, "Generating types...");
5997
- const { space, path, verbose } = command.optsWithGlobals();
6024
+ const { space, path, verbose, suffix, filename, separateFiles } = command.optsWithGlobals();
5998
6025
  const spinner = new Spinner({
5999
- verbose: !isVitest
6026
+ verbose
6000
6027
  });
6001
6028
  try {
6002
6029
  spinner.start(`Generating types...`);
6003
6030
  const componentsData = await readComponentsFiles({
6004
6031
  from: space,
6005
6032
  path,
6006
- suffix: options.suffix,
6033
+ separateFiles,
6034
+ suffix,
6007
6035
  verbose
6008
6036
  });
6009
6037
  let dataSourceData;
@@ -6011,7 +6039,8 @@ generateCmd.action(async (options, command) => {
6011
6039
  dataSourceData = await readDatasourcesFiles({
6012
6040
  from: space,
6013
6041
  path,
6014
- suffix: options.suffix,
6042
+ separateFiles,
6043
+ suffix,
6015
6044
  verbose
6016
6045
  });
6017
6046
  } catch (error) {
@@ -6029,18 +6058,17 @@ generateCmd.action(async (options, command) => {
6029
6058
  ...dataSourceData
6030
6059
  };
6031
6060
  const typedefData = await generateTypes(spaceDataWithComponentsAndDatasources, {
6032
- ...options,
6033
- path
6061
+ ...options
6034
6062
  });
6035
6063
  if (typedefData) {
6036
6064
  await saveTypesToComponentsFile(space, typedefData, {
6037
- filename: options.filename,
6065
+ filename,
6038
6066
  path,
6039
- separateFiles: options.separateFiles
6067
+ separateFiles
6040
6068
  });
6041
6069
  }
6042
6070
  spinner.succeed();
6043
- if (options.separateFiles && options.filename) {
6071
+ if (separateFiles && filename) {
6044
6072
  konsola.warn(`The --filename option is ignored when using --separate-files`);
6045
6073
  }
6046
6074
  konsola.ok(`Successfully generated types for space ${space}`, true);
@@ -6178,23 +6206,23 @@ pullCmd$2.action(async (datasourceName, options, command) => {
6178
6206
  handleError(new CommandError(`Please provide the space as argument --space YOUR_SPACE_ID.`), verbose);
6179
6207
  return;
6180
6208
  }
6181
- const spinnerDatasources = new Spinner({
6182
- verbose: !isVitest
6183
- });
6209
+ const ui = getUI();
6210
+ const logger = getLogger();
6211
+ logger.info("Pulling datasources started", { space, datasourceName });
6212
+ const spinnerDatasources = ui.createSpinner(`Fetching ${chalk.hex(colorPalette.DATASOURCES)("datasources")}`);
6184
6213
  try {
6185
- spinnerDatasources.start(`Fetching ${chalk.hex(colorPalette.DATASOURCES)("datasources")}`);
6186
6214
  let datasources;
6187
6215
  if (datasourceName) {
6188
6216
  const datasource = await fetchDatasource(space, datasourceName);
6189
6217
  if (!datasource) {
6190
- konsola.warn(`No datasource found with name "${datasourceName}"`);
6218
+ spinnerDatasources.failed(`No datasource found with name "${datasourceName}"`);
6191
6219
  return;
6192
6220
  }
6193
6221
  datasources = [datasource];
6194
6222
  } else {
6195
6223
  datasources = await fetchDatasources(space);
6196
6224
  if (!datasources || datasources.length === 0) {
6197
- konsola.warn(`No datasources found in the space ${space}`);
6225
+ spinnerDatasources.failed(`No datasources found in the space ${space}`);
6198
6226
  return;
6199
6227
  }
6200
6228
  }
@@ -6228,6 +6256,8 @@ pullCmd$2.action(async (datasourceName, options, command) => {
6228
6256
  spinnerDatasources.failed(`Fetching ${chalk.hex(colorPalette.DATASOURCES)("Datasources")} - Failed`);
6229
6257
  konsola.br();
6230
6258
  handleError(error, verbose);
6259
+ } finally {
6260
+ logger.info("Pulling datasources finished", { space, datasourceName });
6231
6261
  }
6232
6262
  });
6233
6263
 
@@ -6245,6 +6275,8 @@ pushCmd$2.action(async (datasourceName, options, command) => {
6245
6275
  handleError(new CommandError(`Please provide the target space as argument --space TARGET_SPACE_ID.`), verbose);
6246
6276
  return;
6247
6277
  }
6278
+ const logger = getLogger();
6279
+ logger.info("Pushing datasources started", { space, fromSpace, datasourceName, filter });
6248
6280
  konsola.info(`Attempting to push datasources ${chalk.bold("from")} space ${chalk.hex(colorPalette.DATASOURCES)(fromSpace)} ${chalk.bold("to")} ${chalk.hex(colorPalette.DATASOURCES)(space)}`);
6249
6281
  konsola.br();
6250
6282
  try {
@@ -6288,26 +6320,44 @@ pushCmd$2.action(async (datasourceName, options, command) => {
6288
6320
  successful: [],
6289
6321
  failed: []
6290
6322
  };
6323
+ const ui = getUI();
6291
6324
  for (const datasource of spaceState.local.datasources) {
6292
- const spinner = new Spinner({
6293
- verbose: !isVitest
6294
- });
6295
- spinner.start(`Pushing ${chalk.hex(colorPalette.DATASOURCES)(datasource.name)}`);
6325
+ const spinner = ui.createSpinner(`Pushing ${chalk.hex(colorPalette.DATASOURCES)(datasource.name)}`);
6296
6326
  const existingDatasource = spaceState.target.datasources.get(datasource.name);
6297
6327
  const existingId = existingDatasource?.id;
6298
6328
  const { entries, ...datasourceDefinition } = datasource;
6299
6329
  const result = await upsertDatasource(space, datasourceDefinition, existingId);
6300
6330
  if (result) {
6301
6331
  results.successful.push(datasource.name);
6302
- if (entries && entries.length > 0) {
6303
- for (const entry of entries) {
6304
- const existingEntryId = existingDatasource?.entries?.find((e) => e.name === entry.name)?.id;
6305
- try {
6306
- await upsertDatasourceEntry(space, result.id, entry, existingEntryId);
6307
- } catch (entryError) {
6308
- results.failed.push({ name: datasource.name, error: entryError });
6309
- spinner.failed(`${chalk.hex(colorPalette.DATASOURCES)(datasource.name)} - Failed in ${spinner.elapsedTime.toFixed(2)}ms`);
6310
- }
6332
+ const localEntries = entries ?? [];
6333
+ const existingEntries = existingDatasource?.entries ?? [];
6334
+ const existingEntryMap = new Map(existingEntries.map((e, idx) => [e.name, { entry: e, position: idx + 1 }]));
6335
+ for (let i = 0; i < localEntries.length; i++) {
6336
+ const entry = localEntries[i];
6337
+ const existing = existingEntryMap.get(entry.name);
6338
+ const existingEntryId = existing?.entry.id;
6339
+ const targetPosition = i + 1;
6340
+ if (existing && existing.entry.value === entry.value && existing.entry.dimension_value === entry.dimension_value && existing.position === targetPosition) {
6341
+ logger.info("Skipped datasource entry (unchanged)", { datasource: datasource.name, entry: entry.name, position: targetPosition });
6342
+ continue;
6343
+ }
6344
+ try {
6345
+ await upsertDatasourceEntry(space, result.id, entry, existingEntryId, i + 1);
6346
+ logger.info(existingEntryId ? "Updated datasource entry" : "Created datasource entry", { datasource: datasource.name, entry: entry.name, position: i + 1 });
6347
+ } catch (entryError) {
6348
+ results.failed.push({ name: datasource.name, error: entryError });
6349
+ spinner.failed(`${chalk.hex(colorPalette.DATASOURCES)(datasource.name)} - Failed in ${spinner.elapsedTime.toFixed(2)}ms`);
6350
+ }
6351
+ }
6352
+ const localEntryNames = new Set(localEntries.map((e) => e.name));
6353
+ const staleEntries = existingEntries.filter((e) => !localEntryNames.has(e.name));
6354
+ for (const stale of staleEntries) {
6355
+ try {
6356
+ await deleteDatasourceEntry(space, stale.id);
6357
+ logger.info("Deleted datasource entry", { datasource: datasource.name, entry: stale.name, entryId: stale.id });
6358
+ } catch (entryError) {
6359
+ results.failed.push({ name: datasource.name, error: entryError });
6360
+ spinner.failed(`${chalk.hex(colorPalette.DATASOURCES)(datasource.name)} - Failed in ${spinner.elapsedTime.toFixed(2)}ms`);
6311
6361
  }
6312
6362
  }
6313
6363
  spinner.succeed(`${chalk.hex(colorPalette.DATASOURCES)(datasource.name)} - Completed in ${spinner.elapsedTime.toFixed(2)}ms`);
@@ -6328,6 +6378,8 @@ pushCmd$2.action(async (datasourceName, options, command) => {
6328
6378
  }
6329
6379
  } catch (error) {
6330
6380
  handleError(error, verbose);
6381
+ } finally {
6382
+ logger.info("Pushing datasources finished", { space, fromSpace });
6331
6383
  }
6332
6384
  });
6333
6385
 
@@ -6367,14 +6419,14 @@ deleteCmd.action(async (name, options, command) => {
6367
6419
  handleError(new CommandError("Please provide the space as argument --space YOUR_SPACE_ID."), verbose);
6368
6420
  return;
6369
6421
  }
6370
- const spinner = new Spinner({
6371
- verbose: !isVitest
6372
- });
6422
+ const ui = getUI();
6423
+ const logger = getLogger();
6424
+ logger.info("Deleting datasource started", { space, name, id: options.id });
6373
6425
  try {
6374
6426
  if (options.id) {
6375
- spinner.start(`Deleting datasource...`);
6427
+ const spinner = ui.createSpinner(`Deleting datasource...`);
6376
6428
  await deleteDatasource(space, options.id);
6377
- spinner.succeed();
6429
+ spinner.succeed(`Datasource deleted`);
6378
6430
  konsola.ok(`Datasource ${chalk.hex(colorPalette.DATASOURCES)(options.id)} deleted successfully from space ${space}.`);
6379
6431
  } else {
6380
6432
  const datasource = await fetchDatasource(space, name);
@@ -6395,21 +6447,19 @@ deleteCmd.action(async (name, options, command) => {
6395
6447
  default: false
6396
6448
  });
6397
6449
  if (!confirmed) {
6398
- spinner.failed("Deletion aborted by user.");
6399
6450
  konsola.warn("Deletion aborted by user.");
6400
6451
  return;
6401
6452
  }
6402
6453
  }
6403
- spinner.start(`Deleting datasource...`);
6454
+ const spinner = ui.createSpinner(`Deleting datasource...`);
6404
6455
  await deleteDatasource(space, datasource.id.toString());
6405
- spinner.succeed();
6456
+ spinner.succeed(`Datasource deleted`);
6406
6457
  konsola.ok(`Datasource ${chalk.hex(colorPalette.DATASOURCES)(name)} deleted successfully from space ${space}.`);
6407
6458
  }
6408
6459
  } catch (error) {
6409
- spinner.failed(
6410
- `Failed to delete datasource ${chalk.hex(colorPalette.DATASOURCES)(options.id ? options.id : name)}`
6411
- );
6412
6460
  handleError(error, verbose);
6461
+ } finally {
6462
+ logger.info("Deleting datasource finished", { space, name, id: options.id });
6413
6463
  }
6414
6464
  });
6415
6465
 
@@ -8216,87 +8266,172 @@ const mapReferencesStream = ({
8216
8266
  }
8217
8267
  });
8218
8268
  };
8219
- const makeCreateStoryAPITransport = ({ spaceId }) => async (localStory) => {
8220
- const { id: _id, uuid: _uuid, parent_id: _parentId, is_startpage: _isStartpage, content, ...newStoryData } = localStory;
8221
- if (!localStory.is_folder && !content?.component) {
8222
- throw new Error(`Story "${localStory.slug}" is missing a content type (content.component). Every story must define a content field with a valid component.`);
8223
- }
8224
- const remoteStory = await createStory(spaceId, {
8225
- story: {
8226
- ...newStoryData,
8227
- ...content?.component ? { content: { _uid: "", component: "__migration_artifact__" } } : {}
8228
- },
8229
- publish: 0
8230
- });
8231
- if (!remoteStory) {
8232
- throw new Error("No response!");
8233
- }
8234
- return remoteStory;
8235
- };
8236
- const makeAppendToManifestFSTransport = ({ manifestFile }) => async (localStory, remoteStory) => {
8269
+ const makeAppendToManifestFSTransport = ({ manifestFile }) => async (entry, remoteStory) => {
8237
8270
  const createdAt = (/* @__PURE__ */ new Date()).toISOString();
8238
8271
  await appendToFile(manifestFile, JSON.stringify({
8239
- old_id: localStory.uuid,
8272
+ old_id: entry.uuid,
8240
8273
  new_id: remoteStory.uuid,
8241
8274
  created_at: createdAt
8242
8275
  }));
8243
8276
  await appendToFile(manifestFile, JSON.stringify({
8244
- old_id: localStory.id,
8277
+ old_id: entry.id,
8245
8278
  new_id: remoteStory.id,
8246
8279
  created_at: createdAt
8247
8280
  }));
8248
8281
  };
8249
- const createStoryPlaceholderStream = ({
8282
+ const scanLocalStoryIndex = async ({
8283
+ directoryPath,
8284
+ setTotalStories,
8285
+ onIncrement,
8286
+ onError
8287
+ }) => {
8288
+ const files = (await readDirectory(directoryPath)).filter((f) => extname(f) === ".json");
8289
+ setTotalStories?.(files.length);
8290
+ const entries = [];
8291
+ for (const file of files) {
8292
+ try {
8293
+ const filePath = join(directoryPath, file);
8294
+ const fileContent = await readFile$1(filePath, "utf-8");
8295
+ const story = JSON.parse(fileContent);
8296
+ entries.push({
8297
+ filename: file,
8298
+ id: story.id,
8299
+ uuid: story.uuid ?? "",
8300
+ slug: story.slug ?? "",
8301
+ name: story.name ?? "",
8302
+ full_slug: story.full_slug ?? "",
8303
+ is_folder: story.is_folder ?? false,
8304
+ is_startpage: story.is_startpage === true,
8305
+ parent_id: story.parent_id ?? null,
8306
+ component: story.content?.component
8307
+ });
8308
+ } catch (maybeError) {
8309
+ onError?.(toError(maybeError), file);
8310
+ } finally {
8311
+ onIncrement?.();
8312
+ }
8313
+ }
8314
+ return entries;
8315
+ };
8316
+ const groupStoriesByDepth = (entries) => {
8317
+ const depthMap = /* @__PURE__ */ new Map();
8318
+ for (const entry of entries) {
8319
+ const slug = normalizeFullSlug(entry.full_slug || "");
8320
+ const depth = slug === "" ? 0 : slug.split("/").length - 1;
8321
+ if (!depthMap.has(depth)) {
8322
+ depthMap.set(depth, []);
8323
+ }
8324
+ depthMap.get(depth).push(entry);
8325
+ }
8326
+ const maxDepth = depthMap.size > 0 ? Math.max(...depthMap.keys()) : 0;
8327
+ const levels = [];
8328
+ for (let d = 0; d <= maxDepth; d++) {
8329
+ const level = depthMap.get(d);
8330
+ if (!level || level.length === 0) {
8331
+ continue;
8332
+ }
8333
+ level.sort((a, b) => {
8334
+ if (a.is_folder && !b.is_folder) {
8335
+ return -1;
8336
+ }
8337
+ if (!a.is_folder && b.is_folder) {
8338
+ return 1;
8339
+ }
8340
+ return 0;
8341
+ });
8342
+ levels.push(level);
8343
+ }
8344
+ return levels;
8345
+ };
8346
+ const findSlugMatch = ({
8347
+ entry,
8348
+ existingTargetStories,
8349
+ claimedRemoteIds
8350
+ }) => {
8351
+ const normalizedSlug = entry.full_slug ? normalizeFullSlug(entry.full_slug) : void 0;
8352
+ const slugCandidates = normalizedSlug ? existingTargetStories.bySlug.get(normalizedSlug) : void 0;
8353
+ if (!slugCandidates) {
8354
+ return void 0;
8355
+ }
8356
+ const unclaimed = slugCandidates.filter((ref) => !claimedRemoteIds.has(ref.id));
8357
+ return unclaimed.find((ref) => ref.is_folder === entry.is_folder) ?? unclaimed[0];
8358
+ };
8359
+ const createStoriesForLevel = async ({
8360
+ level,
8361
+ spaceId,
8250
8362
  maps,
8251
8363
  existingTargetStories,
8364
+ claimedRemoteIds,
8252
8365
  isCrossSpace,
8253
- transports,
8254
- onIncrement,
8366
+ dryRun,
8367
+ appendToManifest,
8255
8368
  onStorySuccess,
8256
8369
  onStorySkipped,
8257
8370
  onStoryError
8258
8371
  }) => {
8259
- const processing = /* @__PURE__ */ new Set();
8260
- return new Writable({
8261
- objectMode: true,
8262
- async write(localStory, _encoding, callback) {
8263
- await apiConcurrencyLock.acquire();
8264
- const task = (async () => {
8265
- try {
8266
- const mappedStoryId = maps.stories?.get(localStory.id);
8267
- const mappedRemoteStory = mappedStoryId ? existingTargetStories.byId.get(Number(mappedStoryId)) : void 0;
8268
- if (mappedRemoteStory) {
8269
- onStorySkipped?.(localStory, mappedRemoteStory);
8270
- return;
8271
- }
8272
- const existingBySlug = localStory.full_slug ? existingTargetStories.bySlug.get(localStory.full_slug) : void 0;
8273
- if (existingBySlug) {
8274
- const isMatchConfirmed = isCrossSpace || existingBySlug.uuid === localStory.uuid;
8275
- if (isMatchConfirmed) {
8276
- await transports.appendStoryManifest(localStory, existingBySlug);
8277
- onStorySkipped?.(localStory, existingBySlug);
8278
- return;
8279
- }
8280
- }
8281
- const newRemoteStory = await transports.createStory(localStory);
8282
- await transports.appendStoryManifest(localStory, newRemoteStory);
8283
- onStorySuccess?.(localStory, newRemoteStory);
8284
- } catch (maybeError) {
8285
- onStoryError?.(toError(maybeError), localStory);
8372
+ const processEntry = async (entry) => {
8373
+ await apiConcurrencyLock.acquire();
8374
+ try {
8375
+ const mappedStoryId = maps.stories?.get(entry.id);
8376
+ const mappedRemoteStory = mappedStoryId ? existingTargetStories.byId.get(Number(mappedStoryId)) : void 0;
8377
+ if (mappedRemoteStory) {
8378
+ claimedRemoteIds.add(mappedRemoteStory.id);
8379
+ onStorySkipped?.(entry, mappedRemoteStory, "matched by manifest mapping from a previous push");
8380
+ return;
8381
+ }
8382
+ const match = findSlugMatch({ entry, existingTargetStories, claimedRemoteIds });
8383
+ if (match) {
8384
+ const isMatchConfirmed = isCrossSpace || match.uuid === entry.uuid;
8385
+ if (isMatchConfirmed) {
8386
+ claimedRemoteIds.add(match.id);
8387
+ await appendToManifest(entry, match);
8388
+ onStorySkipped?.(entry, match, "matched by slug in target space");
8389
+ return;
8286
8390
  }
8287
- })();
8288
- processing.add(task);
8289
- task.finally(() => {
8290
- onIncrement?.();
8291
- apiConcurrencyLock.release();
8292
- processing.delete(task);
8391
+ }
8392
+ if (!entry.is_folder && !entry.component) {
8393
+ throw new Error(`Story "${entry.slug}" (${entry.filename}) is missing a content type (content.component). Every story must define a content field with a valid component.`);
8394
+ }
8395
+ const resolvedParentId = entry.parent_id != null ? maps.stories?.get(entry.parent_id) : void 0;
8396
+ if (dryRun) {
8397
+ const fakeRemote = { id: entry.id, uuid: entry.uuid };
8398
+ onStorySuccess?.(entry, fakeRemote);
8399
+ return;
8400
+ }
8401
+ const remoteStory = await createStory(spaceId, {
8402
+ story: {
8403
+ slug: entry.slug,
8404
+ name: entry.name,
8405
+ is_folder: entry.is_folder,
8406
+ ...resolvedParentId != null ? { parent_id: Number(resolvedParentId) } : {},
8407
+ ...entry.is_startpage && resolvedParentId != null ? { is_startpage: true } : {},
8408
+ ...entry.component ? { content: { _uid: "", component: entry.component } } : {}
8409
+ },
8410
+ publish: 0
8293
8411
  });
8294
- callback();
8295
- },
8296
- final(callback) {
8297
- Promise.all(processing).finally(() => callback());
8412
+ if (!remoteStory) {
8413
+ throw new Error("No response!");
8414
+ }
8415
+ await appendToManifest(entry, remoteStory);
8416
+ onStorySuccess?.(entry, remoteStory);
8417
+ } catch (maybeError) {
8418
+ onStoryError?.(toError(maybeError), entry);
8419
+ } finally {
8420
+ apiConcurrencyLock.release();
8298
8421
  }
8299
- });
8422
+ };
8423
+ const folders = level.filter((e) => e.is_folder);
8424
+ const nonFolders = level.filter((e) => !e.is_folder);
8425
+ const folderTasks = [];
8426
+ for (const entry of folders) {
8427
+ folderTasks.push(processEntry(entry));
8428
+ }
8429
+ await Promise.all(folderTasks);
8430
+ const storyTasks = [];
8431
+ for (const entry of nonFolders) {
8432
+ storyTasks.push(processEntry(entry));
8433
+ }
8434
+ await Promise.all(storyTasks);
8300
8435
  };
8301
8436
  const makeWriteStoryFSTransport = ({ directoryPath }) => async (story) => {
8302
8437
  await saveToFile(resolve(directoryPath, getStoryFilename(story)), JSON.stringify(story, null, 2));
@@ -8306,15 +8441,25 @@ const makeWriteStoryAPITransport = ({ spaceId, publish }) => (mappedLocalStory)
8306
8441
  story: mappedLocalStory,
8307
8442
  publish: publish ?? (isStoryPublishedWithoutChanges(mappedLocalStory) ? 1 : 0)
8308
8443
  });
8309
- const makeCleanupStoryFSTransport = ({ directoryPath, maps }) => async (mappedStory) => {
8310
- const mapEntry = maps.stories?.entries().find(([_, v]) => v === mappedStory.uuid);
8311
- const originalUuid = mapEntry?.[0] && typeof mapEntry?.[0] === "string" ? mapEntry?.[0] : mappedStory.uuid;
8312
- const storyFilename = getStoryFilename({
8313
- slug: mappedStory.slug,
8314
- uuid: originalUuid
8315
- });
8316
- const storyFilePath = resolve(directoryPath, storyFilename);
8317
- await unlink(storyFilePath);
8444
+ const makeCleanupStoryFSTransport = ({ directoryPath, maps }) => {
8445
+ const reverseUuidMap = /* @__PURE__ */ new Map();
8446
+ if (maps.stories) {
8447
+ for (const [key, value] of maps.stories.entries()) {
8448
+ if (typeof key === "string") {
8449
+ reverseUuidMap.set(value, key);
8450
+ }
8451
+ }
8452
+ }
8453
+ return async (mappedStory) => {
8454
+ const uuid = mappedStory.uuid ?? "";
8455
+ const originalUuid = reverseUuidMap.get(uuid) ?? uuid;
8456
+ const storyFilename = getStoryFilename({
8457
+ slug: mappedStory.slug,
8458
+ uuid: originalUuid
8459
+ });
8460
+ const storyFilePath = resolve(directoryPath, storyFilename);
8461
+ await unlink(storyFilePath);
8462
+ };
8318
8463
  };
8319
8464
  const writeStoryStream = ({
8320
8465
  transports,
@@ -8921,75 +9066,80 @@ pushCmd.action(async (options, command) => {
8921
9066
  });
8922
9067
  fetchProgress.stop();
8923
9068
  const storiesDirectoryPath = resolveCommandPath(directories.stories, fromSpace, basePath);
9069
+ const scanProgress = ui.createProgressBar({ title: "Scanning Stories...".padEnd(21) });
9070
+ const storyIndex = await scanLocalStoryIndex({
9071
+ directoryPath: storiesDirectoryPath,
9072
+ setTotalStories(total) {
9073
+ scanProgress.setTotal(total);
9074
+ },
9075
+ onIncrement() {
9076
+ scanProgress.increment();
9077
+ },
9078
+ onError(error, filename) {
9079
+ summary.creationResults.failed += 1;
9080
+ handleError(error, verbose, { storyFile: filename });
9081
+ }
9082
+ });
9083
+ const levels = groupStoriesByDepth(storyIndex);
9084
+ scanProgress.stop();
8924
9085
  const creationProgress = ui.createProgressBar({ title: "Creating Stories...".padEnd(21) });
8925
9086
  const processProgress = ui.createProgressBar({ title: "Processing Stories...".padEnd(21) });
8926
9087
  const updateProgress = ui.createProgressBar({ title: "Updating Stories...".padEnd(21) });
8927
- await pipeline$1(
8928
- // Read local stories from `.json` files.
8929
- readLocalStoriesStream({
8930
- directoryPath: storiesDirectoryPath,
8931
- setTotalStories(total) {
8932
- summary.creationResults.total = total;
8933
- summary.processResults.total = total;
8934
- summary.updateResults.total = total;
8935
- creationProgress.setTotal(total);
8936
- processProgress.setTotal(total);
8937
- updateProgress.setTotal(total);
8938
- },
8939
- onStoryError(error, filename) {
8940
- summary.creationResults.failed += 1;
8941
- summary.processResults.total -= 1;
8942
- summary.updateResults.total -= 1;
8943
- processProgress.setTotal(summary.processResults.total);
8944
- updateProgress.setTotal(summary.updateResults.total);
8945
- creationProgress.increment();
8946
- handleError(error, verbose, { storyFile: filename });
8947
- }
8948
- }),
8949
- // Create remote stories.
8950
- createStoryPlaceholderStream({
9088
+ const totalStories = storyIndex.length + summary.creationResults.failed;
9089
+ summary.creationResults.total = totalStories;
9090
+ summary.processResults.total = totalStories;
9091
+ summary.updateResults.total = totalStories;
9092
+ creationProgress.setTotal(totalStories);
9093
+ processProgress.setTotal(totalStories);
9094
+ updateProgress.setTotal(totalStories);
9095
+ const appendToManifest = options.dryRun ? (() => Promise.resolve()) : makeAppendToManifestFSTransport({ manifestFile });
9096
+ const claimedRemoteIds = /* @__PURE__ */ new Set();
9097
+ for (const level of levels) {
9098
+ await createStoriesForLevel({
9099
+ level,
9100
+ spaceId: space,
8951
9101
  maps,
8952
9102
  existingTargetStories,
9103
+ claimedRemoteIds,
8953
9104
  isCrossSpace: fromSpace !== space,
8954
- transports: {
8955
- createStory: options.dryRun ? async (story) => story : makeCreateStoryAPITransport({
8956
- spaceId: space
8957
- }),
8958
- appendStoryManifest: options.dryRun ? () => Promise.resolve() : makeAppendToManifestFSTransport({
8959
- manifestFile
8960
- })
8961
- },
8962
- onStorySuccess(localStory, remoteStory) {
8963
- if (!localStory.uuid || !remoteStory.uuid) {
9105
+ dryRun: options.dryRun ?? false,
9106
+ appendToManifest,
9107
+ onStorySuccess(entry, remoteStory) {
9108
+ if (!entry.uuid || !remoteStory.uuid) {
8964
9109
  throw new Error("Invalid story provided!");
8965
9110
  }
8966
- maps.stories.set(localStory.id, remoteStory.id);
8967
- maps.stories.set(localStory.uuid, remoteStory.uuid);
9111
+ maps.stories.set(entry.id, remoteStory.id);
9112
+ maps.stories.set(entry.uuid, remoteStory.uuid);
8968
9113
  logger.info("Created story", { storyId: remoteStory.uuid });
8969
9114
  summary.creationResults.succeeded += 1;
9115
+ creationProgress.increment();
8970
9116
  },
8971
- onStorySkipped(localStory, remoteStory) {
8972
- if (!localStory.uuid || !remoteStory.uuid) {
9117
+ onStorySkipped(entry, remoteStory, reason) {
9118
+ if (!entry.uuid || !remoteStory.uuid) {
8973
9119
  throw new Error("Invalid story provided!");
8974
9120
  }
8975
- maps.stories.set(localStory.id, remoteStory.id);
8976
- maps.stories.set(localStory.uuid, remoteStory.uuid);
8977
- logger.info("Skipped creating story", { storyId: localStory.uuid });
9121
+ maps.stories.set(entry.id, remoteStory.id);
9122
+ maps.stories.set(entry.uuid, remoteStory.uuid);
9123
+ logger.info(`Skipped creating story: ${reason}`, { storyId: entry.uuid });
8978
9124
  summary.creationResults.skipped += 1;
9125
+ creationProgress.increment();
8979
9126
  },
8980
- onStoryError(error, localStory) {
9127
+ onStoryError(error, entry) {
8981
9128
  summary.creationResults.failed += 1;
8982
9129
  summary.processResults.total -= 1;
8983
9130
  summary.updateResults.total -= 1;
8984
9131
  processProgress.setTotal(summary.processResults.total);
8985
9132
  updateProgress.setTotal(summary.updateResults.total);
8986
- handleError(error, verbose, { storyId: localStory?.uuid });
8987
- },
8988
- onIncrement() {
8989
9133
  creationProgress.increment();
9134
+ handleError(error, verbose, { storyId: entry?.uuid });
8990
9135
  }
8991
- })
8992
- );
9136
+ });
9137
+ }
9138
+ if (summary.creationResults.failed > 0) {
9139
+ const message = `${summary.creationResults.failed} ${summary.creationResults.failed === 1 ? "story" : "stories"} failed to create. References to these stories will be left unmapped.`;
9140
+ ui.warn(message);
9141
+ logger.warn(message);
9142
+ }
8993
9143
  await pipeline$1(
8994
9144
  // Read local stories from `.json` files.
8995
9145
  readLocalStoriesStream({
@@ -9055,7 +9205,7 @@ pushCmd.action(async (options, command) => {
9055
9205
  })
9056
9206
  );
9057
9207
  } catch (maybeError) {
9058
- handleError(toError(maybeError));
9208
+ handleError(toError(maybeError), verbose);
9059
9209
  } finally {
9060
9210
  logger.info("Pushing stories finished", summary);
9061
9211
  ui.stopAllProgressBars();