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.
- package/dist/api/data-migration/component-data-migration.js +2 -2
- package/dist/api/data-migration/migration-run-log.d.ts +5 -1
- package/dist/api/data-migration/migration-run-log.js +13 -7
- package/dist/api/data-migration/write-summary.d.ts +2 -0
- package/dist/api/stories/stories.d.ts +21 -0
- package/dist/api/stories/stories.js +65 -3
- package/dist/api/stories/stories.types.d.ts +1 -0
- package/dist/cli/cli-descriptions.d.ts +1 -1
- package/dist/cli/cli-descriptions.js +2 -2
- package/dist/config/helper.js +6 -7
- package/dist/utils/async-utils.d.ts +1 -0
- package/dist/utils/async-utils.js +16 -0
- package/dist/utils/files.d.ts +1 -0
- package/dist/utils/files.js +26 -15
- package/dist-cjs/api/stories/stories.js +67 -4
- package/dist-cjs/utils/async-utils.js +34 -0
- package/package.json +2 -2
|
@@ -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 =
|
|
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.
|
|
68
|
-
?
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
: "
|
|
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
|
};
|
|
@@ -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
|
|
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:
|
|
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
|
|
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
|
|
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
|
package/dist/config/helper.js
CHANGED
|
@@ -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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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(
|
|
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,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
|
+
};
|
package/dist/utils/files.d.ts
CHANGED
package/dist/utils/files.js
CHANGED
|
@@ -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
|
|
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 =
|
|
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(
|
|
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 =
|
|
107
|
-
const fileName =
|
|
108
|
-
if (!isDirectoryExists(directory
|
|
109
|
-
await createDir(directory
|
|
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 =
|
|
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:
|
|
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:
|
|
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
|
|
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:
|
|
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.
|
|
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": "
|
|
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",
|