sb-mig 6.0.0-beta.6 → 6.0.0-beta.8

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.
@@ -246,7 +246,6 @@ const applySingleMigrationToItems = ({ itemType, itemsToMigrate, preparedMigrati
246
246
  };
247
247
  };
248
248
  export const runMigrationPipelineInMemory = ({ itemType, itemsToMigrate, preparedMigrationConfigs, }) => {
249
- const originalItems = deepClone(itemsToMigrate);
250
249
  let workingItems = deepClone(itemsToMigrate);
251
250
  const stepReports = [];
252
251
  for (const preparedMigrationConfig of preparedMigrationConfigs) {
@@ -283,7 +282,7 @@ export const runMigrationPipelineInMemory = ({ itemType, itemsToMigrate, prepare
283
282
  stepReports.push(stepReport);
284
283
  }
285
284
  const changedItems = workingItems.filter((item, index) => {
286
- const originalItem = originalItems[index];
285
+ const originalItem = itemsToMigrate[index];
287
286
  if (!originalItem) {
288
287
  return true;
289
288
  }
@@ -515,6 +514,7 @@ export const doTheMigration = async ({ itemType = "story", from, itemsToMigrate,
515
514
  options: {
516
515
  publish: Boolean(publish),
517
516
  publishLanguages: resolvedPublishLanguages,
517
+ preservePublishState: Boolean(publish),
518
518
  },
519
519
  }, config);
520
520
  }
@@ -2,7 +2,7 @@ import type { MigrateFrom, MigrationPipelineResult } from "./component-data-migr
2
2
  import type { MutationWriteResult, MutationWriteSummary } from "./write-summary.js";
3
3
  import type { PublishLanguagesOption } from "../stories/stories.types.js";
4
4
  import type { RequestBaseConfig } from "../utils/request.js";
5
- type MigrationRunLogEvent = "update_success" | "update_failed" | "publish_success" | "publish_failed" | "migration_write_summary";
5
+ type MigrationRunLogEvent = "update_success" | "update_failed" | "publish_success" | "publish_failed" | "publish_skipped" | "migration_write_summary";
6
6
  export interface MigrationRunLogRecord {
7
7
  timestamp: string;
8
8
  event: MigrationRunLogEvent;
@@ -37,6 +37,8 @@ export interface MigrationRunLogRecord {
37
37
  status?: number | string;
38
38
  response?: string | null;
39
39
  stage?: "update" | "publish";
40
+ sourcePublishState?: string;
41
+ publishSkippedReason?: string;
40
42
  }>;
41
43
  };
42
44
  item?: {
@@ -49,6 +51,8 @@ export interface MigrationRunLogRecord {
49
51
  status?: number | string | null;
50
52
  response?: string | null;
51
53
  stage?: "update" | "publish";
54
+ sourcePublishState?: string;
55
+ publishSkippedReason?: string;
52
56
  error?: unknown;
53
57
  }
54
58
  interface SaveMigrationRunLogArgs {
@@ -64,13 +64,15 @@ export const buildMigrationRunLogRecords = ({ from, to, itemType, dryRun, publis
64
64
  const value = resolveWriteResultValue(result);
65
65
  const changedItem = resolveChangedItemPayload(pipelineResult.changedItems[index]);
66
66
  const stage = value.stage || "update";
67
- const event = value.ok
68
- ? stage === "publish"
69
- ? "publish_success"
70
- : "update_success"
71
- : stage === "publish"
72
- ? "publish_failed"
73
- : "update_failed";
67
+ const event = value.publishSkippedReason
68
+ ? "publish_skipped"
69
+ : value.ok
70
+ ? stage === "publish"
71
+ ? "publish_success"
72
+ : "update_success"
73
+ : stage === "publish"
74
+ ? "publish_failed"
75
+ : "update_failed";
74
76
  return {
75
77
  ...baseRecord,
76
78
  event,
@@ -86,6 +88,8 @@ export const buildMigrationRunLogRecords = ({ from, to, itemType, dryRun, publis
86
88
  status: value.status || null,
87
89
  response: value.response || null,
88
90
  stage,
91
+ sourcePublishState: value.sourcePublishState,
92
+ publishSkippedReason: value.publishSkippedReason,
89
93
  ...(value.ok ? {} : { error: serializeError(value.error) }),
90
94
  };
91
95
  });
@@ -104,6 +108,8 @@ export const buildMigrationRunLogRecords = ({ from, to, itemType, dryRun, publis
104
108
  status: item.status,
105
109
  response: item.response || null,
106
110
  stage: item.stage,
111
+ sourcePublishState: item.sourcePublishState,
112
+ publishSkippedReason: item.publishSkippedReason,
107
113
  })),
108
114
  },
109
115
  };
@@ -8,6 +8,8 @@ export interface MutationWriteResult {
8
8
  status?: number | string;
9
9
  response?: string | null;
10
10
  publishLanguages?: string[];
11
+ sourcePublishState?: string;
12
+ publishSkippedReason?: string;
11
13
  error?: unknown;
12
14
  }
13
15
  export interface MutationWriteSummary {
@@ -1,4 +1,24 @@
1
1
  import type { GetAllStories, GetStoryById, RemoveStory, CreateStory, UpdateStory, UpdateStories, RemoveAllStories, UpsertStory, DeepUpsertStory, GetStoryBySlug, PublishLanguagesOption } from "./stories.types.js";
2
+ type StoryPublishState = {
3
+ status: "draft";
4
+ shouldPublish: false;
5
+ skipReason: "source_story_draft";
6
+ message: string;
7
+ } | {
8
+ status: "published_with_unpublished_changes";
9
+ shouldPublish: false;
10
+ skipReason: "source_story_has_unpublished_changes";
11
+ message: string;
12
+ } | {
13
+ status: "published_unknown";
14
+ shouldPublish: false;
15
+ skipReason: "source_story_publish_state_unknown";
16
+ message: string;
17
+ } | {
18
+ status: "published_clean";
19
+ shouldPublish: true;
20
+ };
21
+ export declare const resolveStoryPublishState: (story: any) => StoryPublishState;
2
22
  export declare const parsePublishLanguagesOption: (publishLanguages?: string) => PublishLanguagesOption;
3
23
  export declare const resolvePublishLanguageCodes: (publishLanguages: PublishLanguagesOption | undefined, config: {
4
24
  spaceId: string;
@@ -22,3 +42,4 @@ export declare const publishStoryLanguages: ({ storyId, story, languages, }: {
22
42
  export declare const updateStories: UpdateStories;
23
43
  export declare const upsertStory: UpsertStory;
24
44
  export declare const deepUpsertStory: DeepUpsertStory;
45
+ export {};
@@ -1,9 +1,12 @@
1
1
  import chalk from "chalk";
2
+ import { mapWithConcurrency } from "../../utils/async-utils.js";
2
3
  import Logger from "../../utils/logger.js";
3
4
  import { notNullish } from "../../utils/object-utils.js";
4
5
  import { getAllItemsWithPagination } from "../utils/request.js";
5
6
  const resolveStoryLabel = (content, storyId) => content?.full_slug || content?.slug || content?.name || String(storyId);
6
7
  const DEFAULT_PUBLISH_LANGUAGE = "[default]";
8
+ const STORY_CONTENT_FETCH_CONCURRENCY = 10;
9
+ const isSkippedStoryPublishState = (publishState) => publishState?.shouldPublish === false;
7
10
  const isDefaultLanguageToken = (language) => language.toLowerCase() === "default" ||
8
11
  language === DEFAULT_PUBLISH_LANGUAGE;
9
12
  const normalizePublishLanguageCodes = (languages) => {
@@ -19,6 +22,46 @@ const normalizePublishLanguageCodes = (languages) => {
19
22
  }
20
23
  return Array.from(new Set(cleanLanguages));
21
24
  };
25
+ export const resolveStoryPublishState = (story) => {
26
+ if (story?.published !== true) {
27
+ return {
28
+ status: "draft",
29
+ shouldPublish: false,
30
+ skipReason: "source_story_draft",
31
+ message: "source story was draft-only",
32
+ };
33
+ }
34
+ if (story.unpublished_changes === true) {
35
+ return {
36
+ status: "published_with_unpublished_changes",
37
+ shouldPublish: false,
38
+ skipReason: "source_story_has_unpublished_changes",
39
+ message: "source story had unpublished draft changes",
40
+ };
41
+ }
42
+ if (story.unpublished_changes !== false) {
43
+ return {
44
+ status: "published_unknown",
45
+ shouldPublish: false,
46
+ skipReason: "source_story_publish_state_unknown",
47
+ message: "source story publish state was missing unpublished_changes",
48
+ };
49
+ }
50
+ return {
51
+ status: "published_clean",
52
+ shouldPublish: true,
53
+ };
54
+ };
55
+ const withSkippedPublish = ({ updateResult, story, storyId, spaceId, publishLanguages, publishState, }) => {
56
+ const storyLabel = resolveStoryLabel(story, String(storyId));
57
+ Logger.warning(`Skipping publish for story '${storyLabel}' in space '${spaceId}' because ${publishState.message}.`);
58
+ return {
59
+ ...updateResult,
60
+ sourcePublishState: publishState.status,
61
+ publishSkippedReason: publishState.skipReason,
62
+ ...(publishLanguages ? { publishLanguages } : {}),
63
+ };
64
+ };
22
65
  export const parsePublishLanguagesOption = (publishLanguages) => {
23
66
  if (!publishLanguages) {
24
67
  return "default";
@@ -144,7 +187,7 @@ export const getAllStories = async (args, config) => {
144
187
  });
145
188
  Logger.success(`Successfully pre-fetched ${allStoriesWithoutContent.length} stories.`);
146
189
  let heartBeat = 0;
147
- const allStories = await Promise.all(allStoriesWithoutContent.map(async (story) => {
190
+ const allStories = await mapWithConcurrency(allStoriesWithoutContent, STORY_CONTENT_FETCH_CONCURRENCY, async (story) => {
148
191
  const result = await getStoryById(story.id, config);
149
192
  heartBeat++;
150
193
  if (heartBeat % 10 === 0 ||
@@ -152,7 +195,7 @@ export const getAllStories = async (args, config) => {
152
195
  Logger.success(`Successfully fetched ${heartBeat} stories with full content.`);
153
196
  }
154
197
  return result;
155
- }));
198
+ });
156
199
  return allStories;
157
200
  };
158
201
  // GET
@@ -291,6 +334,7 @@ export const publishStoryLanguages = async ({ storyId, story, languages, }, conf
291
334
  export const updateStories = async (args, config) => {
292
335
  const { stories, options, spaceId } = args;
293
336
  const shouldPublishLanguages = options.publish && options.publishLanguages !== undefined;
337
+ const shouldPreservePublishState = Boolean(options.preservePublishState);
294
338
  const publishLanguages = shouldPublishLanguages
295
339
  ? await resolvePublishLanguageCodes(options.publishLanguages, {
296
340
  ...config,
@@ -301,9 +345,27 @@ export const updateStories = async (args, config) => {
301
345
  // Run through stories, and update the space with migrated version of stories
302
346
  stories.map(async (stories) => {
303
347
  const story = stories.story;
348
+ const publishState = shouldPreservePublishState
349
+ ? resolveStoryPublishState(story)
350
+ : undefined;
351
+ const shouldPublishStory = options.publish &&
352
+ (!publishState || publishState.shouldPublish);
304
353
  const updateResult = await updateStory(story, story.id, {
305
- publish: options.publish && !shouldPublishLanguages,
354
+ publish: shouldPublishStory && !shouldPublishLanguages,
355
+ force_update: options.force_update,
306
356
  }, { ...config, spaceId });
357
+ if (options.publish &&
358
+ updateResult?.ok &&
359
+ isSkippedStoryPublishState(publishState)) {
360
+ return withSkippedPublish({
361
+ updateResult,
362
+ story,
363
+ storyId: story.id,
364
+ spaceId,
365
+ publishLanguages,
366
+ publishState,
367
+ });
368
+ }
307
369
  if (!shouldPublishLanguages ||
308
370
  !publishLanguages ||
309
371
  !updateResult?.ok) {
@@ -7,6 +7,7 @@ interface ModifyStoryOptions {
7
7
  publish?: boolean;
8
8
  force_update?: boolean;
9
9
  publishLanguages?: PublishLanguagesOption;
10
+ preservePublishState?: boolean;
10
11
  }
11
12
  export type PublishLanguagesOption = "default" | "all" | string[];
12
13
  export type RemoveStory = (args: {
@@ -1,7 +1,7 @@
1
1
  export declare const mainDescription = "\n USAGE\n $ sb-mig [command]\n \n COMMANDS\n sync Synchronize components, datasources, roles, stories, assets with Storyblok space.\n discover Discover components, migration configs and write to file or stdout.\n backup Command for backing up anything related to Storyblok\n migrate Migrate content from space to space, or from file to space.\n debug Output extra debugging information\n help This screen\n \n Examples\n $ sb-mig sync components --all\n $ sb-mig debug \n";
2
2
  export declare const syncDescription = "\n Usage\n $ sb-mig sync [components|roles|datasources|plugins|content] [space separated file names] or --all\n \n Description\n Synchronize components, roles, datasources, plugins, content with Storyblok space.\n \n COMMANDS\n components - sync components\n roles - sync roles\n datasources - sync datasources\n plugins - sync plugins\n content - sync content (stories, assets) - ! right now destructive, it will move content from 1 space to another, completelly overwriting it\n \n FLAGS\n --all - Sync all components, roles, datasources [components, roles, datasources]\n --presets - Pass it, if u want to sync also with presets (will take longer) [components only]\n --dry-run - Preview planned changes without making writes [components, roles, datasources, plugins, content]\n \n --yes - Skip ask for confirmation (dangerous, but useful in CI/CD) [content only]\n --from - Space ID from which you want to sync content [content only]\n --to - Space ID to which you want to sync content [content only]\n --syncDirection [fromSpaceToFile|fromFileToSpace|fromSpaceToSpace|fromAWStoSpace] \n - Sync direction (from, to) [content only]\n \n EXAMPLES\n $ sb-mig sync components --all\n $ sb-mig sync components --all --dry-run\n $ sb-mig sync components --all --presets\n $ sb-mig sync components accordion accordion-item\n $ sb-mig sync components accordion accordion-item --presets\n \n $ sb-mig sync roles --all\n $ sb-mig sync roles --all --dry-run\n \n $ sb-mig sync datasources --all\n $ sb-mig sync datasources --all --dry-run\n \n $ sb-mig sync plugins my-awesome-plugin - (you have to be in catalog which has ./dist/export.js file with compiled plugin)\n \n $ sb-mig sync content --all --from 12345 --to 12345\n $ sb-mig sync content --stories --from 12345 --to 12345\n $ sb-mig sync content --assets --from 12345 --to 12345\n";
3
3
  export declare const copyDescription = "\n Usage\n $ sb-mig copy\n \n Description\n Copy stuff\n \n COMMANDS\n ?\n \n FLAGS\n ?\n \n EXAMPLES\n $ sb-mig copy ?\n";
4
- export declare const migrateDescription = "\n Usage\n $ sb-mig migrate [content] [space separated file names] or --all --from [spaceId] --to [spaceId] --migration [migration-config-filename]\n $ sb-mig migrate content --all --migration migration-a --migration migration-b --migration migration-c\n \n Description\n Migrate content from space to space, or from file to space. It's potentially dangerous command, so it will ask for confirmation.\n Use with care.\n \n COMMANDS\n content - migrate content \n \n FLAGS\n --from - Space ID from which you want to migrate / or file name if passed '--migrate-from file'\n --fromFilePath - Direct path to stories JSON file when using '--migrate-from file'\n --to - Space ID to which you want to migrate\n --migrate-from - Migrate from (space, file) default: space\n --migration - File name of migration file (without extension). Can be repeated for ordered pipeline in content migration.\n --migrationComponentAlias - Add extra component aliases for a migration. Repeatable. Format: <migration>:<source>=<alias1>,<alias2>\n --migrationComponents - Override the exact component scope for a migration. Repeatable. Format: <migration>:<component1>,<component2>\n --withSlug - Filter stories by full slug (can be repeated)\n --startsWith - Filter stories by starts_with prefix\n --yes - Skip ask for confirmation (dangerous, but useful in CI/CD)\n --dry-run - Preview what would be migrated without making any API changes\n --publish - Publish changed stories immediately after migration. Default: save draft. [content only]\n --publishLanguages - Languages to publish when --publish is set. Values: default, all, or comma-separated Storyblok language codes. [content only]\n --fileName - Stable base name for migration output files (disables timestamp suffix for migration artifacts)\n\n EXAMPLES\n $ sb-mig migrate content --all --from 12345 --to 12345 --migration file-with-migration\n $ sb-mig migrate content --all --from 12345 --to 12345 --migration migration-a --migration migration-b --migration migration-c\n $ sb-mig migrate content --all --from 12345 --to 12345 --migration colorPickerModeValues --migrationComponentAlias colorPickerModeValues:sb-button=sb-open-drift-button\n $ sb-mig migrate content --all --from 12345 --to 12345 --migration colorPickerModeValues --migrationComponentAlias colorPickerModeValues:sb-section=sb-tour-page-section --migrationComponents colorPickerModeValues:sb-section,sb-tour-page-section\n $ sb-mig migrate content --all --from 12345 --to 12345 --migration file-with-migration --withSlug blog/home --withSlug docs/getting-started\n $ sb-mig migrate content --all --from 12345 --to 12345 --migration file-with-migration --startsWith blog/\n $ sb-mig migrate content --all --from 12345 --to 12345 --migration file-with-migration --publish --yes\n $ sb-mig migrate content --all --from 12345 --to 12345 --migration file-with-migration --publish --publishLanguages all --yes\n $ sb-mig migrate content --all --from 12345 --to 12345 --migration file-with-migration --publish --publishLanguages default,fr,de --yes\n $ sb-mig migrate content --all --from 12345 --to 12345 --migration v3toV4AllMigrations --dry-run --fileName brand-hub-v3-v4-run\n $ sb-mig migrate content --all --migrate-from file --from file-with-stories --to 12345 --migration file-with-migration\n $ sb-mig migrate content --all --migrate-from file --fromFilePath sbmig/migrations/dry-run--123---story-to-migrate__2026-2-9_20-51.json --to 12345 --migration migration-a --migration migration-b\n $ sb-mig migrate content my-component-1 my-component-2 --from 12345 --to 12345 --migration file-with-migration\n $ sb-mig migrate content my-component-1 my-component-2 --migrate-from file --from file-with-stories --to 12345 --migration file-with-migration \n";
4
+ export declare const migrateDescription = "\n Usage\n $ sb-mig migrate [content] [space separated file names] or --all --from [spaceId] --to [spaceId] --migration [migration-config-filename]\n $ sb-mig migrate content --all --migration migration-a --migration migration-b --migration migration-c\n \n Description\n Migrate content from space to space, or from file to space. It's potentially dangerous command, so it will ask for confirmation.\n Use with care.\n \n COMMANDS\n content - migrate content \n \n FLAGS\n --from - Space ID from which you want to migrate / or file name if passed '--migrate-from file'\n --fromFilePath - Direct path to stories JSON file when using '--migrate-from file'\n --to - Space ID to which you want to migrate\n --migrate-from - Migrate from (space, file) default: space\n --migration - File name of migration file (without extension). Can be repeated for ordered pipeline in content migration.\n --migrationComponentAlias - Add extra component aliases for a migration. Repeatable. Format: <migration>:<source>=<alias1>,<alias2>\n --migrationComponents - Override the exact component scope for a migration. Repeatable. Format: <migration>:<component1>,<component2>\n --withSlug - Filter stories by full slug (can be repeated)\n --startsWith - Filter stories by starts_with prefix\n --yes - Skip ask for confirmation (dangerous, but useful in CI/CD)\n --dry-run - Preview what would be migrated without making any API changes\n --publish - Publish changed stories after migration only if they were clean-published before migration. Default: save draft. [content only]\n --publishLanguages - Languages to publish when --publish is set. Values: default, all, or comma-separated Storyblok language codes. Skips stories that were draft-only or had unpublished changes before migration. [content only]\n --fileName - Stable base name for migration output files (disables timestamp suffix for migration artifacts)\n\n EXAMPLES\n $ sb-mig migrate content --all --from 12345 --to 12345 --migration file-with-migration\n $ sb-mig migrate content --all --from 12345 --to 12345 --migration migration-a --migration migration-b --migration migration-c\n $ sb-mig migrate content --all --from 12345 --to 12345 --migration colorPickerModeValues --migrationComponentAlias colorPickerModeValues:sb-button=sb-open-drift-button\n $ sb-mig migrate content --all --from 12345 --to 12345 --migration colorPickerModeValues --migrationComponentAlias colorPickerModeValues:sb-section=sb-tour-page-section --migrationComponents colorPickerModeValues:sb-section,sb-tour-page-section\n $ sb-mig migrate content --all --from 12345 --to 12345 --migration file-with-migration --withSlug blog/home --withSlug docs/getting-started\n $ sb-mig migrate content --all --from 12345 --to 12345 --migration file-with-migration --startsWith blog/\n $ sb-mig migrate content --all --from 12345 --to 12345 --migration file-with-migration --publish --yes\n $ sb-mig migrate content --all --from 12345 --to 12345 --migration file-with-migration --publish --publishLanguages all --yes\n $ sb-mig migrate content --all --from 12345 --to 12345 --migration file-with-migration --publish --publishLanguages default,fr,de --yes\n $ sb-mig migrate content --all --from 12345 --to 12345 --migration v3toV4AllMigrations --dry-run --fileName brand-hub-v3-v4-run\n $ sb-mig migrate content --all --migrate-from file --from file-with-stories --to 12345 --migration file-with-migration\n $ sb-mig migrate content --all --migrate-from file --fromFilePath sbmig/migrations/dry-run--123---story-to-migrate__2026-2-9_20-51.json --to 12345 --migration migration-a --migration migration-b\n $ sb-mig migrate content my-component-1 my-component-2 --from 12345 --to 12345 --migration file-with-migration\n $ sb-mig migrate content my-component-1 my-component-2 --migrate-from file --from file-with-stories --to 12345 --migration file-with-migration \n";
5
5
  export declare const revertDescription = "\n Usage\n $ sb-mig revert [content] --migration\n \n Description\n Revert content migration\n \n COMMANDS\n content - revert content migration \n \n FLAGS\n --migration - ???\n --yes - Skip ask for confirmation (dangerous, but useful in CI/CD) \n \n EXAMPLES\n $ sb-mig revert content --migration \n";
6
6
  export declare const discoverDescription = "\n Usage\n $ sb-mig discover [components|migrations] --all --write\n\n Description\n Discover all components or migration configs and write to file or stdout\n\n COMMANDS\n components - discover components\n migrations - discover migration config files\n\n FLAGS\n --all - Discover all components or migration configs\n --write - Write to file\n\n EXAMPLES\n $ sb-mig discover components --all\n $ sb-mig discover components --all --write\n $ sb-mig discover migrations --all\n";
7
7
  export declare const migrationsDescription = "\n Usage\n $ sb-mig migrations recognize\n \n Description\n Recognize migrations you need to apply\n \n COMMANDS\n recognize - recognize migrations\n \n FLAGS \n \n EXAMPLES\n $ sb-mig migrations recognize\n\n";
@@ -98,8 +98,8 @@ export const migrateDescription = `
98
98
  --startsWith - Filter stories by starts_with prefix
99
99
  --yes - Skip ask for confirmation (dangerous, but useful in CI/CD)
100
100
  --dry-run - Preview what would be migrated without making any API changes
101
- --publish - Publish changed stories immediately after migration. Default: save draft. [content only]
102
- --publishLanguages - Languages to publish when --publish is set. Values: default, all, or comma-separated Storyblok language codes. [content only]
101
+ --publish - Publish changed stories after migration only if they were clean-published before migration. Default: save draft. [content only]
102
+ --publishLanguages - Languages to publish when --publish is set. Values: default, all, or comma-separated Storyblok language codes. Skips stories that were draft-only or had unpublished changes before migration. [content only]
103
103
  --fileName - Stable base name for migration output files (disables timestamp suffix for migration artifacts)
104
104
 
105
105
  EXAMPLES
@@ -1,12 +1,12 @@
1
+ import { pathToFileURL } from "url";
1
2
  import Logger from "../utils/logger.js";
3
+ const toImportSpecifier = (filePath) => process.platform === "win32" ? pathToFileURL(filePath).href : filePath;
2
4
  export { defaultConfig } from "./defaultConfig.js";
3
5
  export { SCHEMA } from "./constants.js";
4
6
  export const getStoryblokConfigContent = (data) => {
5
- let prefix = "";
6
- if (process.platform === "win32") {
7
- prefix = "file://";
8
- }
9
- return import(`${prefix}${data.filePath}${data.ext}`)
7
+ const configSpecifier = toImportSpecifier(`${data.filePath}${data.ext}`);
8
+ const fallbackConfigSpecifier = toImportSpecifier(`${data.filePath}.mjs`);
9
+ return import(/* @vite-ignore */ configSpecifier)
10
10
  .then((res) => {
11
11
  Logger.success("Found storyblok.config.js!");
12
12
  return res.default;
@@ -14,10 +14,9 @@ export const getStoryblokConfigContent = (data) => {
14
14
  .catch(() => {
15
15
  Logger.warning("Cannot find requested file with .js extension.");
16
16
  Logger.log("Trying .mjs extension\n");
17
- return import(`${prefix}${data.filePath}.mjs`)
17
+ return import(/* @vite-ignore */ fallbackConfigSpecifier)
18
18
  .then((res) => {
19
19
  Logger.success("Found storyblok.config.mjs!");
20
- console.log("res", res);
21
20
  return res.default;
22
21
  })
23
22
  .catch(() => {
@@ -11,3 +11,4 @@
11
11
  * await delay(1000); // Wait 1 second
12
12
  */
13
13
  export declare const delay: (time: number) => Promise<void>;
14
+ export declare const mapWithConcurrency: <T, R>(items: T[], concurrency: number, mapper: (item: T, index: number) => Promise<R>) => Promise<R[]>;
@@ -11,3 +11,19 @@
11
11
  * await delay(1000); // Wait 1 second
12
12
  */
13
13
  export const delay = (time) => new Promise((resolve) => setTimeout(resolve, time));
14
+ export const mapWithConcurrency = async (items, concurrency, mapper) => {
15
+ if (items.length === 0) {
16
+ return [];
17
+ }
18
+ const limit = Math.max(1, Math.floor(concurrency));
19
+ const results = new Array(items.length);
20
+ let nextIndex = 0;
21
+ const workers = Array.from({ length: Math.min(limit, items.length) }, async () => {
22
+ while (nextIndex < items.length) {
23
+ const currentIndex = nextIndex++;
24
+ results[currentIndex] = await mapper(items[currentIndex], currentIndex);
25
+ }
26
+ });
27
+ await Promise.all(workers);
28
+ return results;
29
+ };
@@ -1,4 +1,5 @@
1
1
  import type { RequestBaseConfig } from "../api/utils/request.js";
2
+ export declare const toImportSpecifier: (filePath: string, platform?: NodeJS.Platform) => string;
2
3
  /**
3
4
  * Asynchronously load a file using dynamic import
4
5
  * Returns the default export of the module
@@ -1,11 +1,25 @@
1
1
  import * as fs from "fs";
2
2
  import { writeFile } from "fs";
3
3
  import { createRequire } from "module";
4
- import path from "path";
4
+ import nodePath from "path";
5
+ import { pathToFileURL } from "url";
5
6
  import pkg from "ncp";
6
7
  import { generateDatestamp } from "./date-utils.js";
7
8
  import Logger from "./logger.js";
8
9
  const { ncp } = pkg;
10
+ const resolveFromCwd = (filePath, pathApi = nodePath) => pathApi.isAbsolute(filePath)
11
+ ? filePath
12
+ : pathApi.resolve(process.cwd(), filePath);
13
+ export const toImportSpecifier = (filePath, platform = process.platform) => {
14
+ if (/^(file|data|node):/.test(filePath)) {
15
+ return filePath;
16
+ }
17
+ const pathApi = platform === "win32" ? nodePath.win32 : nodePath;
18
+ const resolvedPath = resolveFromCwd(filePath, pathApi);
19
+ return platform === "win32"
20
+ ? pathToFileURL(resolvedPath, { windows: true }).href
21
+ : resolvedPath;
22
+ };
9
23
  // ============================================================================
10
24
  // File Content Loading
11
25
  // ============================================================================
@@ -17,7 +31,7 @@ const { ncp } = pkg;
17
31
  * @returns The default export of the imported module
18
32
  */
19
33
  export const getFileContent = (data) => {
20
- return import(data.file)
34
+ return import(/* @vite-ignore */ toImportSpecifier(data.file))
21
35
  .then((res) => {
22
36
  return res.default;
23
37
  })
@@ -56,14 +70,14 @@ export const getFilesContentWithRequire = (data) => {
56
70
  * @returns Parsed package.json object
57
71
  */
58
72
  export const getPackageJson = () => {
59
- const packageJsonPath = path.join(process.cwd(), "package.json");
73
+ const packageJsonPath = nodePath.join(process.cwd(), "package.json");
60
74
  const packageJsonContent = fs.readFileSync(packageJsonPath, "utf-8");
61
75
  const packageJson = JSON.parse(packageJsonContent);
62
76
  return packageJson;
63
77
  };
64
78
  export const isDirectoryExists = (path) => fs.existsSync(path);
65
79
  export const createDir = async (dirPath) => {
66
- await fs.promises.mkdir(`${process.cwd()}/${dirPath}`, {
80
+ await fs.promises.mkdir(resolveFromCwd(dirPath), {
67
81
  recursive: true,
68
82
  });
69
83
  };
@@ -103,10 +117,10 @@ export const copyFolder = async (src, dest) => {
103
117
  });
104
118
  };
105
119
  export const copyFile = async (src, dest) => {
106
- const directory = dest.split("/").slice(0, dest.split("/").length - 1);
107
- const fileName = src.split("/")[src.split("/").length - 1];
108
- if (!isDirectoryExists(directory.join("/"))) {
109
- await createDir(directory.join("/"));
120
+ const directory = nodePath.dirname(dest);
121
+ const fileName = nodePath.basename(src);
122
+ if (!isDirectoryExists(directory)) {
123
+ await createDir(directory);
110
124
  }
111
125
  fs.copyFile(src, dest, (err) => {
112
126
  if (err) {
@@ -141,10 +155,7 @@ export const createAndSaveToFile = async (args, config) => {
141
155
  Logger.success(`All response written to a file: ${fullPath}`);
142
156
  }
143
157
  if (path) {
144
- const folderPath = path
145
- .split("/")
146
- .slice(0, path.split("/").length - 1)
147
- .join("/");
158
+ const folderPath = nodePath.dirname(path);
148
159
  await createDir(folderPath);
149
160
  await createJsonFile(JSON.stringify(res, undefined, 2), path);
150
161
  Logger.success(`All response written to a file: ${path}`);
@@ -169,7 +180,7 @@ export const createAndSaveComponentListToFile = async ({ file, folder, res, time
169
180
  Logger.success(`All components written to a file: ${filename}`);
170
181
  };
171
182
  export const readFile = async (pathToFile) => {
172
- const absolutePath = path.join(process.cwd(), pathToFile);
183
+ const absolutePath = resolveFromCwd(pathToFile);
173
184
  try {
174
185
  const result = await fs.promises.readFile(absolutePath);
175
186
  return result.toString();
@@ -192,13 +203,13 @@ export const dumpToFile = async (path, content) => {
192
203
  };
193
204
  export const getConsumerPackageJson = async () => {
194
205
  const consumerPkg = await getFileContentWithRequire({
195
- file: path.join(process.cwd(), "package.json"),
206
+ file: nodePath.join(process.cwd(), "package.json"),
196
207
  });
197
208
  return consumerPkg;
198
209
  };
199
210
  export const getSbMigPackageJson = async () => {
200
211
  const sbMigPkg = await getFileContentWithRequire({
201
- file: path.join("..", "..", "package.json"),
212
+ file: nodePath.join("..", "..", "package.json"),
202
213
  });
203
214
  return sbMigPkg;
204
215
  };
@@ -3,13 +3,16 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.deepUpsertStory = exports.upsertStory = exports.updateStories = exports.publishStoryLanguages = exports.updateStory = exports.createStory = exports.getStoryBySlug = exports.getStoryById = exports.getAllStories = exports.removeAllStories = exports.removeStory = exports.resolvePublishLanguageCodes = exports.parsePublishLanguagesOption = void 0;
6
+ exports.deepUpsertStory = exports.upsertStory = exports.updateStories = exports.publishStoryLanguages = exports.updateStory = exports.createStory = exports.getStoryBySlug = exports.getStoryById = exports.getAllStories = exports.removeAllStories = exports.removeStory = exports.resolvePublishLanguageCodes = exports.parsePublishLanguagesOption = exports.resolveStoryPublishState = void 0;
7
7
  const chalk_1 = __importDefault(require("chalk"));
8
+ const async_utils_js_1 = require("../../utils/async-utils.js");
8
9
  const logger_js_1 = __importDefault(require("../../utils/logger.js"));
9
10
  const object_utils_js_1 = require("../../utils/object-utils.js");
10
11
  const request_js_1 = require("../utils/request.js");
11
12
  const resolveStoryLabel = (content, storyId) => content?.full_slug || content?.slug || content?.name || String(storyId);
12
13
  const DEFAULT_PUBLISH_LANGUAGE = "[default]";
14
+ const STORY_CONTENT_FETCH_CONCURRENCY = 10;
15
+ const isSkippedStoryPublishState = (publishState) => publishState?.shouldPublish === false;
13
16
  const isDefaultLanguageToken = (language) => language.toLowerCase() === "default" ||
14
17
  language === DEFAULT_PUBLISH_LANGUAGE;
15
18
  const normalizePublishLanguageCodes = (languages) => {
@@ -25,6 +28,47 @@ const normalizePublishLanguageCodes = (languages) => {
25
28
  }
26
29
  return Array.from(new Set(cleanLanguages));
27
30
  };
31
+ const resolveStoryPublishState = (story) => {
32
+ if (story?.published !== true) {
33
+ return {
34
+ status: "draft",
35
+ shouldPublish: false,
36
+ skipReason: "source_story_draft",
37
+ message: "source story was draft-only",
38
+ };
39
+ }
40
+ if (story.unpublished_changes === true) {
41
+ return {
42
+ status: "published_with_unpublished_changes",
43
+ shouldPublish: false,
44
+ skipReason: "source_story_has_unpublished_changes",
45
+ message: "source story had unpublished draft changes",
46
+ };
47
+ }
48
+ if (story.unpublished_changes !== false) {
49
+ return {
50
+ status: "published_unknown",
51
+ shouldPublish: false,
52
+ skipReason: "source_story_publish_state_unknown",
53
+ message: "source story publish state was missing unpublished_changes",
54
+ };
55
+ }
56
+ return {
57
+ status: "published_clean",
58
+ shouldPublish: true,
59
+ };
60
+ };
61
+ exports.resolveStoryPublishState = resolveStoryPublishState;
62
+ const withSkippedPublish = ({ updateResult, story, storyId, spaceId, publishLanguages, publishState, }) => {
63
+ const storyLabel = resolveStoryLabel(story, String(storyId));
64
+ logger_js_1.default.warning(`Skipping publish for story '${storyLabel}' in space '${spaceId}' because ${publishState.message}.`);
65
+ return {
66
+ ...updateResult,
67
+ sourcePublishState: publishState.status,
68
+ publishSkippedReason: publishState.skipReason,
69
+ ...(publishLanguages ? { publishLanguages } : {}),
70
+ };
71
+ };
28
72
  const parsePublishLanguagesOption = (publishLanguages) => {
29
73
  if (!publishLanguages) {
30
74
  return "default";
@@ -154,7 +198,7 @@ const getAllStories = async (args, config) => {
154
198
  });
155
199
  logger_js_1.default.success(`Successfully pre-fetched ${allStoriesWithoutContent.length} stories.`);
156
200
  let heartBeat = 0;
157
- const allStories = await Promise.all(allStoriesWithoutContent.map(async (story) => {
201
+ const allStories = await (0, async_utils_js_1.mapWithConcurrency)(allStoriesWithoutContent, STORY_CONTENT_FETCH_CONCURRENCY, async (story) => {
158
202
  const result = await (0, exports.getStoryById)(story.id, config);
159
203
  heartBeat++;
160
204
  if (heartBeat % 10 === 0 ||
@@ -162,7 +206,7 @@ const getAllStories = async (args, config) => {
162
206
  logger_js_1.default.success(`Successfully fetched ${heartBeat} stories with full content.`);
163
207
  }
164
208
  return result;
165
- }));
209
+ });
166
210
  return allStories;
167
211
  };
168
212
  exports.getAllStories = getAllStories;
@@ -307,6 +351,7 @@ exports.publishStoryLanguages = publishStoryLanguages;
307
351
  const updateStories = async (args, config) => {
308
352
  const { stories, options, spaceId } = args;
309
353
  const shouldPublishLanguages = options.publish && options.publishLanguages !== undefined;
354
+ const shouldPreservePublishState = Boolean(options.preservePublishState);
310
355
  const publishLanguages = shouldPublishLanguages
311
356
  ? await (0, exports.resolvePublishLanguageCodes)(options.publishLanguages, {
312
357
  ...config,
@@ -317,9 +362,27 @@ const updateStories = async (args, config) => {
317
362
  // Run through stories, and update the space with migrated version of stories
318
363
  stories.map(async (stories) => {
319
364
  const story = stories.story;
365
+ const publishState = shouldPreservePublishState
366
+ ? (0, exports.resolveStoryPublishState)(story)
367
+ : undefined;
368
+ const shouldPublishStory = options.publish &&
369
+ (!publishState || publishState.shouldPublish);
320
370
  const updateResult = await (0, exports.updateStory)(story, story.id, {
321
- publish: options.publish && !shouldPublishLanguages,
371
+ publish: shouldPublishStory && !shouldPublishLanguages,
372
+ force_update: options.force_update,
322
373
  }, { ...config, spaceId });
374
+ if (options.publish &&
375
+ updateResult?.ok &&
376
+ isSkippedStoryPublishState(publishState)) {
377
+ return withSkippedPublish({
378
+ updateResult,
379
+ story,
380
+ storyId: story.id,
381
+ spaceId,
382
+ publishLanguages,
383
+ publishState,
384
+ });
385
+ }
323
386
  if (!shouldPublishLanguages ||
324
387
  !publishLanguages ||
325
388
  !updateResult?.ok) {
@@ -0,0 +1,34 @@
1
+ "use strict";
2
+ /**
3
+ * Async utility functions
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.mapWithConcurrency = exports.delay = void 0;
7
+ /**
8
+ * Delay execution for a specified time
9
+ *
10
+ * @param time - Time to delay in milliseconds
11
+ * @returns Promise that resolves after the delay
12
+ *
13
+ * @example
14
+ * await delay(1000); // Wait 1 second
15
+ */
16
+ const delay = (time) => new Promise((resolve) => setTimeout(resolve, time));
17
+ exports.delay = delay;
18
+ const mapWithConcurrency = async (items, concurrency, mapper) => {
19
+ if (items.length === 0) {
20
+ return [];
21
+ }
22
+ const limit = Math.max(1, Math.floor(concurrency));
23
+ const results = new Array(items.length);
24
+ let nextIndex = 0;
25
+ const workers = Array.from({ length: Math.min(limit, items.length) }, async () => {
26
+ while (nextIndex < items.length) {
27
+ const currentIndex = nextIndex++;
28
+ results[currentIndex] = await mapper(items[currentIndex], currentIndex);
29
+ }
30
+ });
31
+ await Promise.all(workers);
32
+ return results;
33
+ };
34
+ exports.mapWithConcurrency = mapWithConcurrency;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sb-mig",
3
- "version": "6.0.0-beta.6",
3
+ "version": "6.0.0-beta.8",
4
4
  "description": "CLI to rule the world. (and handle stuff related to Storyblok CMS)",
5
5
  "author": "Marcin Krawczyk <marckraw@icloud.com>",
6
6
  "license": "MIT",
@@ -42,7 +42,7 @@
42
42
  ],
43
43
  "scripts": {
44
44
  "lint-staged": "node ./src/scripts/fix-esm.js && lint-staged",
45
- "build": "rm -rf dist dist-cjs && tsc -p tsconfig.json && tsc -p tsconfig.api-v2.cjs.json && node ./src/scripts/write-cjs-package.js && chmod +x ./dist/cli/index.js",
45
+ "build": "node ./src/scripts/clean-build.js && tsc -p tsconfig.json && tsc -p tsconfig.api-v2.cjs.json && node ./src/scripts/write-cjs-package.js && node ./src/scripts/make-cli-executable.js",
46
46
  "build:dev": "chokidar 'src/**/*.{js,ts,cjs,mjs}' -c 'npm run build'",
47
47
  "lint": "eslint . --max-warnings=0",
48
48
  "lint:fix": "eslint --fix",