storyblok 4.16.9 → 4.17.0

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.
@@ -64,6 +64,10 @@ interface MigrationsGenerateOptions extends CommandOptions {
64
64
  interface MigrationsRunOptions extends CommandOptions {
65
65
  dryRun?: boolean;
66
66
  filter?: string;
67
+ /**
68
+ * The source space id to read migration files from.
69
+ */
70
+ from?: string;
67
71
  query?: string;
68
72
  startsWith?: string;
69
73
  publish?: 'all' | 'published' | 'published-with-changes';
@@ -64,6 +64,10 @@ interface MigrationsGenerateOptions extends CommandOptions {
64
64
  interface MigrationsRunOptions extends CommandOptions {
65
65
  dryRun?: boolean;
66
66
  filter?: string;
67
+ /**
68
+ * The source space id to read migration files from.
69
+ */
70
+ from?: string;
67
71
  query?: string;
68
72
  startsWith?: string;
69
73
  publish?: 'all' | 'published' | 'published-with-changes';
package/dist/index.mjs CHANGED
@@ -26,7 +26,6 @@ import { Octokit } from 'octokit';
26
26
  import { pipeline as pipeline$1 } from 'node:stream/promises';
27
27
  import { Buffer } from 'node:buffer';
28
28
  import Storyblok from 'storyblok-js-client';
29
- import { createHash } from 'node:crypto';
30
29
 
31
30
  const commands = {
32
31
  LOGIN: "login",
@@ -4429,7 +4428,7 @@ class MigrationStream extends Transform {
4429
4428
  }
4430
4429
  const migrationFunction = await getMigrationFunction(
4431
4430
  migrationFile.name,
4432
- this.options.space,
4431
+ this.options.sourceSpace,
4433
4432
  this.options.path
4434
4433
  );
4435
4434
  this.migrationFunctions.set(migrationFile.name, migrationFunction);
@@ -4756,7 +4755,7 @@ class UpdateStream extends Writable {
4756
4755
  }
4757
4756
  }
4758
4757
 
4759
- const runCmd = migrationsCommand.command("run [componentName]").description("Run migrations").option("--fi, --filter <filter>", "glob filter to apply to the components before pushing").option("-d, --dry-run", "Preview changes without applying them to Storyblok").option("-q, --query <query>", 'Filter stories by content attributes using Storyblok filter query syntax. Example: --query="[highlighted][in]=true"').option("--starts-with <path>", 'Filter stories by path. Example: --starts-with="/en/blog/"').option("--publish <publish>", "Options for publication mode: all | published | published-with-changes").option("-s, --space <space>", "space ID");
4758
+ const runCmd = migrationsCommand.command("run [componentName]").description("Run migrations").option("--fi, --filter <filter>", "glob filter to apply to the components before pushing").option("-d, --dry-run", "Preview changes without applying them to Storyblok").option("-q, --query <query>", 'Filter stories by content attributes using Storyblok filter query syntax. Example: --query="[highlighted][in]=true"').option("--starts-with <path>", 'Filter stories by path. Example: --starts-with="/en/blog/"').option("--publish <publish>", "Options for publication mode: all | published | published-with-changes").option("-f, --from <from>", "source space id").option("-s, --space <space>", "space ID");
4760
4759
  runCmd.action(async (componentName, options, command) => {
4761
4760
  const ui = getUI();
4762
4761
  const logger = getLogger();
@@ -4777,11 +4776,16 @@ runCmd.action(async (componentName, options, command) => {
4777
4776
  handleError(new CommandError(`Please provide the space as argument --space YOUR_SPACE_ID.`), verbose);
4778
4777
  return;
4779
4778
  }
4780
- const { filter, dryRun = false, query, startsWith, publish } = options;
4779
+ const { filter, from, dryRun = false, query, startsWith, publish } = options;
4780
+ const fromSpace = from || space;
4781
+ if (from) {
4782
+ ui.info(`Running migrations ${chalk.bold("from")} space ${chalk.hex(colorPalette.MIGRATIONS)(fromSpace)} ${chalk.bold("on")} space ${chalk.hex(colorPalette.MIGRATIONS)(space)}`);
4783
+ ui.br();
4784
+ }
4781
4785
  try {
4782
4786
  const spinner = ui.createSpinner(`Fetching migration files and stories...`);
4783
4787
  const migrationFiles = await readMigrationFiles({
4784
- space,
4788
+ space: fromSpace,
4785
4789
  path,
4786
4790
  filter
4787
4791
  });
@@ -4789,7 +4793,7 @@ runCmd.action(async (componentName, options, command) => {
4789
4793
  return file.name.match(new RegExp(`^${componentName}(\\..*)?.js$`));
4790
4794
  }) : migrationFiles;
4791
4795
  if (filteredMigrations.length === 0) {
4792
- spinner.failed(`No migration files found${componentName ? ` for component "${componentName}"` : ""}${filter ? ` matching filter "${filter}"` : ""} in space "${space}".`);
4796
+ spinner.failed(`No migration files found${componentName ? ` for component "${componentName}"` : ""}${filter ? ` matching filter "${filter}"` : ""} in space "${fromSpace}".`);
4793
4797
  logger.warn("No migration files found");
4794
4798
  logger.info("Migration finished");
4795
4799
  return;
@@ -4817,6 +4821,7 @@ runCmd.action(async (componentName, options, command) => {
4817
4821
  const migrationStream = new MigrationStream({
4818
4822
  migrationFiles: filteredMigrations,
4819
4823
  space,
4824
+ sourceSpace: fromSpace,
4820
4825
  path,
4821
4826
  componentName,
4822
4827
  onTotal: (total) => {
@@ -7037,10 +7042,6 @@ const updateAssetFolder = async (id, folder, {
7037
7042
  handleAPIError("push_asset_folder", toError(maybeError));
7038
7043
  }
7039
7044
  };
7040
- const sha256 = (data) => {
7041
- const buffer = Buffer.isBuffer(data) ? data : Buffer.from(data);
7042
- return createHash("sha256").update(buffer).digest("hex");
7043
- };
7044
7045
  const downloadAssetFile = async (asset, options) => {
7045
7046
  let url = asset.filename;
7046
7047
  if (asset.is_private) {
@@ -7564,24 +7565,6 @@ const hasId = (a) => {
7564
7565
  const hasShortFilename = (a) => {
7565
7566
  return !!a && typeof a === "object" && "short_filename" in a && typeof a.short_filename === "string";
7566
7567
  };
7567
- const isDataUnchanged = (localAsset, remoteAsset) => {
7568
- for (const key of Object.keys(localAsset)) {
7569
- const local = localAsset[key];
7570
- const remote = remoteAsset[key];
7571
- if (typeof local === "object" || typeof remote === "object") {
7572
- if (JSON.stringify(local) !== JSON.stringify(remote)) {
7573
- return false;
7574
- }
7575
- } else if (local !== remote) {
7576
- return false;
7577
- }
7578
- }
7579
- return true;
7580
- };
7581
- const makeDownloadAssetFileTransport = ({
7582
- assetToken,
7583
- region
7584
- }) => (asset) => downloadAssetFile(asset, { assetToken, region });
7585
7568
  const processAsset = async ({
7586
7569
  localAsset,
7587
7570
  fileBuffer,
@@ -7609,20 +7592,13 @@ const processAsset = async ({
7609
7592
  internal_tag_ids: "internal_tag_ids" in localAsset ? localAsset.internal_tag_ids : remoteAsset.internal_tag_ids,
7610
7593
  meta_data: "meta_data" in localAsset ? localAsset.meta_data : remoteAsset.meta_data
7611
7594
  };
7612
- const remoteFileBuffer = await transports.downloadAssetFile(remoteAsset);
7613
- const isFileUnchanged = sha256(fileBuffer) === sha256(remoteFileBuffer);
7614
- if (isFileUnchanged && isDataUnchanged(updatePayload, remoteAsset)) {
7615
- newRemoteAsset = remoteAsset;
7616
- status = "skipped";
7617
- } else {
7618
- await transports.updateAsset(
7619
- remoteAsset.id,
7620
- { ...updatePayload, short_filename: remoteAsset.short_filename },
7621
- isFileUnchanged ? void 0 : fileBuffer
7622
- );
7623
- newRemoteAsset = { ...remoteAsset, ...updatePayload };
7624
- status = "updated";
7625
- }
7595
+ await transports.updateAsset(
7596
+ remoteAsset.id,
7597
+ { ...updatePayload, short_filename: remoteAsset.short_filename },
7598
+ fileBuffer
7599
+ );
7600
+ newRemoteAsset = { ...remoteAsset, ...updatePayload };
7601
+ status = "updated";
7626
7602
  } else if (hasShortFilename(localAsset)) {
7627
7603
  const createPayload = {
7628
7604
  ...localAsset,
@@ -7644,7 +7620,6 @@ const upsertAssetStream = ({
7644
7620
  maps,
7645
7621
  onIncrement,
7646
7622
  onAssetSuccess,
7647
- onAssetSkipped,
7648
7623
  onAssetError
7649
7624
  }) => {
7650
7625
  const processing = /* @__PURE__ */ new Set();
@@ -7654,7 +7629,7 @@ const upsertAssetStream = ({
7654
7629
  await apiConcurrencyLock$1.acquire();
7655
7630
  const task = (async () => {
7656
7631
  try {
7657
- const { status, remoteAsset } = await processAsset({
7632
+ const { remoteAsset } = await processAsset({
7658
7633
  localAsset,
7659
7634
  fileBuffer: context.fileBuffer,
7660
7635
  assetBinaryPath: context.assetBinaryPath,
@@ -7662,11 +7637,7 @@ const upsertAssetStream = ({
7662
7637
  transports,
7663
7638
  maps
7664
7639
  });
7665
- if (status === "skipped") {
7666
- onAssetSkipped?.(localAsset, remoteAsset);
7667
- } else {
7668
- onAssetSuccess?.(localAsset, remoteAsset);
7669
- }
7640
+ onAssetSuccess?.(localAsset, remoteAsset);
7670
7641
  } catch (maybeError) {
7671
7642
  onAssetError?.(toError(maybeError), localAsset);
7672
7643
  }
@@ -7849,16 +7820,13 @@ pullCmd$1.action(async (options, command) => {
7849
7820
  const traverseAndMapBySchema = (data, {
7850
7821
  schemas,
7851
7822
  maps,
7852
- fieldRefMappers: fieldRefMappers2,
7853
- processedFields,
7854
- missingSchemas
7823
+ fieldRefMappers: fieldRefMappers2
7855
7824
  }) => {
7856
7825
  if (!data?.component) {
7857
7826
  return data ?? {};
7858
7827
  }
7859
7828
  const schema = schemas[data.component];
7860
7829
  if (!schema) {
7861
- missingSchemas.add(data.component);
7862
7830
  return data;
7863
7831
  }
7864
7832
  const dataNew = { ...data };
@@ -7866,17 +7834,12 @@ const traverseAndMapBySchema = (data, {
7866
7834
  const fieldSchema = schema[fieldName.replace(/__i18n__.*/, "")];
7867
7835
  const fieldType = fieldSchema && typeof fieldSchema === "object" && "type" in fieldSchema && fieldSchema.type;
7868
7836
  const fieldRefMapper = typeof fieldType === "string" && fieldRefMappers2[fieldType];
7869
- if (fieldSchema) {
7870
- processedFields.add(fieldSchema);
7871
- }
7872
7837
  if (fieldRefMapper) {
7873
7838
  dataNew[fieldName] = fieldRefMapper(fieldValue, {
7874
7839
  schema: fieldSchema,
7875
7840
  schemas,
7876
7841
  maps,
7877
- fieldRefMappers: fieldRefMappers2,
7878
- processedFields,
7879
- missingSchemas
7842
+ fieldRefMappers: fieldRefMappers2
7880
7843
  });
7881
7844
  }
7882
7845
  }
@@ -7885,17 +7848,13 @@ const traverseAndMapBySchema = (data, {
7885
7848
  const traverseAndMapRichtextDoc = (data, {
7886
7849
  schemas,
7887
7850
  maps,
7888
- fieldRefMappers: fieldRefMappers2,
7889
- processedFields,
7890
- missingSchemas
7851
+ fieldRefMappers: fieldRefMappers2
7891
7852
  }) => {
7892
7853
  if (Array.isArray(data)) {
7893
7854
  return data.map((item) => traverseAndMapRichtextDoc(item, {
7894
7855
  schemas,
7895
7856
  maps,
7896
- fieldRefMappers: fieldRefMappers2,
7897
- processedFields,
7898
- missingSchemas
7857
+ fieldRefMappers: fieldRefMappers2
7899
7858
  }));
7900
7859
  }
7901
7860
  if (data && typeof data === "object") {
@@ -7916,9 +7875,7 @@ const traverseAndMapRichtextDoc = (data, {
7916
7875
  body: (data.attrs?.body ?? []).map((d) => traverseAndMapBySchema(d, {
7917
7876
  schemas,
7918
7877
  maps,
7919
- fieldRefMappers: fieldRefMappers2,
7920
- processedFields,
7921
- missingSchemas
7878
+ fieldRefMappers: fieldRefMappers2
7922
7879
  }))
7923
7880
  }
7924
7881
  };
@@ -7928,21 +7885,17 @@ const traverseAndMapRichtextDoc = (data, {
7928
7885
  newData[k] = traverseAndMapRichtextDoc(value, {
7929
7886
  schemas,
7930
7887
  maps,
7931
- fieldRefMappers: fieldRefMappers2,
7932
- processedFields,
7933
- missingSchemas
7888
+ fieldRefMappers: fieldRefMappers2
7934
7889
  });
7935
7890
  }
7936
7891
  return newData;
7937
7892
  }
7938
7893
  return data;
7939
7894
  };
7940
- const richtextFieldRefMapper = (data, { schemas, maps, fieldRefMappers: fieldRefMappers2, processedFields, missingSchemas }) => traverseAndMapRichtextDoc(data, {
7895
+ const richtextFieldRefMapper = (data, { schemas, maps, fieldRefMappers: fieldRefMappers2 }) => traverseAndMapRichtextDoc(data, {
7941
7896
  schemas,
7942
7897
  maps,
7943
- fieldRefMappers: fieldRefMappers2,
7944
- processedFields,
7945
- missingSchemas
7898
+ fieldRefMappers: fieldRefMappers2
7946
7899
  });
7947
7900
  const multilinkFieldRefMapper = (data, { maps }) => {
7948
7901
  if (data.linktype !== "story") {
@@ -7953,16 +7906,14 @@ const multilinkFieldRefMapper = (data, { maps }) => {
7953
7906
  id: maps.stories?.get(data.id) || data.id
7954
7907
  };
7955
7908
  };
7956
- const bloksFieldRefMapper = (data, { schemas, maps, fieldRefMappers: fieldRefMappers2, processedFields, missingSchemas }) => {
7909
+ const bloksFieldRefMapper = (data, { schemas, maps, fieldRefMappers: fieldRefMappers2 }) => {
7957
7910
  if (!Array.isArray(data)) {
7958
7911
  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", ... }]).`);
7959
7912
  }
7960
7913
  return data.map((d) => traverseAndMapBySchema(d, {
7961
7914
  schemas,
7962
7915
  maps,
7963
- fieldRefMappers: fieldRefMappers2,
7964
- processedFields,
7965
- missingSchemas
7916
+ fieldRefMappers: fieldRefMappers2
7966
7917
  }));
7967
7918
  };
7968
7919
  const assetFieldRefMapper = (data, { maps }) => {
@@ -7997,33 +7948,24 @@ const fieldRefMappers = {
7997
7948
  richtext: richtextFieldRefMapper
7998
7949
  };
7999
7950
  const storyRefMapper = (story, { schemas, maps }) => {
8000
- const processedFields = /* @__PURE__ */ new Set();
8001
- const missingSchemas = /* @__PURE__ */ new Set();
8002
7951
  const alternates = story.alternates ? story.alternates.map((a) => ({
8003
7952
  ...a,
8004
7953
  id: maps.stories?.get(a.id) ?? a.id,
8005
7954
  parent_id: maps.stories?.get(a.parent_id) ?? a.parent_id
8006
7955
  })) : story.alternates;
8007
7956
  const parentId = maps.stories?.get(story.parent_id) ?? story.parent_id;
8008
- const mappedStory = {
7957
+ return {
8009
7958
  ...story,
8010
7959
  content: story.content?.component ? traverseAndMapBySchema(story.content, {
8011
7960
  schemas,
8012
7961
  maps,
8013
- fieldRefMappers,
8014
- processedFields,
8015
- missingSchemas
7962
+ fieldRefMappers
8016
7963
  }) : story.content,
8017
7964
  id: Number(maps.stories?.get(story.id) ?? story.id),
8018
7965
  uuid: String(maps.stories?.get(story.uuid) ?? story.uuid),
8019
7966
  parent_id: parentId != null ? Number(parentId) : 0,
8020
7967
  alternates
8021
7968
  };
8022
- return {
8023
- mappedStory,
8024
- processedFields,
8025
- missingSchemas
8026
- };
8027
7969
  };
8028
7970
 
8029
7971
  const apiConcurrencyLock = new Sema(12);
@@ -8150,8 +8092,8 @@ const mapReferencesStream = ({
8150
8092
  objectMode: true,
8151
8093
  transform(localStory, _encoding, callback) {
8152
8094
  try {
8153
- const { mappedStory, processedFields, missingSchemas } = storyRefMapper(localStory, { schemas, maps });
8154
- onStorySuccess?.(mappedStory, processedFields, missingSchemas);
8095
+ const mappedStory = storyRefMapper(localStory, { schemas, maps });
8096
+ onStorySuccess?.(mappedStory);
8155
8097
  this.push(mappedStory);
8156
8098
  } catch (maybeError) {
8157
8099
  onStoryError?.(toError(maybeError), localStory);
@@ -8394,6 +8336,162 @@ const writeStoryStream = ({
8394
8336
  });
8395
8337
  };
8396
8338
 
8339
+ const walkRichtextBloks = (node, onBlok) => {
8340
+ if (Array.isArray(node)) {
8341
+ for (const item of node) {
8342
+ walkRichtextBloks(item, onBlok);
8343
+ }
8344
+ return;
8345
+ }
8346
+ if (!node || typeof node !== "object") {
8347
+ return;
8348
+ }
8349
+ const obj = node;
8350
+ if (obj.type === "blok" && obj.attrs && typeof obj.attrs === "object") {
8351
+ const body = obj.attrs.body;
8352
+ if (Array.isArray(body)) {
8353
+ for (const blok of body) {
8354
+ onBlok(blok);
8355
+ }
8356
+ }
8357
+ }
8358
+ for (const value of Object.values(obj)) {
8359
+ walkRichtextBloks(value, onBlok);
8360
+ }
8361
+ };
8362
+
8363
+ const RESERVED_KEYS = /* @__PURE__ */ new Set(["component"]);
8364
+ const isReservedKey = (key) => RESERVED_KEYS.has(key) || key.startsWith("_");
8365
+ const MAX_STORIES_PER_ENTRY = 5;
8366
+ const addDrift = (driftByComponent, component, field) => {
8367
+ const existing = driftByComponent.get(component) ?? /* @__PURE__ */ new Set();
8368
+ existing.add(field);
8369
+ driftByComponent.set(component, existing);
8370
+ };
8371
+ const validateStoryAgainstSchemas = (story, schemas) => {
8372
+ const driftByComponent = /* @__PURE__ */ new Map();
8373
+ const missingSchemas = /* @__PURE__ */ new Set();
8374
+ const visit = (data) => {
8375
+ if (!data || typeof data !== "object" || Array.isArray(data)) {
8376
+ return;
8377
+ }
8378
+ const node = data;
8379
+ const componentName = node.component;
8380
+ if (typeof componentName !== "string" || componentName.length === 0) {
8381
+ return;
8382
+ }
8383
+ const schema = schemas[componentName];
8384
+ if (!schema) {
8385
+ missingSchemas.add(componentName);
8386
+ return;
8387
+ }
8388
+ for (const [fieldName, fieldValue] of Object.entries(node)) {
8389
+ if (isReservedKey(fieldName)) {
8390
+ continue;
8391
+ }
8392
+ const normalized = fieldName.replace(/__i18n__.*/, "");
8393
+ const fieldSchema = schema[normalized];
8394
+ if (!fieldSchema) {
8395
+ addDrift(driftByComponent, componentName, normalized);
8396
+ continue;
8397
+ }
8398
+ const fieldType = typeof fieldSchema.type === "string" ? fieldSchema.type : void 0;
8399
+ if (fieldType === "bloks" && Array.isArray(fieldValue)) {
8400
+ for (const item of fieldValue) {
8401
+ visit(item);
8402
+ }
8403
+ } else if (fieldType === "richtext" && fieldValue && typeof fieldValue === "object") {
8404
+ walkRichtextBloks(fieldValue, (blok) => visit(blok));
8405
+ }
8406
+ }
8407
+ };
8408
+ visit(story.content);
8409
+ return { driftByComponent, missingSchemas };
8410
+ };
8411
+ const addStoryToSet = (bag, key, storySlug) => {
8412
+ const existing = bag.get(key) ?? /* @__PURE__ */ new Set();
8413
+ existing.add(storySlug);
8414
+ bag.set(key, existing);
8415
+ };
8416
+ const collectSchemaIssues = async ({
8417
+ directoryPath,
8418
+ schemas
8419
+ }) => {
8420
+ const issues = {
8421
+ driftByComponent: /* @__PURE__ */ new Map(),
8422
+ missingSchemas: /* @__PURE__ */ new Map(),
8423
+ total: 0
8424
+ };
8425
+ try {
8426
+ await pipeline$1(
8427
+ readLocalStoriesStream({ directoryPath }),
8428
+ new Writable({
8429
+ objectMode: true,
8430
+ write(story, _encoding, callback) {
8431
+ issues.total += 1;
8432
+ const storyIdentifier = story.full_slug || story.slug;
8433
+ const { driftByComponent, missingSchemas } = validateStoryAgainstSchemas(story, schemas);
8434
+ for (const component of missingSchemas) {
8435
+ addStoryToSet(issues.missingSchemas, component, storyIdentifier);
8436
+ }
8437
+ for (const [component, fields] of driftByComponent) {
8438
+ const fieldMap = issues.driftByComponent.get(component) ?? /* @__PURE__ */ new Map();
8439
+ for (const field of fields) {
8440
+ addStoryToSet(fieldMap, field, storyIdentifier);
8441
+ }
8442
+ issues.driftByComponent.set(component, fieldMap);
8443
+ }
8444
+ callback();
8445
+ }
8446
+ })
8447
+ );
8448
+ } catch (error) {
8449
+ if (error?.code !== "ENOENT") {
8450
+ throw error;
8451
+ }
8452
+ }
8453
+ return issues;
8454
+ };
8455
+ const formatStoryList = (stories) => {
8456
+ const sorted = [...stories].sort();
8457
+ if (sorted.length <= MAX_STORIES_PER_ENTRY) {
8458
+ return sorted.join(", ");
8459
+ }
8460
+ const shown = sorted.slice(0, MAX_STORIES_PER_ENTRY).join(", ");
8461
+ return `${shown}, and ${sorted.length - MAX_STORIES_PER_ENTRY} more`;
8462
+ };
8463
+ const formatSchemaIssues = (issues) => {
8464
+ const lines = ["Schema validation failed. Push aborted."];
8465
+ if (issues.missingSchemas.size > 0) {
8466
+ lines.push("");
8467
+ lines.push("Missing component schemas:");
8468
+ const sortedMissing = [...issues.missingSchemas.entries()].sort(([a], [b]) => a.localeCompare(b));
8469
+ for (const [component, stories] of sortedMissing) {
8470
+ lines.push(` - ${component} (in stories: ${formatStoryList(stories)})`);
8471
+ }
8472
+ lines.push("");
8473
+ lines.push("If these components exist in Storyblok, run `storyblok components pull` to sync them locally.");
8474
+ lines.push("Otherwise, create them in Storyblok first, or remove the references from the affected stories.");
8475
+ }
8476
+ if (issues.driftByComponent.size > 0) {
8477
+ lines.push("");
8478
+ lines.push("Fields not declared in local schemas:");
8479
+ const sortedComponents = [...issues.driftByComponent.entries()].sort(([a], [b]) => a.localeCompare(b));
8480
+ for (const [component, fieldMap] of sortedComponents) {
8481
+ const sortedFields = [...fieldMap.entries()].sort(([a], [b]) => a.localeCompare(b));
8482
+ for (const [field, stories] of sortedFields) {
8483
+ lines.push(` - ${component}.${field} (in stories: ${formatStoryList(stories)})`);
8484
+ }
8485
+ }
8486
+ lines.push("");
8487
+ lines.push("These fields will be lost when the stories are pushed. To fix, either:");
8488
+ lines.push(" - Add the field to the component in Storyblok, then run `storyblok components pull`");
8489
+ lines.push(" - Or remove the field from the affected story JSON files");
8490
+ }
8491
+ return lines.join("\n");
8492
+ };
8493
+ const hasSchemaIssues = (issues) => issues.missingSchemas.size > 0 || issues.driftByComponent.size > 0;
8494
+
8397
8495
  const PROGRESS_BAR_PADDING = 23;
8398
8496
  const upsertAssetFoldersPipeline = async ({
8399
8497
  directoryPath,
@@ -8443,7 +8541,7 @@ const upsertAssetsPipeline = async ({
8443
8541
  ui
8444
8542
  }) => {
8445
8543
  const assetProgress = ui.createProgressBar({ title: "Assets...".padEnd(PROGRESS_BAR_PADDING) });
8446
- const summary = { total: 0, succeeded: 0, failed: 0, skipped: 0 };
8544
+ const summary = { total: 0, succeeded: 0, failed: 0 };
8447
8545
  const steps = [];
8448
8546
  if (assetBinaryPath && assetData) {
8449
8547
  summary.total = 1;
@@ -8489,20 +8587,6 @@ const upsertAssetsPipeline = async ({
8489
8587
  summary.succeeded += 1;
8490
8588
  logger.info("Uploaded asset", { assetId: remoteAsset.id });
8491
8589
  },
8492
- onAssetSkipped: (localAssetResult, remoteAsset) => {
8493
- if ("id" in localAssetResult && localAssetResult.id) {
8494
- maps.assets.set(localAssetResult.id, {
8495
- old: localAssetResult,
8496
- new: {
8497
- id: remoteAsset.id,
8498
- filename: remoteAsset.filename,
8499
- meta_data: remoteAsset.meta_data
8500
- }
8501
- });
8502
- }
8503
- summary.skipped += 1;
8504
- logger.debug("Skipped asset (unchanged)", { assetId: remoteAsset.id });
8505
- },
8506
8590
  onAssetError: (error, asset) => {
8507
8591
  summary.failed += 1;
8508
8592
  logOnlyError(error, { assetId: asset.id });
@@ -8601,7 +8685,8 @@ const mapAssetReferencesInStoriesPipeline = async ({
8601
8685
  onIncrement() {
8602
8686
  processProgress.increment();
8603
8687
  },
8604
- onStorySuccess(localStory, _, missingSchemas) {
8688
+ onStorySuccess(localStory) {
8689
+ const { missingSchemas } = validateStoryAgainstSchemas(localStory, schemas);
8605
8690
  warnAboutMissingSchemas(missingSchemas, localStory);
8606
8691
  logger.info("Processed story", { storyId: localStory.uuid });
8607
8692
  summaries.storyProcessResults.succeeded += 1;
@@ -8634,7 +8719,7 @@ const mapAssetReferencesInStoriesPipeline = async ({
8634
8719
  return Object.entries(summaries);
8635
8720
  };
8636
8721
 
8637
- const pushCmd$1 = assetsCommand.command("push").argument("[asset]", "path or URL of a single asset to push").option("-s, --space <space>", "space ID").option("-f, --from <from>", "source space id").option("--data <data>", "inline asset data as JSON").option("--short-filename <short-filename>", "override the asset filename").option("--folder <folderId>", "destination asset folder ID").option("--cleanup", "delete local assets and metadata after a successful push (note: does not cleanup manifests)").option("--update-stories", "update file references in stories if necessary", false).option("--asset-token <token>", "asset token for accessing private assets").option("-d, --dry-run", "Preview changes without applying them to Storyblok").description(`Push local assets to a Storyblok space.`);
8722
+ const pushCmd$1 = assetsCommand.command("push").argument("[asset]", "path or URL of a single asset to push").option("-s, --space <space>", "space ID").option("-f, --from <from>", "source space id").option("--data <data>", "inline asset data as JSON").option("--short-filename <short-filename>", "override the asset filename").option("--folder <folderId>", "destination asset folder ID").option("--cleanup", "delete local assets and metadata after a successful push (note: does not cleanup manifests)").option("--update-stories", "update file references in stories if necessary", false).option("-d, --dry-run", "Preview changes without applying them to Storyblok").description(`Push local assets to a Storyblok space.`);
8638
8723
  pushCmd$1.action(async (assetInput, options, command) => {
8639
8724
  const ui = getUI();
8640
8725
  const logger = getLogger();
@@ -8648,7 +8733,6 @@ pushCmd$1.action(async (assetInput, options, command) => {
8648
8733
  }
8649
8734
  const { space: targetSpace, path: basePath, verbose } = command.optsWithGlobals();
8650
8735
  const fromSpace = options.from || targetSpace;
8651
- const assetToken = options.assetToken;
8652
8736
  const { state } = session();
8653
8737
  if (!requireAuthentication(state, verbose)) {
8654
8738
  process.exitCode = 2;
@@ -8659,7 +8743,6 @@ pushCmd$1.action(async (assetInput, options, command) => {
8659
8743
  process.exitCode = 2;
8660
8744
  return;
8661
8745
  }
8662
- const { region } = state;
8663
8746
  const summaries = [];
8664
8747
  let fatalError = false;
8665
8748
  const manifestFile = join(resolveCommandPath(directories.assets, fromSpace, basePath), "manifest.jsonl");
@@ -8706,10 +8789,6 @@ pushCmd$1.action(async (assetInput, options, command) => {
8706
8789
  const createAssetTransport = options.dryRun ? async (asset) => asset : makeCreateAssetAPITransport({ spaceId: targetSpace });
8707
8790
  const updateAssetTransport = options.dryRun ? async () => {
8708
8791
  } : makeUpdateAssetAPITransport({ spaceId: targetSpace });
8709
- const downloadAssetFileTransport = makeDownloadAssetFileTransport({
8710
- assetToken,
8711
- region
8712
- });
8713
8792
  const assetManifestTransport = options.dryRun ? () => Promise.resolve() : makeAppendAssetManifestFSTransport({ manifestFile });
8714
8793
  const cleanupAssetTransport = options.cleanup && !options.dryRun ? makeCleanupAssetFSTransport() : () => Promise.resolve();
8715
8794
  summaries.push(...await upsertAssetsPipeline({
@@ -8722,7 +8801,6 @@ pushCmd$1.action(async (assetInput, options, command) => {
8722
8801
  getAsset: getAssetTransport,
8723
8802
  createAsset: createAssetTransport,
8724
8803
  updateAsset: updateAssetTransport,
8725
- downloadAssetFile: downloadAssetFileTransport,
8726
8804
  appendAssetManifest: assetManifestTransport,
8727
8805
  cleanupAsset: cleanupAssetTransport
8728
8806
  },
@@ -8757,12 +8835,11 @@ pushCmd$1.action(async (assetInput, options, command) => {
8757
8835
  logger.info("Pushing assets finished", { summary });
8758
8836
  const assetsTotal = summary.assetResults?.total ?? 0;
8759
8837
  const assetsSucceeded = summary.assetResults?.succeeded ?? 0;
8760
- const assetsSkipped = summary.assetResults?.skipped ?? 0;
8761
8838
  const assetsFailed = summary.assetResults?.failed ?? 0;
8762
8839
  ui.info(`Push results: ${assetsTotal} processed, ${assetsFailed} assets failed`);
8763
8840
  ui.list([
8764
8841
  `Folders: ${summary.assetFolderResults?.succeeded ?? 0}/${summary.assetFolderResults?.total ?? 0} succeeded, ${summary.assetFolderResults?.failed ?? 0} failed.`,
8765
- `Assets: ${assetsSucceeded}/${assetsTotal} succeeded, ${assetsSkipped} skipped, ${assetsFailed} failed.`
8842
+ `Assets: ${assetsSucceeded}/${assetsTotal} succeeded, ${assetsFailed} failed.`
8766
8843
  ]);
8767
8844
  for (const [name, reportSummary] of summaries) {
8768
8845
  reporter.addSummary(name, reportSummary);
@@ -8914,32 +8991,6 @@ pushCmd.action(async (options, command) => {
8914
8991
  return;
8915
8992
  }
8916
8993
  const pendingWarnings = [];
8917
- const warnedPlugins = /* @__PURE__ */ new Set();
8918
- const warnAboutCustomPlugins = (fields, story) => {
8919
- for (const field of fields) {
8920
- if (field.type === "custom" && typeof field.field_type === "string") {
8921
- if (warnedPlugins.has(field.field_type)) {
8922
- continue;
8923
- }
8924
- warnedPlugins.add(field.field_type);
8925
- const message = `The custom plugin "${field.field_type}" may contain references that require manual updates.`;
8926
- pendingWarnings.push(message);
8927
- logger.warn(message, { storyId: story.uuid });
8928
- }
8929
- }
8930
- };
8931
- const missingSchemaWarnings = /* @__PURE__ */ new Set();
8932
- const warnAboutMissingSchemas = (missingSchemas, story) => {
8933
- for (const schemaName of missingSchemas) {
8934
- if (missingSchemaWarnings.has(schemaName)) {
8935
- continue;
8936
- }
8937
- const message = `The component "${schemaName}" was not found. Please run \`storyblok components pull\` to fetch the latest components.`;
8938
- pendingWarnings.push(message);
8939
- logger.warn(message, { storyId: story.uuid });
8940
- missingSchemaWarnings.add(schemaName);
8941
- }
8942
- };
8943
8994
  const summary = {
8944
8995
  creationResults: { total: 0, succeeded: 0, skipped: 0, failed: 0 },
8945
8996
  processResults: { total: 0, succeeded: 0, failed: 0 },
@@ -8960,13 +9011,49 @@ pushCmd.action(async (options, command) => {
8960
9011
  logger.error(message);
8961
9012
  return;
8962
9013
  }
9014
+ const storiesDirectoryPath = resolveCommandPath(directories.stories, fromSpace, basePath);
9015
+ const schemaIssues = await collectSchemaIssues({ directoryPath: storiesDirectoryPath, schemas });
9016
+ if (hasSchemaIssues(schemaIssues)) {
9017
+ const message = formatSchemaIssues(schemaIssues);
9018
+ ui.error(message);
9019
+ logger.error(message);
9020
+ const total = Math.max(schemaIssues.total, 1);
9021
+ summary.creationResults.total = total;
9022
+ summary.creationResults.failed = total;
9023
+ return;
9024
+ }
9025
+ const warnAboutCustomPlugins = (story) => {
9026
+ const warned = /* @__PURE__ */ new Set();
9027
+ const visit = (node) => {
9028
+ if (Array.isArray(node)) {
9029
+ for (const item of node) {
9030
+ visit(item);
9031
+ }
9032
+ return;
9033
+ }
9034
+ if (!node || typeof node !== "object") {
9035
+ return;
9036
+ }
9037
+ const obj = node;
9038
+ const plugin = obj.plugin;
9039
+ if (typeof plugin === "string" && !warned.has(plugin)) {
9040
+ warned.add(plugin);
9041
+ const message = `The custom plugin "${plugin}" may contain references that require manual updates.`;
9042
+ pendingWarnings.push(message);
9043
+ logger.warn(message, { storyId: story.uuid });
9044
+ }
9045
+ for (const value of Object.values(obj)) {
9046
+ visit(value);
9047
+ }
9048
+ };
9049
+ visit(story.content);
9050
+ };
8963
9051
  const fetchProgress = ui.createProgressBar({ title: "Matching Stories...".padEnd(21) });
8964
9052
  const existingTargetStories = await prefetchTargetStories(space, {
8965
9053
  onTotal: (total) => fetchProgress.setTotal(total),
8966
9054
  onIncrement: (count) => fetchProgress.increment(count)
8967
9055
  });
8968
9056
  fetchProgress.stop();
8969
- const storiesDirectoryPath = resolveCommandPath(directories.stories, fromSpace, basePath);
8970
9057
  const scanProgress = ui.createProgressBar({ title: "Scanning Stories...".padEnd(21) });
8971
9058
  const storyIndex = await scanLocalStoryIndex({
8972
9059
  directoryPath: storiesDirectoryPath,
@@ -9070,9 +9157,8 @@ pushCmd.action(async (options, command) => {
9070
9157
  onIncrement() {
9071
9158
  processProgress.increment();
9072
9159
  },
9073
- onStorySuccess(localStory, processedFields, missingSchemas) {
9074
- warnAboutCustomPlugins(processedFields, localStory);
9075
- warnAboutMissingSchemas(missingSchemas, localStory);
9160
+ onStorySuccess(localStory) {
9161
+ warnAboutCustomPlugins(localStory);
9076
9162
  logger.info("Processed story", { storyId: localStory.uuid });
9077
9163
  summary.processResults.succeeded += 1;
9078
9164
  },