sb-mig 6.0.0 → 6.1.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/contentHubApi.js +14 -4
- package/dist/api/data-migration/component-data-migration.d.ts +5 -3
- package/dist/api/data-migration/component-data-migration.js +14 -4
- package/dist/api/data-migration/migration-run-log.d.ts +3 -1
- package/dist/api/data-migration/migration-run-log.js +2 -1
- package/dist/api/managementApi.d.ts +14 -0
- package/dist/api/stories/index.d.ts +2 -0
- package/dist/api/stories/index.js +1 -0
- package/dist/api/stories/language-publish-state.d.ts +48 -0
- package/dist/api/stories/language-publish-state.js +230 -0
- package/dist/api/stories/stories.js +44 -5
- package/dist/api/stories/stories.types.d.ts +11 -0
- package/dist/api/testApi.d.ts +14 -0
- package/dist/cli/cli-descriptions.d.ts +3 -2
- package/dist/cli/cli-descriptions.js +25 -0
- package/dist/cli/commands/init.js +10 -1
- package/dist/cli/commands/language-publish-state.d.ts +2 -0
- package/dist/cli/commands/language-publish-state.js +19 -0
- package/dist/cli/commands/migrate.js +12 -0
- package/dist/cli/index.js +37 -1
- package/dist/config/defaultConfig.js +2 -1
- package/dist/utils/path-utils.js +6 -2
- package/dist/utils/url-utils.d.ts +8 -0
- package/dist/utils/url-utils.js +12 -0
- package/dist-cjs/api/stories/stories.js +44 -5
- package/dist-cjs/utils/path-utils.js +6 -2
- package/package.json +1 -1
|
@@ -1,15 +1,25 @@
|
|
|
1
1
|
import Logger from "../utils/logger.js";
|
|
2
|
+
import { buildUrl } from "../utils/url-utils.js";
|
|
2
3
|
const getAllStories = async (args, config) => {
|
|
3
4
|
const { spaceId, storiesFilename } = args;
|
|
4
5
|
Logger.warning("Trying to get all stories from Content Hub...");
|
|
5
|
-
|
|
6
|
-
|
|
6
|
+
if (!config.contentHubOriginUrl) {
|
|
7
|
+
throw new Error("contentHubOriginUrl is required to fetch stories.");
|
|
8
|
+
}
|
|
9
|
+
const url = buildUrl({
|
|
10
|
+
baseUrl: config.contentHubOriginUrl,
|
|
11
|
+
pathname: "getStories",
|
|
12
|
+
searchParams: {
|
|
13
|
+
spaceId,
|
|
14
|
+
...(storiesFilename ? { storiesFilename } : {}),
|
|
15
|
+
},
|
|
16
|
+
});
|
|
7
17
|
const authorizationToken = config.contentHubAuthorizationToken;
|
|
8
18
|
if (config.debug) {
|
|
9
|
-
console.log("This is url: ", url);
|
|
19
|
+
console.log("This is url: ", url.toString());
|
|
10
20
|
}
|
|
11
21
|
try {
|
|
12
|
-
const response = await fetch(url, {
|
|
22
|
+
const response = await fetch(url.toString(), {
|
|
13
23
|
method: "GET",
|
|
14
24
|
headers: {
|
|
15
25
|
Authorization: authorizationToken,
|
|
@@ -48,6 +48,7 @@ interface MigrateItems {
|
|
|
48
48
|
publishLanguages?: PublishLanguagesOption;
|
|
49
49
|
fromFilePath?: string;
|
|
50
50
|
fileName?: string;
|
|
51
|
+
languagePublishStatePath?: string;
|
|
51
52
|
preparedMigrationConfigs?: PreparedMigrationConfig[];
|
|
52
53
|
}
|
|
53
54
|
export type MapperDefinition = (data: any) => any;
|
|
@@ -70,8 +71,8 @@ export declare const runMigrationPipelineInMemory: ({ itemType, itemsToMigrate,
|
|
|
70
71
|
itemsToMigrate: any[];
|
|
71
72
|
preparedMigrationConfigs: PreparedMigrationConfig[];
|
|
72
73
|
}) => MigrationPipelineResult;
|
|
73
|
-
export declare const migrateAllComponentsDataInStories: ({ itemType, migrationConfig, migrateFrom, from, to, filters, dryRun, publish, publishLanguages, fromFilePath, fileName, migrationComponentAliases, migrationComponentOverrides, }: Omit<MigrateItems, "componentsToMigrate" | "preparedMigrationConfigs">, config: RequestBaseConfig) => Promise<void>;
|
|
74
|
-
export declare const doTheMigration: ({ itemType, from, itemsToMigrate, migrationConfig, migrationConfigs, to, dryRun, publish, publishLanguages, migrateFrom, fromFilePath, fileName, }: {
|
|
74
|
+
export declare const migrateAllComponentsDataInStories: ({ itemType, migrationConfig, migrateFrom, from, to, filters, dryRun, publish, publishLanguages, fromFilePath, fileName, languagePublishStatePath, migrationComponentAliases, migrationComponentOverrides, }: Omit<MigrateItems, "componentsToMigrate" | "preparedMigrationConfigs">, config: RequestBaseConfig) => Promise<void>;
|
|
75
|
+
export declare const doTheMigration: ({ itemType, from, itemsToMigrate, migrationConfig, migrationConfigs, to, dryRun, publish, publishLanguages, migrateFrom, fromFilePath, fileName, languagePublishStatePath, }: {
|
|
75
76
|
itemType?: "story" | "preset";
|
|
76
77
|
from: string;
|
|
77
78
|
itemsToMigrate: any[];
|
|
@@ -84,6 +85,7 @@ export declare const doTheMigration: ({ itemType, from, itemsToMigrate, migratio
|
|
|
84
85
|
migrateFrom: MigrateFrom;
|
|
85
86
|
fromFilePath?: string;
|
|
86
87
|
fileName?: string;
|
|
88
|
+
languagePublishStatePath?: string;
|
|
87
89
|
}, config: RequestBaseConfig) => Promise<void>;
|
|
88
|
-
export declare const migrateProvidedComponentsDataInStories: ({ itemType, migrationConfig, migrateFrom, from, to, componentsToMigrate, filters, dryRun, publish, publishLanguages, fromFilePath, fileName, preparedMigrationConfigs, migrationComponentAliases, migrationComponentOverrides, }: MigrateItems, config: RequestBaseConfig) => Promise<void>;
|
|
90
|
+
export declare const migrateProvidedComponentsDataInStories: ({ itemType, migrationConfig, migrateFrom, from, to, componentsToMigrate, filters, dryRun, publish, publishLanguages, fromFilePath, fileName, languagePublishStatePath, preparedMigrationConfigs, migrationComponentAliases, migrationComponentOverrides, }: MigrateItems, config: RequestBaseConfig) => Promise<void>;
|
|
89
91
|
export {};
|
|
@@ -7,6 +7,7 @@ import Logger from "../../utils/logger.js";
|
|
|
7
7
|
import { modifyOrCreateAppliedMigrationsFile } from "../../utils/migrations.js";
|
|
8
8
|
import { isObjectEmpty } from "../../utils/object-utils.js";
|
|
9
9
|
import { managementApi } from "../managementApi.js";
|
|
10
|
+
import { loadLanguagePublishStateMap } from "../stories/language-publish-state.js";
|
|
10
11
|
import { buildPreMigrationBackupBaseName, resolveOutputFileBaseName, shouldUseDatestampForArtifacts, } from "./file-naming.js";
|
|
11
12
|
import { extendMigrationMapperWithAliases, resolveMigrationComponentsToMigrate, } from "./migration-component-scope.js";
|
|
12
13
|
import { saveMigrationRunLog } from "./migration-run-log.js";
|
|
@@ -295,7 +296,7 @@ export const runMigrationPipelineInMemory = ({ itemType, itemsToMigrate, prepare
|
|
|
295
296
|
totalItems: workingItems.length,
|
|
296
297
|
};
|
|
297
298
|
};
|
|
298
|
-
const savePipelineSummary = async ({ artifactBaseName, useDatestamp, from, itemType, dryRun, publish, publishLanguages, migrateFrom, fromFilePath, pipelineResult, }, config) => {
|
|
299
|
+
const savePipelineSummary = async ({ artifactBaseName, useDatestamp, from, itemType, dryRun, publish, publishLanguages, migrateFrom, fromFilePath, languagePublishStatePath, pipelineResult, }, config) => {
|
|
299
300
|
await createAndSaveToFile({
|
|
300
301
|
datestamp: useDatestamp,
|
|
301
302
|
ext: "json",
|
|
@@ -307,6 +308,7 @@ const savePipelineSummary = async ({ artifactBaseName, useDatestamp, from, itemT
|
|
|
307
308
|
migrateFrom,
|
|
308
309
|
from,
|
|
309
310
|
fromFilePath: fromFilePath || null,
|
|
311
|
+
languagePublishStatePath: languagePublishStatePath || null,
|
|
310
312
|
},
|
|
311
313
|
writeMode: itemType === "story" && publish ? "publish" : "save",
|
|
312
314
|
publishLanguages: itemType === "story" && publish
|
|
@@ -384,7 +386,7 @@ const loadItemsToMigrate = async ({ itemType, migrateFrom, from, filters, fromFi
|
|
|
384
386
|
spaceId: from,
|
|
385
387
|
});
|
|
386
388
|
};
|
|
387
|
-
export const migrateAllComponentsDataInStories = async ({ itemType, migrationConfig, migrateFrom, from, to, filters, dryRun, publish, publishLanguages, fromFilePath, fileName, migrationComponentAliases, migrationComponentOverrides, }, config) => {
|
|
389
|
+
export const migrateAllComponentsDataInStories = async ({ itemType, migrationConfig, migrateFrom, from, to, filters, dryRun, publish, publishLanguages, fromFilePath, fileName, languagePublishStatePath, migrationComponentAliases, migrationComponentOverrides, }, config) => {
|
|
388
390
|
Logger.warning(`Trying to migrate all ${itemType} from ${migrateFrom}, ${from} to ${to}...`);
|
|
389
391
|
const preparedMigrationConfigs = prepareMigrationConfigs({
|
|
390
392
|
migrationConfig,
|
|
@@ -407,10 +409,11 @@ export const migrateAllComponentsDataInStories = async ({ itemType, migrationCon
|
|
|
407
409
|
publishLanguages,
|
|
408
410
|
fromFilePath,
|
|
409
411
|
fileName,
|
|
412
|
+
languagePublishStatePath,
|
|
410
413
|
preparedMigrationConfigs,
|
|
411
414
|
}, config);
|
|
412
415
|
};
|
|
413
|
-
export const doTheMigration = async ({ itemType = "story", from, itemsToMigrate, migrationConfig, migrationConfigs, to, dryRun, publish, publishLanguages, migrateFrom, fromFilePath, fileName, }, config) => {
|
|
416
|
+
export const doTheMigration = async ({ itemType = "story", from, itemsToMigrate, migrationConfig, migrationConfigs, to, dryRun, publish, publishLanguages, migrateFrom, fromFilePath, fileName, languagePublishStatePath, }, config) => {
|
|
414
417
|
const preparedMigrationConfigs = migrationConfigs ||
|
|
415
418
|
prepareMigrationConfigs({
|
|
416
419
|
migrationConfig: migrationConfig || [],
|
|
@@ -484,6 +487,7 @@ export const doTheMigration = async ({ itemType = "story", from, itemsToMigrate,
|
|
|
484
487
|
publishLanguages,
|
|
485
488
|
migrateFrom,
|
|
486
489
|
fromFilePath,
|
|
490
|
+
languagePublishStatePath,
|
|
487
491
|
pipelineResult,
|
|
488
492
|
}, config);
|
|
489
493
|
if (dryRun) {
|
|
@@ -500,6 +504,9 @@ export const doTheMigration = async ({ itemType = "story", from, itemsToMigrate,
|
|
|
500
504
|
}
|
|
501
505
|
let writeResults = [];
|
|
502
506
|
let resolvedPublishLanguages;
|
|
507
|
+
const languagePublishStateMap = languagePublishStatePath
|
|
508
|
+
? loadLanguagePublishStateMap(languagePublishStatePath)
|
|
509
|
+
: undefined;
|
|
503
510
|
if (itemType === "story") {
|
|
504
511
|
if (publish && publishLanguages !== undefined) {
|
|
505
512
|
resolvedPublishLanguages =
|
|
@@ -515,6 +522,7 @@ export const doTheMigration = async ({ itemType = "story", from, itemsToMigrate,
|
|
|
515
522
|
publish: Boolean(publish),
|
|
516
523
|
publishLanguages: resolvedPublishLanguages,
|
|
517
524
|
preservePublishState: Boolean(publish),
|
|
525
|
+
languagePublishStateMap,
|
|
518
526
|
},
|
|
519
527
|
}, config);
|
|
520
528
|
}
|
|
@@ -539,6 +547,7 @@ export const doTheMigration = async ({ itemType = "story", from, itemsToMigrate,
|
|
|
539
547
|
resolvedPublishLanguages,
|
|
540
548
|
migrateFrom,
|
|
541
549
|
fromFilePath,
|
|
550
|
+
languagePublishStatePath,
|
|
542
551
|
pipelineResult,
|
|
543
552
|
writeResults,
|
|
544
553
|
writeSummary,
|
|
@@ -570,7 +579,7 @@ const saveBackupToFile = async ({ itemType, res, folder, filename }, config) =>
|
|
|
570
579
|
res: res,
|
|
571
580
|
}, config);
|
|
572
581
|
};
|
|
573
|
-
export const migrateProvidedComponentsDataInStories = async ({ itemType, migrationConfig, migrateFrom, from, to, componentsToMigrate, filters, dryRun, publish, publishLanguages, fromFilePath, fileName, preparedMigrationConfigs, migrationComponentAliases, migrationComponentOverrides, }, config) => {
|
|
582
|
+
export const migrateProvidedComponentsDataInStories = async ({ itemType, migrationConfig, migrateFrom, from, to, componentsToMigrate, filters, dryRun, publish, publishLanguages, fromFilePath, fileName, languagePublishStatePath, preparedMigrationConfigs, migrationComponentAliases, migrationComponentOverrides, }, config) => {
|
|
574
583
|
const resolvedMigrationConfigs = preparedMigrationConfigs ||
|
|
575
584
|
prepareMigrationConfigs({
|
|
576
585
|
migrationConfig,
|
|
@@ -610,5 +619,6 @@ export const migrateProvidedComponentsDataInStories = async ({ itemType, migrati
|
|
|
610
619
|
migrateFrom,
|
|
611
620
|
fromFilePath,
|
|
612
621
|
fileName,
|
|
622
|
+
languagePublishStatePath,
|
|
613
623
|
}, config);
|
|
614
624
|
};
|
|
@@ -12,6 +12,7 @@ export interface MigrationRunLogRecord {
|
|
|
12
12
|
migrateFrom: MigrateFrom;
|
|
13
13
|
from: string;
|
|
14
14
|
fromFilePath: string | null;
|
|
15
|
+
languagePublishStatePath?: string | null;
|
|
15
16
|
};
|
|
16
17
|
target: {
|
|
17
18
|
to: string;
|
|
@@ -67,11 +68,12 @@ interface SaveMigrationRunLogArgs {
|
|
|
67
68
|
resolvedPublishLanguages?: string[];
|
|
68
69
|
migrateFrom: MigrateFrom;
|
|
69
70
|
fromFilePath?: string;
|
|
71
|
+
languagePublishStatePath?: string;
|
|
70
72
|
pipelineResult: MigrationPipelineResult;
|
|
71
73
|
writeResults: PromiseSettledResult<MutationWriteResult>[];
|
|
72
74
|
writeSummary: MutationWriteSummary;
|
|
73
75
|
}
|
|
74
|
-
export declare const buildMigrationRunLogRecords: ({ from, to, itemType, dryRun, publish, publishLanguages, resolvedPublishLanguages, migrateFrom, fromFilePath, pipelineResult, writeResults, writeSummary, }: Omit<SaveMigrationRunLogArgs, "artifactBaseName" | "useDatestamp">) => MigrationRunLogRecord[];
|
|
76
|
+
export declare const buildMigrationRunLogRecords: ({ from, to, itemType, dryRun, publish, publishLanguages, resolvedPublishLanguages, migrateFrom, fromFilePath, languagePublishStatePath, pipelineResult, writeResults, writeSummary, }: Omit<SaveMigrationRunLogArgs, "artifactBaseName" | "useDatestamp">) => MigrationRunLogRecord[];
|
|
75
77
|
export declare const recordsToJsonl: (records: MigrationRunLogRecord[]) => string;
|
|
76
78
|
export declare const saveMigrationRunLog: (args: SaveMigrationRunLogArgs, config: RequestBaseConfig) => Promise<void>;
|
|
77
79
|
export {};
|
|
@@ -31,7 +31,7 @@ const resolveWriteResultValue = (result) => {
|
|
|
31
31
|
error: result.reason,
|
|
32
32
|
};
|
|
33
33
|
};
|
|
34
|
-
export const buildMigrationRunLogRecords = ({ from, to, itemType, dryRun, publish, publishLanguages, resolvedPublishLanguages, migrateFrom, fromFilePath, pipelineResult, writeResults, writeSummary, }) => {
|
|
34
|
+
export const buildMigrationRunLogRecords = ({ from, to, itemType, dryRun, publish, publishLanguages, resolvedPublishLanguages, migrateFrom, fromFilePath, languagePublishStatePath, pipelineResult, writeResults, writeSummary, }) => {
|
|
35
35
|
const timestamp = new Date().toISOString();
|
|
36
36
|
const runId = `${itemType}-${timestamp}`;
|
|
37
37
|
const baseRecord = {
|
|
@@ -42,6 +42,7 @@ export const buildMigrationRunLogRecords = ({ from, to, itemType, dryRun, publis
|
|
|
42
42
|
migrateFrom,
|
|
43
43
|
from,
|
|
44
44
|
fromFilePath: fromFilePath || null,
|
|
45
|
+
languagePublishStatePath: languagePublishStatePath || null,
|
|
45
46
|
},
|
|
46
47
|
target: {
|
|
47
48
|
to,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as stories from "./stories/index.js";
|
|
1
2
|
export declare const managementApi: {
|
|
2
3
|
assets: {
|
|
3
4
|
getAllAssets: import("./assets/assets.types.js").GetAllAssets;
|
|
@@ -77,6 +78,19 @@ export declare const managementApi: {
|
|
|
77
78
|
removeAllStories: import("./stories/stories.types.js").RemoveAllStories;
|
|
78
79
|
upsertStory: import("./stories/stories.types.js").UpsertStory;
|
|
79
80
|
backupStories: import("./stories/stories.types.js").BackupStories;
|
|
81
|
+
buildLanguagePublishStateMap: (args: import("./stories/language-publish-state.js").BuildLanguagePublishStateMapArgs, config: import("./utils/request.js").RequestBaseConfig) => Promise<{
|
|
82
|
+
generatedAt: string;
|
|
83
|
+
source: {
|
|
84
|
+
spaceId: string;
|
|
85
|
+
startsWith: string | null;
|
|
86
|
+
withSlug: string[] | null;
|
|
87
|
+
};
|
|
88
|
+
languages: string[];
|
|
89
|
+
storyCount: number;
|
|
90
|
+
statesByLanguage: Record<string, any>;
|
|
91
|
+
stories: any;
|
|
92
|
+
}>;
|
|
93
|
+
loadLanguagePublishStateMap: (languagePublishStatePath: string) => stories.LanguagePublishStateMap;
|
|
80
94
|
};
|
|
81
95
|
spaces: {
|
|
82
96
|
getAllSpaces: import("./spaces/spaces.types.js").GetAllSpaces;
|
|
@@ -1,2 +1,4 @@
|
|
|
1
1
|
export { createStory, updateStory, getStoryById, removeStory, getStoryBySlug, updateStories, publishStoryLanguages, parsePublishLanguagesOption, resolvePublishLanguageCodes, getAllStories, removeAllStories, upsertStory, } from "./stories.js";
|
|
2
2
|
export { backupStories } from "./backup.js";
|
|
3
|
+
export { buildLanguagePublishStateMap, loadLanguagePublishStateMap, } from "./language-publish-state.js";
|
|
4
|
+
export type { LanguagePublishState, LanguagePublishStateMap, LanguagePublishStateMapEntry, } from "./language-publish-state.js";
|
|
@@ -1,2 +1,3 @@
|
|
|
1
1
|
export { createStory, updateStory, getStoryById, removeStory, getStoryBySlug, updateStories, publishStoryLanguages, parsePublishLanguagesOption, resolvePublishLanguageCodes, getAllStories, removeAllStories, upsertStory, } from "./stories.js";
|
|
2
2
|
export { backupStories } from "./backup.js";
|
|
3
|
+
export { buildLanguagePublishStateMap, loadLanguagePublishStateMap, } from "./language-publish-state.js";
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { RequestBaseConfig } from "../utils/request.js";
|
|
2
|
+
export type LanguagePublishState = "published_clean" | "published_with_unpublished_changes" | "draft_never_published" | "unpublished_historical" | "draft_or_unpublished" | "missing" | "published_unknown" | "error";
|
|
3
|
+
export interface BuildLanguagePublishStateMapArgs {
|
|
4
|
+
from: string;
|
|
5
|
+
accessToken?: string;
|
|
6
|
+
languages?: string;
|
|
7
|
+
startsWith?: string;
|
|
8
|
+
withSlug?: string[];
|
|
9
|
+
fileName?: string;
|
|
10
|
+
outputPath?: string;
|
|
11
|
+
}
|
|
12
|
+
export interface LanguagePublishStateMapEntry {
|
|
13
|
+
id?: number | string;
|
|
14
|
+
uuid?: string;
|
|
15
|
+
name?: string;
|
|
16
|
+
fullSlug?: string;
|
|
17
|
+
languages?: Record<string, {
|
|
18
|
+
state?: LanguagePublishState;
|
|
19
|
+
[key: string]: unknown;
|
|
20
|
+
}>;
|
|
21
|
+
cleanPublishedLanguages?: string[];
|
|
22
|
+
[key: string]: unknown;
|
|
23
|
+
}
|
|
24
|
+
export interface LanguagePublishStateMap {
|
|
25
|
+
generatedAt?: string;
|
|
26
|
+
source?: {
|
|
27
|
+
spaceId?: string;
|
|
28
|
+
startsWith?: string | null;
|
|
29
|
+
withSlug?: string[] | null;
|
|
30
|
+
};
|
|
31
|
+
languages?: string[];
|
|
32
|
+
storyCount?: number;
|
|
33
|
+
statesByLanguage?: Record<string, Record<string, number>>;
|
|
34
|
+
stories?: Record<string, LanguagePublishStateMapEntry>;
|
|
35
|
+
}
|
|
36
|
+
export declare const loadLanguagePublishStateMap: (languagePublishStatePath: string) => LanguagePublishStateMap;
|
|
37
|
+
export declare const buildLanguagePublishStateMap: (args: BuildLanguagePublishStateMapArgs, config: RequestBaseConfig) => Promise<{
|
|
38
|
+
generatedAt: string;
|
|
39
|
+
source: {
|
|
40
|
+
spaceId: string;
|
|
41
|
+
startsWith: string | null;
|
|
42
|
+
withSlug: string[] | null;
|
|
43
|
+
};
|
|
44
|
+
languages: string[];
|
|
45
|
+
storyCount: number;
|
|
46
|
+
statesByLanguage: Record<string, any>;
|
|
47
|
+
stories: any;
|
|
48
|
+
}>;
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { createHash } from "crypto";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { mapWithConcurrency } from "../../utils/async-utils.js";
|
|
5
|
+
import { createAndSaveToFile } from "../../utils/files.js";
|
|
6
|
+
import Logger from "../../utils/logger.js";
|
|
7
|
+
import { getAllStories, getStoryBySlug } from "./stories.js";
|
|
8
|
+
const DEFAULT_LANGUAGE = "[default]";
|
|
9
|
+
const DELIVERY_CHECK_CONCURRENCY = 5;
|
|
10
|
+
const cleanStoryblokContent = (value) => {
|
|
11
|
+
if (Array.isArray(value)) {
|
|
12
|
+
return value.map(cleanStoryblokContent);
|
|
13
|
+
}
|
|
14
|
+
if (value && typeof value === "object") {
|
|
15
|
+
return Object.fromEntries(Object.keys(value)
|
|
16
|
+
.filter((key) => key !== "_editable")
|
|
17
|
+
.sort()
|
|
18
|
+
.map((key) => [key, cleanStoryblokContent(value[key])]));
|
|
19
|
+
}
|
|
20
|
+
return value;
|
|
21
|
+
};
|
|
22
|
+
const hashContent = (value) => createHash("sha256")
|
|
23
|
+
.update(JSON.stringify(cleanStoryblokContent(value)))
|
|
24
|
+
.digest("hex");
|
|
25
|
+
const normalizeLanguages = (languages) => {
|
|
26
|
+
if (!languages || languages.trim().length === 0) {
|
|
27
|
+
return "all";
|
|
28
|
+
}
|
|
29
|
+
if (languages.trim().toLowerCase() === "all") {
|
|
30
|
+
return "all";
|
|
31
|
+
}
|
|
32
|
+
const normalized = languages
|
|
33
|
+
.split(",")
|
|
34
|
+
.map((language) => language.trim())
|
|
35
|
+
.filter(Boolean)
|
|
36
|
+
.map((language) => language.toLowerCase() === "default" ? DEFAULT_LANGUAGE : language);
|
|
37
|
+
return Array.from(new Set(normalized));
|
|
38
|
+
};
|
|
39
|
+
const readSpaceLanguageCodes = (spaceResponse) => {
|
|
40
|
+
const space = spaceResponse?.data?.space || spaceResponse?.space || {};
|
|
41
|
+
const languages = space.languages;
|
|
42
|
+
if (!Array.isArray(languages)) {
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
return languages
|
|
46
|
+
.map((language) => typeof language === "string" ? language : language?.code)
|
|
47
|
+
.filter((language) => Boolean(language));
|
|
48
|
+
};
|
|
49
|
+
const resolveLanguages = async (languages, config) => {
|
|
50
|
+
const normalized = normalizeLanguages(languages);
|
|
51
|
+
if (normalized !== "all") {
|
|
52
|
+
return normalized;
|
|
53
|
+
}
|
|
54
|
+
const response = await config.sbApi.get(`spaces/${config.spaceId}`);
|
|
55
|
+
return [DEFAULT_LANGUAGE, ...readSpaceLanguageCodes(response)];
|
|
56
|
+
};
|
|
57
|
+
const classifyDefaultLanguageState = (story) => {
|
|
58
|
+
if (story?.published === true && story?.unpublished_changes === false) {
|
|
59
|
+
return "published_clean";
|
|
60
|
+
}
|
|
61
|
+
if (story?.published === true && story?.unpublished_changes === true) {
|
|
62
|
+
return "published_with_unpublished_changes";
|
|
63
|
+
}
|
|
64
|
+
if (story?.published === false && story?.published_at) {
|
|
65
|
+
return "unpublished_historical";
|
|
66
|
+
}
|
|
67
|
+
if (story?.published === false) {
|
|
68
|
+
return "draft_never_published";
|
|
69
|
+
}
|
|
70
|
+
return "published_unknown";
|
|
71
|
+
};
|
|
72
|
+
const classifyTranslatedLanguageState = ({ published, draft, }) => {
|
|
73
|
+
if (published.status === 200 && draft.status === 200) {
|
|
74
|
+
if (!published.contentHash || !draft.contentHash) {
|
|
75
|
+
return "published_unknown";
|
|
76
|
+
}
|
|
77
|
+
return published.contentHash === draft.contentHash
|
|
78
|
+
? "published_clean"
|
|
79
|
+
: "published_with_unpublished_changes";
|
|
80
|
+
}
|
|
81
|
+
if (published.status === 404 && draft.status === 200) {
|
|
82
|
+
return "draft_or_unpublished";
|
|
83
|
+
}
|
|
84
|
+
if (published.status === 404 && draft.status === 404) {
|
|
85
|
+
return "missing";
|
|
86
|
+
}
|
|
87
|
+
if (published.status === 200) {
|
|
88
|
+
return "published_unknown";
|
|
89
|
+
}
|
|
90
|
+
return "error";
|
|
91
|
+
};
|
|
92
|
+
const fetchDeliveryStory = async ({ deliveryApiUrl, accessToken, slug, version, language, }) => {
|
|
93
|
+
const url = new URL(`${deliveryApiUrl.replace(/\/$/, "")}/cdn/stories/${slug}`);
|
|
94
|
+
url.searchParams.set("token", accessToken);
|
|
95
|
+
url.searchParams.set("version", version);
|
|
96
|
+
url.searchParams.set("cv", String(Date.now()));
|
|
97
|
+
if (language && language !== DEFAULT_LANGUAGE) {
|
|
98
|
+
url.searchParams.set("language", language);
|
|
99
|
+
}
|
|
100
|
+
const response = await fetch(url);
|
|
101
|
+
const data = await response.json().catch(() => null);
|
|
102
|
+
const story = data?.story;
|
|
103
|
+
return {
|
|
104
|
+
status: response.status,
|
|
105
|
+
ok: response.ok,
|
|
106
|
+
fullSlug: story?.full_slug || null,
|
|
107
|
+
publishedAt: story?.published_at || null,
|
|
108
|
+
contentHash: story?.content ? hashContent(story.content) : null,
|
|
109
|
+
};
|
|
110
|
+
};
|
|
111
|
+
const loadStoriesForLanguageState = async ({ startsWith, withSlug, }, config) => {
|
|
112
|
+
if (withSlug && withSlug.length > 0) {
|
|
113
|
+
const stories = await Promise.all(withSlug.map((slug) => getStoryBySlug(slug, config)));
|
|
114
|
+
return stories.filter(Boolean);
|
|
115
|
+
}
|
|
116
|
+
if (startsWith) {
|
|
117
|
+
return getAllStories({ options: { starts_with: startsWith } }, config);
|
|
118
|
+
}
|
|
119
|
+
return getAllStories({}, config);
|
|
120
|
+
};
|
|
121
|
+
export const loadLanguagePublishStateMap = (languagePublishStatePath) => {
|
|
122
|
+
const resolvedPath = path.isAbsolute(languagePublishStatePath)
|
|
123
|
+
? languagePublishStatePath
|
|
124
|
+
: path.resolve(process.cwd(), languagePublishStatePath);
|
|
125
|
+
const fileContent = fs.readFileSync(resolvedPath, "utf8");
|
|
126
|
+
const parsed = JSON.parse(fileContent);
|
|
127
|
+
if (!parsed || typeof parsed !== "object" || !parsed.stories) {
|
|
128
|
+
throw new Error(`Language publish-state file '${languagePublishStatePath}' is missing a stories map.`);
|
|
129
|
+
}
|
|
130
|
+
return parsed;
|
|
131
|
+
};
|
|
132
|
+
export const buildLanguagePublishStateMap = async (args, config) => {
|
|
133
|
+
const accessToken = args.accessToken || config.accessToken;
|
|
134
|
+
if (!args.from) {
|
|
135
|
+
throw new Error("--from is required.");
|
|
136
|
+
}
|
|
137
|
+
if (!accessToken) {
|
|
138
|
+
throw new Error("--accessToken is required when config accessToken is empty.");
|
|
139
|
+
}
|
|
140
|
+
const sourceConfig = { ...config, spaceId: args.from };
|
|
141
|
+
const deliveryApiUrl = config.storyblokDeliveryApiUrl || "https://api.storyblok.com/v2";
|
|
142
|
+
const languages = await resolveLanguages(args.languages, sourceConfig);
|
|
143
|
+
const uniqueLanguages = Array.from(new Set(languages));
|
|
144
|
+
const stories = (await loadStoriesForLanguageState({
|
|
145
|
+
startsWith: args.startsWith,
|
|
146
|
+
withSlug: args.withSlug,
|
|
147
|
+
}, sourceConfig)).filter((item) => item?.story && item.story.is_folder !== true);
|
|
148
|
+
Logger.success(`Fetched ${stories.length} source stories from space '${args.from}'.`);
|
|
149
|
+
const storyEntries = await mapWithConcurrency(stories, DELIVERY_CHECK_CONCURRENCY, async (item) => {
|
|
150
|
+
const story = item.story;
|
|
151
|
+
const languagesByCode = {};
|
|
152
|
+
for (const language of uniqueLanguages) {
|
|
153
|
+
if (language === DEFAULT_LANGUAGE) {
|
|
154
|
+
languagesByCode[language] = {
|
|
155
|
+
state: classifyDefaultLanguageState(story),
|
|
156
|
+
source: "management",
|
|
157
|
+
published: story.published,
|
|
158
|
+
unpublishedChanges: story.unpublished_changes,
|
|
159
|
+
publishedAt: story.published_at || null,
|
|
160
|
+
firstPublishedAt: story.first_published_at || null,
|
|
161
|
+
};
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
const [published, draft] = await Promise.all([
|
|
165
|
+
fetchDeliveryStory({
|
|
166
|
+
deliveryApiUrl,
|
|
167
|
+
accessToken,
|
|
168
|
+
slug: story.full_slug,
|
|
169
|
+
version: "published",
|
|
170
|
+
language,
|
|
171
|
+
}),
|
|
172
|
+
fetchDeliveryStory({
|
|
173
|
+
deliveryApiUrl,
|
|
174
|
+
accessToken,
|
|
175
|
+
slug: story.full_slug,
|
|
176
|
+
version: "draft",
|
|
177
|
+
language,
|
|
178
|
+
}),
|
|
179
|
+
]);
|
|
180
|
+
languagesByCode[language] = {
|
|
181
|
+
state: classifyTranslatedLanguageState({
|
|
182
|
+
published,
|
|
183
|
+
draft,
|
|
184
|
+
}),
|
|
185
|
+
source: "delivery",
|
|
186
|
+
published,
|
|
187
|
+
draft,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
return {
|
|
191
|
+
id: story.id,
|
|
192
|
+
uuid: story.uuid,
|
|
193
|
+
name: story.name,
|
|
194
|
+
fullSlug: story.full_slug,
|
|
195
|
+
languages: languagesByCode,
|
|
196
|
+
cleanPublishedLanguages: Object.entries(languagesByCode)
|
|
197
|
+
.filter(([, value]) => value.state === "published_clean")
|
|
198
|
+
.map(([language]) => language),
|
|
199
|
+
};
|
|
200
|
+
});
|
|
201
|
+
const result = {
|
|
202
|
+
generatedAt: new Date().toISOString(),
|
|
203
|
+
source: {
|
|
204
|
+
spaceId: args.from,
|
|
205
|
+
startsWith: args.startsWith || null,
|
|
206
|
+
withSlug: args.withSlug || null,
|
|
207
|
+
},
|
|
208
|
+
languages: uniqueLanguages,
|
|
209
|
+
storyCount: storyEntries.length,
|
|
210
|
+
statesByLanguage: uniqueLanguages.reduce((acc, language) => {
|
|
211
|
+
acc[language] = storyEntries.reduce((stateAcc, story) => {
|
|
212
|
+
const state = story.languages[language]?.state || "missing";
|
|
213
|
+
stateAcc[state] = (stateAcc[state] || 0) + 1;
|
|
214
|
+
return stateAcc;
|
|
215
|
+
}, {});
|
|
216
|
+
return acc;
|
|
217
|
+
}, {}),
|
|
218
|
+
stories: Object.fromEntries(storyEntries.map((story) => [story.fullSlug, story])),
|
|
219
|
+
};
|
|
220
|
+
await createAndSaveToFile({
|
|
221
|
+
ext: "json",
|
|
222
|
+
datestamp: !args.fileName && !args.outputPath,
|
|
223
|
+
filename: args.fileName || `${args.from}---language-publish-state-map`,
|
|
224
|
+
folder: "language-publish-state",
|
|
225
|
+
path: args.outputPath,
|
|
226
|
+
res: result,
|
|
227
|
+
}, config);
|
|
228
|
+
Logger.success(`Language publish-state map created for ${storyEntries.length} stories.`);
|
|
229
|
+
return result;
|
|
230
|
+
};
|
|
@@ -121,6 +121,31 @@ export const resolvePublishLanguageCodes = async (publishLanguages, config) => {
|
|
|
121
121
|
...spaceLanguageCodes,
|
|
122
122
|
]);
|
|
123
123
|
};
|
|
124
|
+
const isPublishLanguageStateClean = (languagePublishStateMap, story, language) => {
|
|
125
|
+
const storyEntry = languagePublishStateMap?.stories?.[story.full_slug];
|
|
126
|
+
const languageState = storyEntry?.languages?.[language]?.state;
|
|
127
|
+
if (!languageState) {
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
130
|
+
return languageState === "published_clean";
|
|
131
|
+
};
|
|
132
|
+
const resolvePublishLanguagesForStory = ({ story, publishState, publishLanguages, languagePublishStateMap, }) => {
|
|
133
|
+
if (!languagePublishStateMap) {
|
|
134
|
+
return publishState && !publishState.shouldPublish
|
|
135
|
+
? []
|
|
136
|
+
: publishLanguages;
|
|
137
|
+
}
|
|
138
|
+
return publishLanguages.filter((language) => {
|
|
139
|
+
if (language === DEFAULT_PUBLISH_LANGUAGE) {
|
|
140
|
+
return !publishState || publishState.shouldPublish;
|
|
141
|
+
}
|
|
142
|
+
const languageStateAllowsPublish = isPublishLanguageStateClean(languagePublishStateMap, story, language);
|
|
143
|
+
if (languageStateAllowsPublish !== undefined) {
|
|
144
|
+
return languageStateAllowsPublish;
|
|
145
|
+
}
|
|
146
|
+
return !publishState || publishState.shouldPublish;
|
|
147
|
+
});
|
|
148
|
+
};
|
|
124
149
|
const resolveStoryblokErrorResponse = (err) => {
|
|
125
150
|
if (typeof err?.response === "string" && err.response.trim().length > 0) {
|
|
126
151
|
return err.response.trim();
|
|
@@ -348,33 +373,47 @@ export const updateStories = async (args, config) => {
|
|
|
348
373
|
const publishState = shouldPreservePublishState
|
|
349
374
|
? resolveStoryPublishState(story)
|
|
350
375
|
: undefined;
|
|
376
|
+
const storyPublishLanguages = publishLanguages
|
|
377
|
+
? resolvePublishLanguagesForStory({
|
|
378
|
+
story,
|
|
379
|
+
publishState,
|
|
380
|
+
publishLanguages,
|
|
381
|
+
languagePublishStateMap: options.languagePublishStateMap,
|
|
382
|
+
})
|
|
383
|
+
: undefined;
|
|
351
384
|
const shouldPublishStory = options.publish &&
|
|
352
|
-
(!publishState || publishState.shouldPublish)
|
|
385
|
+
(!publishState || publishState.shouldPublish) &&
|
|
386
|
+
(!shouldPublishLanguages ||
|
|
387
|
+
storyPublishLanguages?.includes(DEFAULT_PUBLISH_LANGUAGE));
|
|
353
388
|
const updateResult = await updateStory(story, story.id, {
|
|
354
389
|
publish: shouldPublishStory && !shouldPublishLanguages,
|
|
355
390
|
force_update: options.force_update,
|
|
356
391
|
}, { ...config, spaceId });
|
|
357
392
|
if (options.publish &&
|
|
358
393
|
updateResult?.ok &&
|
|
359
|
-
isSkippedStoryPublishState(publishState)
|
|
394
|
+
isSkippedStoryPublishState(publishState) &&
|
|
395
|
+
(!storyPublishLanguages || storyPublishLanguages.length === 0)) {
|
|
360
396
|
return withSkippedPublish({
|
|
361
397
|
updateResult,
|
|
362
398
|
story,
|
|
363
399
|
storyId: story.id,
|
|
364
400
|
spaceId,
|
|
365
|
-
publishLanguages
|
|
401
|
+
publishLanguages: options.languagePublishStateMap
|
|
402
|
+
? storyPublishLanguages
|
|
403
|
+
: publishLanguages,
|
|
366
404
|
publishState,
|
|
367
405
|
});
|
|
368
406
|
}
|
|
369
407
|
if (!shouldPublishLanguages ||
|
|
370
|
-
!
|
|
408
|
+
!storyPublishLanguages ||
|
|
409
|
+
storyPublishLanguages.length === 0 ||
|
|
371
410
|
!updateResult?.ok) {
|
|
372
411
|
return updateResult;
|
|
373
412
|
}
|
|
374
413
|
return publishStoryLanguages({
|
|
375
414
|
storyId: story.id,
|
|
376
415
|
story,
|
|
377
|
-
languages:
|
|
416
|
+
languages: storyPublishLanguages,
|
|
378
417
|
}, { ...config, spaceId });
|
|
379
418
|
}));
|
|
380
419
|
};
|
|
@@ -8,8 +8,19 @@ interface ModifyStoryOptions {
|
|
|
8
8
|
force_update?: boolean;
|
|
9
9
|
publishLanguages?: PublishLanguagesOption;
|
|
10
10
|
preservePublishState?: boolean;
|
|
11
|
+
languagePublishStateMap?: LanguagePublishStateMap;
|
|
11
12
|
}
|
|
12
13
|
export type PublishLanguagesOption = "default" | "all" | string[];
|
|
14
|
+
export interface LanguagePublishStateMap {
|
|
15
|
+
stories?: Record<string, {
|
|
16
|
+
languages?: Record<string, {
|
|
17
|
+
state?: string;
|
|
18
|
+
[key: string]: unknown;
|
|
19
|
+
}>;
|
|
20
|
+
cleanPublishedLanguages?: string[];
|
|
21
|
+
[key: string]: unknown;
|
|
22
|
+
}>;
|
|
23
|
+
}
|
|
13
24
|
export type RemoveStory = (args: {
|
|
14
25
|
storyId: string;
|
|
15
26
|
}, config: RequestBaseConfig) => Promise<any>;
|
package/dist/api/testApi.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as stories from "./stories/index.js";
|
|
1
2
|
export declare const testApi: {
|
|
2
3
|
assets: {
|
|
3
4
|
getAllAssets: import("./assets/assets.types.js").GetAllAssets;
|
|
@@ -77,6 +78,19 @@ export declare const testApi: {
|
|
|
77
78
|
removeAllStories: import("./stories/stories.types.js").RemoveAllStories;
|
|
78
79
|
upsertStory: import("./stories/stories.types.js").UpsertStory;
|
|
79
80
|
backupStories: import("./stories/stories.types.js").BackupStories;
|
|
81
|
+
buildLanguagePublishStateMap: (args: import("./stories/language-publish-state.js").BuildLanguagePublishStateMapArgs, config: import("./utils/request.js").RequestBaseConfig) => Promise<{
|
|
82
|
+
generatedAt: string;
|
|
83
|
+
source: {
|
|
84
|
+
spaceId: string;
|
|
85
|
+
startsWith: string | null;
|
|
86
|
+
withSlug: string[] | null;
|
|
87
|
+
};
|
|
88
|
+
languages: string[];
|
|
89
|
+
storyCount: number;
|
|
90
|
+
statesByLanguage: Record<string, any>;
|
|
91
|
+
stories: any;
|
|
92
|
+
}>;
|
|
93
|
+
loadLanguagePublishStateMap: (languagePublishStatePath: string) => stories.LanguagePublishStateMap;
|
|
80
94
|
};
|
|
81
95
|
spaces: {
|
|
82
96
|
getAllSpaces: import("./spaces/spaces.types.js").GetAllSpaces;
|
|
@@ -1,7 +1,8 @@
|
|
|
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";
|
|
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 language-publish-state\n Build a read-only Storyblok story language publish-state map.\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
|
+
export declare const languagePublishStateDescription = "\n Usage\n $ sb-mig language-publish-state --from [spaceId]\n\n Description\n Read stories from a source Storyblok space and write a JSON map of default and translated language publication states.\n This command is read-only against Storyblok. It uses Management API for story listing and default-language state, and Delivery API for translated language published/draft comparisons.\n\n FLAGS\n --from - Source space ID to inspect\n --accessToken - Optional source space Delivery API access token override. Falls back to configured accessToken.\n --languages - Languages to inspect: all, default,fr,de. Default: all\n --withSlug - Exact story full_slug to inspect. Can be repeated.\n --startsWith - Filter stories by starts_with prefix\n --fileName - Stable output base name under sbmig/language-publish-state\n --outputPath - Explicit output path for the generated JSON file\n\n EXAMPLES\n $ sb-mig language-publish-state --from 12345 --startsWith about-ef --languages all --fileName about-ef-prod\n $ sb-mig language-publish-state --from 12345 --accessToken xxx --withSlug about-ef/testimonials --languages default,fr\n";
|
|
2
3
|
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
4
|
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 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
|
+
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 --languagePublishStatePath - JSON file generated by 'language-publish-state'. Matching non-default language entries override language publishing; missing entries fall back to normal publish behavior. [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 all --languagePublishStatePath sbmig/language-publish-state/prod-language-state.json --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
6
|
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
7
|
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
8
|
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";
|
|
@@ -7,6 +7,8 @@ export const mainDescription = `
|
|
|
7
7
|
discover Discover components, migration configs and write to file or stdout.
|
|
8
8
|
backup Command for backing up anything related to Storyblok
|
|
9
9
|
migrate Migrate content from space to space, or from file to space.
|
|
10
|
+
language-publish-state
|
|
11
|
+
Build a read-only Storyblok story language publish-state map.
|
|
10
12
|
debug Output extra debugging information
|
|
11
13
|
help This screen
|
|
12
14
|
|
|
@@ -14,6 +16,27 @@ export const mainDescription = `
|
|
|
14
16
|
$ sb-mig sync components --all
|
|
15
17
|
$ sb-mig debug
|
|
16
18
|
`;
|
|
19
|
+
export const languagePublishStateDescription = `
|
|
20
|
+
Usage
|
|
21
|
+
$ sb-mig language-publish-state --from [spaceId]
|
|
22
|
+
|
|
23
|
+
Description
|
|
24
|
+
Read stories from a source Storyblok space and write a JSON map of default and translated language publication states.
|
|
25
|
+
This command is read-only against Storyblok. It uses Management API for story listing and default-language state, and Delivery API for translated language published/draft comparisons.
|
|
26
|
+
|
|
27
|
+
FLAGS
|
|
28
|
+
--from - Source space ID to inspect
|
|
29
|
+
--accessToken - Optional source space Delivery API access token override. Falls back to configured accessToken.
|
|
30
|
+
--languages - Languages to inspect: all, default,fr,de. Default: all
|
|
31
|
+
--withSlug - Exact story full_slug to inspect. Can be repeated.
|
|
32
|
+
--startsWith - Filter stories by starts_with prefix
|
|
33
|
+
--fileName - Stable output base name under sbmig/language-publish-state
|
|
34
|
+
--outputPath - Explicit output path for the generated JSON file
|
|
35
|
+
|
|
36
|
+
EXAMPLES
|
|
37
|
+
$ sb-mig language-publish-state --from 12345 --startsWith about-ef --languages all --fileName about-ef-prod
|
|
38
|
+
$ sb-mig language-publish-state --from 12345 --accessToken xxx --withSlug about-ef/testimonials --languages default,fr
|
|
39
|
+
`;
|
|
17
40
|
export const syncDescription = `
|
|
18
41
|
Usage
|
|
19
42
|
$ sb-mig sync [components|roles|datasources|plugins|content] [space separated file names] or --all
|
|
@@ -100,6 +123,7 @@ export const migrateDescription = `
|
|
|
100
123
|
--dry-run - Preview what would be migrated without making any API changes
|
|
101
124
|
--publish - Publish changed stories after migration only if they were clean-published before migration. Default: save draft. [content only]
|
|
102
125
|
--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]
|
|
126
|
+
--languagePublishStatePath - JSON file generated by 'language-publish-state'. Matching non-default language entries override language publishing; missing entries fall back to normal publish behavior. [content only]
|
|
103
127
|
--fileName - Stable base name for migration output files (disables timestamp suffix for migration artifacts)
|
|
104
128
|
|
|
105
129
|
EXAMPLES
|
|
@@ -111,6 +135,7 @@ export const migrateDescription = `
|
|
|
111
135
|
$ sb-mig migrate content --all --from 12345 --to 12345 --migration file-with-migration --startsWith blog/
|
|
112
136
|
$ sb-mig migrate content --all --from 12345 --to 12345 --migration file-with-migration --publish --yes
|
|
113
137
|
$ sb-mig migrate content --all --from 12345 --to 12345 --migration file-with-migration --publish --publishLanguages all --yes
|
|
138
|
+
$ sb-mig migrate content --all --from 12345 --to 12345 --migration file-with-migration --publish --publishLanguages all --languagePublishStatePath sbmig/language-publish-state/prod-language-state.json --yes
|
|
114
139
|
$ sb-mig migrate content --all --from 12345 --to 12345 --migration file-with-migration --publish --publishLanguages default,fr,de --yes
|
|
115
140
|
$ sb-mig migrate content --all --from 12345 --to 12345 --migration v3toV4AllMigrations --dry-run --fileName brand-hub-v3-v4-run
|
|
116
141
|
$ sb-mig migrate content --all --migrate-from file --from file-with-stories --to 12345 --migration file-with-migration
|
|
@@ -4,6 +4,7 @@ import { v4 as uuidv4 } from "uuid";
|
|
|
4
4
|
import { managementApi } from "../../api/managementApi.js";
|
|
5
5
|
import { storyblokApiMapping } from "../../config/constants.js";
|
|
6
6
|
import Logger from "../../utils/logger.js";
|
|
7
|
+
import { buildUrl } from "../../utils/url-utils.js";
|
|
7
8
|
import { apiConfig } from "../api-config.js";
|
|
8
9
|
const INIT_COMMANDS = {
|
|
9
10
|
project: "project",
|
|
@@ -59,10 +60,18 @@ export const init = async (props) => {
|
|
|
59
60
|
console.log(e);
|
|
60
61
|
}
|
|
61
62
|
try {
|
|
63
|
+
const previewDomainUrl = buildUrl({
|
|
64
|
+
baseUrl: "https://localhost:3000",
|
|
65
|
+
pathname: "api/preview/preview",
|
|
66
|
+
searchParams: {
|
|
67
|
+
secret: STORYBLOK_PREVIEW_SECRET,
|
|
68
|
+
slug: "",
|
|
69
|
+
},
|
|
70
|
+
});
|
|
62
71
|
await managementApi.spaces.updateSpace({
|
|
63
72
|
spaceId,
|
|
64
73
|
params: {
|
|
65
|
-
domain:
|
|
74
|
+
domain: previewDomainUrl.toString(),
|
|
66
75
|
},
|
|
67
76
|
}, { ...apiConfig, sbApi: localSbApi });
|
|
68
77
|
Logger.success("Successfully updated space domain");
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { managementApi } from "../../api/managementApi.js";
|
|
2
|
+
import { apiConfig } from "../api-config.js";
|
|
3
|
+
export const languagePublishState = async ({ flags }) => {
|
|
4
|
+
const withSlugFlag = flags["withSlug"];
|
|
5
|
+
const withSlug = Array.isArray(withSlugFlag)
|
|
6
|
+
? withSlugFlag
|
|
7
|
+
: withSlugFlag
|
|
8
|
+
? [withSlugFlag]
|
|
9
|
+
: undefined;
|
|
10
|
+
await managementApi.stories.buildLanguagePublishStateMap({
|
|
11
|
+
from: flags["from"],
|
|
12
|
+
accessToken: flags["accessToken"],
|
|
13
|
+
languages: flags["languages"],
|
|
14
|
+
startsWith: flags["startsWith"],
|
|
15
|
+
withSlug,
|
|
16
|
+
fileName: flags["fileName"],
|
|
17
|
+
outputPath: flags["outputPath"],
|
|
18
|
+
}, apiConfig);
|
|
19
|
+
};
|
|
@@ -44,6 +44,7 @@ export const migrate = async (props) => {
|
|
|
44
44
|
"dryRun",
|
|
45
45
|
"publish",
|
|
46
46
|
"publishLanguages",
|
|
47
|
+
"languagePublishStatePath",
|
|
47
48
|
"fileName",
|
|
48
49
|
]);
|
|
49
50
|
Logger.warning(`This feature is in BETA. Use it at your own risk. The API might change in the future. (Probably in a standard Prisma like migration way)`);
|
|
@@ -65,6 +66,7 @@ export const migrate = async (props) => {
|
|
|
65
66
|
const dryRun = flags["dryRun"];
|
|
66
67
|
const publish = Boolean(flags["publish"]);
|
|
67
68
|
const publishLanguagesFlag = flags["publishLanguages"];
|
|
69
|
+
const languagePublishStatePath = flags["languagePublishStatePath"];
|
|
68
70
|
const publishLanguages = publishLanguagesFlag
|
|
69
71
|
? parsePublishLanguagesOption(publishLanguagesFlag)
|
|
70
72
|
: undefined;
|
|
@@ -82,6 +84,10 @@ export const migrate = async (props) => {
|
|
|
82
84
|
if (!publish && publishLanguagesFlag) {
|
|
83
85
|
throw new Error("--publishLanguages requires --publish for 'migrate content'.");
|
|
84
86
|
}
|
|
87
|
+
if (languagePublishStatePath &&
|
|
88
|
+
(!publish || !publishLanguagesFlag)) {
|
|
89
|
+
throw new Error("--languagePublishStatePath requires --publish and --publishLanguages for 'migrate content'.");
|
|
90
|
+
}
|
|
85
91
|
if (isIt("empty")) {
|
|
86
92
|
const componentsToMigrate = unpackElements(input) || [""];
|
|
87
93
|
const migrateFrom = "space";
|
|
@@ -112,6 +118,7 @@ export const migrate = async (props) => {
|
|
|
112
118
|
publishLanguages,
|
|
113
119
|
fromFilePath,
|
|
114
120
|
fileName,
|
|
121
|
+
languagePublishStatePath,
|
|
115
122
|
}, apiConfig);
|
|
116
123
|
};
|
|
117
124
|
if (dryRun) {
|
|
@@ -141,6 +148,7 @@ export const migrate = async (props) => {
|
|
|
141
148
|
publishLanguages,
|
|
142
149
|
fromFilePath,
|
|
143
150
|
fileName,
|
|
151
|
+
languagePublishStatePath,
|
|
144
152
|
}, apiConfig);
|
|
145
153
|
};
|
|
146
154
|
if (dryRun) {
|
|
@@ -175,6 +183,7 @@ export const migrate = async (props) => {
|
|
|
175
183
|
const to = getTo(flags, apiConfig);
|
|
176
184
|
const publish = Boolean(flags["publish"]);
|
|
177
185
|
const publishLanguagesFlag = flags["publishLanguages"];
|
|
186
|
+
const languagePublishStatePath = flags["languagePublishStatePath"];
|
|
178
187
|
if (migrationConfigs.length === 0) {
|
|
179
188
|
throw new Error("Missing migration config. Pass exactly one --migration value for presets.");
|
|
180
189
|
}
|
|
@@ -184,6 +193,9 @@ export const migrate = async (props) => {
|
|
|
184
193
|
if (publishLanguagesFlag) {
|
|
185
194
|
throw new Error("--publishLanguages is only supported for 'migrate content'. Presets cannot be published.");
|
|
186
195
|
}
|
|
196
|
+
if (languagePublishStatePath) {
|
|
197
|
+
throw new Error("--languagePublishStatePath is only supported for 'migrate content'. Presets cannot be published.");
|
|
198
|
+
}
|
|
187
199
|
if (migrationConfigs.length > 1) {
|
|
188
200
|
throw new Error("Multiple --migration values are currently supported only for 'migrate content'. Presets support a single migration config.");
|
|
189
201
|
}
|
package/dist/cli/index.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
#! /usr/bin/env node
|
|
2
2
|
import meow from "meow";
|
|
3
|
-
import { backupDescription, debugDescription, mainDescription, syncDescription, removeDescription, initDescription, discoverDescription, migrateDescription, revertDescription, migrationsDescription, copyDescription, } from "./cli-descriptions.js";
|
|
3
|
+
import { backupDescription, debugDescription, mainDescription, syncDescription, removeDescription, initDescription, discoverDescription, migrateDescription, languagePublishStateDescription, revertDescription, migrationsDescription, copyDescription, } from "./cli-descriptions.js";
|
|
4
4
|
import { backup } from "./commands/backup.js";
|
|
5
5
|
import { copyCommand } from "./commands/copy.js";
|
|
6
6
|
import { debug } from "./commands/debug.js";
|
|
7
7
|
import { discover } from "./commands/discover.js";
|
|
8
8
|
import { init } from "./commands/init.js";
|
|
9
|
+
import { languagePublishState } from "./commands/language-publish-state.js";
|
|
9
10
|
import { migrate } from "./commands/migrate.js";
|
|
10
11
|
import { migrations } from "./commands/migrations.js";
|
|
11
12
|
import { remove } from "./commands/remove.js";
|
|
@@ -87,6 +88,9 @@ app.migrate = () => ({
|
|
|
87
88
|
publishLanguages: {
|
|
88
89
|
type: "string",
|
|
89
90
|
},
|
|
91
|
+
languagePublishStatePath: {
|
|
92
|
+
type: "string",
|
|
93
|
+
},
|
|
90
94
|
fileName: {
|
|
91
95
|
type: "string",
|
|
92
96
|
},
|
|
@@ -96,6 +100,38 @@ app.migrate = () => ({
|
|
|
96
100
|
migrate(cli);
|
|
97
101
|
},
|
|
98
102
|
});
|
|
103
|
+
app["language-publish-state"] = () => ({
|
|
104
|
+
cli: meow(languagePublishStateDescription, {
|
|
105
|
+
importMeta: import.meta,
|
|
106
|
+
booleanDefault: undefined,
|
|
107
|
+
flags: {
|
|
108
|
+
from: {
|
|
109
|
+
type: "string",
|
|
110
|
+
},
|
|
111
|
+
accessToken: {
|
|
112
|
+
type: "string",
|
|
113
|
+
},
|
|
114
|
+
languages: {
|
|
115
|
+
type: "string",
|
|
116
|
+
default: "all",
|
|
117
|
+
},
|
|
118
|
+
withSlug: {
|
|
119
|
+
type: "string",
|
|
120
|
+
isMultiple: true,
|
|
121
|
+
},
|
|
122
|
+
startsWith: {
|
|
123
|
+
type: "string",
|
|
124
|
+
},
|
|
125
|
+
fileName: {
|
|
126
|
+
type: "string",
|
|
127
|
+
},
|
|
128
|
+
outputPath: {
|
|
129
|
+
type: "string",
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
}),
|
|
133
|
+
action: (cli) => languagePublishState(cli),
|
|
134
|
+
});
|
|
99
135
|
app.revert = () => ({
|
|
100
136
|
cli: meow(revertDescription, {
|
|
101
137
|
importMeta: import.meta,
|
|
@@ -43,7 +43,8 @@ export const defaultConfig = (pkg, path, env) => {
|
|
|
43
43
|
oauthToken: env["STORYBLOK_OAUTH_TOKEN"] ?? "",
|
|
44
44
|
openaiToken: env["OPENAI_API_KEY"] ?? "",
|
|
45
45
|
spaceId: env["STORYBLOK_SPACE_ID"] ?? "",
|
|
46
|
-
accessToken: env["
|
|
46
|
+
accessToken: env["STORYBLOK_ACCESS_TOKEN"] ||
|
|
47
|
+
env["GATSBY_STORYBLOK_ACCESS_TOKEN"] ||
|
|
47
48
|
env["NEXT_PUBLIC_STORYBLOK_ACCESS_TOKEN"] ||
|
|
48
49
|
"",
|
|
49
50
|
boilerplateSpaceId: "172677", // this is id of Content seed for nextjs boilerplate space
|
package/dist/utils/path-utils.js
CHANGED
|
@@ -22,6 +22,10 @@ export const extractComponentName = (filePath) => {
|
|
|
22
22
|
const lastElement = normalized.substring(normalized.lastIndexOf("/") + 1);
|
|
23
23
|
return lastElement.replace(/\.ts$/, "");
|
|
24
24
|
};
|
|
25
|
+
const getFileName = (filePath) => {
|
|
26
|
+
const normalized = filePath.replace(/\\/g, "/");
|
|
27
|
+
return normalized.substring(normalized.lastIndexOf("/") + 1);
|
|
28
|
+
};
|
|
25
29
|
/**
|
|
26
30
|
* Normalizes an array of directory segments for glob pattern usage.
|
|
27
31
|
* Handles the glob.sync quirk where single segments don't need braces,
|
|
@@ -102,13 +106,13 @@ export const exactFilesPatterns = ({ mainDirectory, componentDirectories, fileNa
|
|
|
102
106
|
export const compare = (request) => {
|
|
103
107
|
const { local, external } = request;
|
|
104
108
|
const splittedLocal = local.map((p) => ({
|
|
105
|
-
name:
|
|
109
|
+
name: getFileName(p),
|
|
106
110
|
p,
|
|
107
111
|
}));
|
|
108
112
|
const localNames = new Set(splittedLocal.map((file) => file.name));
|
|
109
113
|
const splittedExternal = external
|
|
110
114
|
.map((p) => ({
|
|
111
|
-
name:
|
|
115
|
+
name: getFileName(p),
|
|
112
116
|
p,
|
|
113
117
|
}))
|
|
114
118
|
.filter((file) => {
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export type SearchParamValue = string | number | boolean | null | undefined;
|
|
2
|
+
type BuildUrlArgs = {
|
|
3
|
+
baseUrl: string;
|
|
4
|
+
pathname?: string;
|
|
5
|
+
searchParams?: Record<string, SearchParamValue>;
|
|
6
|
+
};
|
|
7
|
+
export declare const buildUrl: ({ baseUrl, pathname, searchParams, }: BuildUrlArgs) => URL;
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
const withTrailingSlash = (value) => value.endsWith("/") ? value : `${value}/`;
|
|
2
|
+
const normalizePathname = (pathname = "") => pathname.replace(/^\/+/, "");
|
|
3
|
+
export const buildUrl = ({ baseUrl, pathname, searchParams = {}, }) => {
|
|
4
|
+
const url = new URL(normalizePathname(pathname), withTrailingSlash(baseUrl));
|
|
5
|
+
for (const [key, value] of Object.entries(searchParams)) {
|
|
6
|
+
if (value === undefined || value === null) {
|
|
7
|
+
continue;
|
|
8
|
+
}
|
|
9
|
+
url.searchParams.set(key, String(value));
|
|
10
|
+
}
|
|
11
|
+
return url;
|
|
12
|
+
};
|
|
@@ -130,6 +130,31 @@ const resolvePublishLanguageCodes = async (publishLanguages, config) => {
|
|
|
130
130
|
]);
|
|
131
131
|
};
|
|
132
132
|
exports.resolvePublishLanguageCodes = resolvePublishLanguageCodes;
|
|
133
|
+
const isPublishLanguageStateClean = (languagePublishStateMap, story, language) => {
|
|
134
|
+
const storyEntry = languagePublishStateMap?.stories?.[story.full_slug];
|
|
135
|
+
const languageState = storyEntry?.languages?.[language]?.state;
|
|
136
|
+
if (!languageState) {
|
|
137
|
+
return undefined;
|
|
138
|
+
}
|
|
139
|
+
return languageState === "published_clean";
|
|
140
|
+
};
|
|
141
|
+
const resolvePublishLanguagesForStory = ({ story, publishState, publishLanguages, languagePublishStateMap, }) => {
|
|
142
|
+
if (!languagePublishStateMap) {
|
|
143
|
+
return publishState && !publishState.shouldPublish
|
|
144
|
+
? []
|
|
145
|
+
: publishLanguages;
|
|
146
|
+
}
|
|
147
|
+
return publishLanguages.filter((language) => {
|
|
148
|
+
if (language === DEFAULT_PUBLISH_LANGUAGE) {
|
|
149
|
+
return !publishState || publishState.shouldPublish;
|
|
150
|
+
}
|
|
151
|
+
const languageStateAllowsPublish = isPublishLanguageStateClean(languagePublishStateMap, story, language);
|
|
152
|
+
if (languageStateAllowsPublish !== undefined) {
|
|
153
|
+
return languageStateAllowsPublish;
|
|
154
|
+
}
|
|
155
|
+
return !publishState || publishState.shouldPublish;
|
|
156
|
+
});
|
|
157
|
+
};
|
|
133
158
|
const resolveStoryblokErrorResponse = (err) => {
|
|
134
159
|
if (typeof err?.response === "string" && err.response.trim().length > 0) {
|
|
135
160
|
return err.response.trim();
|
|
@@ -365,33 +390,47 @@ const updateStories = async (args, config) => {
|
|
|
365
390
|
const publishState = shouldPreservePublishState
|
|
366
391
|
? (0, exports.resolveStoryPublishState)(story)
|
|
367
392
|
: undefined;
|
|
393
|
+
const storyPublishLanguages = publishLanguages
|
|
394
|
+
? resolvePublishLanguagesForStory({
|
|
395
|
+
story,
|
|
396
|
+
publishState,
|
|
397
|
+
publishLanguages,
|
|
398
|
+
languagePublishStateMap: options.languagePublishStateMap,
|
|
399
|
+
})
|
|
400
|
+
: undefined;
|
|
368
401
|
const shouldPublishStory = options.publish &&
|
|
369
|
-
(!publishState || publishState.shouldPublish)
|
|
402
|
+
(!publishState || publishState.shouldPublish) &&
|
|
403
|
+
(!shouldPublishLanguages ||
|
|
404
|
+
storyPublishLanguages?.includes(DEFAULT_PUBLISH_LANGUAGE));
|
|
370
405
|
const updateResult = await (0, exports.updateStory)(story, story.id, {
|
|
371
406
|
publish: shouldPublishStory && !shouldPublishLanguages,
|
|
372
407
|
force_update: options.force_update,
|
|
373
408
|
}, { ...config, spaceId });
|
|
374
409
|
if (options.publish &&
|
|
375
410
|
updateResult?.ok &&
|
|
376
|
-
isSkippedStoryPublishState(publishState)
|
|
411
|
+
isSkippedStoryPublishState(publishState) &&
|
|
412
|
+
(!storyPublishLanguages || storyPublishLanguages.length === 0)) {
|
|
377
413
|
return withSkippedPublish({
|
|
378
414
|
updateResult,
|
|
379
415
|
story,
|
|
380
416
|
storyId: story.id,
|
|
381
417
|
spaceId,
|
|
382
|
-
publishLanguages
|
|
418
|
+
publishLanguages: options.languagePublishStateMap
|
|
419
|
+
? storyPublishLanguages
|
|
420
|
+
: publishLanguages,
|
|
383
421
|
publishState,
|
|
384
422
|
});
|
|
385
423
|
}
|
|
386
424
|
if (!shouldPublishLanguages ||
|
|
387
|
-
!
|
|
425
|
+
!storyPublishLanguages ||
|
|
426
|
+
storyPublishLanguages.length === 0 ||
|
|
388
427
|
!updateResult?.ok) {
|
|
389
428
|
return updateResult;
|
|
390
429
|
}
|
|
391
430
|
return (0, exports.publishStoryLanguages)({
|
|
392
431
|
storyId: story.id,
|
|
393
432
|
story,
|
|
394
|
-
languages:
|
|
433
|
+
languages: storyPublishLanguages,
|
|
395
434
|
}, { ...config, spaceId });
|
|
396
435
|
}));
|
|
397
436
|
};
|
|
@@ -29,6 +29,10 @@ const extractComponentName = (filePath) => {
|
|
|
29
29
|
return lastElement.replace(/\.ts$/, "");
|
|
30
30
|
};
|
|
31
31
|
exports.extractComponentName = extractComponentName;
|
|
32
|
+
const getFileName = (filePath) => {
|
|
33
|
+
const normalized = filePath.replace(/\\/g, "/");
|
|
34
|
+
return normalized.substring(normalized.lastIndexOf("/") + 1);
|
|
35
|
+
};
|
|
32
36
|
/**
|
|
33
37
|
* Normalizes an array of directory segments for glob pattern usage.
|
|
34
38
|
* Handles the glob.sync quirk where single segments don't need braces,
|
|
@@ -112,13 +116,13 @@ exports.exactFilesPatterns = exactFilesPatterns;
|
|
|
112
116
|
const compare = (request) => {
|
|
113
117
|
const { local, external } = request;
|
|
114
118
|
const splittedLocal = local.map((p) => ({
|
|
115
|
-
name:
|
|
119
|
+
name: getFileName(p),
|
|
116
120
|
p,
|
|
117
121
|
}));
|
|
118
122
|
const localNames = new Set(splittedLocal.map((file) => file.name));
|
|
119
123
|
const splittedExternal = external
|
|
120
124
|
.map((p) => ({
|
|
121
|
-
name:
|
|
125
|
+
name: getFileName(p),
|
|
122
126
|
p,
|
|
123
127
|
}))
|
|
124
128
|
.filter((file) => {
|