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 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
- async function handleMigrations({
3102
- migrationFiles,
3103
- stories,
3104
- space,
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
- const migrationFunction = await getMigrationFunction(migrationFile.name, space, path);
3129
- if (!migrationFunction) {
3130
- stories.forEach((story) => {
3131
- results.failed.push({
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(`Failed to load migration function from file "${migrationFile.name}"`)
3191
+ error: new Error("Story content is missing")
3135
3192
  });
3136
- });
3137
- continue;
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
- const targetComponent = componentName || getComponentNameFromFilename(migrationFile.name);
3140
- for (const story of stories) {
3141
- if (!story.content) {
3142
- results.failed.push({
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("Story content is missing")
3224
+ error: new Error(`Failed to load migration function from file "${migrationFile.name}"`)
3146
3225
  });
3147
- continue;
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
- try {
3152
- const modified = applyMigrationToAllBlocks(storyContent, migrationFunction, targetComponent);
3153
- const newContentHash = hash(storyContent);
3154
- const contentChanged = originalContentHash !== newContentHash;
3155
- if (modified && contentChanged) {
3156
- const spinner = new Spinner({ verbose: !isVitest });
3157
- spinner.start(`Applying migration ${chalk.hex(colorPalette.MIGRATIONS)(migrationFile.name)} to story ${chalk.hex(colorPalette.PRIMARY)(story.name || story.id.toString())}...`);
3158
- spinner.succeed(`Migration ${chalk.hex(colorPalette.MIGRATIONS)(migrationFile.name)} applied to story ${chalk.hex(colorPalette.PRIMARY)(story.name || story.id.toString())} - Completed in ${spinner.elapsedTime.toFixed(2)}ms`);
3159
- results.successful.push({
3160
- storyId: story.id,
3161
- name: story.name,
3162
- migrationName: migrationFile.name,
3163
- content: storyContent
3164
- });
3165
- } else if (modified && !contentChanged) {
3166
- results.skipped.push({
3167
- storyId: story.id,
3168
- name: story.name,
3169
- migrationName: migrationFile.name,
3170
- reason: "No changes detected after migration"
3171
- });
3172
- } else {
3173
- const baseComponent = targetComponent.split(".")[0];
3174
- results.skipped.push({
3175
- storyId: story.id,
3176
- name: story.name,
3177
- migrationName: migrationFile.name,
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
- error
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
- return results;
3194
- }
3195
- function summarizeMigrationResults(results) {
3196
- const { successful, failed, skipped } = results;
3197
- const successfulStoryIds = new Set(successful.map((result) => result.storyId));
3198
- const failedStoryIds = new Set(failed.map((result) => result.storyId));
3199
- const successfulMigrations = new Set(successful.map((result) => result.migrationName));
3200
- konsola.br();
3201
- konsola.ok(`Successfully applied ${successfulMigrations.size} migrations to ${successfulStoryIds.size} stories`, true);
3202
- const skippedByReason = skipped.reduce((acc, item) => {
3203
- if (!acc[item.reason]) {
3204
- acc[item.reason] = [];
3205
- }
3206
- acc[item.reason].push(item);
3207
- return acc;
3208
- }, {});
3209
- if (Object.keys(skippedByReason).length > 0) {
3210
- konsola.info(`Skipped migrations:`);
3211
- for (const [reason, items] of Object.entries(skippedByReason)) {
3212
- const uniqueStories = new Set(items.map((item) => item.storyId));
3213
- konsola.info(` \u2022 ${reason}: ${uniqueStories.size} stories`);
3214
- }
3215
- }
3216
- if (failed.length > 0) {
3217
- konsola.warn(`- Failed to apply migrations to ${failedStoryIds.size} stories`, true);
3218
- const failuresByStory = /* @__PURE__ */ new Map();
3219
- failed.forEach(({ storyId, migrationName, error }) => {
3220
- if (!failuresByStory.has(storyId)) {
3221
- failuresByStory.set(storyId, []);
3222
- }
3223
- failuresByStory.get(storyId)?.push({ migrationName, error });
3224
- });
3225
- failuresByStory.forEach((failures, storyId) => {
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 story.published && !story.unpublished_changes;
3316
+ return true;
3239
3317
  };
3240
3318
  const isStoryWithUnpublishedChanges = (story) => {
3241
- return story.published && story.unpublished_changes;
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 storiesSpinner = new Spinner({ verbose: !isVitest }).start(`Fetching stories...`);
3288
- const stories = await fetchAllStoriesByComponent(
3289
- {
3290
- spaceId: space
3291
- },
3292
- // Filter options
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
- if (!stories || stories.length === 0) {
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
- password,
3332
- region
3506
+ onTotal: (total) => {
3507
+ updateProgress.setTotal(total);
3508
+ },
3509
+ onProgress: () => {
3510
+ migrationsProgress.increment();
3511
+ }
3333
3512
  });
3334
- summarizeMigrationResults(migrationResults);
3335
- if (migrationResults.successful.length > 0 && !dryRun) {
3336
- const updateSpinner = new Spinner({ verbose: !isVitest }).start(`Updating stories in Storyblok...`);
3337
- const storiesByIdMap = /* @__PURE__ */ new Map();
3338
- migrationResults.successful.forEach((result) => {
3339
- const originalStory = validStories.find((s) => s.id === result.storyId);
3340
- storiesByIdMap.set(result.storyId, {
3341
- id: result.storyId,
3342
- name: result.name || "",
3343
- content: result.content,
3344
- published: originalStory?.published,
3345
- published_at: originalStory?.published_at || void 0,
3346
- unpublished_changes: originalStory?.unpublished_changes
3347
- });
3348
- });
3349
- const storiesToUpdate = Array.from(storiesByIdMap.values());
3350
- if (storiesToUpdate.length === 0) {
3351
- updateSpinner.succeed(`No stories need to be updated in Storyblok.`);
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
- if (failCount > 0) {
3390
- konsola.warn(`Updated ${successCount} stories successfully, ${failCount} failed.`);
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(toCamelCase(`${prefix}_${sanitizedName}_${suffix}`));
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(toCamelCase(propertyType));
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(toCamelCase(propertyType));
3987
- propertyTypeAnnotation[key].tsType = excludedLinktypes.length > 0 ? `Exclude<Storyblok${componentType}, ${excludedLinktypes.join(" | ")}>` : componentType;
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) => a.name.localeCompare(b.name));
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.5.0";
5166
+ const version = "4.6.0";
5024
5167
  const pkg = {
5025
5168
  version: version};
5026
5169