storyblok 4.6.3 → 4.6.6

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
@@ -9,7 +9,7 @@ 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
11
  import { RateLimit, Sema } from 'async-sema';
12
- import fs, { mkdir, writeFile, readFile as readFile$1, access, readdir } from 'node:fs/promises';
12
+ import fs, { mkdir, writeFile, readFile as readFile$1, appendFile, access, readdir } from 'node:fs/promises';
13
13
  import path, { join, parse, resolve } from 'node:path';
14
14
  import filenamify from 'filenamify';
15
15
  import { exec, spawn } from 'node:child_process';
@@ -668,6 +668,22 @@ const saveToFile = async (filePath, data, options) => {
668
668
  handleFileSystemError("write", writeError);
669
669
  }
670
670
  };
671
+ const appendToFile = async (filePath, data, options) => {
672
+ const resolvedPath = parse(filePath).dir;
673
+ try {
674
+ await mkdir(resolvedPath, { recursive: true });
675
+ } catch (mkdirError) {
676
+ handleFileSystemError("mkdir", mkdirError);
677
+ return;
678
+ }
679
+ try {
680
+ const dataWithNewline = data.endsWith("\n") ? data : `${data}
681
+ `;
682
+ await appendFile(filePath, dataWithNewline, options);
683
+ } catch (writeError) {
684
+ handleFileSystemError("write", writeError);
685
+ }
686
+ };
671
687
  const readFile = async (filePath) => {
672
688
  try {
673
689
  return await readFile$1(filePath, "utf8");
@@ -840,7 +856,7 @@ const loginStrategy = {
840
856
  short: "Email"
841
857
  },
842
858
  {
843
- name: "With Token (SSO)",
859
+ name: "With Token (Personal Access Token \u2013 works also for SSO accounts)",
844
860
  value: "login-with-token",
845
861
  short: "Token"
846
862
  }
@@ -899,8 +915,13 @@ program$i.command(commands.LOGIN).description("Login to the Storyblok CLI").opti
899
915
  try {
900
916
  const strategy = await select(loginStrategy);
901
917
  if (strategy === "login-with-token") {
918
+ konsola.info([
919
+ "\u{1F511} You can use a Personal Access Token to log in.",
920
+ "This works for all accounts, including SSO accounts.",
921
+ `Generate one in your Storyblok account settings: ${chalk.underline.blue("https://app.storyblok.com/#/me/account?tab=token")}`
922
+ ].join("\n"));
902
923
  const userToken = await password({
903
- message: "Please enter your token:",
924
+ message: "Please enter your Personal Access Token:",
904
925
  validate: (value) => {
905
926
  return value.length > 0;
906
927
  }
@@ -3048,69 +3069,61 @@ async function getMigrationFunction(fileName, space, basePath) {
3048
3069
  }
3049
3070
  }
3050
3071
  function applyMigrationToAllBlocks(content, migrationFunction, targetComponent) {
3072
+ let processed = false;
3051
3073
  if (!content || typeof content !== "object") {
3052
- return false;
3074
+ return processed;
3053
3075
  }
3054
- let modified = false;
3055
3076
  const baseTargetComponent = targetComponent.split(".")[0];
3077
+ let migratedContent = null;
3056
3078
  if (content.component === baseTargetComponent) {
3057
- const migratedContent = migrationFunction({ ...content });
3058
- Object.assign(content, migratedContent);
3059
- modified = true;
3060
- }
3061
- for (const key in content) {
3062
- if (Object.prototype.hasOwnProperty.call(content, key)) {
3063
- const value = content[key];
3064
- if (Array.isArray(value)) {
3065
- for (let i = 0; i < value.length; i++) {
3066
- if (value[i] && typeof value[i] === "object") {
3067
- const blockModified = applyMigrationToAllBlocks(value[i], migrationFunction, targetComponent);
3068
- modified = modified || blockModified;
3069
- }
3079
+ migratedContent = migrationFunction({ ...content });
3080
+ processed = true;
3081
+ }
3082
+ const uniqueKeys = /* @__PURE__ */ new Set([...Object.keys(content), ...Object.keys(migratedContent || {})]);
3083
+ for (const key of uniqueKeys) {
3084
+ if (migratedContent) {
3085
+ if (!(key in migratedContent)) {
3086
+ delete content[key];
3087
+ continue;
3088
+ }
3089
+ content[key] = migratedContent[key];
3090
+ }
3091
+ if (Array.isArray(content[key])) {
3092
+ for (const value of content[key]) {
3093
+ if (value && typeof value === "object") {
3094
+ const blockProcessed = applyMigrationToAllBlocks(value, migrationFunction, targetComponent);
3095
+ processed = processed || blockProcessed;
3070
3096
  }
3071
- } else if (value && typeof value === "object") {
3072
- const blockModified = applyMigrationToAllBlocks(value, migrationFunction, targetComponent);
3073
- modified = modified || blockModified;
3074
3097
  }
3098
+ } else if (content[key] && typeof content[key] === "object") {
3099
+ const blockProcessed = applyMigrationToAllBlocks(content[key], migrationFunction, targetComponent);
3100
+ processed = processed || blockProcessed;
3075
3101
  }
3076
3102
  }
3077
- return modified;
3103
+ return processed;
3078
3104
  }
3079
3105
 
3080
3106
  async function saveRollbackData({
3081
3107
  space,
3082
3108
  path,
3083
- stories,
3084
- migrationFile
3109
+ story,
3110
+ migrationTimestamp,
3111
+ migrationNames
3085
3112
  }) {
3086
3113
  const rollbackData = {
3087
- stories: stories.map((story) => ({
3088
- storyId: story.id,
3089
- name: story.name,
3090
- content: story.content
3091
- }))
3114
+ storyId: story.id,
3115
+ name: story.name,
3116
+ content: story.content
3092
3117
  };
3093
3118
  const rollbacksPath = resolvePath(path, `migrations/${space}/rollbacks`);
3094
- const timestamp = Date.now();
3095
- const rollbackFileName = `${migrationFile.replace(".js", "")}.${timestamp}.json`;
3119
+ const componentNames = migrationNames.map((n) => getComponentNameFromFilename(n));
3120
+ const rollbackName = [...new Set(componentNames)].join("~");
3121
+ const rollbackFileName = `${rollbackName}.${migrationTimestamp}.jsonl`;
3096
3122
  const rollbackFilePath = join(rollbacksPath, rollbackFileName);
3097
- try {
3098
- await saveToFile(
3099
- rollbackFilePath,
3100
- JSON.stringify(rollbackData, null, 2)
3101
- );
3102
- } catch (error) {
3103
- if (error.code === "ENOENT") {
3104
- const fs = await import('node:fs/promises');
3105
- await fs.mkdir(rollbacksPath, { recursive: true });
3106
- await saveToFile(
3107
- rollbackFilePath,
3108
- JSON.stringify(rollbackData, null, 2)
3109
- );
3110
- } else {
3111
- throw error;
3112
- }
3113
- }
3123
+ await appendToFile(
3124
+ rollbackFilePath,
3125
+ JSON.stringify(rollbackData)
3126
+ );
3114
3127
  }
3115
3128
  async function readRollbackFile({
3116
3129
  space,
@@ -3120,8 +3133,10 @@ async function readRollbackFile({
3120
3133
  try {
3121
3134
  const resolvedPath = resolvePath(path, `migrations/${space}/rollbacks`);
3122
3135
  const rollbackFilePath = join(resolvedPath, migrationFile);
3123
- const filePath = rollbackFilePath.endsWith(".json") ? rollbackFilePath : `${rollbackFilePath}.json`;
3124
- return JSON.parse(await readFile$1(filePath, "utf-8"));
3136
+ const filePath = rollbackFilePath.endsWith(".jsonl") ? rollbackFilePath : `${rollbackFilePath}.jsonl`;
3137
+ return {
3138
+ stories: (await readFile$1(filePath, "utf-8")).trim().split("\n").filter(Boolean).map((x) => JSON.parse(x))
3139
+ };
3125
3140
  } catch (error) {
3126
3141
  throw new CommandError(`Failed to read rollback file: ${error.message}`);
3127
3142
  }
@@ -3140,6 +3155,7 @@ class MigrationStream extends Transform {
3140
3155
  totalProcessed: 0
3141
3156
  };
3142
3157
  }
3158
+ timestamp = Date.now();
3143
3159
  results;
3144
3160
  migrationFunctions = /* @__PURE__ */ new Map();
3145
3161
  totalProcessed = 0;
@@ -3160,66 +3176,71 @@ class MigrationStream extends Transform {
3160
3176
  callback(error);
3161
3177
  }
3162
3178
  }
3163
- async processStory(story) {
3164
- if (!story.content) {
3165
- for (const migrationFile of this.options.migrationFiles) {
3166
- this.results.failed.push({
3167
- storyId: story.id,
3168
- migrationName: migrationFile.name,
3169
- error: new Error("Story content is missing")
3170
- });
3171
- }
3172
- return [];
3179
+ async getOrLoadMigrationFunction(migrationFile) {
3180
+ if (this.migrationFunctions.has(migrationFile.name)) {
3181
+ return this.migrationFunctions.get(migrationFile.name);
3173
3182
  }
3183
+ const migrationFunction = await getMigrationFunction(
3184
+ migrationFile.name,
3185
+ this.options.space,
3186
+ this.options.path
3187
+ );
3188
+ this.migrationFunctions.set(migrationFile.name, migrationFunction);
3189
+ return migrationFunction;
3190
+ }
3191
+ async processStory(story) {
3174
3192
  const relevantMigrations = this.options.componentName ? this.options.migrationFiles.filter((file) => {
3175
3193
  const targetComponent = getComponentNameFromFilename(file.name);
3176
3194
  return targetComponent.split(".")[0] === this.options.componentName;
3177
3195
  }) : this.options.migrationFiles;
3196
+ if (!story.content) {
3197
+ this.results.failed.push({
3198
+ storyId: story.id,
3199
+ migrationNames: relevantMigrations.map((m) => m.name),
3200
+ error: new Error("Story content is missing")
3201
+ });
3202
+ return [];
3203
+ }
3178
3204
  const successfulResults = [];
3179
- for (const migrationFile of relevantMigrations) {
3180
- const result = await this.applyMigrationToStory(story, migrationFile);
3181
- if (result) {
3182
- successfulResults.push(result);
3183
- }
3205
+ const result = await this.applyMigrationsToStory(story, relevantMigrations);
3206
+ if (result) {
3207
+ successfulResults.push(result);
3184
3208
  }
3185
3209
  return successfulResults;
3186
3210
  }
3187
- async applyMigrationToStory(story, migrationFile) {
3211
+ async applyMigrationsToStory(story, migrationFiles) {
3212
+ const migrationNames = migrationFiles.map((f) => f.name);
3188
3213
  try {
3189
- let migrationFunction = this.migrationFunctions.get(migrationFile.name);
3190
- if (!migrationFunction) {
3191
- migrationFunction = await getMigrationFunction(
3192
- migrationFile.name,
3193
- this.options.space,
3194
- this.options.path
3195
- );
3196
- this.migrationFunctions.set(migrationFile.name, migrationFunction);
3197
- }
3198
- if (!migrationFunction) {
3199
- this.results.failed.push({
3200
- storyId: story.id,
3201
- migrationName: migrationFile.name,
3202
- error: new Error(`Failed to load migration function from file "${migrationFile.name}"`)
3203
- });
3204
- return null;
3205
- }
3206
- await saveRollbackData({
3207
- space: this.options.space,
3208
- path: this.options.path,
3209
- stories: [{ id: story.id, name: story.name || "", content: story.content }],
3210
- migrationFile: migrationFile.name
3211
- });
3212
3214
  const storyContent = structuredClone(story.content);
3213
- const originalContentHash = hash(story.content);
3214
- const targetComponent = this.options.componentName || getComponentNameFromFilename(migrationFile.name);
3215
- const modified = applyMigrationToAllBlocks(storyContent, migrationFunction, targetComponent);
3215
+ const originalContentHash = hash(storyContent);
3216
+ let processed = false;
3217
+ for (const migrationFile of migrationFiles) {
3218
+ const migrationFunction = await this.getOrLoadMigrationFunction(migrationFile);
3219
+ if (!migrationFunction) {
3220
+ this.results.failed.push({
3221
+ storyId: story.id,
3222
+ migrationNames,
3223
+ error: new Error(`Failed to load migration function from file "${migrationFile.name}"`)
3224
+ });
3225
+ return null;
3226
+ }
3227
+ const targetComponent = this.options.componentName || getComponentNameFromFilename(migrationFile.name);
3228
+ processed = applyMigrationToAllBlocks(storyContent, migrationFunction, targetComponent);
3229
+ }
3216
3230
  const newContentHash = hash(storyContent);
3217
3231
  const contentChanged = originalContentHash !== newContentHash;
3218
- if (modified && contentChanged) {
3232
+ if (processed && contentChanged) {
3233
+ await saveRollbackData({
3234
+ space: this.options.space,
3235
+ path: this.options.path,
3236
+ story: { id: story.id, name: story.name || "", content: story.content },
3237
+ migrationTimestamp: this.timestamp,
3238
+ migrationNames
3239
+ });
3219
3240
  this.results.successful.push({
3220
3241
  storyId: story.id,
3221
3242
  name: story.name,
3222
- migrationName: migrationFile.name,
3243
+ migrationNames,
3223
3244
  content: storyContent
3224
3245
  });
3225
3246
  return {
@@ -3227,28 +3248,32 @@ class MigrationStream extends Transform {
3227
3248
  name: story.name,
3228
3249
  content: storyContent
3229
3250
  };
3230
- } else if (modified && !contentChanged) {
3251
+ } else if (processed && !contentChanged) {
3231
3252
  this.results.skipped.push({
3232
3253
  storyId: story.id,
3233
3254
  name: story.name,
3234
- migrationName: migrationFile.name,
3255
+ migrationNames,
3235
3256
  reason: "No changes detected after migration"
3236
3257
  });
3237
3258
  return null;
3238
3259
  } else {
3239
- const baseComponent = targetComponent.split(".")[0];
3260
+ const reason = migrationFiles.map((migrationFile) => {
3261
+ const targetComponent = this.options.componentName || getComponentNameFromFilename(migrationFile.name);
3262
+ const baseComponent = targetComponent.split(".")[0];
3263
+ return baseComponent === this.options.componentName ? `No matching components found for ${migrationFile.name}` : `Different component target ${migrationFile.name}`;
3264
+ }).join("\n");
3240
3265
  this.results.skipped.push({
3241
3266
  storyId: story.id,
3242
3267
  name: story.name,
3243
- migrationName: migrationFile.name,
3244
- reason: baseComponent === this.options.componentName ? "No matching components found" : "Different component target"
3268
+ migrationNames,
3269
+ reason
3245
3270
  });
3246
3271
  return null;
3247
3272
  }
3248
3273
  } catch (error) {
3249
3274
  this.results.failed.push({
3250
3275
  storyId: story.id,
3251
- migrationName: migrationFile.name,
3276
+ migrationNames,
3252
3277
  error
3253
3278
  });
3254
3279
  return null;
@@ -3346,8 +3371,9 @@ class UpdateStream extends Writable {
3346
3371
  } else if (this.options.publish === "all") {
3347
3372
  payload.publish = 1;
3348
3373
  }
3349
- const updatedStory = await updateStory(this.options.space, storyId, payload);
3350
- if (updatedStory) {
3374
+ const updatedStory = !this.options.dryRun && await updateStory(this.options.space, storyId, payload);
3375
+ const isStoryUpdated = Boolean(updatedStory);
3376
+ if (isStoryUpdated || this.options.dryRun) {
3351
3377
  this.results.successful.push({ storyId, name: storyName });
3352
3378
  this.results.totalProcessed++;
3353
3379
  this.options.onProgress?.(this.results.totalProcessed);
@@ -3406,6 +3432,10 @@ class UpdateStream extends Writable {
3406
3432
  const program$8 = getProgram();
3407
3433
  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) => {
3408
3434
  konsola.title(`${commands.MIGRATIONS}`, colorPalette.MIGRATIONS, componentName ? `Running migrations for component ${componentName}...` : "Running migrations...");
3435
+ if (options.dryRun) {
3436
+ konsola.warn(`DRY RUN MODE ENABLED: No changes will be made.
3437
+ `);
3438
+ }
3409
3439
  const verbose = program$8.opts().verbose;
3410
3440
  const { filter, dryRun = false, query, startsWith, publish } = options;
3411
3441
  const { space, path } = migrationsCommand.opts();
@@ -3535,6 +3565,13 @@ migrationsCommand.command("rollback [migrationFile]").description("Rollback a mi
3535
3565
  handleError(new CommandError(`Please provide the space as argument --space YOUR_SPACE_ID.`), verbose);
3536
3566
  return;
3537
3567
  }
3568
+ const { password, region } = state;
3569
+ mapiClient({
3570
+ token: {
3571
+ accessToken: password
3572
+ },
3573
+ region
3574
+ });
3538
3575
  try {
3539
3576
  const rollbackData = await readRollbackFile({
3540
3577
  space,
@@ -3985,6 +4022,7 @@ const storyblokSchemas = /* @__PURE__ */ new Map([
3985
4022
  ]);
3986
4023
 
3987
4024
  const STORY_TYPE = "ISbStoryData";
4025
+ const DEFAULT_COMPONENT_FILENAME = "storyblok-components";
3988
4026
  const DEFAULT_TYPEDEFS_HEADER = [
3989
4027
  "// This file was generated by the storyblok CLI.",
3990
4028
  "// DO NOT MODIFY THIS FILE BY HAND."
@@ -4247,8 +4285,8 @@ const generateTypes = async (spaceData, options = {
4247
4285
  handleError(error);
4248
4286
  }
4249
4287
  };
4250
- const saveTypesToFile = async (space, typedefString, options) => {
4251
- const { filename = "storyblok-components", path } = options;
4288
+ const saveTypesToComponentsFile = async (space, typedefString, options) => {
4289
+ const { filename = DEFAULT_COMPONENT_FILENAME, path } = options;
4252
4290
  const resolvedPath = path ? resolve(process.cwd(), path, "types", space) : resolvePath(path, `types/${space}`);
4253
4291
  try {
4254
4292
  await saveToFile(join(resolvedPath, `${filename}.d.ts`), typedefString);
@@ -4257,7 +4295,7 @@ const saveTypesToFile = async (space, typedefString, options) => {
4257
4295
  }
4258
4296
  };
4259
4297
  const generateStoryblokTypes = async (options = {}) => {
4260
- const { filename = "storyblok", path } = options;
4298
+ const { path } = options;
4261
4299
  try {
4262
4300
  const storyblokTypesPath = resolve(__dirname, "./index.d.ts");
4263
4301
  const storyblokTypesContent = readFileSync(storyblokTypesPath, "utf-8");
@@ -4268,7 +4306,7 @@ const generateStoryblokTypes = async (options = {}) => {
4268
4306
  storyblokTypesContent
4269
4307
  ].join("\n");
4270
4308
  const resolvedPath = path ? resolve(process.cwd(), path, "types") : resolvePath(path, "types");
4271
- await saveToFile(join(resolvedPath, `${filename}.d.ts`), typeDefs);
4309
+ await saveToFile(join(resolvedPath, `storyblok.d.ts`), typeDefs);
4272
4310
  return true;
4273
4311
  } catch (error) {
4274
4312
  handleFileSystemError("read", error);
@@ -4277,7 +4315,10 @@ const generateStoryblokTypes = async (options = {}) => {
4277
4315
  };
4278
4316
 
4279
4317
  const program$5 = getProgram();
4280
- typesCommand.command("generate").description("Generate types d.ts for your component schemas").option("--sf, --separate-files", "").option("--strict", "strict mode, no loose typing").option("--type-prefix <prefix>", "prefix to be prepended to all generated component type names").option("--type-suffix <suffix>", "suffix to be appended to all generated component type names").option("--suffix <suffix>", "Components suffix").option("--custom-fields-parser <path>", "Path to the parser file for Custom Field Types").option("--compiler-options <options>", "path to the compiler options from json-schema-to-typescript").action(async (options) => {
4318
+ typesCommand.command("generate").description("Generate types d.ts for your component schemas").option("--sf, --separate-files", "Generate one .d.ts file per component instead of a single combined file").option(
4319
+ "--filename <name>",
4320
+ "Base file name for all component types when generating a single declarations file (e.g. components.d.ts). Ignored when using --separate-files."
4321
+ ).option("--strict", "strict mode, no loose typing").option("--type-prefix <prefix>", "prefix to be prepended to all generated component type names").option("--type-suffix <suffix>", "suffix to be appended to all generated component type names").option("--suffix <suffix>", "Components suffix").option("--custom-fields-parser <path>", "Path to the parser file for Custom Field Types").option("--compiler-options <options>", "path to the compiler options from json-schema-to-typescript").action(async (options) => {
4281
4322
  konsola.title(`${commands.TYPES}`, colorPalette.TYPES, "Generating types...");
4282
4323
  const verbose = program$5.opts().verbose;
4283
4324
  const { space, path } = typesCommand.opts();
@@ -4292,7 +4333,6 @@ typesCommand.command("generate").description("Generate types d.ts for your compo
4292
4333
  path
4293
4334
  });
4294
4335
  await generateStoryblokTypes({
4295
- ...options,
4296
4336
  path
4297
4337
  });
4298
4338
  const spaceDataWithDatasources = {
@@ -4304,8 +4344,8 @@ typesCommand.command("generate").description("Generate types d.ts for your compo
4304
4344
  path
4305
4345
  });
4306
4346
  if (typedefString) {
4307
- await saveTypesToFile(space, typedefString, {
4308
- ...options,
4347
+ await saveTypesToComponentsFile(space, typedefString, {
4348
+ filename: options.filename,
4309
4349
  path
4310
4350
  });
4311
4351
  }
@@ -5141,7 +5181,7 @@ program$1.command(`${commands.CREATE} [project-path]`).alias("c").description(`S
5141
5181
  konsola.br();
5142
5182
  });
5143
5183
 
5144
- const version = "4.6.3";
5184
+ const version = "4.6.6";
5145
5185
  const pkg = {
5146
5186
  version: version};
5147
5187