sb-mig
Version:
CLI to rule the world. (and handle stuff related to Storyblok CMS)
272 lines (271 loc) • 10.5 kB
JavaScript
import path from "path";
import chalk from "chalk";
import { discoverMigrationConfig, discoverStories, LOOKUP_TYPE, SCOPE, } from "../../cli/utils/discover.js";
import storyblokConfig from "../../config/config.js";
import { createAndSaveToFile } from "../../utils/files.js";
import Logger from "../../utils/logger.js";
import { getFilesContentWithRequire, isObjectEmpty } from "../../utils/main.js";
import { modifyOrCreateAppliedMigrationsFile } from "../../utils/migrations.js";
import { managementApi } from "../managementApi.js";
function replaceComponentData({ parent, key, components, mapper, depth, maxDepth, sumOfReplacing, }) {
let currentMaxDepth = depth;
if (storyblokConfig.debug) {
Logger.warning(`Current max depth: ${depth}`);
}
if (typeof parent[key] === "object") {
if (parent[key]?.component &&
components.includes(parent[key].component)) {
const { ...rest } = parent[key];
const { data: dataToReplace, wasReplaced } = mapper[parent[key].component](parent[key]);
parent[key] = { ...rest, ...dataToReplace };
if (storyblokConfig.debug) {
console.log(chalk.yellow(`______________ In __________________________________________`));
console.log(" ");
console.log(` Data from ${chalk.blue(dataToReplace.component)} component,\n with _uid: ${chalk.blue(dataToReplace._uid)} `);
console.log("Was it replaced? ", wasReplaced);
console.log(chalk.yellow(`____________________________________________________________`));
console.log(" ");
}
if (wasReplaced) {
sumOfReplacing[dataToReplace.component] = sumOfReplacing[dataToReplace.component]
? sumOfReplacing[dataToReplace.component] + 1
: 1;
console.log("Sum of replacing: ");
console.log(sumOfReplacing);
}
}
if (Array.isArray(parent[key])) {
for (let i = 0; i < parent[key].length; i++) {
const childMaxDepth = replaceComponentData({
parent: parent[key],
key: i,
components,
mapper,
depth: depth + 1,
maxDepth,
sumOfReplacing,
});
currentMaxDepth = Math.max(currentMaxDepth, childMaxDepth);
}
}
else {
for (const subKey in parent[key]) {
const childMaxDepth = replaceComponentData({
parent: parent[key],
key: subKey,
components,
mapper,
depth: depth + 1,
maxDepth,
sumOfReplacing,
});
currentMaxDepth = Math.max(currentMaxDepth, childMaxDepth);
}
}
}
return currentMaxDepth;
}
export const prepareStoriesFromLocalFile = ({ from }) => {
// Get all stories to be migrated
const allLocalStories = discoverStories({
scope: SCOPE.local,
type: LOOKUP_TYPE.fileName,
fileNames: [from],
});
// Get content of all stories to be migrated
const storiesFileContent = getFilesContentWithRequire({
files: allLocalStories,
})[0];
if (!storiesFileContent) {
throw new Error(`Couldn't receive data from provided stories filename: ${chalk.red(from)}`);
}
return storiesFileContent;
};
export const prepareMigrationConfig = ({ migrationConfig }) => {
// Get migration config file with migration definition
const migrationConfigFiles = discoverMigrationConfig({
scope: SCOPE.local,
type: LOOKUP_TYPE.fileName,
fileNames: [migrationConfig],
});
// Get content of migration config file
const migrationConfigFileContent = getFilesContentWithRequire({
files: migrationConfigFiles,
})[0];
if (isObjectEmpty(migrationConfigFileContent)) {
throw new Error("Migration config file is empty. Please provide default exported config object with components map to migrate");
}
if (!migrationConfigFileContent) {
throw new Error("Migration config probably doesnt exist. Create one");
}
Logger.success(`Migration config loaded.`);
return migrationConfigFileContent;
};
export const migrateAllComponentsDataInStories = async ({ itemType, migrationConfig, migrateFrom, from, to, }, config) => {
Logger.warning(`Trying to migrate all ${itemType} from ${migrateFrom}, ${from} to ${to}...`);
const migrationConfigFileContent = prepareMigrationConfig({
migrationConfig,
});
// Taking every component defined in const config = {} in migration config file
const componentsToMigrate = Object.keys(migrationConfigFileContent);
if (storyblokConfig.debug) {
Logger.warning("_________ Components in stories to migrate ___________");
console.log(componentsToMigrate);
}
await migrateProvidedComponentsDataInStories({
itemType,
migrationConfig,
migrateFrom,
from,
to,
componentsToMigrate,
}, config);
};
export const doTheMigration = async ({ itemType = "story", from, itemsToMigrate, componentsToMigrate, migrationConfigFileContent, migrationConfig, to, }, config) => {
const arrayOfMaxDepths = [];
const migratedItems = itemsToMigrate.map((item, index) => {
const sumOfReplacing = {};
if (storyblokConfig.debug) {
Logger.success(`# ${index} #`);
}
const json = itemType === "story" ? item[itemType].content : item[itemType];
const maxDepth = replaceComponentData({
parent: { root: json },
key: "root",
components: componentsToMigrate,
mapper: migrationConfigFileContent,
depth: 0,
maxDepth: 0,
sumOfReplacing,
});
arrayOfMaxDepths.push(maxDepth);
if (Object.keys(sumOfReplacing).length > 0) {
console.log(" ");
console.log(`Migration in ${chalk.magenta(itemType === "story"
? item[itemType].full_slug
: item[itemType].name)} page: `);
componentsToMigrate.forEach((component) => {
if (sumOfReplacing[component]) {
console.log(`${chalk.blue(component)} component data was replaced: ${sumOfReplacing[component]} times.`);
}
});
}
if (Object.keys(sumOfReplacing).length > 0) {
return {
...item,
[itemType]: itemType === "story"
? {
...item[itemType],
content: json,
}
: {
...json,
},
};
}
else {
return null;
}
});
const maxDepth = Math.max(...arrayOfMaxDepths);
if (storyblokConfig.debug) {
console.log(" ");
if (maxDepth > 30) {
Logger.error(`Max depth: ${maxDepth}`);
}
else {
Logger.success(`Max depth: ${maxDepth}`);
}
console.log(" ");
}
const isListEmpty = (list) => list.filter((item) => item).length;
if (!isListEmpty(migratedItems)) {
console.log("# No Stories to update #");
}
else {
console.log(`${migratedItems.length} stories to migrate`);
}
const notNullMigratedItems = migratedItems.filter((item) => item);
// Saving result with migrated version of stories into file
await createAndSaveToFile({
datestamp: true,
ext: "json",
filename: `${from}---${itemType}-to-migrate`,
folder: "migrations",
res: notNullMigratedItems,
}, config);
await modifyOrCreateAppliedMigrationsFile(migrationConfig, itemType);
if (itemType === "story") {
await managementApi.stories.updateStories({
stories: notNullMigratedItems,
spaceId: to,
options: { publish: false },
}, config);
}
else if (itemType === "preset") {
await managementApi.presets.updatePresets({
presets: notNullMigratedItems,
spaceId: to,
options: {},
}, config);
}
};
const saveBackupToFile = async ({ itemType, res, folder, filename }, config) => {
await createAndSaveToFile({
ext: "json",
datestamp: true,
suffix: itemType === "story" ? ".sb.stories" : ".sb.presets",
filename,
folder,
res: res,
}, config);
};
export const migrateProvidedComponentsDataInStories = async ({ itemType, migrationConfig, migrateFrom, from, to, componentsToMigrate, }, config) => {
const migrationConfigFileContent = prepareMigrationConfig({
migrationConfig,
});
if (migrateFrom === "file") {
Logger.log("Migrating using file....");
// Get all stories to be migrated from file
const itemsToMigrate = prepareStoriesFromLocalFile({ from });
await doTheMigration({
itemsToMigrate,
componentsToMigrate,
migrationConfigFileContent,
to,
}, config);
}
else if (migrateFrom === "space") {
// Get all stories to be migrated from storyblok space
let itemsToMigrate = [];
if (itemType === "story") {
itemsToMigrate = await managementApi.stories.getAllStories({}, {
...config,
spaceId: from,
});
}
else if (itemType === "preset") {
itemsToMigrate = await managementApi.presets.getAllPresets({
...config,
spaceId: from,
});
}
const backupFolder = path.join("backup", itemType);
// save stories to file as backup
await saveBackupToFile({
itemType,
filename: `before__${migrationConfig}__${from}`,
folder: backupFolder,
res: itemsToMigrate,
}, config);
await doTheMigration({
itemType,
itemsToMigrate,
componentsToMigrate,
migrationConfigFileContent,
migrationConfig,
from,
to,
}, config);
}
};