storyblok 4.15.0 → 4.15.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.
package/dist/index.mjs CHANGED
@@ -11,7 +11,7 @@ import { Command } from 'commander';
11
11
  import path, { join, resolve as resolve$1, parse, dirname as dirname$1, extname, relative, basename } from 'node:path';
12
12
  import { MultiBar, Presets } from 'cli-progress';
13
13
  import { Spinner } from '@topcli/spinner';
14
- import fs, { mkdir, writeFile, readFile as readFile$1, appendFile, access, constants, readdir, unlink, stat } from 'node:fs/promises';
14
+ import fs, { mkdir, writeFile, readFile as readFile$1, appendFile, access, constants, readdir, unlink } from 'node:fs/promises';
15
15
  import filenamify from 'filenamify';
16
16
  import { ManagementApiClient } from '@storyblok/management-api-client';
17
17
  import { RateLimit, Sema } from 'async-sema';
@@ -103,6 +103,44 @@ const regionNames = {
103
103
  ({
104
104
  SB_Agent_Version: process.env.npm_package_version || "4.x"
105
105
  });
106
+ const SUPPORTED_ASSET_EXTENSIONS = /* @__PURE__ */ new Set([
107
+ // Images: image/png, image/x-png, image/gif, image/jpeg, image/avif, image/svg+xml, image/webp
108
+ ".jpg",
109
+ ".jpeg",
110
+ ".png",
111
+ ".gif",
112
+ ".webp",
113
+ ".avif",
114
+ ".svg",
115
+ // Video: video/*, application/mp4, application/x-mpegurl, application/vnd.apple.mpegurl
116
+ ".mp4",
117
+ ".mov",
118
+ ".avi",
119
+ ".webm",
120
+ ".wmv",
121
+ ".mkv",
122
+ ".flv",
123
+ ".ogv",
124
+ ".3gp",
125
+ ".m4v",
126
+ ".mpg",
127
+ ".mpeg",
128
+ ".m3u8",
129
+ // Audio: audio/*
130
+ ".mp3",
131
+ ".wav",
132
+ ".ogg",
133
+ ".aac",
134
+ ".flac",
135
+ ".wma",
136
+ ".m4a",
137
+ ".opus",
138
+ // Documents: application/msword, text/plain, application/pdf, application/vnd.openxmlformats-officedocument.wordprocessingml.document
139
+ ".pdf",
140
+ ".doc",
141
+ ".docx",
142
+ ".txt"
143
+ ]);
106
144
  const directories = {
107
145
  assets: "assets",
108
146
  components: "components",
@@ -651,25 +689,38 @@ const API_ACTIONS = {
651
689
  const API_ERRORS = {
652
690
  unauthorized: "The user is not authorized to access the API",
653
691
  network_error: "No response from server, please check if you are correctly connected to internet",
692
+ server_error: "The server returned an error",
654
693
  invalid_credentials: "The provided credentials are invalid",
655
694
  timeout: "The API request timed out",
656
695
  generic: "Error fetching data from the API",
657
696
  not_found: "The requested resource was not found",
658
697
  unprocessable_entity: "The request was well-formed but was unable to be followed due to semantic errors"
659
698
  };
699
+ function getErrorId(status) {
700
+ switch (status) {
701
+ case 401:
702
+ return "unauthorized";
703
+ case 404:
704
+ return "not_found";
705
+ case 422:
706
+ return "unprocessable_entity";
707
+ default:
708
+ return status >= 500 ? "server_error" : "generic";
709
+ }
710
+ }
660
711
  function handleAPIError(action, error, customMessage) {
661
712
  if (error instanceof FetchError) {
662
- const status = error.response.status;
663
- switch (status) {
664
- case 401:
665
- throw new APIError("unauthorized", action, error, customMessage);
666
- case 404:
667
- throw new APIError("not_found", action, error, customMessage);
668
- case 422:
669
- throw new APIError("unprocessable_entity", action, error, customMessage);
670
- default:
671
- throw new APIError("network_error", action, error, customMessage);
672
- }
713
+ const errorId = getErrorId(error.response.status);
714
+ throw new APIError(errorId, action, error, customMessage);
715
+ }
716
+ const response = error?.response;
717
+ if (response?.status) {
718
+ const wrappedError = new FetchError(
719
+ response.statusText ?? error.message,
720
+ { status: response.status, statusText: response.statusText ?? "", data: response.data }
721
+ );
722
+ const errorId = getErrorId(response.status);
723
+ throw new APIError(errorId, action, wrappedError, customMessage);
673
724
  }
674
725
  throw new APIError("generic", action, error, customMessage);
675
726
  }
@@ -1888,34 +1939,10 @@ const getUser = async (token, region) => {
1888
1939
  });
1889
1940
  return data?.user;
1890
1941
  } catch (error) {
1891
- if (error instanceof FetchError) {
1892
- const status = error.response.status;
1893
- switch (status) {
1894
- case 401:
1895
- throw new APIError("unauthorized", "get_user", error, `The token provided ${chalk.bold(maskToken(token))} is invalid.
1896
- Please make sure you are using the correct token and try again.`);
1897
- default:
1898
- throw new APIError("network_error", "get_user", error);
1899
- }
1900
- }
1901
- if (typeof error === "string" && error === "Unauthorized") {
1902
- const mockFetchError = new FetchError("Non-JSON response", {
1903
- status: 401,
1904
- statusText: "Unauthorized",
1905
- data: null
1906
- });
1907
- throw new APIError("unauthorized", "get_user", mockFetchError, `The token provided ${chalk.bold(maskToken(token))} is invalid.
1908
- Please make sure you are using the correct token and try again.`);
1909
- }
1910
- if (typeof error === "object" && error !== null && Object.keys(error).length === 0) {
1911
- const mockFetchError = new FetchError("Network Error", {
1912
- status: 500,
1913
- statusText: "Internal Server Error",
1914
- data: null
1915
- });
1916
- throw new APIError("network_error", "get_user", mockFetchError);
1917
- }
1918
- throw new APIError("generic", "get_user", error);
1942
+ const status = error?.response?.status;
1943
+ const customMessage = status === 401 ? `The token provided ${chalk.bold(maskToken(token))} is invalid.
1944
+ Please make sure you are using the correct token and try again.` : void 0;
1945
+ handleAPIError("get_user", error, customMessage);
1919
1946
  }
1920
1947
  };
1921
1948
 
@@ -1926,17 +1953,7 @@ const loginWithToken = async (token, region) => {
1926
1953
  if (error instanceof APIError) {
1927
1954
  throw error;
1928
1955
  }
1929
- if (error instanceof FetchError) {
1930
- const status = error.response.status;
1931
- switch (status) {
1932
- case 401:
1933
- throw new APIError("unauthorized", "login_with_token", error, `The token provided ${chalk.bold(maskToken(token))} is invalid.
1934
- Please make sure you are using the correct token and try again.`);
1935
- default:
1936
- throw new APIError("network_error", "login_with_token", error);
1937
- }
1938
- }
1939
- throw new APIError("generic", "login_with_token", error, "The provided credentials are invalid");
1956
+ handleAPIError("login_with_token", error);
1940
1957
  }
1941
1958
  };
1942
1959
  const loginWithEmailAndPassword = async (email, password, region) => {
@@ -4025,13 +4042,13 @@ const createStory = async (spaceId, payload) => {
4025
4042
  },
4026
4043
  body: {
4027
4044
  story: payload.story,
4028
- publish: payload.publish
4045
+ ...payload.publish ? { publish: payload.publish } : {}
4029
4046
  },
4030
4047
  throwOnError: true
4031
4048
  });
4032
4049
  return data?.story;
4033
- } catch (maybeError) {
4034
- handleAPIError("create_story", toError(maybeError));
4050
+ } catch (error) {
4051
+ handleAPIError("create_story", error);
4035
4052
  }
4036
4053
  };
4037
4054
  const updateStory = async (spaceId, storyId, payload) => {
@@ -4045,18 +4062,20 @@ const updateStory = async (spaceId, storyId, payload) => {
4045
4062
  body: {
4046
4063
  story: payload.story,
4047
4064
  force_update: payload.force_update === "1" ? "1" : "0",
4048
- publish: payload.publish
4065
+ ...payload.publish ? { publish: payload.publish } : {}
4049
4066
  },
4050
4067
  throwOnError: true
4051
4068
  });
4052
- const { story } = data;
4069
+ const story = data?.story;
4053
4070
  if (!story) {
4054
4071
  throw new Error("Failed to update story");
4055
4072
  }
4056
4073
  return story;
4057
- } catch (maybeError) {
4058
- handleAPIError("update_story", toError(maybeError));
4059
- throw maybeError;
4074
+ } catch (error) {
4075
+ if (error instanceof Error && error.message === "Failed to update story") {
4076
+ throw error;
4077
+ }
4078
+ handleAPIError("update_story", error);
4060
4079
  }
4061
4080
  };
4062
4081
 
@@ -7460,31 +7479,25 @@ const readLocalAssetsStream = ({
7460
7479
  const iterator = async function* readAssets() {
7461
7480
  try {
7462
7481
  const files = await readdir(directoryPath);
7463
- const metadataFiles = files.filter((file) => file.endsWith(".json") && file !== "manifest.jsonl");
7464
- setTotalAssets?.(metadataFiles.length);
7465
- for (const file of metadataFiles) {
7466
- const filePath = join(directoryPath, file);
7482
+ const binaryFiles = files.filter((f) => SUPPORTED_ASSET_EXTENSIONS.has(extname(f).toLowerCase()));
7483
+ setTotalAssets?.(binaryFiles.length);
7484
+ for (const file of binaryFiles) {
7485
+ const binaryFilePath = join(directoryPath, file);
7467
7486
  try {
7468
- const statResult = await stat(filePath);
7469
- if (!statResult.isFile()) {
7470
- continue;
7471
- }
7472
- const metadataContent = await readFile$1(filePath, "utf8");
7473
- const assetRaw = JSON.parse(metadataContent);
7487
+ const sidecar = await loadSidecarAssetData(binaryFilePath);
7488
+ const shortFilename = sidecar.short_filename || (sidecar.filename ? basename(sidecar.filename) : void 0) || file;
7474
7489
  const asset = {
7475
- ...assetRaw,
7476
- short_filename: assetRaw.short_filename || basename(assetRaw.filename)
7490
+ ...sidecar,
7491
+ short_filename: shortFilename
7477
7492
  };
7478
- const baseName = parse(file).name;
7479
- const extFromMetadata = extname(asset.short_filename || asset.filename) || "";
7480
- const assetBinaryPath = join(directoryPath, `${baseName}${extFromMetadata}`);
7481
- const fileBuffer = await readFile$1(assetBinaryPath);
7493
+ const fileBuffer = await readFile$1(binaryFilePath);
7494
+ const sidecarPath = getSidecarFilename(binaryFilePath);
7482
7495
  yield {
7483
7496
  asset,
7484
7497
  context: {
7485
7498
  fileBuffer,
7486
- assetBinaryPath,
7487
- assetPath: filePath
7499
+ assetBinaryPath: binaryFilePath,
7500
+ assetPath: sidecarPath
7488
7501
  }
7489
7502
  };
7490
7503
  } catch (maybeError) {
@@ -7868,6 +7881,9 @@ const traverseAndMapBySchema = (data, {
7868
7881
  processedFields,
7869
7882
  missingSchemas
7870
7883
  }) => {
7884
+ if (!data?.component) {
7885
+ return data ?? {};
7886
+ }
7871
7887
  const schema = schemas[data.component];
7872
7888
  if (!schema) {
7873
7889
  missingSchemas.add(data.component);
@@ -7911,7 +7927,7 @@ const traverseAndMapRichtextDoc = (data, {
7911
7927
  }));
7912
7928
  }
7913
7929
  if (data && typeof data === "object") {
7914
- if (data.type === "link" && data.attrs.linktype === "story") {
7930
+ if (data.type === "link" && data.attrs?.linktype === "story") {
7915
7931
  return {
7916
7932
  ...data,
7917
7933
  attrs: {
@@ -7925,7 +7941,7 @@ const traverseAndMapRichtextDoc = (data, {
7925
7941
  ...data,
7926
7942
  attrs: {
7927
7943
  ...data.attrs,
7928
- body: data.attrs.body.map((d) => traverseAndMapBySchema(d, {
7944
+ body: (data.attrs?.body ?? []).map((d) => traverseAndMapBySchema(d, {
7929
7945
  schemas,
7930
7946
  maps,
7931
7947
  fieldRefMappers: fieldRefMappers2,
@@ -7967,7 +7983,7 @@ const multilinkFieldRefMapper = (data, { maps }) => {
7967
7983
  };
7968
7984
  const bloksFieldRefMapper = (data, { schemas, maps, fieldRefMappers: fieldRefMappers2, processedFields, missingSchemas }) => {
7969
7985
  if (!Array.isArray(data)) {
7970
- throw new TypeError("Invalid data!");
7986
+ throw new TypeError(`Invalid bloks field: expected an array, but received ${JSON.stringify(data)}. Please make sure your bloks field value is an array of components (e.g. [{ component: "my_blok", ... }]).`);
7971
7987
  }
7972
7988
  return data.map((d) => traverseAndMapBySchema(d, {
7973
7989
  schemas,
@@ -7986,7 +8002,7 @@ const assetFieldRefMapper = (data, { maps }) => {
7986
8002
  };
7987
8003
  const multiassetFieldRefMapper = (data, options) => {
7988
8004
  if (!Array.isArray(data)) {
7989
- throw new TypeError("Invalid data!");
8005
+ throw new TypeError(`Invalid multiasset field: expected an array, but received ${JSON.stringify(data)}. Please make sure your multiasset field value is an array of asset objects (e.g. [{ filename: "...", id: 123 }]).`);
7990
8006
  }
7991
8007
  return data.map((d) => assetFieldRefMapper(d, options));
7992
8008
  };
@@ -8009,23 +8025,23 @@ const storyRefMapper = (story, { schemas, maps }) => {
8009
8025
  const missingSchemas = /* @__PURE__ */ new Set();
8010
8026
  const alternates = story.alternates ? story.alternates.map((a) => ({
8011
8027
  ...a,
8012
- id: maps.stories?.get(a.id) || a.id,
8013
- parent_id: maps.stories?.get(a.parent_id) || a.parent_id
8028
+ id: maps.stories?.get(a.id) ?? a.id,
8029
+ parent_id: maps.stories?.get(a.parent_id) ?? a.parent_id
8014
8030
  })) : story.alternates;
8015
- const parentId = maps.stories?.get(story.parent_id) || story.parent_id;
8031
+ const parentId = maps.stories?.get(story.parent_id) ?? story.parent_id;
8016
8032
  const mappedStory = {
8017
8033
  ...story,
8018
- content: traverseAndMapBySchema(story.content, {
8034
+ content: story.content?.component ? traverseAndMapBySchema(story.content, {
8019
8035
  schemas,
8020
8036
  maps,
8021
8037
  fieldRefMappers,
8022
8038
  processedFields,
8023
8039
  missingSchemas
8024
- }),
8025
- id: Number(maps.stories?.get(story.id) || story.id),
8026
- uuid: String(maps.stories?.get(story.uuid) || story.uuid),
8040
+ }) : story.content,
8041
+ id: Number(maps.stories?.get(story.id) ?? story.id),
8042
+ uuid: String(maps.stories?.get(story.uuid) ?? story.uuid),
8027
8043
  // @ts-expect-error Our types are wrong.
8028
- parent_id: parentId ? Number(parentId) : null,
8044
+ parent_id: parentId != null ? Number(parentId) : null,
8029
8045
  alternates
8030
8046
  };
8031
8047
  return {
@@ -8187,14 +8203,14 @@ const getRemoteStory = async ({ spaceId, storyId }) => {
8187
8203
  return data?.story;
8188
8204
  };
8189
8205
  const makeCreateStoryAPITransport = ({ spaceId }) => async (localStory) => {
8190
- const { id: _id, uuid: _uuid, content, parent_id: _p, ...newStoryData } = localStory;
8206
+ const { id: _id, uuid: _uuid, parent_id: _parentId, content, ...newStoryData } = localStory;
8207
+ if (!localStory.is_folder && !content?.component) {
8208
+ throw new Error(`Story "${localStory.slug}" is missing a content type (content.component). Every story must define a content field with a valid component.`);
8209
+ }
8191
8210
  const remoteStory = await createStory(spaceId, {
8192
8211
  story: {
8193
8212
  ...newStoryData,
8194
- content: {
8195
- _uid: "",
8196
- component: "__tmp__"
8197
- }
8213
+ ...content?.component ? { content: { _uid: "", component: "__migration_artifact__" } } : {}
8198
8214
  },
8199
8215
  publish: 0
8200
8216
  });
@@ -8270,7 +8286,7 @@ const makeWriteStoryFSTransport = ({ directoryPath }) => async (story) => {
8270
8286
  };
8271
8287
  const makeWriteStoryAPITransport = ({ spaceId, publish }) => (mappedLocalStory) => updateStory(spaceId, mappedLocalStory.id, {
8272
8288
  story: mappedLocalStory,
8273
- publish: publish ?? (mappedLocalStory.published ? 1 : 0)
8289
+ publish: publish ?? (isStoryPublishedWithoutChanges(mappedLocalStory) ? 1 : 0)
8274
8290
  });
8275
8291
  const makeCleanupStoryFSTransport = ({ directoryPath, maps }) => async (mappedStory) => {
8276
8292
  const mapEntry = maps.stories?.entries().find(([_, v]) => v === mappedStory.uuid);
@@ -8896,14 +8912,14 @@ pushCmd.action(async (options, command) => {
8896
8912
  processProgress.setTotal(total);
8897
8913
  updateProgress.setTotal(total);
8898
8914
  },
8899
- onStoryError(error) {
8915
+ onStoryError(error, filename) {
8900
8916
  summary.creationResults.failed += 1;
8901
8917
  summary.processResults.total -= 1;
8902
8918
  summary.updateResults.total -= 1;
8903
8919
  processProgress.setTotal(summary.processResults.total);
8904
8920
  updateProgress.setTotal(summary.updateResults.total);
8905
8921
  creationProgress.increment();
8906
- handleError(error, verbose);
8922
+ handleError(error, verbose, { storyFile: filename });
8907
8923
  }
8908
8924
  }),
8909
8925
  // Create remote stories.
@@ -8912,7 +8928,6 @@ pushCmd.action(async (options, command) => {
8912
8928
  spaceId: space,
8913
8929
  transports: {
8914
8930
  createStory: options.dryRun ? async (story) => story : makeCreateStoryAPITransport({
8915
- maps,
8916
8931
  spaceId: space
8917
8932
  }),
8918
8933
  appendStoryManifest: options.dryRun ? () => Promise.resolve() : makeAppendToManifestFSTransport({
@@ -8937,13 +8952,13 @@ pushCmd.action(async (options, command) => {
8937
8952
  logger.info("Skipped creating story", { storyId: localStory.uuid });
8938
8953
  summary.creationResults.skipped += 1;
8939
8954
  },
8940
- onStoryError(error) {
8955
+ onStoryError(error, localStory) {
8941
8956
  summary.creationResults.failed += 1;
8942
8957
  summary.processResults.total -= 1;
8943
8958
  summary.updateResults.total -= 1;
8944
8959
  processProgress.setTotal(summary.processResults.total);
8945
8960
  updateProgress.setTotal(summary.updateResults.total);
8946
- handleError(error, verbose);
8961
+ handleError(error, verbose, { storyId: localStory?.uuid });
8947
8962
  },
8948
8963
  onIncrement() {
8949
8964
  creationProgress.increment();
@@ -8963,13 +8978,13 @@ pushCmd.action(async (options, command) => {
8963
8978
  processProgress.setTotal(total);
8964
8979
  updateProgress.setTotal(total);
8965
8980
  },
8966
- onStoryError(error) {
8981
+ onStoryError(error, filename) {
8967
8982
  summary.creationResults.failed += 1;
8968
8983
  summary.processResults.total -= 1;
8969
8984
  summary.updateResults.total -= 1;
8970
8985
  processProgress.setTotal(summary.processResults.total);
8971
8986
  updateProgress.setTotal(summary.updateResults.total);
8972
- handleError(error, verbose);
8987
+ handleError(error, verbose, { storyFile: filename });
8973
8988
  }
8974
8989
  }),
8975
8990
  // Map all references to numeric ids and uuids.