storyblok 4.5.0 → 4.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.mjs +431 -288
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -1
package/dist/index.mjs
CHANGED
|
@@ -8,6 +8,7 @@ import { readPackageUp } from 'read-package-up';
|
|
|
8
8
|
import { Spinner } from '@topcli/spinner';
|
|
9
9
|
import { select, password, input, confirm } from '@inquirer/prompts';
|
|
10
10
|
import { ManagementApiClient } from '@storyblok/management-api-client';
|
|
11
|
+
import { RateLimit, Sema } from 'async-sema';
|
|
11
12
|
import fs, { mkdir, writeFile, readFile as readFile$1, access, readdir } from 'node:fs/promises';
|
|
12
13
|
import path, { join, parse, resolve } from 'node:path';
|
|
13
14
|
import filenamify from 'filenamify';
|
|
@@ -15,7 +16,9 @@ import { exec, spawn } from 'node:child_process';
|
|
|
15
16
|
import { promisify } from 'node:util';
|
|
16
17
|
import { getRegion } from '@storyblok/region-helper';
|
|
17
18
|
import { minimatch } from 'minimatch';
|
|
19
|
+
import { Readable, pipeline, Transform, Writable } from 'node:stream';
|
|
18
20
|
import { hash } from 'ohash';
|
|
21
|
+
import { MultiBar, Presets } from 'cli-progress';
|
|
19
22
|
import { compile } from 'json-schema-to-typescript';
|
|
20
23
|
import { readFileSync } from 'node:fs';
|
|
21
24
|
import open from 'open';
|
|
@@ -409,12 +412,13 @@ function requireAuthentication(state, verbose = false) {
|
|
|
409
412
|
return true;
|
|
410
413
|
}
|
|
411
414
|
|
|
412
|
-
const toPascalCase = (str) => {
|
|
413
|
-
return str.replace(/(?:^|_)(\w)/g, (_, char) => char.toUpperCase());
|
|
414
|
-
};
|
|
415
415
|
const toCamelCase = (str) => {
|
|
416
416
|
return str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase()).replace(/_/g, "").replace(/-([a-z])/g, (_, letter) => letter.toUpperCase()).replace(/[^a-z0-9]([a-z])/gi, (_, letter) => letter.toUpperCase()).replace(/[^a-z0-9]/gi, "");
|
|
417
417
|
};
|
|
418
|
+
const toPascalCase = (str) => {
|
|
419
|
+
const camelCase = toCamelCase(str);
|
|
420
|
+
return camelCase ? camelCase[0].toUpperCase() + camelCase.slice(1) : camelCase;
|
|
421
|
+
};
|
|
418
422
|
const capitalize = (str) => {
|
|
419
423
|
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
420
424
|
};
|
|
@@ -533,17 +537,28 @@ const getStoryblokUrl = (region = "eu") => {
|
|
|
533
537
|
|
|
534
538
|
let instance = null;
|
|
535
539
|
let storedConfig = null;
|
|
540
|
+
const lim = RateLimit(6, {
|
|
541
|
+
uniformDistribution: true
|
|
542
|
+
});
|
|
536
543
|
function configsAreEqual(config1, config2) {
|
|
537
544
|
return JSON.stringify(config1) === JSON.stringify(config2);
|
|
538
545
|
}
|
|
539
546
|
function mapiClient(options) {
|
|
540
547
|
if (!instance && options) {
|
|
541
548
|
instance = new ManagementApiClient(options);
|
|
549
|
+
instance.interceptors.request.use(async (request) => {
|
|
550
|
+
await lim();
|
|
551
|
+
return request;
|
|
552
|
+
});
|
|
542
553
|
storedConfig = options;
|
|
543
554
|
} else if (!instance) {
|
|
544
555
|
throw new Error("MAPI client not initialized. Call mapiClient with configuration first.");
|
|
545
556
|
} else if (options && storedConfig && !configsAreEqual(options, storedConfig)) {
|
|
546
557
|
instance = new ManagementApiClient(options);
|
|
558
|
+
instance.interceptors.request.use(async (request) => {
|
|
559
|
+
await lim();
|
|
560
|
+
return request;
|
|
561
|
+
});
|
|
547
562
|
storedConfig = options;
|
|
548
563
|
}
|
|
549
564
|
return instance;
|
|
@@ -2854,63 +2869,6 @@ const fetchStories = async (spaceId, params) => {
|
|
|
2854
2869
|
handleAPIError("pull_stories", error);
|
|
2855
2870
|
}
|
|
2856
2871
|
};
|
|
2857
|
-
const fetchAllStories = async (spaceId, params) => {
|
|
2858
|
-
try {
|
|
2859
|
-
const allStories = [];
|
|
2860
|
-
let currentPage = 1;
|
|
2861
|
-
let hasMorePages = true;
|
|
2862
|
-
const perPage = 100;
|
|
2863
|
-
while (hasMorePages) {
|
|
2864
|
-
const result = await fetchStories(spaceId, {
|
|
2865
|
-
...params,
|
|
2866
|
-
per_page: perPage,
|
|
2867
|
-
page: currentPage
|
|
2868
|
-
});
|
|
2869
|
-
if (!result) {
|
|
2870
|
-
break;
|
|
2871
|
-
}
|
|
2872
|
-
const { stories, headers } = result;
|
|
2873
|
-
if (stories && stories.length > 0) {
|
|
2874
|
-
allStories.push(...stories);
|
|
2875
|
-
const total = headers.get("Total");
|
|
2876
|
-
const perPageHeader = headers.get("Per-Page");
|
|
2877
|
-
if (total && perPageHeader) {
|
|
2878
|
-
const totalCount = Number(total);
|
|
2879
|
-
const perPageCount = Number(perPageHeader);
|
|
2880
|
-
const totalPages = Math.ceil(totalCount / perPageCount);
|
|
2881
|
-
hasMorePages = currentPage < totalPages;
|
|
2882
|
-
} else {
|
|
2883
|
-
hasMorePages = stories.length === perPage;
|
|
2884
|
-
}
|
|
2885
|
-
} else {
|
|
2886
|
-
hasMorePages = false;
|
|
2887
|
-
}
|
|
2888
|
-
currentPage++;
|
|
2889
|
-
}
|
|
2890
|
-
return allStories;
|
|
2891
|
-
} catch (error) {
|
|
2892
|
-
handleAPIError("pull_stories", error);
|
|
2893
|
-
}
|
|
2894
|
-
};
|
|
2895
|
-
async function fetchAllStoriesByComponent(spaceOptions, filterOptions) {
|
|
2896
|
-
const { spaceId } = spaceOptions;
|
|
2897
|
-
const { componentName = "", query, starts_with } = filterOptions || {};
|
|
2898
|
-
const params = {
|
|
2899
|
-
...starts_with && { starts_with }
|
|
2900
|
-
};
|
|
2901
|
-
if (componentName) {
|
|
2902
|
-
params.contain_component = componentName;
|
|
2903
|
-
}
|
|
2904
|
-
if (query) {
|
|
2905
|
-
params.filter_query = query.startsWith("filter_query") ? query : `filter_query${query}`;
|
|
2906
|
-
}
|
|
2907
|
-
try {
|
|
2908
|
-
const stories = await fetchAllStories(spaceId, params);
|
|
2909
|
-
return stories ?? [];
|
|
2910
|
-
} catch (error) {
|
|
2911
|
-
handleAPIError("pull_stories", error);
|
|
2912
|
-
}
|
|
2913
|
-
}
|
|
2914
2872
|
const fetchStory = async (spaceId, storyId) => {
|
|
2915
2873
|
try {
|
|
2916
2874
|
const client = mapiClient();
|
|
@@ -2947,6 +2905,98 @@ const updateStory = async (spaceId, storyId, payload) => {
|
|
|
2947
2905
|
}
|
|
2948
2906
|
};
|
|
2949
2907
|
|
|
2908
|
+
async function* storiesIterator(spaceId, params, onTotal) {
|
|
2909
|
+
try {
|
|
2910
|
+
let perPage = 500;
|
|
2911
|
+
const transformedParams = {
|
|
2912
|
+
...params
|
|
2913
|
+
};
|
|
2914
|
+
if (params?.componentName && typeof params.componentName === "string") {
|
|
2915
|
+
transformedParams.contain_component = params.componentName;
|
|
2916
|
+
delete transformedParams.componentName;
|
|
2917
|
+
}
|
|
2918
|
+
if (params?.query && typeof params.query === "string") {
|
|
2919
|
+
transformedParams.filter_query = params.query.startsWith("filter_query") ? params.query : `filter_query${params.query}`;
|
|
2920
|
+
delete transformedParams.query;
|
|
2921
|
+
}
|
|
2922
|
+
const result = await fetchStories(spaceId, {
|
|
2923
|
+
...transformedParams,
|
|
2924
|
+
per_page: perPage,
|
|
2925
|
+
page: 1
|
|
2926
|
+
});
|
|
2927
|
+
if (!result) {
|
|
2928
|
+
return;
|
|
2929
|
+
}
|
|
2930
|
+
const { headers } = result;
|
|
2931
|
+
const total = Number(headers.get("Total"));
|
|
2932
|
+
perPage = Number(headers.get("Per-Page"));
|
|
2933
|
+
const totalPages = Math.ceil(Number(total) / perPage);
|
|
2934
|
+
if (onTotal) {
|
|
2935
|
+
onTotal(total);
|
|
2936
|
+
}
|
|
2937
|
+
for (let page = 1; page <= totalPages; page++) {
|
|
2938
|
+
const result2 = await fetchStories(spaceId, {
|
|
2939
|
+
...transformedParams,
|
|
2940
|
+
per_page: perPage,
|
|
2941
|
+
page
|
|
2942
|
+
});
|
|
2943
|
+
if (!result2) {
|
|
2944
|
+
return;
|
|
2945
|
+
}
|
|
2946
|
+
const { stories } = result2;
|
|
2947
|
+
for (const story of stories) {
|
|
2948
|
+
yield story;
|
|
2949
|
+
}
|
|
2950
|
+
}
|
|
2951
|
+
} catch (error) {
|
|
2952
|
+
handleAPIError("pull_stories", error);
|
|
2953
|
+
}
|
|
2954
|
+
}
|
|
2955
|
+
class StoriesStream extends Transform {
|
|
2956
|
+
constructor(spaceId, batchSize, onProgress) {
|
|
2957
|
+
super({
|
|
2958
|
+
objectMode: true
|
|
2959
|
+
});
|
|
2960
|
+
this.spaceId = spaceId;
|
|
2961
|
+
this.batchSize = batchSize;
|
|
2962
|
+
this.onProgress = onProgress;
|
|
2963
|
+
this.semaphore = new Sema(this.batchSize, {
|
|
2964
|
+
capacity: this.batchSize
|
|
2965
|
+
});
|
|
2966
|
+
}
|
|
2967
|
+
semaphore;
|
|
2968
|
+
async _transform(chunk, _encoding, callback) {
|
|
2969
|
+
await this.semaphore.acquire();
|
|
2970
|
+
fetchStory(this.spaceId, chunk.id.toString()).then((story) => {
|
|
2971
|
+
this.push(story);
|
|
2972
|
+
this.onProgress?.();
|
|
2973
|
+
}).finally(() => {
|
|
2974
|
+
this.semaphore.release();
|
|
2975
|
+
});
|
|
2976
|
+
callback();
|
|
2977
|
+
}
|
|
2978
|
+
_flush(callback) {
|
|
2979
|
+
this.semaphore.drain().then(() => {
|
|
2980
|
+
callback();
|
|
2981
|
+
});
|
|
2982
|
+
}
|
|
2983
|
+
}
|
|
2984
|
+
const createStoriesStream = async ({
|
|
2985
|
+
spaceId,
|
|
2986
|
+
params,
|
|
2987
|
+
batchSize = 100,
|
|
2988
|
+
onTotal,
|
|
2989
|
+
onProgress
|
|
2990
|
+
}) => {
|
|
2991
|
+
const iterator = storiesIterator(spaceId, params, onTotal);
|
|
2992
|
+
const listStoriesStream = Readable.from(iterator);
|
|
2993
|
+
return pipeline(listStoriesStream, new StoriesStream(spaceId, batchSize, onProgress), (err) => {
|
|
2994
|
+
if (err) {
|
|
2995
|
+
console.error(err);
|
|
2996
|
+
}
|
|
2997
|
+
});
|
|
2998
|
+
};
|
|
2999
|
+
|
|
2950
3000
|
async function readJavascriptFile(filePath) {
|
|
2951
3001
|
try {
|
|
2952
3002
|
const content = await readFile$1(filePath, "utf-8");
|
|
@@ -3098,149 +3148,283 @@ async function readRollbackFile({
|
|
|
3098
3148
|
}
|
|
3099
3149
|
}
|
|
3100
3150
|
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
path,
|
|
3106
|
-
componentName
|
|
3107
|
-
}) {
|
|
3108
|
-
const results = {
|
|
3109
|
-
successful: [],
|
|
3110
|
-
failed: [],
|
|
3111
|
-
skipped: []
|
|
3112
|
-
};
|
|
3113
|
-
const relevantMigrations = componentName ? migrationFiles.filter((file) => {
|
|
3114
|
-
const targetComponent = getComponentNameFromFilename(file.name);
|
|
3115
|
-
return targetComponent.split(".")[0] === componentName;
|
|
3116
|
-
}) : migrationFiles;
|
|
3117
|
-
for (const migrationFile of relevantMigrations) {
|
|
3118
|
-
const validStories = stories.filter((story) => story.content);
|
|
3119
|
-
if (validStories.length === 0) {
|
|
3120
|
-
continue;
|
|
3121
|
-
}
|
|
3122
|
-
await saveRollbackData({
|
|
3123
|
-
space,
|
|
3124
|
-
path,
|
|
3125
|
-
stories: validStories,
|
|
3126
|
-
migrationFile: migrationFile.name
|
|
3151
|
+
class MigrationStream extends Transform {
|
|
3152
|
+
constructor(options) {
|
|
3153
|
+
super({
|
|
3154
|
+
objectMode: true
|
|
3127
3155
|
});
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3156
|
+
this.options = options;
|
|
3157
|
+
this.results = {
|
|
3158
|
+
successful: [],
|
|
3159
|
+
failed: [],
|
|
3160
|
+
skipped: [],
|
|
3161
|
+
totalProcessed: 0
|
|
3162
|
+
};
|
|
3163
|
+
}
|
|
3164
|
+
results;
|
|
3165
|
+
migrationFunctions = /* @__PURE__ */ new Map();
|
|
3166
|
+
totalProcessed = 0;
|
|
3167
|
+
_transform(chunk, _encoding, callback) {
|
|
3168
|
+
try {
|
|
3169
|
+
this.processStory(chunk).then((results) => {
|
|
3170
|
+
this.results.totalProcessed++;
|
|
3171
|
+
this.options.onProgress?.(this.results.totalProcessed);
|
|
3172
|
+
if (results.length > 0) {
|
|
3173
|
+
this.totalProcessed += results.length;
|
|
3174
|
+
this.options.onTotal?.(this.totalProcessed);
|
|
3175
|
+
for (const result of results) {
|
|
3176
|
+
this.push(result);
|
|
3177
|
+
}
|
|
3178
|
+
}
|
|
3179
|
+
});
|
|
3180
|
+
callback();
|
|
3181
|
+
} catch (error) {
|
|
3182
|
+
callback(error);
|
|
3183
|
+
}
|
|
3184
|
+
}
|
|
3185
|
+
async processStory(story) {
|
|
3186
|
+
if (!story.content) {
|
|
3187
|
+
for (const migrationFile of this.options.migrationFiles) {
|
|
3188
|
+
this.results.failed.push({
|
|
3132
3189
|
storyId: story.id,
|
|
3133
3190
|
migrationName: migrationFile.name,
|
|
3134
|
-
error: new Error(
|
|
3191
|
+
error: new Error("Story content is missing")
|
|
3135
3192
|
});
|
|
3136
|
-
}
|
|
3137
|
-
|
|
3193
|
+
}
|
|
3194
|
+
return [];
|
|
3195
|
+
}
|
|
3196
|
+
const relevantMigrations = this.options.componentName ? this.options.migrationFiles.filter((file) => {
|
|
3197
|
+
const targetComponent = getComponentNameFromFilename(file.name);
|
|
3198
|
+
return targetComponent.split(".")[0] === this.options.componentName;
|
|
3199
|
+
}) : this.options.migrationFiles;
|
|
3200
|
+
const successfulResults = [];
|
|
3201
|
+
for (const migrationFile of relevantMigrations) {
|
|
3202
|
+
const result = await this.applyMigrationToStory(story, migrationFile);
|
|
3203
|
+
if (result) {
|
|
3204
|
+
successfulResults.push(result);
|
|
3205
|
+
}
|
|
3138
3206
|
}
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
3207
|
+
return successfulResults;
|
|
3208
|
+
}
|
|
3209
|
+
async applyMigrationToStory(story, migrationFile) {
|
|
3210
|
+
try {
|
|
3211
|
+
let migrationFunction = this.migrationFunctions.get(migrationFile.name);
|
|
3212
|
+
if (!migrationFunction) {
|
|
3213
|
+
migrationFunction = await getMigrationFunction(
|
|
3214
|
+
migrationFile.name,
|
|
3215
|
+
this.options.space,
|
|
3216
|
+
this.options.path
|
|
3217
|
+
);
|
|
3218
|
+
this.migrationFunctions.set(migrationFile.name, migrationFunction);
|
|
3219
|
+
}
|
|
3220
|
+
if (!migrationFunction) {
|
|
3221
|
+
this.results.failed.push({
|
|
3143
3222
|
storyId: story.id,
|
|
3144
3223
|
migrationName: migrationFile.name,
|
|
3145
|
-
error: new Error(
|
|
3224
|
+
error: new Error(`Failed to load migration function from file "${migrationFile.name}"`)
|
|
3146
3225
|
});
|
|
3147
|
-
|
|
3226
|
+
return null;
|
|
3148
3227
|
}
|
|
3228
|
+
await saveRollbackData({
|
|
3229
|
+
space: this.options.space,
|
|
3230
|
+
path: this.options.path,
|
|
3231
|
+
stories: [{ id: story.id, name: story.name || "", content: story.content }],
|
|
3232
|
+
migrationFile: migrationFile.name
|
|
3233
|
+
});
|
|
3149
3234
|
const storyContent = structuredClone(story.content);
|
|
3150
3235
|
const originalContentHash = hash(story.content);
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
reason: baseComponent === componentName ? "No matching components found" : "Different component target"
|
|
3179
|
-
});
|
|
3180
|
-
}
|
|
3181
|
-
} catch (error) {
|
|
3182
|
-
const spinner = new Spinner({ verbose: !isVitest });
|
|
3183
|
-
spinner.start(`Applying migration ${chalk.hex(colorPalette.MIGRATIONS)(migrationFile.name)} to story ${chalk.hex(colorPalette.PRIMARY)(story.name || story.id.toString())}...`);
|
|
3184
|
-
spinner.failed(`Failed to apply migration ${chalk.hex(colorPalette.MIGRATIONS)(migrationFile.name)} to story ${chalk.hex(colorPalette.PRIMARY)(story.name || story.id.toString())}`);
|
|
3185
|
-
results.failed.push({
|
|
3236
|
+
const targetComponent = this.options.componentName || getComponentNameFromFilename(migrationFile.name);
|
|
3237
|
+
const modified = applyMigrationToAllBlocks(storyContent, migrationFunction, targetComponent);
|
|
3238
|
+
const newContentHash = hash(storyContent);
|
|
3239
|
+
const contentChanged = originalContentHash !== newContentHash;
|
|
3240
|
+
if (modified && contentChanged) {
|
|
3241
|
+
this.results.successful.push({
|
|
3242
|
+
storyId: story.id,
|
|
3243
|
+
name: story.name,
|
|
3244
|
+
migrationName: migrationFile.name,
|
|
3245
|
+
content: storyContent
|
|
3246
|
+
});
|
|
3247
|
+
return {
|
|
3248
|
+
storyId: story.id,
|
|
3249
|
+
name: story.name,
|
|
3250
|
+
content: storyContent
|
|
3251
|
+
};
|
|
3252
|
+
} else if (modified && !contentChanged) {
|
|
3253
|
+
this.results.skipped.push({
|
|
3254
|
+
storyId: story.id,
|
|
3255
|
+
name: story.name,
|
|
3256
|
+
migrationName: migrationFile.name,
|
|
3257
|
+
reason: "No changes detected after migration"
|
|
3258
|
+
});
|
|
3259
|
+
return null;
|
|
3260
|
+
} else {
|
|
3261
|
+
const baseComponent = targetComponent.split(".")[0];
|
|
3262
|
+
this.results.skipped.push({
|
|
3186
3263
|
storyId: story.id,
|
|
3264
|
+
name: story.name,
|
|
3187
3265
|
migrationName: migrationFile.name,
|
|
3188
|
-
|
|
3266
|
+
reason: baseComponent === this.options.componentName ? "No matching components found" : "Different component target"
|
|
3189
3267
|
});
|
|
3268
|
+
return null;
|
|
3190
3269
|
}
|
|
3270
|
+
} catch (error) {
|
|
3271
|
+
this.results.failed.push({
|
|
3272
|
+
storyId: story.id,
|
|
3273
|
+
migrationName: migrationFile.name,
|
|
3274
|
+
error
|
|
3275
|
+
});
|
|
3276
|
+
return null;
|
|
3191
3277
|
}
|
|
3192
3278
|
}
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
}
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
konsola.error(`Story ID ${storyId}:`);
|
|
3227
|
-
failures.forEach(({ migrationName, error }) => {
|
|
3228
|
-
konsola.error(`- Migration ${migrationName}: ${error.message}`);
|
|
3229
|
-
});
|
|
3230
|
-
});
|
|
3231
|
-
} else {
|
|
3232
|
-
konsola.ok(`No failures reported`);
|
|
3279
|
+
_flush(callback) {
|
|
3280
|
+
callback();
|
|
3281
|
+
}
|
|
3282
|
+
/**
|
|
3283
|
+
* Get the migration results
|
|
3284
|
+
*/
|
|
3285
|
+
getResults() {
|
|
3286
|
+
return this.results;
|
|
3287
|
+
}
|
|
3288
|
+
/**
|
|
3289
|
+
* Get a summary of the migration results
|
|
3290
|
+
*/
|
|
3291
|
+
getSummary() {
|
|
3292
|
+
const { successful, failed, skipped } = this.results;
|
|
3293
|
+
const successfulStoryIds = new Set(successful.map((result) => result.storyId));
|
|
3294
|
+
let summary = `Migration Results: ${successfulStoryIds.size} stories updated, ${skipped.length} stories skipped`;
|
|
3295
|
+
if (skipped.length > 0) {
|
|
3296
|
+
const skippedByReason = skipped.reduce((acc, item) => {
|
|
3297
|
+
if (!acc[item.reason]) {
|
|
3298
|
+
acc[item.reason] = 0;
|
|
3299
|
+
}
|
|
3300
|
+
acc[item.reason]++;
|
|
3301
|
+
return acc;
|
|
3302
|
+
}, {});
|
|
3303
|
+
const skippedReasons = Object.entries(skippedByReason).map(([reason, count]) => `${reason}: ${count}`).join(", ");
|
|
3304
|
+
summary += ` (${skippedReasons})`;
|
|
3305
|
+
}
|
|
3306
|
+
if (failed.length > 0) {
|
|
3307
|
+
const failedStoryIds = new Set(failed.map((result) => result.storyId));
|
|
3308
|
+
summary += `, ${failedStoryIds.size} stories failed`;
|
|
3309
|
+
}
|
|
3310
|
+
summary += `.`;
|
|
3311
|
+
return summary;
|
|
3233
3312
|
}
|
|
3234
|
-
konsola.br();
|
|
3235
3313
|
}
|
|
3236
3314
|
|
|
3237
3315
|
const isStoryPublishedWithoutChanges = (story) => {
|
|
3238
|
-
return
|
|
3316
|
+
return true;
|
|
3239
3317
|
};
|
|
3240
3318
|
const isStoryWithUnpublishedChanges = (story) => {
|
|
3241
|
-
return story.
|
|
3319
|
+
return story.unpublished_changes;
|
|
3242
3320
|
};
|
|
3243
3321
|
|
|
3322
|
+
class UpdateStream extends Writable {
|
|
3323
|
+
constructor(options) {
|
|
3324
|
+
super({
|
|
3325
|
+
objectMode: true
|
|
3326
|
+
});
|
|
3327
|
+
this.options = options;
|
|
3328
|
+
this.batchSize = options.batchSize || 10;
|
|
3329
|
+
this.results = {
|
|
3330
|
+
successful: [],
|
|
3331
|
+
failed: [],
|
|
3332
|
+
totalProcessed: 0
|
|
3333
|
+
};
|
|
3334
|
+
this.semaphore = new Sema(this.batchSize, {
|
|
3335
|
+
capacity: this.batchSize
|
|
3336
|
+
});
|
|
3337
|
+
}
|
|
3338
|
+
results;
|
|
3339
|
+
batchSize;
|
|
3340
|
+
semaphore;
|
|
3341
|
+
async _write(chunk, _encoding, callback) {
|
|
3342
|
+
try {
|
|
3343
|
+
await this.semaphore.acquire();
|
|
3344
|
+
this.updateStory(chunk).finally(() => {
|
|
3345
|
+
this.semaphore.release();
|
|
3346
|
+
});
|
|
3347
|
+
callback();
|
|
3348
|
+
} catch (error) {
|
|
3349
|
+
callback(error);
|
|
3350
|
+
}
|
|
3351
|
+
}
|
|
3352
|
+
async updateStory(migrationResult) {
|
|
3353
|
+
const { storyId, name, content } = migrationResult;
|
|
3354
|
+
const storyName = name || storyId.toString();
|
|
3355
|
+
try {
|
|
3356
|
+
const payload = {
|
|
3357
|
+
story: {
|
|
3358
|
+
content,
|
|
3359
|
+
id: storyId,
|
|
3360
|
+
name: storyName
|
|
3361
|
+
},
|
|
3362
|
+
force_update: "1"
|
|
3363
|
+
};
|
|
3364
|
+
if (this.options.publish === "published" && isStoryPublishedWithoutChanges({ published: true, unpublished_changes: false })) {
|
|
3365
|
+
payload.publish = 1;
|
|
3366
|
+
} else if (this.options.publish === "published-with-changes" && isStoryWithUnpublishedChanges({ published: true, unpublished_changes: true })) {
|
|
3367
|
+
payload.publish = 1;
|
|
3368
|
+
} else if (this.options.publish === "all") {
|
|
3369
|
+
payload.publish = 1;
|
|
3370
|
+
}
|
|
3371
|
+
const updatedStory = await updateStory(this.options.space, storyId, payload);
|
|
3372
|
+
if (updatedStory) {
|
|
3373
|
+
this.results.successful.push({ storyId, name: storyName });
|
|
3374
|
+
this.results.totalProcessed++;
|
|
3375
|
+
this.options.onProgress?.(this.results.totalProcessed);
|
|
3376
|
+
} else {
|
|
3377
|
+
this.results.failed.push({
|
|
3378
|
+
storyId,
|
|
3379
|
+
name: storyName,
|
|
3380
|
+
error: new Error("Update returned null")
|
|
3381
|
+
});
|
|
3382
|
+
this.results.totalProcessed++;
|
|
3383
|
+
this.options.onProgress?.(this.results.totalProcessed);
|
|
3384
|
+
}
|
|
3385
|
+
} catch (error) {
|
|
3386
|
+
this.results.failed.push({
|
|
3387
|
+
storyId,
|
|
3388
|
+
name: storyName,
|
|
3389
|
+
error
|
|
3390
|
+
});
|
|
3391
|
+
this.results.totalProcessed++;
|
|
3392
|
+
this.options.onProgress?.(this.results.totalProcessed);
|
|
3393
|
+
}
|
|
3394
|
+
}
|
|
3395
|
+
async _destroy(error, callback) {
|
|
3396
|
+
try {
|
|
3397
|
+
await this.semaphore.drain();
|
|
3398
|
+
callback();
|
|
3399
|
+
} catch (batchError) {
|
|
3400
|
+
callback(batchError);
|
|
3401
|
+
return;
|
|
3402
|
+
}
|
|
3403
|
+
callback(error);
|
|
3404
|
+
}
|
|
3405
|
+
/**
|
|
3406
|
+
* Get the update results
|
|
3407
|
+
*/
|
|
3408
|
+
getResults() {
|
|
3409
|
+
return this.results;
|
|
3410
|
+
}
|
|
3411
|
+
/**
|
|
3412
|
+
* Get a summary of the update results
|
|
3413
|
+
*/
|
|
3414
|
+
getSummary() {
|
|
3415
|
+
const { successful, failed, totalProcessed } = this.results;
|
|
3416
|
+
if (totalProcessed === 0) {
|
|
3417
|
+
return `No stories required updates.`;
|
|
3418
|
+
}
|
|
3419
|
+
let summary = `Update Results: ${successful.length} stories updated`;
|
|
3420
|
+
if (failed.length > 0) {
|
|
3421
|
+
summary += `, ${failed.length} stories failed`;
|
|
3422
|
+
}
|
|
3423
|
+
summary += `.`;
|
|
3424
|
+
return summary;
|
|
3425
|
+
}
|
|
3426
|
+
}
|
|
3427
|
+
|
|
3244
3428
|
const program$8 = getProgram();
|
|
3245
3429
|
migrationsCommand.command("run [componentName]").description("Run migrations").option("--fi, --filter <filter>", "glob filter to apply to the components before pushing").option("-d, --dry-run", "Preview changes without applying them to Storyblok").option("-q, --query <query>", 'Filter stories by content attributes using Storyblok filter query syntax. Example: --query="[highlighted][in]=true"').option("--starts-with <path>", 'Filter stories by path. Example: --starts-with="/en/blog/"').option("--publish <publish>", "Options for publication mode: all | published | published-with-changes").action(async (componentName, options) => {
|
|
3246
3430
|
konsola.title(`${commands.MIGRATIONS}`, colorPalette.MIGRATIONS, componentName ? `Running migrations for component ${componentName}...` : "Running migrations...");
|
|
@@ -3284,119 +3468,76 @@ migrationsCommand.command("run [componentName]").description("Run migrations").o
|
|
|
3284
3468
|
return;
|
|
3285
3469
|
}
|
|
3286
3470
|
spinner.succeed(`Found ${filteredMigrations.length} migration files.`);
|
|
3287
|
-
const
|
|
3288
|
-
|
|
3289
|
-
{
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3471
|
+
const multiBar = new MultiBar({
|
|
3472
|
+
clearOnComplete: false,
|
|
3473
|
+
format: `${chalk.bold(" {title} ")} ${chalk.hex(colorPalette.PRIMARY)("[{bar}]")} {percentage}% | {eta_formatted} | {value}/{total} processed`,
|
|
3474
|
+
etaBuffer: 60
|
|
3475
|
+
}, Presets.rect);
|
|
3476
|
+
const storiesProgress = multiBar.create(0, 0, {
|
|
3477
|
+
title: "Fetching Stories...".padEnd(19)
|
|
3478
|
+
});
|
|
3479
|
+
const migrationsProgress = multiBar.create(0, 0, {
|
|
3480
|
+
title: "Applying Migrations".padEnd(19)
|
|
3481
|
+
});
|
|
3482
|
+
const updateProgress = multiBar.create(0, 0, {
|
|
3483
|
+
title: "Updating Stories...".padEnd(19)
|
|
3484
|
+
});
|
|
3485
|
+
const storiesStream = await createStoriesStream({
|
|
3486
|
+
spaceId: space,
|
|
3487
|
+
params: {
|
|
3294
3488
|
componentName,
|
|
3295
3489
|
query,
|
|
3296
3490
|
starts_with: startsWith
|
|
3491
|
+
},
|
|
3492
|
+
batchSize: 100,
|
|
3493
|
+
onTotal: (total) => {
|
|
3494
|
+
storiesProgress.setTotal(total);
|
|
3495
|
+
migrationsProgress.setTotal(total);
|
|
3496
|
+
},
|
|
3497
|
+
onProgress: () => {
|
|
3498
|
+
storiesProgress.increment();
|
|
3297
3499
|
}
|
|
3298
|
-
);
|
|
3299
|
-
|
|
3300
|
-
storiesSpinner.failed(`No stories found${componentName ? ` for component "${componentName}"` : ""}.`);
|
|
3301
|
-
return;
|
|
3302
|
-
}
|
|
3303
|
-
const storiesWithContent = await Promise.all(stories.map(async (story) => {
|
|
3304
|
-
const fullStory = await fetchStory(space, story.id.toString());
|
|
3305
|
-
return {
|
|
3306
|
-
...story,
|
|
3307
|
-
content: fullStory?.content
|
|
3308
|
-
};
|
|
3309
|
-
}));
|
|
3310
|
-
const validStories = storiesWithContent.filter((story) => story.content);
|
|
3311
|
-
const filterParts = [];
|
|
3312
|
-
if (componentName) {
|
|
3313
|
-
filterParts.push(`component "${componentName}"`);
|
|
3314
|
-
}
|
|
3315
|
-
if (startsWith) {
|
|
3316
|
-
filterParts.push(chalk.hex(colorPalette.PRIMARY)(`starts_with=${startsWith}`));
|
|
3317
|
-
}
|
|
3318
|
-
if (query) {
|
|
3319
|
-
filterParts.push(chalk.hex(colorPalette.PRIMARY)(`filter_query=${query}`));
|
|
3320
|
-
}
|
|
3321
|
-
const filterMessage = filterParts.length > 0 ? ` (filtered by ${filterParts.join(" and ")})` : "";
|
|
3322
|
-
storiesSpinner.succeed(`Fetched ${validStories.length} ${validStories.length === 1 ? "story" : "stories"} with related content${filterMessage}.`);
|
|
3323
|
-
const processingSpinner = new Spinner({ verbose: !isVitest }).start(`Processing migrations...`);
|
|
3324
|
-
processingSpinner.succeed(`Starting to process ${validStories.length} stories with ${filteredMigrations.length} migrations...`);
|
|
3325
|
-
const migrationResults = await handleMigrations({
|
|
3500
|
+
});
|
|
3501
|
+
const migrationStream = new MigrationStream({
|
|
3326
3502
|
migrationFiles: filteredMigrations,
|
|
3327
|
-
stories: validStories,
|
|
3328
3503
|
space,
|
|
3329
3504
|
path,
|
|
3330
3505
|
componentName,
|
|
3331
|
-
|
|
3332
|
-
|
|
3506
|
+
onTotal: (total) => {
|
|
3507
|
+
updateProgress.setTotal(total);
|
|
3508
|
+
},
|
|
3509
|
+
onProgress: () => {
|
|
3510
|
+
migrationsProgress.increment();
|
|
3511
|
+
}
|
|
3333
3512
|
});
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
} else {
|
|
3353
|
-
updateSpinner.succeed(`Found ${storiesToUpdate.length} stories to update.`);
|
|
3354
|
-
let successCount = 0;
|
|
3355
|
-
let failCount = 0;
|
|
3356
|
-
for (const story of storiesToUpdate) {
|
|
3357
|
-
const storySpinner = new Spinner({ verbose: !isVitest }).start(`Updating story ${chalk.hex(colorPalette.PRIMARY)(story.name || story.id.toString())}...`);
|
|
3358
|
-
const payload = {
|
|
3359
|
-
story: {
|
|
3360
|
-
content: story.content,
|
|
3361
|
-
id: story.id,
|
|
3362
|
-
name: story.name
|
|
3363
|
-
},
|
|
3364
|
-
force_update: "1"
|
|
3365
|
-
};
|
|
3366
|
-
if (publish === "published" && isStoryPublishedWithoutChanges(story)) {
|
|
3367
|
-
payload.publish = 1;
|
|
3368
|
-
}
|
|
3369
|
-
if (publish === "published-with-changes" && isStoryWithUnpublishedChanges(story)) {
|
|
3370
|
-
payload.publish = 1;
|
|
3371
|
-
}
|
|
3372
|
-
if (publish === "all") {
|
|
3373
|
-
payload.publish = 1;
|
|
3374
|
-
}
|
|
3375
|
-
try {
|
|
3376
|
-
const updatedStory = await updateStory(space, story.id, payload);
|
|
3377
|
-
if (updatedStory) {
|
|
3378
|
-
successCount++;
|
|
3379
|
-
storySpinner.succeed(`Updated story ${chalk.hex(colorPalette.PRIMARY)(story.name || story.id.toString())} - Completed in ${storySpinner.elapsedTime.toFixed(2)}ms`);
|
|
3380
|
-
} else {
|
|
3381
|
-
failCount++;
|
|
3382
|
-
storySpinner.failed(`Failed to update story ${chalk.hex(colorPalette.PRIMARY)(story.name || story.id.toString())}`);
|
|
3383
|
-
}
|
|
3384
|
-
} catch (error) {
|
|
3385
|
-
failCount++;
|
|
3386
|
-
storySpinner.failed(`Failed to update story ${chalk.hex(colorPalette.PRIMARY)(story.name || story.id.toString())}: ${error.message}`);
|
|
3513
|
+
const updateStream = new UpdateStream({
|
|
3514
|
+
space,
|
|
3515
|
+
publish,
|
|
3516
|
+
dryRun,
|
|
3517
|
+
batchSize: 100,
|
|
3518
|
+
onProgress: () => {
|
|
3519
|
+
updateProgress.increment();
|
|
3520
|
+
}
|
|
3521
|
+
});
|
|
3522
|
+
return new Promise((resolve, reject) => {
|
|
3523
|
+
pipeline(
|
|
3524
|
+
storiesStream,
|
|
3525
|
+
migrationStream,
|
|
3526
|
+
updateStream,
|
|
3527
|
+
(err) => {
|
|
3528
|
+
if (err) {
|
|
3529
|
+
reject(err);
|
|
3530
|
+
return;
|
|
3387
3531
|
}
|
|
3532
|
+
multiBar.stop();
|
|
3533
|
+
const migrationSummary = migrationStream.getSummary();
|
|
3534
|
+
konsola.info(migrationSummary);
|
|
3535
|
+
const updateSummary = updateStream.getSummary();
|
|
3536
|
+
konsola.info(updateSummary);
|
|
3537
|
+
resolve();
|
|
3388
3538
|
}
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
} else if (successCount > 0) {
|
|
3392
|
-
konsola.ok(`Successfully updated ${successCount} stories in Storyblok.`, true);
|
|
3393
|
-
}
|
|
3394
|
-
}
|
|
3395
|
-
} else if (dryRun) {
|
|
3396
|
-
konsola.info(`Dry run mode: No stories were updated in Storyblok.`);
|
|
3397
|
-
} else if (migrationResults.successful.length === 0) {
|
|
3398
|
-
konsola.info(`No stories were modified by the migrations.`);
|
|
3399
|
-
}
|
|
3539
|
+
);
|
|
3540
|
+
});
|
|
3400
3541
|
} catch (error) {
|
|
3401
3542
|
handleError(error, verbose);
|
|
3402
3543
|
}
|
|
@@ -3949,7 +4090,7 @@ const getComponentType = (componentName, options) => {
|
|
|
3949
4090
|
const prefix = options.typePrefix ?? "";
|
|
3950
4091
|
const suffix = options.typeSuffix ?? "";
|
|
3951
4092
|
const sanitizedName = componentName.replace(/[^a-z0-9]/gi, "_").replace(/_+/g, "_").replace(/^_+|_+$/g, "");
|
|
3952
|
-
const componentType = toPascalCase(
|
|
4093
|
+
const componentType = toPascalCase(`${prefix}_${sanitizedName}_${suffix}`);
|
|
3953
4094
|
const isFirstCharacterNumber = !Number.isNaN(Number.parseInt(componentType.charAt(0)));
|
|
3954
4095
|
return isFirstCharacterNumber ? `_${componentType}` : componentType;
|
|
3955
4096
|
};
|
|
@@ -3975,7 +4116,7 @@ const getComponentPropertiesTypeAnnotations = async (component, options, spaceDa
|
|
|
3975
4116
|
};
|
|
3976
4117
|
}
|
|
3977
4118
|
if (Array.from(storyblokSchemas.keys()).includes(propertyType)) {
|
|
3978
|
-
const componentType = toPascalCase(
|
|
4119
|
+
const componentType = toPascalCase(propertyType);
|
|
3979
4120
|
propertyTypeAnnotation[key].tsType = `Storyblok${componentType}`;
|
|
3980
4121
|
}
|
|
3981
4122
|
if (propertyType === "multilink") {
|
|
@@ -3983,8 +4124,8 @@ const getComponentPropertiesTypeAnnotations = async (component, options, spaceDa
|
|
|
3983
4124
|
...!schema.email_link_type ? ['{ linktype?: "email" }'] : [],
|
|
3984
4125
|
...!schema.asset_link_type ? ['{ linktype?: "asset" }'] : []
|
|
3985
4126
|
];
|
|
3986
|
-
const componentType = toPascalCase(
|
|
3987
|
-
propertyTypeAnnotation[key].tsType = excludedLinktypes.length > 0 ? `Exclude
|
|
4127
|
+
const componentType = `Storyblok${toPascalCase(propertyType)}`;
|
|
4128
|
+
propertyTypeAnnotation[key].tsType = excludedLinktypes.length > 0 ? `Exclude<${componentType}, ${excludedLinktypes.join(" | ")}>` : componentType;
|
|
3988
4129
|
}
|
|
3989
4130
|
if (propertyType === "bloks") {
|
|
3990
4131
|
if (schema.restrict_components) {
|
|
@@ -4811,7 +4952,8 @@ const repositoryToTemplate = (repo) => {
|
|
|
4811
4952
|
template: repo.clone_url,
|
|
4812
4953
|
location: port ? `https://localhost:${port}/` : "https://localhost:3000/",
|
|
4813
4954
|
description: repo.description,
|
|
4814
|
-
updated_at: repo.updated_at
|
|
4955
|
+
updated_at: repo.updated_at,
|
|
4956
|
+
stars: repo.stargazers_count
|
|
4815
4957
|
};
|
|
4816
4958
|
};
|
|
4817
4959
|
const fetchBlueprintRepositories = async () => {
|
|
@@ -4823,7 +4965,7 @@ const fetchBlueprintRepositories = async () => {
|
|
|
4823
4965
|
order: "desc",
|
|
4824
4966
|
per_page: 100
|
|
4825
4967
|
});
|
|
4826
|
-
const blueprints = data.items.filter((repo) => repo.name.startsWith("blueprint-core-")).map(repositoryToTemplate).sort((a, b) =>
|
|
4968
|
+
const blueprints = data.items.filter((repo) => repo.name.startsWith("blueprint-core-")).map(repositoryToTemplate).sort((a, b) => (b.stars || 0) - (a.stars || 0));
|
|
4827
4969
|
return blueprints;
|
|
4828
4970
|
} catch (error) {
|
|
4829
4971
|
handleAPIError("fetch_blueprints", error, "Failed to fetch blueprints from GitHub");
|
|
@@ -5010,7 +5152,8 @@ program$1.command(`${commands.CREATE} [project-path]`).alias("c").description(`S
|
|
|
5010
5152
|
cd ${finalProjectPath}
|
|
5011
5153
|
npm install
|
|
5012
5154
|
npm run dev
|
|
5013
|
-
`);
|
|
5155
|
+
`);
|
|
5156
|
+
konsola.info(`Or check the dedicated guide at: ${chalk.hex(colorPalette.PRIMARY)(`https://www.storyblok.com/docs/guides/${technologyTemplate}`)}`);
|
|
5014
5157
|
} catch (error) {
|
|
5015
5158
|
spinnerSpace.failed();
|
|
5016
5159
|
spinnerBlueprints.failed();
|
|
@@ -5020,7 +5163,7 @@ program$1.command(`${commands.CREATE} [project-path]`).alias("c").description(`S
|
|
|
5020
5163
|
konsola.br();
|
|
5021
5164
|
});
|
|
5022
5165
|
|
|
5023
|
-
const version = "4.
|
|
5166
|
+
const version = "4.6.0";
|
|
5024
5167
|
const pkg = {
|
|
5025
5168
|
version: version};
|
|
5026
5169
|
|