storyblok 4.17.4 → 4.18.0-alpha.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 +2281 -349
- package/dist/index.mjs.map +1 -1
- package/package.json +7 -4
package/dist/index.mjs
CHANGED
|
@@ -7,10 +7,10 @@ import { homedir } from 'node:os';
|
|
|
7
7
|
import { loadConfig as loadConfig$1, SUPPORTED_EXTENSIONS } from 'c12';
|
|
8
8
|
import chalk from 'chalk';
|
|
9
9
|
import { readPackageUp } from 'read-package-up';
|
|
10
|
-
import { Command } from 'commander';
|
|
10
|
+
import { Command, Option } from 'commander';
|
|
11
11
|
import { MultiBar, Presets } from 'cli-progress';
|
|
12
12
|
import { Spinner } from '@topcli/spinner';
|
|
13
|
-
import fs, { mkdir, writeFile,
|
|
13
|
+
import fs, { mkdir, writeFile, access, constants, readFile as readFile$1, appendFile, readdir, unlink } from 'node:fs/promises';
|
|
14
14
|
import filenamify from 'filenamify';
|
|
15
15
|
import { createManagementApiClient, normalizeAssetUrl } from '@storyblok/management-api-client';
|
|
16
16
|
import { select, password, input, confirm } from '@inquirer/prompts';
|
|
@@ -26,6 +26,7 @@ import { Octokit } from 'octokit';
|
|
|
26
26
|
import { pipeline as pipeline$1 } from 'node:stream/promises';
|
|
27
27
|
import { Buffer } from 'node:buffer';
|
|
28
28
|
import Storyblok from 'storyblok-js-client';
|
|
29
|
+
import { createTwoFilesPatch } from 'diff';
|
|
29
30
|
|
|
30
31
|
const commands = {
|
|
31
32
|
LOGIN: "login",
|
|
@@ -41,7 +42,8 @@ const commands = {
|
|
|
41
42
|
LOGS: "logs",
|
|
42
43
|
REPORTS: "reports",
|
|
43
44
|
ASSETS: "assets",
|
|
44
|
-
STORIES: "stories"
|
|
45
|
+
STORIES: "stories",
|
|
46
|
+
SCHEMA: "schema"
|
|
45
47
|
};
|
|
46
48
|
const colorPalette = {
|
|
47
49
|
PRIMARY: "#8d60ff",
|
|
@@ -61,7 +63,8 @@ const colorPalette = {
|
|
|
61
63
|
LOGS: "#4ade80",
|
|
62
64
|
REPORTS: "#4ade80",
|
|
63
65
|
ASSETS: "#f97316",
|
|
64
|
-
STORIES: "#a185ff"
|
|
66
|
+
STORIES: "#a185ff",
|
|
67
|
+
SCHEMA: "#e91e63"
|
|
65
68
|
};
|
|
66
69
|
const regions = {
|
|
67
70
|
EU: "eu",
|
|
@@ -148,18 +151,6 @@ const directories = {
|
|
|
148
151
|
stories: "stories"
|
|
149
152
|
};
|
|
150
153
|
|
|
151
|
-
const chunk = (items, size) => {
|
|
152
|
-
const all = Array.from(items);
|
|
153
|
-
if (all.length === 0) {
|
|
154
|
-
return [];
|
|
155
|
-
}
|
|
156
|
-
const chunks = [];
|
|
157
|
-
for (let i = 0; i < all.length; i += size) {
|
|
158
|
-
chunks.push(all.slice(i, i + size));
|
|
159
|
-
}
|
|
160
|
-
return chunks;
|
|
161
|
-
};
|
|
162
|
-
|
|
163
154
|
function isPlainObject(value) {
|
|
164
155
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
165
156
|
}
|
|
@@ -1017,11 +1008,11 @@ function requireAuthentication(state, verbose = false) {
|
|
|
1017
1008
|
return true;
|
|
1018
1009
|
}
|
|
1019
1010
|
|
|
1020
|
-
const toCamelCase = (str) => {
|
|
1011
|
+
const toCamelCase$1 = (str) => {
|
|
1021
1012
|
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, "");
|
|
1022
1013
|
};
|
|
1023
1014
|
const toPascalCase = (str) => {
|
|
1024
|
-
const camelCase = toCamelCase(str);
|
|
1015
|
+
const camelCase = toCamelCase$1(str);
|
|
1025
1016
|
return camelCase ? camelCase[0].toUpperCase() + camelCase.slice(1) : camelCase;
|
|
1026
1017
|
};
|
|
1027
1018
|
const capitalize = (str) => {
|
|
@@ -1119,6 +1110,25 @@ function getPackageJson() {
|
|
|
1119
1110
|
return packageJson$1;
|
|
1120
1111
|
}
|
|
1121
1112
|
|
|
1113
|
+
async function fetchAllPages(fetchFunction, extractDataFunction) {
|
|
1114
|
+
const items = [];
|
|
1115
|
+
let page = 1;
|
|
1116
|
+
while (true) {
|
|
1117
|
+
const { data, response } = await fetchFunction(page);
|
|
1118
|
+
const totalHeader = response.headers.get("total");
|
|
1119
|
+
const fetchedItems = extractDataFunction(data);
|
|
1120
|
+
items.push(...fetchedItems);
|
|
1121
|
+
if (!totalHeader) {
|
|
1122
|
+
return items;
|
|
1123
|
+
}
|
|
1124
|
+
const total = Number(totalHeader);
|
|
1125
|
+
if (Number.isNaN(total) || items.length >= total || fetchedItems.length === 0) {
|
|
1126
|
+
return items;
|
|
1127
|
+
}
|
|
1128
|
+
page++;
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1122
1132
|
const __filename$1 = fileURLToPath(import.meta.url);
|
|
1123
1133
|
const __dirname$1 = dirname(__filename$1);
|
|
1124
1134
|
function isRegion(value) {
|
|
@@ -1207,6 +1217,10 @@ class UI {
|
|
|
1207
1217
|
this.br();
|
|
1208
1218
|
}
|
|
1209
1219
|
}
|
|
1220
|
+
/** Plain console.log passthrough — use for preformatted or multi-line text. */
|
|
1221
|
+
log(message) {
|
|
1222
|
+
this.console?.log(message);
|
|
1223
|
+
}
|
|
1210
1224
|
list(items) {
|
|
1211
1225
|
for (const item of items) {
|
|
1212
1226
|
this.console?.log(` ${item}`);
|
|
@@ -1394,16 +1408,14 @@ async function fileExists(path) {
|
|
|
1394
1408
|
return false;
|
|
1395
1409
|
}
|
|
1396
1410
|
}
|
|
1397
|
-
function
|
|
1398
|
-
return
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
return true;
|
|
1406
|
-
});
|
|
1411
|
+
function consolidatedFilename(defaultFilename, suffix) {
|
|
1412
|
+
return suffix ? `${defaultFilename}.${suffix}.json` : `${defaultFilename}.json`;
|
|
1413
|
+
}
|
|
1414
|
+
async function shouldUseSeparateFiles(resolvedPath, defaultFilename, separateFiles, suffix) {
|
|
1415
|
+
if (separateFiles !== void 0) {
|
|
1416
|
+
return separateFiles;
|
|
1417
|
+
}
|
|
1418
|
+
return !await fileExists(join(resolvedPath, consolidatedFilename(defaultFilename, suffix)));
|
|
1407
1419
|
}
|
|
1408
1420
|
|
|
1409
1421
|
const REPORT_STATUS = {
|
|
@@ -2115,14 +2127,14 @@ async function performInteractiveLogin(options) {
|
|
|
2115
2127
|
}
|
|
2116
2128
|
}
|
|
2117
2129
|
|
|
2118
|
-
const program$
|
|
2130
|
+
const program$f = getProgram();
|
|
2119
2131
|
const allRegionsText = Object.values(regions).join(",");
|
|
2120
|
-
program$
|
|
2132
|
+
program$f.command(commands.LOGIN).description("Login to the Storyblok CLI").option("-t, --token <token>", "Token to login directly without questions, like for CI environments").option(
|
|
2121
2133
|
"-r, --region <region>",
|
|
2122
2134
|
`The region you would like to work in. Please keep in mind that the region must match the region of your space. This region flag will be used for the other cli's commands. You can use the values: ${allRegionsText}.`
|
|
2123
2135
|
).action(async (options) => {
|
|
2124
2136
|
konsola.title(`${commands.LOGIN}`, colorPalette.LOGIN);
|
|
2125
|
-
const verbose = program$
|
|
2137
|
+
const verbose = program$f.opts().verbose;
|
|
2126
2138
|
const { token, region } = options;
|
|
2127
2139
|
const { state, updateSession, persistCredentials } = session();
|
|
2128
2140
|
if (state.isLoggedIn && !state.envLogin) {
|
|
@@ -2180,10 +2192,10 @@ program$e.command(commands.LOGIN).description("Login to the Storyblok CLI").opti
|
|
|
2180
2192
|
konsola.br();
|
|
2181
2193
|
});
|
|
2182
2194
|
|
|
2183
|
-
const program$
|
|
2184
|
-
program$
|
|
2195
|
+
const program$e = getProgram();
|
|
2196
|
+
program$e.command(commands.LOGOUT).description("Logout from the Storyblok CLI").action(async () => {
|
|
2185
2197
|
konsola.title(`${commands.LOGOUT}`, colorPalette.LOGOUT);
|
|
2186
|
-
const verbose = program$
|
|
2198
|
+
const verbose = program$e.opts().verbose;
|
|
2187
2199
|
try {
|
|
2188
2200
|
const { state } = session();
|
|
2189
2201
|
if (!state.isLoggedIn || !state.password || !state.region) {
|
|
@@ -2230,10 +2242,10 @@ async function openSignupInBrowser(url) {
|
|
|
2230
2242
|
}
|
|
2231
2243
|
}
|
|
2232
2244
|
|
|
2233
|
-
const program$
|
|
2234
|
-
program$
|
|
2245
|
+
const program$d = getProgram();
|
|
2246
|
+
program$d.command(commands.SIGNUP).description("Sign up for Storyblok").action(async () => {
|
|
2235
2247
|
konsola.title(`${commands.SIGNUP}`, colorPalette.SIGNUP);
|
|
2236
|
-
const verbose = program$
|
|
2248
|
+
const verbose = program$d.opts().verbose;
|
|
2237
2249
|
const { state } = session();
|
|
2238
2250
|
if (state.isLoggedIn && !state.envLogin) {
|
|
2239
2251
|
konsola.ok(`You are already logged in. If you want to signup with a different account, please logout first.`);
|
|
@@ -2254,10 +2266,10 @@ program$c.command(commands.SIGNUP).description("Sign up for Storyblok").action(a
|
|
|
2254
2266
|
konsola.br();
|
|
2255
2267
|
});
|
|
2256
2268
|
|
|
2257
|
-
const program$
|
|
2258
|
-
program$
|
|
2269
|
+
const program$c = getProgram();
|
|
2270
|
+
program$c.command(commands.USER).description("Get the current user").action(async () => {
|
|
2259
2271
|
konsola.title(`${commands.USER}`, colorPalette.USER);
|
|
2260
|
-
const verbose = program$
|
|
2272
|
+
const verbose = program$c.opts().verbose;
|
|
2261
2273
|
const { state } = session();
|
|
2262
2274
|
if (!requireAuthentication(state)) {
|
|
2263
2275
|
return;
|
|
@@ -2285,117 +2297,24 @@ program$b.command(commands.USER).description("Get the current user").action(asyn
|
|
|
2285
2297
|
konsola.br();
|
|
2286
2298
|
});
|
|
2287
2299
|
|
|
2288
|
-
const program$
|
|
2289
|
-
const componentsCommand = program$
|
|
2290
|
-
|
|
2291
|
-
function isComponent(item) {
|
|
2292
|
-
return "schema" in item;
|
|
2293
|
-
}
|
|
2294
|
-
function isPreset(item) {
|
|
2295
|
-
return "component_id" in item && "preset" in item;
|
|
2296
|
-
}
|
|
2297
|
-
function isInternalTag(item) {
|
|
2298
|
-
return "object_type" in item;
|
|
2299
|
-
}
|
|
2300
|
-
function isComponentGroup(item) {
|
|
2301
|
-
return "uuid" in item && !("schema" in item);
|
|
2302
|
-
}
|
|
2303
|
-
async function loadComponents(directoryPath, options) {
|
|
2304
|
-
const files = await readDirectory(directoryPath);
|
|
2305
|
-
const { suffix } = options ?? {};
|
|
2306
|
-
const componentMap = /* @__PURE__ */ new Map();
|
|
2307
|
-
const groupMap = /* @__PURE__ */ new Map();
|
|
2308
|
-
const tagMap = /* @__PURE__ */ new Map();
|
|
2309
|
-
const presets = [];
|
|
2310
|
-
const duplicates = [];
|
|
2311
|
-
for (const file of filterJsonBySuffix(files, suffix)) {
|
|
2312
|
-
const { data, error } = await readJsonFile(join(directoryPath, file));
|
|
2313
|
-
if (error) {
|
|
2314
|
-
handleFileSystemError("read", error);
|
|
2315
|
-
continue;
|
|
2316
|
-
}
|
|
2317
|
-
for (const item of data) {
|
|
2318
|
-
if (isPreset(item)) {
|
|
2319
|
-
presets.push(item);
|
|
2320
|
-
} else if (isInternalTag(item)) {
|
|
2321
|
-
if (!item.id) {
|
|
2322
|
-
throw new Error('Internal tag is missing "id"!');
|
|
2323
|
-
}
|
|
2324
|
-
tagMap.set(item.id, item);
|
|
2325
|
-
} else if (isComponent(item)) {
|
|
2326
|
-
const existing = componentMap.get(item.name);
|
|
2327
|
-
if (existing) {
|
|
2328
|
-
duplicates.push(`Component "${item.name}" found in both "${existing.file}" and "${file}"`);
|
|
2329
|
-
}
|
|
2330
|
-
componentMap.set(item.name, { component: item, file });
|
|
2331
|
-
} else if (isComponentGroup(item)) {
|
|
2332
|
-
groupMap.set(item.id, item);
|
|
2333
|
-
}
|
|
2334
|
-
}
|
|
2335
|
-
}
|
|
2336
|
-
if (duplicates.length) {
|
|
2337
|
-
throw new FileSystemError(
|
|
2338
|
-
"invalid_argument",
|
|
2339
|
-
"read",
|
|
2340
|
-
new Error("Duplicate components detected"),
|
|
2341
|
-
`Duplicate components found in ${directoryPath}:
|
|
2342
|
-
|
|
2343
|
-
${duplicates.join("\n")}
|
|
2344
|
-
|
|
2345
|
-
This can happen when multiple environment snapshots (e.g. components.json and components.dev.json) or mixed formats coexist in the same directory.
|
|
2346
|
-
|
|
2347
|
-
To fix this, either:
|
|
2348
|
-
- Use --suffix <env> to target a specific environment (e.g. --suffix dev)
|
|
2349
|
-
- Clean up the directory and pull components again in the format you intend`
|
|
2350
|
-
);
|
|
2351
|
-
}
|
|
2352
|
-
return {
|
|
2353
|
-
components: [...componentMap.values()].map(({ component }) => component),
|
|
2354
|
-
groups: [...groupMap.values()],
|
|
2355
|
-
presets,
|
|
2356
|
-
internalTags: [...tagMap.values()]
|
|
2357
|
-
};
|
|
2358
|
-
}
|
|
2300
|
+
const program$b = getProgram();
|
|
2301
|
+
const componentsCommand = program$b.command(commands.COMPONENTS).alias("comp").description(`Manage your space's block schema`);
|
|
2359
2302
|
|
|
2360
2303
|
const DEFAULT_COMPONENTS_FILENAME = "components";
|
|
2361
|
-
const DEFAULT_GROUPS_FILENAME = "groups";
|
|
2304
|
+
const DEFAULT_GROUPS_FILENAME$1 = "groups";
|
|
2362
2305
|
const DEFAULT_PRESETS_FILENAME = "presets";
|
|
2363
2306
|
const DEFAULT_TAGS_FILENAME = "tags";
|
|
2364
2307
|
|
|
2365
|
-
async function fetchAllPages(fetchFunction, extractDataFunction) {
|
|
2366
|
-
const items = [];
|
|
2367
|
-
let page = 1;
|
|
2368
|
-
while (true) {
|
|
2369
|
-
const { data, response } = await fetchFunction(page);
|
|
2370
|
-
const totalHeader = response.headers.get("total");
|
|
2371
|
-
const fetchedItems = extractDataFunction(data);
|
|
2372
|
-
items.push(...fetchedItems);
|
|
2373
|
-
if (!totalHeader) {
|
|
2374
|
-
return items;
|
|
2375
|
-
}
|
|
2376
|
-
const total = Number(totalHeader);
|
|
2377
|
-
if (Number.isNaN(total) || items.length >= total || fetchedItems.length === 0) {
|
|
2378
|
-
return items;
|
|
2379
|
-
}
|
|
2380
|
-
page++;
|
|
2381
|
-
}
|
|
2382
|
-
}
|
|
2383
|
-
|
|
2384
2308
|
const fetchComponents = async (spaceId) => {
|
|
2385
2309
|
try {
|
|
2386
2310
|
const client = getMapiClient();
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
},
|
|
2395
|
-
throwOnError: true
|
|
2396
|
-
}),
|
|
2397
|
-
(data) => data?.components ?? []
|
|
2398
|
-
);
|
|
2311
|
+
const { data } = await client.components.list({
|
|
2312
|
+
path: {
|
|
2313
|
+
space_id: Number(spaceId)
|
|
2314
|
+
},
|
|
2315
|
+
throwOnError: true
|
|
2316
|
+
});
|
|
2317
|
+
return data?.components ?? [];
|
|
2399
2318
|
} catch (error) {
|
|
2400
2319
|
handleAPIError("pull_components", error);
|
|
2401
2320
|
}
|
|
@@ -2403,19 +2322,16 @@ const fetchComponents = async (spaceId) => {
|
|
|
2403
2322
|
const fetchComponent = async (spaceId, componentName) => {
|
|
2404
2323
|
try {
|
|
2405
2324
|
const client = getMapiClient();
|
|
2406
|
-
const
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
}),
|
|
2417
|
-
(data) => data?.components ?? []
|
|
2418
|
-
);
|
|
2325
|
+
const { data } = await client.components.list({
|
|
2326
|
+
path: {
|
|
2327
|
+
space_id: Number(spaceId)
|
|
2328
|
+
},
|
|
2329
|
+
query: {
|
|
2330
|
+
search: componentName
|
|
2331
|
+
},
|
|
2332
|
+
throwOnError: true
|
|
2333
|
+
});
|
|
2334
|
+
const matches = data?.components ?? [];
|
|
2419
2335
|
return matches.find((c) => c.name === componentName);
|
|
2420
2336
|
} catch (error) {
|
|
2421
2337
|
handleAPIError("pull_components", error, `Failed to fetch component ${componentName}`);
|
|
@@ -2482,7 +2398,7 @@ const saveComponentsToFiles = async (space, spaceData, options) => {
|
|
|
2482
2398
|
const presetsFilePath = join(resolvedPath, suffix ? `${sanitizedName}.${DEFAULT_PRESETS_FILENAME}.${suffix}.json` : `${sanitizedName}.${DEFAULT_PRESETS_FILENAME}.json`);
|
|
2483
2399
|
await saveToFile(presetsFilePath, JSON.stringify(componentPresets, null, 2));
|
|
2484
2400
|
}
|
|
2485
|
-
const groupsFilePath = join(resolvedPath, suffix ? `${DEFAULT_GROUPS_FILENAME}.${suffix}.json` : `${DEFAULT_GROUPS_FILENAME}.json`);
|
|
2401
|
+
const groupsFilePath = join(resolvedPath, suffix ? `${DEFAULT_GROUPS_FILENAME$1}.${suffix}.json` : `${DEFAULT_GROUPS_FILENAME$1}.json`);
|
|
2486
2402
|
await saveToFile(groupsFilePath, JSON.stringify(groups, null, 2));
|
|
2487
2403
|
const internalTagsFilePath = join(resolvedPath, suffix ? `${DEFAULT_TAGS_FILENAME}.${suffix}.json` : `${DEFAULT_TAGS_FILENAME}.json`);
|
|
2488
2404
|
await saveToFile(internalTagsFilePath, JSON.stringify(internalTags, null, 2));
|
|
@@ -2492,7 +2408,7 @@ const saveComponentsToFiles = async (space, spaceData, options) => {
|
|
|
2492
2408
|
const componentsFilePath = join(resolvedPath, suffix ? `${filename}.${suffix}.json` : `${filename}.json`);
|
|
2493
2409
|
await saveToFile(componentsFilePath, JSON.stringify(components, null, 2));
|
|
2494
2410
|
if (groups.length > 0) {
|
|
2495
|
-
const groupsFilePath = join(resolvedPath, suffix ? `${DEFAULT_GROUPS_FILENAME}.${suffix}.json` : `${DEFAULT_GROUPS_FILENAME}.json`);
|
|
2411
|
+
const groupsFilePath = join(resolvedPath, suffix ? `${DEFAULT_GROUPS_FILENAME$1}.${suffix}.json` : `${DEFAULT_GROUPS_FILENAME$1}.json`);
|
|
2496
2412
|
await saveToFile(groupsFilePath, JSON.stringify(groups, null, 2));
|
|
2497
2413
|
}
|
|
2498
2414
|
if (presets.length > 0) {
|
|
@@ -2508,6 +2424,21 @@ const saveComponentsToFiles = async (space, spaceData, options) => {
|
|
|
2508
2424
|
}
|
|
2509
2425
|
};
|
|
2510
2426
|
|
|
2427
|
+
function isSchemaField$1(value) {
|
|
2428
|
+
return typeof value === "object" && value !== null && "type" in value;
|
|
2429
|
+
}
|
|
2430
|
+
function toWritableSchema(schema) {
|
|
2431
|
+
if (!schema) {
|
|
2432
|
+
return void 0;
|
|
2433
|
+
}
|
|
2434
|
+
const result = {};
|
|
2435
|
+
for (const [key, value] of Object.entries(schema)) {
|
|
2436
|
+
if (isSchemaField$1(value)) {
|
|
2437
|
+
result[key] = value;
|
|
2438
|
+
}
|
|
2439
|
+
}
|
|
2440
|
+
return result;
|
|
2441
|
+
}
|
|
2511
2442
|
const pushComponent = async (space, component) => {
|
|
2512
2443
|
try {
|
|
2513
2444
|
const client = getMapiClient();
|
|
@@ -2542,10 +2473,23 @@ const updateComponent = async (space, componentId, component) => {
|
|
|
2542
2473
|
}
|
|
2543
2474
|
};
|
|
2544
2475
|
const upsertComponent = async (space, component, existingId) => {
|
|
2476
|
+
const { name, display_name, schema, is_root, is_nestable, component_group_uuid, color, icon, preview_field, internal_tag_ids } = component;
|
|
2477
|
+
const payload = {
|
|
2478
|
+
name,
|
|
2479
|
+
display_name,
|
|
2480
|
+
schema: toWritableSchema(schema),
|
|
2481
|
+
is_root,
|
|
2482
|
+
is_nestable,
|
|
2483
|
+
component_group_uuid: component_group_uuid ?? void 0,
|
|
2484
|
+
color: color ?? void 0,
|
|
2485
|
+
icon: icon ?? void 0,
|
|
2486
|
+
preview_field: preview_field ?? void 0,
|
|
2487
|
+
internal_tag_ids
|
|
2488
|
+
};
|
|
2545
2489
|
if (existingId) {
|
|
2546
|
-
return await updateComponent(space, existingId,
|
|
2490
|
+
return await updateComponent(space, existingId, payload);
|
|
2547
2491
|
} else {
|
|
2548
|
-
return await pushComponent(space,
|
|
2492
|
+
return await pushComponent(space, payload);
|
|
2549
2493
|
}
|
|
2550
2494
|
};
|
|
2551
2495
|
const pushComponentGroup = async (space, componentGroup) => {
|
|
@@ -2648,7 +2592,7 @@ const pushComponentInternalTag = async (space, componentInternalTag) => {
|
|
|
2648
2592
|
path: {
|
|
2649
2593
|
space_id: Number(space)
|
|
2650
2594
|
},
|
|
2651
|
-
body: componentInternalTag,
|
|
2595
|
+
body: { internal_tag: componentInternalTag },
|
|
2652
2596
|
throwOnError: true
|
|
2653
2597
|
});
|
|
2654
2598
|
return data.internal_tag;
|
|
@@ -2663,7 +2607,7 @@ const updateComponentInternalTag = async (space, tagId, componentInternalTag) =>
|
|
|
2663
2607
|
path: {
|
|
2664
2608
|
space_id: Number(space)
|
|
2665
2609
|
},
|
|
2666
|
-
body: componentInternalTag,
|
|
2610
|
+
body: { internal_tag: componentInternalTag },
|
|
2667
2611
|
throwOnError: true
|
|
2668
2612
|
});
|
|
2669
2613
|
return data.internal_tag;
|
|
@@ -2679,15 +2623,11 @@ const upsertComponentInternalTag = async (space, tag, existingId) => {
|
|
|
2679
2623
|
}
|
|
2680
2624
|
};
|
|
2681
2625
|
const readComponentsFiles = async (options) => {
|
|
2682
|
-
const { from, path, suffix } = options;
|
|
2626
|
+
const { from, path, separateFiles, suffix } = options;
|
|
2683
2627
|
const resolvedPath = resolvePath(path, `components/${from}`);
|
|
2684
|
-
let result;
|
|
2685
2628
|
try {
|
|
2686
|
-
|
|
2629
|
+
await readdir(resolvedPath);
|
|
2687
2630
|
} catch (error) {
|
|
2688
|
-
if (error instanceof FileSystemError && error.code !== "ENOENT") {
|
|
2689
|
-
throw error;
|
|
2690
|
-
}
|
|
2691
2631
|
const message = `No local components found for space ${chalk.bold(from)}. To push components, you need to pull them first:
|
|
2692
2632
|
|
|
2693
2633
|
1. Pull the components from your source space:
|
|
@@ -2702,16 +2642,89 @@ const readComponentsFiles = async (options) => {
|
|
|
2702
2642
|
message
|
|
2703
2643
|
);
|
|
2704
2644
|
}
|
|
2705
|
-
if (
|
|
2645
|
+
if (await shouldUseSeparateFiles(resolvedPath, DEFAULT_COMPONENTS_FILENAME, separateFiles, suffix)) {
|
|
2646
|
+
return await readSeparateFiles$1(resolvedPath, suffix);
|
|
2647
|
+
}
|
|
2648
|
+
return await readConsolidatedFiles$1(resolvedPath, suffix);
|
|
2649
|
+
};
|
|
2650
|
+
async function readSeparateFiles$1(resolvedPath, suffix) {
|
|
2651
|
+
const files = await readdir(resolvedPath);
|
|
2652
|
+
const components = [];
|
|
2653
|
+
const presets = [];
|
|
2654
|
+
let groups = [];
|
|
2655
|
+
let internalTags = [];
|
|
2656
|
+
const filteredFiles = files.filter((file) => {
|
|
2657
|
+
if (suffix) {
|
|
2658
|
+
return file.endsWith(`.${suffix}.json`);
|
|
2659
|
+
} else {
|
|
2660
|
+
return !/\.\w+\.json$/.test(file) || file.endsWith(".presets.json");
|
|
2661
|
+
}
|
|
2662
|
+
});
|
|
2663
|
+
for (const file of filteredFiles) {
|
|
2664
|
+
const filePath = join(resolvedPath, file);
|
|
2665
|
+
if (file === `${DEFAULT_GROUPS_FILENAME$1}.json` || file === `${DEFAULT_GROUPS_FILENAME$1}.${suffix}.json`) {
|
|
2666
|
+
const result = await readJsonFile(filePath);
|
|
2667
|
+
if (result.error) {
|
|
2668
|
+
handleFileSystemError("read", result.error);
|
|
2669
|
+
continue;
|
|
2670
|
+
}
|
|
2671
|
+
groups = result.data;
|
|
2672
|
+
} else if (file === `${DEFAULT_TAGS_FILENAME}.json` || file === `${DEFAULT_TAGS_FILENAME}.${suffix}.json`) {
|
|
2673
|
+
const result = await readJsonFile(filePath);
|
|
2674
|
+
if (result.error) {
|
|
2675
|
+
handleFileSystemError("read", result.error);
|
|
2676
|
+
continue;
|
|
2677
|
+
}
|
|
2678
|
+
internalTags = result.data;
|
|
2679
|
+
} else if (file.endsWith(`.${DEFAULT_PRESETS_FILENAME}.json`) || file.endsWith(`.${DEFAULT_PRESETS_FILENAME}.${suffix}.json`)) {
|
|
2680
|
+
const result = await readJsonFile(filePath);
|
|
2681
|
+
if (result.error) {
|
|
2682
|
+
handleFileSystemError("read", result.error);
|
|
2683
|
+
continue;
|
|
2684
|
+
}
|
|
2685
|
+
presets.push(...result.data);
|
|
2686
|
+
} else if (file.endsWith(".json") || file.endsWith(`${suffix}.json`)) {
|
|
2687
|
+
if (file === `${DEFAULT_COMPONENTS_FILENAME}.json` || file === `${DEFAULT_COMPONENTS_FILENAME}.${suffix}.json`) {
|
|
2688
|
+
continue;
|
|
2689
|
+
}
|
|
2690
|
+
const result = await readJsonFile(filePath);
|
|
2691
|
+
if (result.error) {
|
|
2692
|
+
handleFileSystemError("read", result.error);
|
|
2693
|
+
continue;
|
|
2694
|
+
}
|
|
2695
|
+
components.push(...result.data);
|
|
2696
|
+
}
|
|
2697
|
+
}
|
|
2698
|
+
return {
|
|
2699
|
+
components,
|
|
2700
|
+
groups,
|
|
2701
|
+
presets,
|
|
2702
|
+
internalTags
|
|
2703
|
+
};
|
|
2704
|
+
}
|
|
2705
|
+
async function readConsolidatedFiles$1(resolvedPath, suffix) {
|
|
2706
|
+
const componentsPath = join(resolvedPath, suffix ? `${DEFAULT_COMPONENTS_FILENAME}.${suffix}.json` : `${DEFAULT_COMPONENTS_FILENAME}.json`);
|
|
2707
|
+
const componentsResult = await readJsonFile(componentsPath);
|
|
2708
|
+
if (componentsResult.error || !componentsResult.data.length) {
|
|
2706
2709
|
throw new FileSystemError(
|
|
2707
2710
|
"file_not_found",
|
|
2708
2711
|
"read",
|
|
2709
|
-
new Error("
|
|
2710
|
-
`No components found in ${
|
|
2712
|
+
componentsResult.error || new Error("Components file is empty"),
|
|
2713
|
+
`No components found in ${componentsPath}. Please make sure you have pulled the components first.`
|
|
2711
2714
|
);
|
|
2712
2715
|
}
|
|
2713
|
-
|
|
2714
|
-
}
|
|
2716
|
+
const [groupsResult, presetsResult, tagsResult] = await Promise.all([
|
|
2717
|
+
readJsonFile(join(resolvedPath, suffix ? `${DEFAULT_GROUPS_FILENAME$1}.${suffix}.json` : `${DEFAULT_GROUPS_FILENAME$1}.json`)),
|
|
2718
|
+
readJsonFile(join(resolvedPath, suffix ? `${DEFAULT_PRESETS_FILENAME}.${suffix}.json` : `${DEFAULT_PRESETS_FILENAME}.json`)),
|
|
2719
|
+
readJsonFile(join(resolvedPath, suffix ? `${DEFAULT_TAGS_FILENAME}.${suffix}.json` : `${DEFAULT_TAGS_FILENAME}.json`))
|
|
2720
|
+
]);
|
|
2721
|
+
return {
|
|
2722
|
+
components: componentsResult.data,
|
|
2723
|
+
groups: groupsResult.data,
|
|
2724
|
+
presets: presetsResult.data,
|
|
2725
|
+
internalTags: tagsResult.data
|
|
2726
|
+
};
|
|
2727
|
+
}
|
|
2715
2728
|
|
|
2716
2729
|
const pullCmd$4 = componentsCommand.command("pull [componentName]").option("-f, --filename <filename>", "custom name to be used in file(s) name instead of space id").option("--sf, --separate-files", "Argument to create a single file for each component").option("--su, --suffix <suffix>", "suffix to add to the file name (e.g. components.<suffix>.json)").option("-s, --space <space>", "space ID").description(`Download your space's components schema as json. Optionally specify a component name to pull a single component.`);
|
|
2717
2730
|
pullCmd$4.action(async (componentName, options, command) => {
|
|
@@ -3742,7 +3755,7 @@ async function pushWithDependencyGraph(space, spaceState, maxConcurrency = getAc
|
|
|
3742
3755
|
return results;
|
|
3743
3756
|
}
|
|
3744
3757
|
|
|
3745
|
-
const pushCmd$3 = componentsCommand.command("push [componentName]").description(`Push your space's components schema as json`).option("-f, --from <from>", "source space id").option("--fi, --filter <filter>", "glob filter to apply to the components before pushing").option("--sf, --separate-files", "Read from separate files instead of consolidated files", false).option("--su, --suffix <suffix>", "
|
|
3758
|
+
const pushCmd$3 = componentsCommand.command("push [componentName]").description(`Push your space's components schema as json`).option("-f, --from <from>", "source space id").option("--fi, --filter <filter>", "glob filter to apply to the components before pushing").option("--sf, --separate-files", "Read from separate files instead of consolidated files", false).option("--su, --suffix <suffix>", "Suffix to add to the component name").option("-s, --space <space>", "space ID");
|
|
3746
3759
|
pushCmd$3.action(async (componentName, options, command) => {
|
|
3747
3760
|
const ui = getUI();
|
|
3748
3761
|
const logger = getLogger();
|
|
@@ -3959,8 +3972,8 @@ const saveLanguagesToFile = async (space, internationalizationOptions, options)
|
|
|
3959
3972
|
}
|
|
3960
3973
|
};
|
|
3961
3974
|
|
|
3962
|
-
const program$
|
|
3963
|
-
const languagesCommand = program$
|
|
3975
|
+
const program$a = getProgram();
|
|
3976
|
+
const languagesCommand = program$a.command(commands.LANGUAGES).alias("lang").description(`Manage your space's languages`);
|
|
3964
3977
|
const pullCmd$3 = languagesCommand.command("pull").description(`Download your space's languages schema as json`).option("-f, --filename <filename>", "filename to save the file as <filename>.<suffix>.json").option("--su, --suffix <suffix>", "suffix to add to the file name (e.g. languages.<suffix>.json). By default, the space ID is used.").option("-s, --space <space>", "space ID");
|
|
3965
3978
|
pullCmd$3.action(async (options, command) => {
|
|
3966
3979
|
konsola.title(`${commands.LANGUAGES}`, colorPalette.LANGUAGES);
|
|
@@ -4006,8 +4019,8 @@ pullCmd$3.action(async (options, command) => {
|
|
|
4006
4019
|
konsola.br();
|
|
4007
4020
|
});
|
|
4008
4021
|
|
|
4009
|
-
const program$
|
|
4010
|
-
const migrationsCommand = program$
|
|
4022
|
+
const program$9 = getProgram();
|
|
4023
|
+
const migrationsCommand = program$9.command(commands.MIGRATIONS).alias("mig").description(`Manage your space's migrations`);
|
|
4011
4024
|
|
|
4012
4025
|
const getMigrationTemplate = () => {
|
|
4013
4026
|
return `export default function (block) {
|
|
@@ -4174,84 +4187,40 @@ const updateStory = async (spaceId, storyId, payload) => {
|
|
|
4174
4187
|
handleAPIError("update_story", error);
|
|
4175
4188
|
}
|
|
4176
4189
|
};
|
|
4177
|
-
const
|
|
4178
|
-
const PREFETCH_PER_PAGE = 100;
|
|
4179
|
-
const addRef = (result, story) => {
|
|
4180
|
-
const ref = { id: story.id, uuid: story.uuid, is_folder: story.is_folder };
|
|
4181
|
-
if (story.full_slug) {
|
|
4182
|
-
const key = normalizeFullSlug(story.full_slug);
|
|
4183
|
-
const existing = result.bySlug.get(key);
|
|
4184
|
-
if (existing) {
|
|
4185
|
-
if (!existing.some((r) => r.id === ref.id)) {
|
|
4186
|
-
existing.push(ref);
|
|
4187
|
-
}
|
|
4188
|
-
} else {
|
|
4189
|
-
result.bySlug.set(key, [ref]);
|
|
4190
|
-
}
|
|
4191
|
-
}
|
|
4192
|
-
result.byId.set(story.id, ref);
|
|
4193
|
-
};
|
|
4194
|
-
const fetchChunkAllPages = async (spaceId, params, onPageStories) => {
|
|
4195
|
-
let page = 1;
|
|
4196
|
-
while (true) {
|
|
4197
|
-
const response = await fetchStories(spaceId, { ...params, page, per_page: PREFETCH_PER_PAGE });
|
|
4198
|
-
if (!response) {
|
|
4199
|
-
return;
|
|
4200
|
-
}
|
|
4201
|
-
onPageStories(response.stories);
|
|
4202
|
-
const total = Number(response.headers.get("Total"));
|
|
4203
|
-
const perPage = Number(response.headers.get("Per-Page")) || PREFETCH_PER_PAGE;
|
|
4204
|
-
if (!Number.isFinite(total) || total <= page * perPage) {
|
|
4205
|
-
return;
|
|
4206
|
-
}
|
|
4207
|
-
page++;
|
|
4208
|
-
}
|
|
4209
|
-
};
|
|
4210
|
-
const prefetchTargetStoriesByKeys = async (spaceId, keys, options) => {
|
|
4190
|
+
const prefetchTargetStories = async (spaceId, options) => {
|
|
4211
4191
|
const result = {
|
|
4212
4192
|
bySlug: /* @__PURE__ */ new Map(),
|
|
4213
4193
|
byId: /* @__PURE__ */ new Map()
|
|
4214
4194
|
};
|
|
4215
|
-
|
|
4216
|
-
|
|
4217
|
-
|
|
4218
|
-
|
|
4219
|
-
|
|
4220
|
-
|
|
4221
|
-
const idSet = /* @__PURE__ */ new Set();
|
|
4222
|
-
for (const id of keys.ids) {
|
|
4223
|
-
if (typeof id === "number" && Number.isFinite(id)) {
|
|
4224
|
-
idSet.add(id);
|
|
4195
|
+
let page = 1;
|
|
4196
|
+
let totalPages = 1;
|
|
4197
|
+
while (page <= totalPages) {
|
|
4198
|
+
const response = await fetchStories(spaceId, { page, per_page: 100 });
|
|
4199
|
+
if (!response) {
|
|
4200
|
+
break;
|
|
4225
4201
|
}
|
|
4226
|
-
|
|
4227
|
-
|
|
4228
|
-
|
|
4229
|
-
|
|
4230
|
-
|
|
4231
|
-
|
|
4232
|
-
|
|
4233
|
-
|
|
4234
|
-
|
|
4235
|
-
|
|
4236
|
-
|
|
4237
|
-
|
|
4238
|
-
|
|
4239
|
-
}
|
|
4240
|
-
|
|
4241
|
-
options?.onIncrement?.(slugs.length);
|
|
4242
|
-
})());
|
|
4243
|
-
}
|
|
4244
|
-
for (const ids of idChunks) {
|
|
4245
|
-
requests.push((async () => {
|
|
4246
|
-
await fetchChunkAllPages(spaceId, { by_ids: ids.join(",") }, (stories) => {
|
|
4247
|
-
for (const story of stories) {
|
|
4248
|
-
addRef(result, story);
|
|
4202
|
+
const total = Number(response.headers.get("Total"));
|
|
4203
|
+
const perPage = Number(response.headers.get("Per-Page"));
|
|
4204
|
+
totalPages = Math.ceil(total / perPage);
|
|
4205
|
+
if (page === 1) {
|
|
4206
|
+
options?.onTotal?.(total);
|
|
4207
|
+
}
|
|
4208
|
+
for (const story of response.stories) {
|
|
4209
|
+
const ref = { id: story.id, uuid: story.uuid, is_folder: story.is_folder };
|
|
4210
|
+
if (story.full_slug) {
|
|
4211
|
+
const key = normalizeFullSlug(story.full_slug);
|
|
4212
|
+
const existing = result.bySlug.get(key);
|
|
4213
|
+
if (existing) {
|
|
4214
|
+
existing.push(ref);
|
|
4215
|
+
} else {
|
|
4216
|
+
result.bySlug.set(key, [ref]);
|
|
4249
4217
|
}
|
|
4250
|
-
}
|
|
4251
|
-
|
|
4252
|
-
}
|
|
4218
|
+
}
|
|
4219
|
+
result.byId.set(story.id, ref);
|
|
4220
|
+
}
|
|
4221
|
+
options?.onIncrement?.(response.stories.length);
|
|
4222
|
+
page++;
|
|
4253
4223
|
}
|
|
4254
|
-
await Promise.all(requests);
|
|
4255
4224
|
return result;
|
|
4256
4225
|
};
|
|
4257
4226
|
|
|
@@ -4708,26 +4677,45 @@ const isStoryPublishedWithoutChanges = (story) => {
|
|
|
4708
4677
|
const isStoryWithUnpublishedChanges = (story) => {
|
|
4709
4678
|
return story.published && story.unpublished_changes;
|
|
4710
4679
|
};
|
|
4680
|
+
const toComponent = (maybeComponent) => {
|
|
4681
|
+
if (maybeComponent.component_group_uuid === void 0) {
|
|
4682
|
+
return null;
|
|
4683
|
+
}
|
|
4684
|
+
return maybeComponent;
|
|
4685
|
+
};
|
|
4711
4686
|
const findComponentSchemas = async (directoryPath) => {
|
|
4712
|
-
|
|
4713
|
-
const { components } = await loadComponents(directoryPath);
|
|
4714
|
-
const schemas = {};
|
|
4715
|
-
for (const component of components) {
|
|
4716
|
-
schemas[component.name] = component.schema;
|
|
4717
|
-
}
|
|
4718
|
-
return schemas;
|
|
4719
|
-
} catch (error) {
|
|
4687
|
+
const files = await readdir(directoryPath).catch((error) => {
|
|
4720
4688
|
if (error.code === "ENOENT") {
|
|
4721
|
-
return
|
|
4722
|
-
}
|
|
4723
|
-
if (error instanceof FileSystemError) {
|
|
4724
|
-
error.message = `Failed to load component schemas for content validation.
|
|
4725
|
-
|
|
4726
|
-
${error.message}`;
|
|
4727
|
-
throw error;
|
|
4689
|
+
return [];
|
|
4728
4690
|
}
|
|
4729
4691
|
throw error;
|
|
4692
|
+
});
|
|
4693
|
+
const fileContents = files.filter((f) => extname(f) === ".json").map((f) => {
|
|
4694
|
+
const filePath = join(directoryPath, f);
|
|
4695
|
+
const fileContent = readFileSync(filePath, "utf-8");
|
|
4696
|
+
return JSON.parse(fileContent);
|
|
4697
|
+
});
|
|
4698
|
+
const components = [];
|
|
4699
|
+
for (const content of fileContents) {
|
|
4700
|
+
if (Array.isArray(content)) {
|
|
4701
|
+
for (const maybeComponent of content) {
|
|
4702
|
+
const component2 = toComponent(maybeComponent);
|
|
4703
|
+
if (component2) {
|
|
4704
|
+
components.push(component2);
|
|
4705
|
+
}
|
|
4706
|
+
}
|
|
4707
|
+
continue;
|
|
4708
|
+
}
|
|
4709
|
+
const component = toComponent(content);
|
|
4710
|
+
if (component) {
|
|
4711
|
+
components.push(component);
|
|
4712
|
+
}
|
|
4730
4713
|
}
|
|
4714
|
+
const schemas = {};
|
|
4715
|
+
for (const component of components) {
|
|
4716
|
+
schemas[component.name] = component.schema;
|
|
4717
|
+
}
|
|
4718
|
+
return schemas;
|
|
4731
4719
|
};
|
|
4732
4720
|
const getStoryFilename = (story) => {
|
|
4733
4721
|
return `${story.slug}_${story.uuid}.json`;
|
|
@@ -5059,8 +5047,8 @@ rollbackCmd.action(async (migrationFile, _options, command) => {
|
|
|
5059
5047
|
}
|
|
5060
5048
|
});
|
|
5061
5049
|
|
|
5062
|
-
const program$
|
|
5063
|
-
const typesCommand = program$
|
|
5050
|
+
const program$8 = getProgram();
|
|
5051
|
+
const typesCommand = program$8.command(commands.TYPES).alias("ts").description(`Generate types d.ts for your component schemas`);
|
|
5064
5052
|
|
|
5065
5053
|
const getAssetJSONSchema = (title) => ({
|
|
5066
5054
|
$id: "#/asset",
|
|
@@ -5671,7 +5659,7 @@ const getPropertyTypeAnnotation = (property, prefix, suffix) => {
|
|
|
5671
5659
|
}
|
|
5672
5660
|
};
|
|
5673
5661
|
function getStoryType(property, prefix, suffix) {
|
|
5674
|
-
return `${STORY_TYPE}<${prefix ?? ""}${capitalize(toCamelCase(property))}${suffix ?? ""}>`;
|
|
5662
|
+
return `${STORY_TYPE}<${prefix ?? ""}${capitalize(toCamelCase$1(property))}${suffix ?? ""}>`;
|
|
5675
5663
|
}
|
|
5676
5664
|
const getComponentType = (componentName, options) => {
|
|
5677
5665
|
const prefix = options.typePrefix ?? "";
|
|
@@ -5961,6 +5949,8 @@ const generateStoryblokTypes = async (options = {}) => {
|
|
|
5961
5949
|
}
|
|
5962
5950
|
};
|
|
5963
5951
|
|
|
5952
|
+
const DEFAULT_DATASOURCES_FILENAME = "datasources";
|
|
5953
|
+
|
|
5964
5954
|
const pushDatasource = async (spaceId, datasource) => {
|
|
5965
5955
|
try {
|
|
5966
5956
|
const client = getMapiClient();
|
|
@@ -6062,15 +6052,11 @@ const deleteDatasourceEntry = async (spaceId, entryId) => {
|
|
|
6062
6052
|
handleAPIError("delete_datasource_entry", error, `Failed to delete datasource entry ${entryId}`);
|
|
6063
6053
|
}
|
|
6064
6054
|
};
|
|
6065
|
-
function isDatasource(item) {
|
|
6066
|
-
return typeof item === "object" && item !== null && "slug" in item && typeof item.slug === "string";
|
|
6067
|
-
}
|
|
6068
6055
|
const readDatasourcesFiles = async (options) => {
|
|
6069
|
-
const { from, path, suffix } = options;
|
|
6056
|
+
const { from, path, separateFiles, suffix } = options;
|
|
6070
6057
|
const resolvedPath = resolvePath(path, `datasources/${from}`);
|
|
6071
|
-
let files;
|
|
6072
6058
|
try {
|
|
6073
|
-
|
|
6059
|
+
await readdir(resolvedPath);
|
|
6074
6060
|
} catch (error) {
|
|
6075
6061
|
const message = `No local datasources found for space ${chalk.bold(from)}. To push datasources, you need to pull them first:
|
|
6076
6062
|
|
|
@@ -6086,53 +6072,56 @@ const readDatasourcesFiles = async (options) => {
|
|
|
6086
6072
|
message
|
|
6087
6073
|
);
|
|
6088
6074
|
}
|
|
6089
|
-
|
|
6090
|
-
|
|
6091
|
-
|
|
6092
|
-
|
|
6093
|
-
|
|
6094
|
-
|
|
6095
|
-
|
|
6075
|
+
if (await shouldUseSeparateFiles(resolvedPath, DEFAULT_DATASOURCES_FILENAME, separateFiles, suffix)) {
|
|
6076
|
+
return await readSeparateFiles(resolvedPath, suffix);
|
|
6077
|
+
}
|
|
6078
|
+
return await readConsolidatedFiles(resolvedPath, suffix);
|
|
6079
|
+
};
|
|
6080
|
+
async function readSeparateFiles(resolvedPath, suffix) {
|
|
6081
|
+
const files = await readdir(resolvedPath);
|
|
6082
|
+
const datasources = [];
|
|
6083
|
+
const filteredFiles = files.filter((file) => {
|
|
6084
|
+
if (suffix) {
|
|
6085
|
+
return file.endsWith(`.${suffix}.json`);
|
|
6086
|
+
} else {
|
|
6087
|
+
return !/\.\w+\.json$/.test(file);
|
|
6096
6088
|
}
|
|
6097
|
-
|
|
6098
|
-
|
|
6099
|
-
|
|
6100
|
-
|
|
6101
|
-
|
|
6102
|
-
|
|
6103
|
-
datasourceMap.set(item.slug, { datasource: item, file });
|
|
6089
|
+
});
|
|
6090
|
+
for (const file of filteredFiles) {
|
|
6091
|
+
const filePath = join(resolvedPath, file);
|
|
6092
|
+
if (file.endsWith(".json") || file.endsWith(`${suffix}.json`)) {
|
|
6093
|
+
if (file === `${DEFAULT_DATASOURCES_FILENAME}.json` || new RegExp(`^${DEFAULT_DATASOURCES_FILENAME}\\.\\w+\\.json$`).test(file)) {
|
|
6094
|
+
continue;
|
|
6104
6095
|
}
|
|
6096
|
+
const result = await readJsonFile(filePath);
|
|
6097
|
+
if (result.error) {
|
|
6098
|
+
handleFileSystemError("read", result.error);
|
|
6099
|
+
continue;
|
|
6100
|
+
}
|
|
6101
|
+
datasources.push(...result.data);
|
|
6105
6102
|
}
|
|
6106
6103
|
}
|
|
6107
|
-
|
|
6108
|
-
|
|
6109
|
-
|
|
6110
|
-
|
|
6111
|
-
|
|
6112
|
-
|
|
6113
|
-
|
|
6114
|
-
|
|
6115
|
-
|
|
6116
|
-
This can happen when multiple environment snapshots (e.g. datasources.json and datasources.dev.json) or mixed formats coexist in the same directory.
|
|
6117
|
-
|
|
6118
|
-
To fix this, either:
|
|
6119
|
-
- Use --suffix <env> to target a specific environment (e.g. --suffix dev)
|
|
6120
|
-
- Clean up the directory and pull datasources again in the format you intend`
|
|
6121
|
-
);
|
|
6122
|
-
}
|
|
6123
|
-
const datasources = [...datasourceMap.values()].map(({ datasource }) => datasource);
|
|
6124
|
-
if (!datasources.length) {
|
|
6104
|
+
return {
|
|
6105
|
+
datasources
|
|
6106
|
+
};
|
|
6107
|
+
}
|
|
6108
|
+
async function readConsolidatedFiles(resolvedPath, suffix) {
|
|
6109
|
+
const datasourcesPath = join(resolvedPath, suffix ? `${DEFAULT_DATASOURCES_FILENAME}.${suffix}.json` : `${DEFAULT_DATASOURCES_FILENAME}.json`);
|
|
6110
|
+
const datasourcesResult = await readJsonFile(datasourcesPath);
|
|
6111
|
+
if (datasourcesResult.error || !datasourcesResult.data.length) {
|
|
6125
6112
|
throw new FileSystemError(
|
|
6126
6113
|
"file_not_found",
|
|
6127
6114
|
"read",
|
|
6128
|
-
new Error("
|
|
6129
|
-
`No datasources found in ${
|
|
6115
|
+
datasourcesResult.error || new Error("Datasources file is empty"),
|
|
6116
|
+
`No datasources found in ${datasourcesPath}. Please make sure you have pulled the datasources first.`
|
|
6130
6117
|
);
|
|
6131
6118
|
}
|
|
6132
|
-
return {
|
|
6133
|
-
|
|
6134
|
-
|
|
6135
|
-
|
|
6119
|
+
return {
|
|
6120
|
+
datasources: datasourcesResult.data
|
|
6121
|
+
};
|
|
6122
|
+
}
|
|
6123
|
+
|
|
6124
|
+
const generateCmd = typesCommand.command("generate").description("Generate types d.ts for your component schemas").option(
|
|
6136
6125
|
"--filename <name>",
|
|
6137
6126
|
"Base file name for all component types when generating a single declarations file (e.g. components.d.ts). Ignored when using --separate-files."
|
|
6138
6127
|
).option("--sf, --separate-files", "Generate one .d.ts file per component instead of a single combined file").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").option("-s, --space <space>", "space ID");
|
|
@@ -6195,10 +6184,8 @@ generateCmd.action(async (options, command) => {
|
|
|
6195
6184
|
}
|
|
6196
6185
|
});
|
|
6197
6186
|
|
|
6198
|
-
const program$
|
|
6199
|
-
const datasourcesCommand = program$
|
|
6200
|
-
|
|
6201
|
-
const DEFAULT_DATASOURCES_FILENAME = "datasources";
|
|
6187
|
+
const program$7 = getProgram();
|
|
6188
|
+
const datasourcesCommand = program$7.command(commands.DATASOURCES).alias("ds").description(`Manage your space's datasources`);
|
|
6202
6189
|
|
|
6203
6190
|
const fetchDatasourceEntries = async (spaceId, datasourceId) => {
|
|
6204
6191
|
try {
|
|
@@ -6368,7 +6355,7 @@ pullCmd$2.action(async (datasourceName, options, command) => {
|
|
|
6368
6355
|
}
|
|
6369
6356
|
});
|
|
6370
6357
|
|
|
6371
|
-
const pushCmd$2 = datasourcesCommand.command("push [datasourceName]").description(`Push your space's datasources schema as json`).option("-f, --from <from>", "source space id").option("--fi, --filter <filter>", "glob filter to apply to the datasources before pushing").option("--sf, --separate-files", "Read from separate files instead of consolidated files").option("--su, --suffix <suffix>", "
|
|
6358
|
+
const pushCmd$2 = datasourcesCommand.command("push [datasourceName]").description(`Push your space's datasources schema as json`).option("-f, --from <from>", "source space id").option("--fi, --filter <filter>", "glob filter to apply to the datasources before pushing").option("--sf, --separate-files", "Read from separate files instead of consolidated files").option("--su, --suffix <suffix>", "Suffix to add to the datasource name").option("-s, --space <space>", "space ID");
|
|
6372
6359
|
pushCmd$2.action(async (datasourceName, options, command) => {
|
|
6373
6360
|
konsola.title(`${commands.DATASOURCES}`, colorPalette.DATASOURCES, datasourceName ? `Pushing datasource ${datasourceName}...` : "Pushing datasources...");
|
|
6374
6361
|
const { space, path, verbose } = command.optsWithGlobals();
|
|
@@ -6749,13 +6736,13 @@ async function promptForLogin(verbose) {
|
|
|
6749
6736
|
return null;
|
|
6750
6737
|
}
|
|
6751
6738
|
}
|
|
6752
|
-
const program$
|
|
6753
|
-
program$
|
|
6739
|
+
const program$6 = getProgram();
|
|
6740
|
+
program$6.command(`${commands.CREATE} [project-path]`).alias("c").description(`Scaffold a new project using Storyblok`).option("-t, --template <template>", "technology starter template").option("-b, --blueprint <blueprint>", "[DEPRECATED] use --template instead").option("--skip-space", "skip space creation").option("--token <token>", "Storyblok access token (skip space creation and use this token)").option(
|
|
6754
6741
|
"-r, --region <region>",
|
|
6755
6742
|
`The region to apply to the generated project template (does not affect space creation).`
|
|
6756
6743
|
).action(async (projectPath, options) => {
|
|
6757
6744
|
konsola.title(`${commands.CREATE}`, colorPalette.CREATE);
|
|
6758
|
-
const verbose = program$
|
|
6745
|
+
const verbose = program$6.opts().verbose;
|
|
6759
6746
|
const { template, blueprint, token } = options;
|
|
6760
6747
|
if (options.region && !isRegion(options.region)) {
|
|
6761
6748
|
handleError(new CommandError(`The provided region: ${options.region} is not valid. Please use one of the following values: ${Object.values(regions).join(" | ")}`));
|
|
@@ -6974,8 +6961,8 @@ program$5.command(`${commands.CREATE} [project-path]`).alias("c").description(`S
|
|
|
6974
6961
|
konsola.br();
|
|
6975
6962
|
});
|
|
6976
6963
|
|
|
6977
|
-
const program$
|
|
6978
|
-
const logsCommand = program$
|
|
6964
|
+
const program$5 = getProgram();
|
|
6965
|
+
const logsCommand = program$5.command(commands.LOGS).alias("lg").description(`Inspect and manage logs.`);
|
|
6979
6966
|
|
|
6980
6967
|
const listCmd$1 = logsCommand.command("list").description("List logs").option("-s, --space <space>", "space ID");
|
|
6981
6968
|
listCmd$1.action(async (_options, command) => {
|
|
@@ -7000,8 +6987,8 @@ pruneCmd$1.action(async (options, command) => {
|
|
|
7000
6987
|
ui.info(`Deleted ${deletedFilesCount} log file${deletedFilesCount === 1 ? "" : "s"}`);
|
|
7001
6988
|
});
|
|
7002
6989
|
|
|
7003
|
-
const program$
|
|
7004
|
-
const reportsCommand = program$
|
|
6990
|
+
const program$4 = getProgram();
|
|
6991
|
+
const reportsCommand = program$4.command(commands.REPORTS).alias("rp").description("Inspect and manage reports.");
|
|
7005
6992
|
|
|
7006
6993
|
const listCmd = reportsCommand.command("list").description("List reports").option("-s, --space <space>", "space ID");
|
|
7007
6994
|
listCmd.action(async (_options, command) => {
|
|
@@ -7026,8 +7013,8 @@ pruneCmd.action(async (options, command) => {
|
|
|
7026
7013
|
ui.info(`Deleted ${deletedFilesCount} report file${deletedFilesCount === 1 ? "" : "s"}`);
|
|
7027
7014
|
});
|
|
7028
7015
|
|
|
7029
|
-
const program$
|
|
7030
|
-
const assetsCommand = program$
|
|
7016
|
+
const program$3 = getProgram();
|
|
7017
|
+
const assetsCommand = program$3.command(commands.ASSETS).description(`Manage your space's assets`);
|
|
7031
7018
|
|
|
7032
7019
|
const fetchAssets = async ({ spaceId, params }) => {
|
|
7033
7020
|
try {
|
|
@@ -7217,7 +7204,7 @@ const isRemoteSource = (assetBinaryPath) => {
|
|
|
7217
7204
|
return false;
|
|
7218
7205
|
}
|
|
7219
7206
|
};
|
|
7220
|
-
const isValidManifestEntry = (entry) => Boolean(typeof entry.old_id === "number" && typeof entry.new_id === "number" && entry.
|
|
7207
|
+
const isValidManifestEntry = (entry) => Boolean(typeof entry.old_id === "number" && typeof entry.new_id === "number" && entry.new_filename);
|
|
7221
7208
|
const loadAssetMap = async (manifestFile) => {
|
|
7222
7209
|
const manifest = await loadManifest(manifestFile);
|
|
7223
7210
|
const entries = manifest.filter(isValidManifestEntry).map((e) => [
|
|
@@ -7918,8 +7905,8 @@ const traverseAndMapBySchema = (data, {
|
|
|
7918
7905
|
const dataNew = { ...data };
|
|
7919
7906
|
for (const [fieldName, fieldValue] of Object.entries(data)) {
|
|
7920
7907
|
const fieldSchema = schema[fieldName.replace(/__i18n__.*/, "")];
|
|
7921
|
-
const fieldType = fieldSchema && typeof fieldSchema === "object" && "type" in fieldSchema
|
|
7922
|
-
const fieldRefMapper = typeof fieldType === "string"
|
|
7908
|
+
const fieldType = fieldSchema && typeof fieldSchema === "object" && "type" in fieldSchema ? fieldSchema.type : void 0;
|
|
7909
|
+
const fieldRefMapper = typeof fieldType === "string" ? fieldRefMappers2[fieldType] : void 0;
|
|
7923
7910
|
if (fieldRefMapper) {
|
|
7924
7911
|
dataNew[fieldName] = fieldRefMapper(fieldValue, {
|
|
7925
7912
|
schema: fieldSchema,
|
|
@@ -7984,6 +7971,9 @@ const richtextFieldRefMapper = (data, { schemas, maps, fieldRefMappers: fieldRef
|
|
|
7984
7971
|
fieldRefMappers: fieldRefMappers2
|
|
7985
7972
|
});
|
|
7986
7973
|
const multilinkFieldRefMapper = (data, { maps }) => {
|
|
7974
|
+
if (!data || typeof data !== "object") {
|
|
7975
|
+
return data;
|
|
7976
|
+
}
|
|
7987
7977
|
if (data.linktype !== "story") {
|
|
7988
7978
|
return data;
|
|
7989
7979
|
}
|
|
@@ -8003,6 +7993,9 @@ const bloksFieldRefMapper = (data, { schemas, maps, fieldRefMappers: fieldRefMap
|
|
|
8003
7993
|
}));
|
|
8004
7994
|
};
|
|
8005
7995
|
const assetFieldRefMapper = (data, { maps }) => {
|
|
7996
|
+
if (!data || typeof data !== "object") {
|
|
7997
|
+
return data;
|
|
7998
|
+
}
|
|
8006
7999
|
const mappedAsset = typeof data.id === "number" ? maps.assets?.get(data.id) : void 0;
|
|
8007
8000
|
if (!mappedAsset) {
|
|
8008
8001
|
return data;
|
|
@@ -8020,7 +8013,7 @@ const multiassetFieldRefMapper = (data, options) => {
|
|
|
8020
8013
|
return data.map((d) => assetFieldRefMapper(d, options));
|
|
8021
8014
|
};
|
|
8022
8015
|
const optionsFieldRefMapper = (data, { schema, maps }) => {
|
|
8023
|
-
if (schema.source !== "internal_stories" || !Array.isArray(data)) {
|
|
8016
|
+
if (!schema || !("source" in schema) || schema.source !== "internal_stories" || !Array.isArray(data)) {
|
|
8024
8017
|
return data;
|
|
8025
8018
|
}
|
|
8026
8019
|
return data.map((d) => maps.stories?.get(d) || d);
|
|
@@ -8937,8 +8930,8 @@ pushCmd$1.action(async (assetInput, options, command) => {
|
|
|
8937
8930
|
}
|
|
8938
8931
|
});
|
|
8939
8932
|
|
|
8940
|
-
const program$
|
|
8941
|
-
const storiesCommand = program$
|
|
8933
|
+
const program$2 = getProgram();
|
|
8934
|
+
const storiesCommand = program$2.command(commands.STORIES).description(`Manage your space's stories`);
|
|
8942
8935
|
|
|
8943
8936
|
const pullCmd = storiesCommand.command("pull").option("-s, --space <space>", "space ID").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/"').description(`Download your space's stories as separate json files.`);
|
|
8944
8937
|
pullCmd.action(async (options, command) => {
|
|
@@ -9230,6 +9223,12 @@ pushCmd.action(async (options, command) => {
|
|
|
9230
9223
|
};
|
|
9231
9224
|
visit(story.content);
|
|
9232
9225
|
};
|
|
9226
|
+
const fetchProgress = ui.createProgressBar({ title: "Matching Stories...".padEnd(21) });
|
|
9227
|
+
const existingTargetStories = await prefetchTargetStories(space, {
|
|
9228
|
+
onTotal: (total) => fetchProgress.setTotal(total),
|
|
9229
|
+
onIncrement: (count) => fetchProgress.increment(count)
|
|
9230
|
+
});
|
|
9231
|
+
fetchProgress.stop();
|
|
9233
9232
|
const scanProgress = ui.createProgressBar({ title: "Scanning Stories...".padEnd(21) });
|
|
9234
9233
|
const storyIndex = await scanLocalStoryIndex({
|
|
9235
9234
|
directoryPath: storiesDirectoryPath,
|
|
@@ -9248,24 +9247,6 @@ pushCmd.action(async (options, command) => {
|
|
|
9248
9247
|
});
|
|
9249
9248
|
const levels = groupStoriesByDepth(storyIndex);
|
|
9250
9249
|
scanProgress.stop();
|
|
9251
|
-
const localSlugs = storyIndex.map((entry) => entry.full_slug).filter(Boolean);
|
|
9252
|
-
const localIdSet = new Set(storyIndex.map((entry) => entry.id));
|
|
9253
|
-
const manifestIds = [];
|
|
9254
|
-
for (const [key, value] of maps.stories.entries()) {
|
|
9255
|
-
if (typeof key === "number" && localIdSet.has(key) && typeof value === "number") {
|
|
9256
|
-
manifestIds.push(value);
|
|
9257
|
-
}
|
|
9258
|
-
}
|
|
9259
|
-
const fetchProgress = ui.createProgressBar({ title: "Matching Stories...".padEnd(21) });
|
|
9260
|
-
const existingTargetStories = await prefetchTargetStoriesByKeys(
|
|
9261
|
-
space,
|
|
9262
|
-
{ slugs: localSlugs, ids: manifestIds },
|
|
9263
|
-
{
|
|
9264
|
-
onTotal: (total) => fetchProgress.setTotal(total),
|
|
9265
|
-
onIncrement: (count) => fetchProgress.increment(count)
|
|
9266
|
-
}
|
|
9267
|
-
);
|
|
9268
|
-
fetchProgress.stop();
|
|
9269
9250
|
const creationProgress = ui.createProgressBar({ title: "Creating Stories...".padEnd(21) });
|
|
9270
9251
|
const processProgress = ui.createProgressBar({ title: "Processing Stories...".padEnd(21) });
|
|
9271
9252
|
const updateProgress = ui.createProgressBar({ title: "Updating Stories...".padEnd(21) });
|
|
@@ -9443,6 +9424,1957 @@ pushCmd.action(async (options, command) => {
|
|
|
9443
9424
|
}
|
|
9444
9425
|
});
|
|
9445
9426
|
|
|
9427
|
+
const program$1 = getProgram();
|
|
9428
|
+
const schemaCommand = program$1.command(commands.SCHEMA).description(`Manage your space's schema from code`);
|
|
9429
|
+
|
|
9430
|
+
function isObject(value) {
|
|
9431
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
9432
|
+
}
|
|
9433
|
+
function isComponent(value) {
|
|
9434
|
+
return isObject(value) && typeof value.name === "string" && "schema" in value && isObject(value.schema);
|
|
9435
|
+
}
|
|
9436
|
+
function isDatasource(value) {
|
|
9437
|
+
return isObject(value) && typeof value.name === "string" && typeof value.slug === "string" && !("schema" in value);
|
|
9438
|
+
}
|
|
9439
|
+
function isComponentFolder(value) {
|
|
9440
|
+
return isObject(value) && typeof value.name === "string" && !("schema" in value) && !("slug" in value) && ("uuid" in value || "parent_id" in value);
|
|
9441
|
+
}
|
|
9442
|
+
function isSchemaObject(value) {
|
|
9443
|
+
return isObject(value) && ("blocks" in value || "blockFolders" in value || "datasources" in value);
|
|
9444
|
+
}
|
|
9445
|
+
function classifyExports(moduleExports) {
|
|
9446
|
+
const components = [];
|
|
9447
|
+
const componentFolders = [];
|
|
9448
|
+
const datasources = [];
|
|
9449
|
+
function collect(value) {
|
|
9450
|
+
if (isComponent(value)) {
|
|
9451
|
+
components.push(value);
|
|
9452
|
+
} else if (isDatasource(value)) {
|
|
9453
|
+
datasources.push(value);
|
|
9454
|
+
} else if (isComponentFolder(value)) {
|
|
9455
|
+
componentFolders.push(value);
|
|
9456
|
+
}
|
|
9457
|
+
}
|
|
9458
|
+
for (const value of Object.values(moduleExports)) {
|
|
9459
|
+
if (isSchemaObject(value)) {
|
|
9460
|
+
for (const group of Object.values(value)) {
|
|
9461
|
+
if (isObject(group)) {
|
|
9462
|
+
for (const entity of Object.values(group)) {
|
|
9463
|
+
collect(entity);
|
|
9464
|
+
}
|
|
9465
|
+
}
|
|
9466
|
+
}
|
|
9467
|
+
} else {
|
|
9468
|
+
collect(value);
|
|
9469
|
+
}
|
|
9470
|
+
}
|
|
9471
|
+
return { components, componentFolders, datasources };
|
|
9472
|
+
}
|
|
9473
|
+
async function loadSchema(entryPath) {
|
|
9474
|
+
const { createJiti } = await import('jiti');
|
|
9475
|
+
const jiti = createJiti(import.meta.url, {
|
|
9476
|
+
interopDefault: true
|
|
9477
|
+
});
|
|
9478
|
+
const absolutePath = (await import('pathe')).resolve(entryPath);
|
|
9479
|
+
const mod = await jiti.import(absolutePath);
|
|
9480
|
+
return classifyExports(mod);
|
|
9481
|
+
}
|
|
9482
|
+
|
|
9483
|
+
const COMPONENT_STRIP_KEYS = /* @__PURE__ */ new Set([
|
|
9484
|
+
"id",
|
|
9485
|
+
"created_at",
|
|
9486
|
+
"updated_at",
|
|
9487
|
+
"real_name",
|
|
9488
|
+
// API-computed display/technical name, read-only
|
|
9489
|
+
"preset_id",
|
|
9490
|
+
// Instance-level preset selection, not part of schema definition
|
|
9491
|
+
"all_presets",
|
|
9492
|
+
// Computed list of presets, managed via /presets API
|
|
9493
|
+
"internal_tags_list",
|
|
9494
|
+
// Read-only expanded form of internal_tag_ids ({id, name} objects)
|
|
9495
|
+
"content_type_asset_preview",
|
|
9496
|
+
// Read-only, not in ComponentCreate/ComponentUpdate
|
|
9497
|
+
"image",
|
|
9498
|
+
// Read-only preview image URL
|
|
9499
|
+
"preview_tmpl",
|
|
9500
|
+
// Read-only preview template
|
|
9501
|
+
"metadata"
|
|
9502
|
+
// Not in current API types, stripped defensively
|
|
9503
|
+
]);
|
|
9504
|
+
const DATASOURCE_STRIP_KEYS = /* @__PURE__ */ new Set(["id", "created_at", "updated_at"]);
|
|
9505
|
+
const DATASOURCE_DIMENSION_STRIP_KEYS = /* @__PURE__ */ new Set(["id", "datasource_id", "created_at", "updated_at"]);
|
|
9506
|
+
const FOLDER_INIT_STRIP_KEYS = /* @__PURE__ */ new Set(["id"]);
|
|
9507
|
+
const FOLDER_STRIP_KEYS = /* @__PURE__ */ new Set(["id", "uuid"]);
|
|
9508
|
+
const DATASOURCE_DEFAULTS = {
|
|
9509
|
+
dimensions: []
|
|
9510
|
+
};
|
|
9511
|
+
const COMPONENT_DEFAULTS = {
|
|
9512
|
+
display_name: "",
|
|
9513
|
+
color: "",
|
|
9514
|
+
icon: "",
|
|
9515
|
+
preview_field: "",
|
|
9516
|
+
internal_tag_ids: []
|
|
9517
|
+
};
|
|
9518
|
+
function applyDefaults(entity, defaults) {
|
|
9519
|
+
const result = { ...entity };
|
|
9520
|
+
for (const [key, defaultValue] of Object.entries(defaults)) {
|
|
9521
|
+
if (result[key] === void 0 || result[key] === null) {
|
|
9522
|
+
Object.assign(result, { [key]: defaultValue });
|
|
9523
|
+
}
|
|
9524
|
+
}
|
|
9525
|
+
return result;
|
|
9526
|
+
}
|
|
9527
|
+
const INDENT = " ";
|
|
9528
|
+
function formatValue(value, depth) {
|
|
9529
|
+
const indent = INDENT.repeat(depth);
|
|
9530
|
+
const innerIndent = INDENT.repeat(depth + 1);
|
|
9531
|
+
if (value === null || value === void 0) {
|
|
9532
|
+
return String(value);
|
|
9533
|
+
}
|
|
9534
|
+
if (typeof value === "string") {
|
|
9535
|
+
return `'${value.replace(/'/g, "\\'")}'`;
|
|
9536
|
+
}
|
|
9537
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
9538
|
+
return String(value);
|
|
9539
|
+
}
|
|
9540
|
+
if (Array.isArray(value)) {
|
|
9541
|
+
if (value.length === 0) {
|
|
9542
|
+
return "[]";
|
|
9543
|
+
}
|
|
9544
|
+
const items = value.map((item) => `${innerIndent}${formatValue(item, depth + 1)},`);
|
|
9545
|
+
return `[
|
|
9546
|
+
${items.join("\n")}
|
|
9547
|
+
${indent}]`;
|
|
9548
|
+
}
|
|
9549
|
+
if (typeof value === "object") {
|
|
9550
|
+
const entries = Object.entries(value).filter(([, v]) => v !== void 0 && v !== null).sort(([a], [b]) => a.localeCompare(b));
|
|
9551
|
+
if (entries.length === 0) {
|
|
9552
|
+
return "{}";
|
|
9553
|
+
}
|
|
9554
|
+
const props = entries.map(
|
|
9555
|
+
([key, val]) => `${innerIndent}${key}: ${formatValue(val, depth + 1)},`
|
|
9556
|
+
);
|
|
9557
|
+
return `{
|
|
9558
|
+
${props.join("\n")}
|
|
9559
|
+
${indent}}`;
|
|
9560
|
+
}
|
|
9561
|
+
return String(value);
|
|
9562
|
+
}
|
|
9563
|
+
function fileTimestamp(iso) {
|
|
9564
|
+
return iso.replace(/[:.]/g, "-");
|
|
9565
|
+
}
|
|
9566
|
+
function stripKeys(obj, keysToStrip) {
|
|
9567
|
+
const result = {};
|
|
9568
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
9569
|
+
if (!keysToStrip.has(key) && value !== void 0 && value !== null) {
|
|
9570
|
+
result[key] = value;
|
|
9571
|
+
}
|
|
9572
|
+
}
|
|
9573
|
+
return result;
|
|
9574
|
+
}
|
|
9575
|
+
|
|
9576
|
+
function sortSchemaByPos$1(schema) {
|
|
9577
|
+
const entries = Object.entries(schema).filter(([key]) => key !== "_uid" && key !== "component").sort(([, a], [, b]) => {
|
|
9578
|
+
const posA = typeof a.pos === "number" ? a.pos : Infinity;
|
|
9579
|
+
const posB = typeof b.pos === "number" ? b.pos : Infinity;
|
|
9580
|
+
return posA - posB;
|
|
9581
|
+
});
|
|
9582
|
+
return Object.fromEntries(
|
|
9583
|
+
entries.map(([key, field]) => {
|
|
9584
|
+
const { id, ...rest } = field;
|
|
9585
|
+
return [key, rest];
|
|
9586
|
+
})
|
|
9587
|
+
);
|
|
9588
|
+
}
|
|
9589
|
+
function serializeComponent(component) {
|
|
9590
|
+
const clean = stripKeys(component, COMPONENT_STRIP_KEYS);
|
|
9591
|
+
if (clean.schema && typeof clean.schema === "object") {
|
|
9592
|
+
clean.schema = sortSchemaByPos$1(clean.schema);
|
|
9593
|
+
}
|
|
9594
|
+
const ordered = {};
|
|
9595
|
+
if (clean.name !== void 0) {
|
|
9596
|
+
ordered.name = clean.name;
|
|
9597
|
+
}
|
|
9598
|
+
if (clean.display_name !== void 0) {
|
|
9599
|
+
ordered.display_name = clean.display_name;
|
|
9600
|
+
}
|
|
9601
|
+
if (clean.is_root !== void 0) {
|
|
9602
|
+
ordered.is_root = clean.is_root;
|
|
9603
|
+
}
|
|
9604
|
+
if (clean.is_nestable !== void 0) {
|
|
9605
|
+
ordered.is_nestable = clean.is_nestable;
|
|
9606
|
+
}
|
|
9607
|
+
const handled = /* @__PURE__ */ new Set(["name", "display_name", "is_root", "is_nestable", "schema"]);
|
|
9608
|
+
for (const [key, value] of Object.entries(clean).sort(([a], [b]) => a.localeCompare(b))) {
|
|
9609
|
+
if (!handled.has(key)) {
|
|
9610
|
+
ordered[key] = value;
|
|
9611
|
+
}
|
|
9612
|
+
}
|
|
9613
|
+
if (clean.schema !== void 0) {
|
|
9614
|
+
ordered.schema = clean.schema;
|
|
9615
|
+
}
|
|
9616
|
+
return `defineBlock(${formatValue(ordered, 0)})`;
|
|
9617
|
+
}
|
|
9618
|
+
function serializeDatasource(datasource) {
|
|
9619
|
+
const clean = stripKeys(datasource, DATASOURCE_STRIP_KEYS);
|
|
9620
|
+
if (Array.isArray(clean.dimensions)) {
|
|
9621
|
+
clean.dimensions = clean.dimensions.map(
|
|
9622
|
+
(dim) => typeof dim === "object" && dim !== null ? stripKeys(dim, DATASOURCE_DIMENSION_STRIP_KEYS) : dim
|
|
9623
|
+
);
|
|
9624
|
+
}
|
|
9625
|
+
const ordered = {};
|
|
9626
|
+
if (clean.name !== void 0) {
|
|
9627
|
+
ordered.name = clean.name;
|
|
9628
|
+
}
|
|
9629
|
+
if (clean.slug !== void 0) {
|
|
9630
|
+
ordered.slug = clean.slug;
|
|
9631
|
+
}
|
|
9632
|
+
const handled = /* @__PURE__ */ new Set(["name", "slug"]);
|
|
9633
|
+
for (const [key, value] of Object.entries(clean).sort(([a], [b]) => a.localeCompare(b))) {
|
|
9634
|
+
if (!handled.has(key)) {
|
|
9635
|
+
ordered[key] = value;
|
|
9636
|
+
}
|
|
9637
|
+
}
|
|
9638
|
+
return `defineDatasource(${formatValue(ordered, 0)})`;
|
|
9639
|
+
}
|
|
9640
|
+
function serializeComponentFolder(folder) {
|
|
9641
|
+
const clean = stripKeys(folder, FOLDER_STRIP_KEYS);
|
|
9642
|
+
const ordered = {};
|
|
9643
|
+
if (clean.name !== void 0) {
|
|
9644
|
+
ordered.name = clean.name;
|
|
9645
|
+
}
|
|
9646
|
+
const handled = /* @__PURE__ */ new Set(["name"]);
|
|
9647
|
+
for (const [key, value] of Object.entries(clean).sort(([a], [b]) => a.localeCompare(b))) {
|
|
9648
|
+
if (!handled.has(key)) {
|
|
9649
|
+
ordered[key] = value;
|
|
9650
|
+
}
|
|
9651
|
+
}
|
|
9652
|
+
return `defineBlockFolder(${formatValue(ordered, 0)})`;
|
|
9653
|
+
}
|
|
9654
|
+
|
|
9655
|
+
function diffEntity(type, name, localSerialized, remoteSerialized) {
|
|
9656
|
+
if (!remoteSerialized && localSerialized) {
|
|
9657
|
+
return { type, name, action: "create", diff: null, local: null, remote: null };
|
|
9658
|
+
}
|
|
9659
|
+
if (remoteSerialized && !localSerialized) {
|
|
9660
|
+
return { type, name, action: "stale", diff: null, local: null, remote: null };
|
|
9661
|
+
}
|
|
9662
|
+
if (localSerialized === remoteSerialized) {
|
|
9663
|
+
return { type, name, action: "unchanged", diff: null, local: null, remote: null };
|
|
9664
|
+
}
|
|
9665
|
+
const patch = createTwoFilesPatch(
|
|
9666
|
+
`remote/${name}`,
|
|
9667
|
+
`local/${name}`,
|
|
9668
|
+
remoteSerialized,
|
|
9669
|
+
localSerialized,
|
|
9670
|
+
"remote",
|
|
9671
|
+
"local"
|
|
9672
|
+
);
|
|
9673
|
+
return { type, name, action: "update", diff: patch, local: null, remote: null };
|
|
9674
|
+
}
|
|
9675
|
+
function diffSchema(local, remote) {
|
|
9676
|
+
const diffs = [];
|
|
9677
|
+
const processedComponentNames = /* @__PURE__ */ new Set();
|
|
9678
|
+
for (const comp of local.components) {
|
|
9679
|
+
processedComponentNames.add(comp.name);
|
|
9680
|
+
const remoteComp = remote.components.get(comp.name);
|
|
9681
|
+
const localSerialized = serializeComponent(applyDefaults(comp, COMPONENT_DEFAULTS));
|
|
9682
|
+
const remoteSerialized = remoteComp ? serializeComponent(applyDefaults(remoteComp, COMPONENT_DEFAULTS)) : null;
|
|
9683
|
+
diffs.push(diffEntity("component", comp.name, localSerialized, remoteSerialized));
|
|
9684
|
+
}
|
|
9685
|
+
for (const [name] of remote.components) {
|
|
9686
|
+
if (!processedComponentNames.has(name)) {
|
|
9687
|
+
diffs.push(diffEntity("component", name, null, "stale"));
|
|
9688
|
+
}
|
|
9689
|
+
}
|
|
9690
|
+
const processedFolderNames = /* @__PURE__ */ new Set();
|
|
9691
|
+
for (const folder of local.componentFolders) {
|
|
9692
|
+
processedFolderNames.add(folder.name);
|
|
9693
|
+
const remoteFolder = remote.componentFolders.get(folder.name);
|
|
9694
|
+
const localSerialized = serializeComponentFolder(folder);
|
|
9695
|
+
const remoteSerialized = remoteFolder ? serializeComponentFolder(remoteFolder) : null;
|
|
9696
|
+
diffs.push(diffEntity("componentFolder", folder.name, localSerialized, remoteSerialized));
|
|
9697
|
+
}
|
|
9698
|
+
for (const [name] of remote.componentFolders) {
|
|
9699
|
+
if (!processedFolderNames.has(name)) {
|
|
9700
|
+
diffs.push(diffEntity("componentFolder", name, null, "stale"));
|
|
9701
|
+
}
|
|
9702
|
+
}
|
|
9703
|
+
const processedDatasourceNames = /* @__PURE__ */ new Set();
|
|
9704
|
+
for (const ds of local.datasources) {
|
|
9705
|
+
processedDatasourceNames.add(ds.name);
|
|
9706
|
+
const remoteDs = remote.datasources.get(ds.name);
|
|
9707
|
+
const localSerialized = serializeDatasource(applyDefaults(ds, DATASOURCE_DEFAULTS));
|
|
9708
|
+
const remoteSerialized = remoteDs ? serializeDatasource(applyDefaults(remoteDs, DATASOURCE_DEFAULTS)) : null;
|
|
9709
|
+
diffs.push(diffEntity("datasource", ds.name, localSerialized, remoteSerialized));
|
|
9710
|
+
}
|
|
9711
|
+
for (const [name] of remote.datasources) {
|
|
9712
|
+
if (!processedDatasourceNames.has(name)) {
|
|
9713
|
+
diffs.push(diffEntity("datasource", name, null, "stale"));
|
|
9714
|
+
}
|
|
9715
|
+
}
|
|
9716
|
+
return {
|
|
9717
|
+
diffs,
|
|
9718
|
+
creates: diffs.filter((d) => d.action === "create").length,
|
|
9719
|
+
updates: diffs.filter((d) => d.action === "update").length,
|
|
9720
|
+
unchanged: diffs.filter((d) => d.action === "unchanged").length,
|
|
9721
|
+
stale: diffs.filter((d) => d.action === "stale").length
|
|
9722
|
+
};
|
|
9723
|
+
}
|
|
9724
|
+
|
|
9725
|
+
async function fetchRemoteSchema(spaceId) {
|
|
9726
|
+
const client = getMapiClient();
|
|
9727
|
+
const spaceIdNum = Number(spaceId);
|
|
9728
|
+
const [componentsRes, foldersRes, rawDatasources] = await Promise.all([
|
|
9729
|
+
client.components.list({ path: { space_id: spaceIdNum }, throwOnError: true }),
|
|
9730
|
+
client.componentFolders.list({ path: { space_id: spaceIdNum }, throwOnError: true }),
|
|
9731
|
+
fetchAllPages(
|
|
9732
|
+
(page) => client.datasources.list({ path: { space_id: spaceIdNum }, query: { page }, throwOnError: true }),
|
|
9733
|
+
(data) => data?.datasources ?? []
|
|
9734
|
+
)
|
|
9735
|
+
]);
|
|
9736
|
+
const rawComponents = componentsRes.data?.components ?? [];
|
|
9737
|
+
const rawComponentFolders = foldersRes.data?.component_groups ?? [];
|
|
9738
|
+
const remote = {
|
|
9739
|
+
components: new Map(rawComponents.map((c) => [c.name, c])),
|
|
9740
|
+
componentFolders: new Map(rawComponentFolders.map((f) => [f.name, f])),
|
|
9741
|
+
datasources: new Map(rawDatasources.map((d) => [d.name, d]))
|
|
9742
|
+
};
|
|
9743
|
+
return { remote, rawComponents, rawComponentFolders, rawDatasources };
|
|
9744
|
+
}
|
|
9745
|
+
|
|
9746
|
+
function isRecord(value) {
|
|
9747
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
9748
|
+
}
|
|
9749
|
+
function isSchemaField(value) {
|
|
9750
|
+
return isRecord(value) && "type" in value;
|
|
9751
|
+
}
|
|
9752
|
+
function toSchemaRecord(schema) {
|
|
9753
|
+
const result = {};
|
|
9754
|
+
for (const [key, value] of Object.entries(schema)) {
|
|
9755
|
+
if (key === "_uid" || key === "component" || !isSchemaField(value)) {
|
|
9756
|
+
continue;
|
|
9757
|
+
}
|
|
9758
|
+
result[key] = value;
|
|
9759
|
+
}
|
|
9760
|
+
return result;
|
|
9761
|
+
}
|
|
9762
|
+
function buildComponentPayload(input) {
|
|
9763
|
+
if (!isRecord(input)) {
|
|
9764
|
+
return { name: "" };
|
|
9765
|
+
}
|
|
9766
|
+
return {
|
|
9767
|
+
name: typeof input.name === "string" ? input.name : "",
|
|
9768
|
+
// Fields in COMPONENT_DEFAULTS are always sent with their reset value so that
|
|
9769
|
+
// removing a field from the local schema actually clears it on the API.
|
|
9770
|
+
// (Root-level fields are additive on MAPI update — omitting preserves the old value.)
|
|
9771
|
+
display_name: typeof input.display_name === "string" ? input.display_name : "",
|
|
9772
|
+
color: typeof input.color === "string" ? input.color : "",
|
|
9773
|
+
icon: typeof input.icon === "string" ? input.icon : "",
|
|
9774
|
+
preview_field: typeof input.preview_field === "string" ? input.preview_field : "",
|
|
9775
|
+
internal_tag_ids: Array.isArray(input.internal_tag_ids) ? input.internal_tag_ids : [],
|
|
9776
|
+
// Conditionally sent: only included when explicitly set in local schema
|
|
9777
|
+
...isRecord(input.schema) && { schema: toSchemaRecord(input.schema) },
|
|
9778
|
+
...typeof input.is_root === "boolean" && { is_root: input.is_root },
|
|
9779
|
+
...typeof input.is_nestable === "boolean" && { is_nestable: input.is_nestable },
|
|
9780
|
+
...typeof input.component_group_uuid === "string" && { component_group_uuid: input.component_group_uuid }
|
|
9781
|
+
};
|
|
9782
|
+
}
|
|
9783
|
+
function toComponentCreate(input) {
|
|
9784
|
+
return buildComponentPayload(input);
|
|
9785
|
+
}
|
|
9786
|
+
function toComponentUpdate(input) {
|
|
9787
|
+
return buildComponentPayload(input);
|
|
9788
|
+
}
|
|
9789
|
+
function toComponentFolderCreate(input) {
|
|
9790
|
+
if (!isRecord(input)) {
|
|
9791
|
+
return { name: "" };
|
|
9792
|
+
}
|
|
9793
|
+
return {
|
|
9794
|
+
name: typeof input.name === "string" ? input.name : "",
|
|
9795
|
+
...typeof input.parent_id === "number" && { parent_id: input.parent_id }
|
|
9796
|
+
};
|
|
9797
|
+
}
|
|
9798
|
+
function toDatasourceCreate(input) {
|
|
9799
|
+
if (!isRecord(input)) {
|
|
9800
|
+
return { name: "", slug: "" };
|
|
9801
|
+
}
|
|
9802
|
+
const result = {
|
|
9803
|
+
name: typeof input.name === "string" ? input.name : "",
|
|
9804
|
+
slug: typeof input.slug === "string" ? input.slug : ""
|
|
9805
|
+
};
|
|
9806
|
+
if (Array.isArray(input.dimensions)) {
|
|
9807
|
+
result.dimensions_attributes = input.dimensions.filter((d) => isRecord(d) && typeof d.name === "string" && typeof d.entry_value === "string").map((d) => ({
|
|
9808
|
+
name: d.name,
|
|
9809
|
+
entry_value: d.entry_value
|
|
9810
|
+
}));
|
|
9811
|
+
}
|
|
9812
|
+
return result;
|
|
9813
|
+
}
|
|
9814
|
+
function toDatasourceUpdate(input, remote) {
|
|
9815
|
+
const base = toDatasourceCreate(input);
|
|
9816
|
+
const localDims = base.dimensions_attributes ?? [];
|
|
9817
|
+
const remoteDims = remote.dimensions ?? [];
|
|
9818
|
+
if (remoteDims.length === 0) {
|
|
9819
|
+
return base;
|
|
9820
|
+
}
|
|
9821
|
+
const localKeys = new Set(localDims.map((d) => `${d.name}::${d.entry_value}`));
|
|
9822
|
+
const destroyEntries = remoteDims.filter((rd) => rd.id != null && !localKeys.has(`${rd.name}::${rd.entry_value}`)).map((rd) => ({ id: rd.id, _destroy: true }));
|
|
9823
|
+
if (destroyEntries.length > 0) {
|
|
9824
|
+
return {
|
|
9825
|
+
...base,
|
|
9826
|
+
dimensions_attributes: [...localDims, ...destroyEntries]
|
|
9827
|
+
};
|
|
9828
|
+
}
|
|
9829
|
+
return base;
|
|
9830
|
+
}
|
|
9831
|
+
|
|
9832
|
+
function formatDiffOutput(result, options) {
|
|
9833
|
+
const lines = [];
|
|
9834
|
+
const byType = {
|
|
9835
|
+
component: [],
|
|
9836
|
+
componentFolder: [],
|
|
9837
|
+
datasource: []
|
|
9838
|
+
};
|
|
9839
|
+
for (const diff of result.diffs) {
|
|
9840
|
+
byType[diff.type].push(diff);
|
|
9841
|
+
}
|
|
9842
|
+
const willDelete = options?.delete ?? false;
|
|
9843
|
+
const icons = {
|
|
9844
|
+
create: chalk.green("+"),
|
|
9845
|
+
update: chalk.yellow("~"),
|
|
9846
|
+
unchanged: chalk.dim("="),
|
|
9847
|
+
stale: chalk.red("-")
|
|
9848
|
+
};
|
|
9849
|
+
const sections = [
|
|
9850
|
+
["Components", byType.component],
|
|
9851
|
+
["Component Folders", byType.componentFolder],
|
|
9852
|
+
["Datasources", byType.datasource]
|
|
9853
|
+
];
|
|
9854
|
+
for (const [label, diffs] of sections) {
|
|
9855
|
+
if (diffs.length === 0) {
|
|
9856
|
+
continue;
|
|
9857
|
+
}
|
|
9858
|
+
lines.push(chalk.bold(label));
|
|
9859
|
+
for (const diff of diffs) {
|
|
9860
|
+
const icon = icons[diff.action] ?? " ";
|
|
9861
|
+
const name = diff.action === "stale" ? chalk.red(diff.name) : diff.name;
|
|
9862
|
+
const actionLabel = diff.action === "stale" && willDelete ? "delete" : diff.action;
|
|
9863
|
+
lines.push(` ${icon} ${name} ${chalk.dim(`(${actionLabel})`)}`);
|
|
9864
|
+
if (diff.diff) {
|
|
9865
|
+
for (const line of diff.diff.split("\n")) {
|
|
9866
|
+
if (line.startsWith("+") && !line.startsWith("+++")) {
|
|
9867
|
+
lines.push(` ${chalk.green(line)}`);
|
|
9868
|
+
} else if (line.startsWith("-") && !line.startsWith("---")) {
|
|
9869
|
+
lines.push(` ${chalk.red(line)}`);
|
|
9870
|
+
}
|
|
9871
|
+
}
|
|
9872
|
+
}
|
|
9873
|
+
}
|
|
9874
|
+
lines.push("");
|
|
9875
|
+
}
|
|
9876
|
+
const summary = [
|
|
9877
|
+
result.creates > 0 ? chalk.green(`${result.creates} to create`) : null,
|
|
9878
|
+
result.updates > 0 ? chalk.yellow(`${result.updates} to update`) : null,
|
|
9879
|
+
result.unchanged > 0 ? chalk.dim(`${result.unchanged} unchanged`) : null,
|
|
9880
|
+
result.stale > 0 ? chalk.red(`${result.stale} ${willDelete ? "to delete" : "stale"}`) : null
|
|
9881
|
+
].filter(Boolean).join(", ");
|
|
9882
|
+
lines.push(`Summary: ${summary}`);
|
|
9883
|
+
return lines.join("\n");
|
|
9884
|
+
}
|
|
9885
|
+
async function executePush(spaceId, local, remote, diffResult, options) {
|
|
9886
|
+
const client = getMapiClient();
|
|
9887
|
+
const spaceIdNum = Number(spaceId);
|
|
9888
|
+
let created = 0;
|
|
9889
|
+
let updated = 0;
|
|
9890
|
+
let deleted = 0;
|
|
9891
|
+
const createdFolderUuids = /* @__PURE__ */ new Map();
|
|
9892
|
+
const folderDiffs = diffResult.diffs.filter((d) => d.type === "componentFolder");
|
|
9893
|
+
const folderResults = await Promise.allSettled(
|
|
9894
|
+
folderDiffs.map(async (diff) => {
|
|
9895
|
+
const localFolder = local.componentFolders.find((f) => f.name === diff.name);
|
|
9896
|
+
if (diff.action === "create" && localFolder) {
|
|
9897
|
+
const response = await client.componentFolders.create({
|
|
9898
|
+
path: { space_id: spaceIdNum },
|
|
9899
|
+
body: { component_group: toComponentFolderCreate(localFolder) },
|
|
9900
|
+
throwOnError: true
|
|
9901
|
+
});
|
|
9902
|
+
return { action: "created", uuid: response.data?.component_group?.uuid };
|
|
9903
|
+
}
|
|
9904
|
+
if (diff.action === "update" && localFolder) {
|
|
9905
|
+
const existing = remote.componentFolders.get(diff.name);
|
|
9906
|
+
if (existing?.id) {
|
|
9907
|
+
await client.componentFolders.update(existing.id, {
|
|
9908
|
+
path: { space_id: spaceIdNum },
|
|
9909
|
+
body: { component_group: toComponentFolderCreate(localFolder) },
|
|
9910
|
+
throwOnError: true
|
|
9911
|
+
});
|
|
9912
|
+
return { action: "updated" };
|
|
9913
|
+
}
|
|
9914
|
+
}
|
|
9915
|
+
})
|
|
9916
|
+
);
|
|
9917
|
+
for (let i = 0; i < folderResults.length; i++) {
|
|
9918
|
+
const result = folderResults[i];
|
|
9919
|
+
const diff = folderDiffs[i];
|
|
9920
|
+
if (result.status === "fulfilled") {
|
|
9921
|
+
if (result.value?.action === "created") {
|
|
9922
|
+
if (result.value.uuid) {
|
|
9923
|
+
createdFolderUuids.set(diff.name, result.value.uuid);
|
|
9924
|
+
}
|
|
9925
|
+
created++;
|
|
9926
|
+
} else if (result.value?.action === "updated") {
|
|
9927
|
+
updated++;
|
|
9928
|
+
}
|
|
9929
|
+
} else {
|
|
9930
|
+
const eventId = diff.action === "create" ? "push_component_group" : "update_component_group";
|
|
9931
|
+
handleAPIError(eventId, result.reason, `Failed to ${diff.action} component folder ${diff.name}`);
|
|
9932
|
+
}
|
|
9933
|
+
}
|
|
9934
|
+
const skippedComponents = /* @__PURE__ */ new Set();
|
|
9935
|
+
if (options.pendingFolderAssignments) {
|
|
9936
|
+
const logger = getLogger();
|
|
9937
|
+
for (const [folderName, componentNames] of options.pendingFolderAssignments) {
|
|
9938
|
+
const remoteUuid = createdFolderUuids.get(folderName);
|
|
9939
|
+
if (!remoteUuid) {
|
|
9940
|
+
logger.warn(`Could not resolve folder '${folderName}' \u2014 skipping components: ${componentNames.join(", ")}`);
|
|
9941
|
+
for (const name of componentNames) {
|
|
9942
|
+
skippedComponents.add(name);
|
|
9943
|
+
}
|
|
9944
|
+
continue;
|
|
9945
|
+
}
|
|
9946
|
+
for (const compName of componentNames) {
|
|
9947
|
+
const comp = local.components.find((c) => c.name === compName);
|
|
9948
|
+
if (comp) {
|
|
9949
|
+
comp.component_group_uuid = remoteUuid;
|
|
9950
|
+
}
|
|
9951
|
+
}
|
|
9952
|
+
}
|
|
9953
|
+
}
|
|
9954
|
+
const componentDiffs = diffResult.diffs.filter((d) => d.type === "component" && !skippedComponents.has(d.name));
|
|
9955
|
+
const componentResults = await Promise.allSettled(
|
|
9956
|
+
componentDiffs.map(async (diff) => {
|
|
9957
|
+
const localComp = local.components.find((c) => c.name === diff.name);
|
|
9958
|
+
if (diff.action === "create" && localComp) {
|
|
9959
|
+
await client.components.create({
|
|
9960
|
+
path: { space_id: spaceIdNum },
|
|
9961
|
+
body: { component: toComponentCreate(localComp) },
|
|
9962
|
+
throwOnError: true
|
|
9963
|
+
});
|
|
9964
|
+
return "created";
|
|
9965
|
+
}
|
|
9966
|
+
if (diff.action === "update" && localComp) {
|
|
9967
|
+
const existing = remote.components.get(diff.name);
|
|
9968
|
+
if (existing?.id) {
|
|
9969
|
+
await client.components.update(existing.id, {
|
|
9970
|
+
path: { space_id: spaceIdNum },
|
|
9971
|
+
body: { component: toComponentUpdate(localComp) },
|
|
9972
|
+
throwOnError: true
|
|
9973
|
+
});
|
|
9974
|
+
return "updated";
|
|
9975
|
+
}
|
|
9976
|
+
}
|
|
9977
|
+
})
|
|
9978
|
+
);
|
|
9979
|
+
for (let i = 0; i < componentResults.length; i++) {
|
|
9980
|
+
const result = componentResults[i];
|
|
9981
|
+
const diff = componentDiffs[i];
|
|
9982
|
+
if (result.status === "fulfilled") {
|
|
9983
|
+
if (result.value === "created") {
|
|
9984
|
+
created++;
|
|
9985
|
+
} else if (result.value === "updated") {
|
|
9986
|
+
updated++;
|
|
9987
|
+
}
|
|
9988
|
+
} else {
|
|
9989
|
+
const eventId = diff.action === "create" ? "push_component" : "update_component";
|
|
9990
|
+
handleAPIError(eventId, result.reason, `Failed to ${diff.action} component ${diff.name}`);
|
|
9991
|
+
}
|
|
9992
|
+
}
|
|
9993
|
+
const datasourceDiffs = diffResult.diffs.filter((d) => d.type === "datasource");
|
|
9994
|
+
const datasourceResults = await Promise.allSettled(
|
|
9995
|
+
datasourceDiffs.map(async (diff) => {
|
|
9996
|
+
const localDs = local.datasources.find((d) => d.name === diff.name);
|
|
9997
|
+
if (diff.action === "create" && localDs) {
|
|
9998
|
+
await client.datasources.create({
|
|
9999
|
+
path: { space_id: spaceIdNum },
|
|
10000
|
+
body: { datasource: toDatasourceCreate(localDs) },
|
|
10001
|
+
throwOnError: true
|
|
10002
|
+
});
|
|
10003
|
+
return "created";
|
|
10004
|
+
}
|
|
10005
|
+
if (diff.action === "update" && localDs) {
|
|
10006
|
+
const existing = remote.datasources.get(diff.name);
|
|
10007
|
+
if (existing?.id) {
|
|
10008
|
+
await client.datasources.update(existing.id, {
|
|
10009
|
+
path: { space_id: spaceIdNum },
|
|
10010
|
+
body: { datasource: toDatasourceUpdate(localDs, existing) },
|
|
10011
|
+
throwOnError: true
|
|
10012
|
+
});
|
|
10013
|
+
return "updated";
|
|
10014
|
+
}
|
|
10015
|
+
}
|
|
10016
|
+
})
|
|
10017
|
+
);
|
|
10018
|
+
for (let i = 0; i < datasourceResults.length; i++) {
|
|
10019
|
+
const result = datasourceResults[i];
|
|
10020
|
+
const diff = datasourceDiffs[i];
|
|
10021
|
+
if (result.status === "fulfilled") {
|
|
10022
|
+
if (result.value === "created") {
|
|
10023
|
+
created++;
|
|
10024
|
+
} else if (result.value === "updated") {
|
|
10025
|
+
updated++;
|
|
10026
|
+
}
|
|
10027
|
+
} else {
|
|
10028
|
+
const eventId = diff.action === "create" ? "push_datasource" : "update_datasource";
|
|
10029
|
+
handleAPIError(eventId, result.reason, `Failed to ${diff.action} datasource ${diff.name}`);
|
|
10030
|
+
}
|
|
10031
|
+
}
|
|
10032
|
+
if (options.delete) {
|
|
10033
|
+
const staleComponents = diffResult.diffs.filter((d) => d.type === "component" && d.action === "stale");
|
|
10034
|
+
const deleteComponentResults = await Promise.allSettled(
|
|
10035
|
+
staleComponents.map(async (diff) => {
|
|
10036
|
+
const existing = remote.components.get(diff.name);
|
|
10037
|
+
if (existing?.id) {
|
|
10038
|
+
await client.components.delete(existing.id, {
|
|
10039
|
+
path: { space_id: spaceIdNum },
|
|
10040
|
+
throwOnError: true
|
|
10041
|
+
});
|
|
10042
|
+
return true;
|
|
10043
|
+
}
|
|
10044
|
+
})
|
|
10045
|
+
);
|
|
10046
|
+
for (let i = 0; i < deleteComponentResults.length; i++) {
|
|
10047
|
+
const result = deleteComponentResults[i];
|
|
10048
|
+
if (result.status === "fulfilled") {
|
|
10049
|
+
if (result.value) {
|
|
10050
|
+
deleted++;
|
|
10051
|
+
}
|
|
10052
|
+
} else {
|
|
10053
|
+
handleAPIError("push_component", result.reason, `Failed to delete component ${staleComponents[i].name}`);
|
|
10054
|
+
}
|
|
10055
|
+
}
|
|
10056
|
+
const staleDatasources = diffResult.diffs.filter((d) => d.type === "datasource" && d.action === "stale");
|
|
10057
|
+
const deleteDatasourceResults = await Promise.allSettled(
|
|
10058
|
+
staleDatasources.map(async (diff) => {
|
|
10059
|
+
const existing = remote.datasources.get(diff.name);
|
|
10060
|
+
if (existing?.id) {
|
|
10061
|
+
await client.datasources.delete(existing.id, {
|
|
10062
|
+
path: { space_id: spaceIdNum },
|
|
10063
|
+
throwOnError: true
|
|
10064
|
+
});
|
|
10065
|
+
return true;
|
|
10066
|
+
}
|
|
10067
|
+
})
|
|
10068
|
+
);
|
|
10069
|
+
for (let i = 0; i < deleteDatasourceResults.length; i++) {
|
|
10070
|
+
const result = deleteDatasourceResults[i];
|
|
10071
|
+
if (result.status === "fulfilled") {
|
|
10072
|
+
if (result.value) {
|
|
10073
|
+
deleted++;
|
|
10074
|
+
}
|
|
10075
|
+
} else {
|
|
10076
|
+
handleAPIError("delete_datasource", result.reason, `Failed to delete datasource ${staleDatasources[i].name}`);
|
|
10077
|
+
}
|
|
10078
|
+
}
|
|
10079
|
+
const staleFolders = diffResult.diffs.filter((d) => d.type === "componentFolder" && d.action === "stale");
|
|
10080
|
+
const deleteFolderResults = await Promise.allSettled(
|
|
10081
|
+
staleFolders.map(async (diff) => {
|
|
10082
|
+
const existing = remote.componentFolders.get(diff.name);
|
|
10083
|
+
if (existing?.id) {
|
|
10084
|
+
await client.componentFolders.delete(existing.id, {
|
|
10085
|
+
path: { space_id: spaceIdNum },
|
|
10086
|
+
throwOnError: true
|
|
10087
|
+
});
|
|
10088
|
+
return true;
|
|
10089
|
+
}
|
|
10090
|
+
})
|
|
10091
|
+
);
|
|
10092
|
+
for (let i = 0; i < deleteFolderResults.length; i++) {
|
|
10093
|
+
const result = deleteFolderResults[i];
|
|
10094
|
+
if (result.status === "fulfilled") {
|
|
10095
|
+
if (result.value) {
|
|
10096
|
+
deleted++;
|
|
10097
|
+
}
|
|
10098
|
+
} else {
|
|
10099
|
+
handleAPIError("push_component_group", result.reason, `Failed to delete folder ${staleFolders[i].name}`);
|
|
10100
|
+
}
|
|
10101
|
+
}
|
|
10102
|
+
}
|
|
10103
|
+
return { created, updated, deleted };
|
|
10104
|
+
}
|
|
10105
|
+
function buildChangesetEntries(diffResult, local, remote, options) {
|
|
10106
|
+
const changes = [];
|
|
10107
|
+
for (const diff of diffResult.diffs) {
|
|
10108
|
+
if (diff.action === "unchanged") {
|
|
10109
|
+
continue;
|
|
10110
|
+
}
|
|
10111
|
+
if (diff.action === "stale" && !options.delete) {
|
|
10112
|
+
continue;
|
|
10113
|
+
}
|
|
10114
|
+
const action = diff.action === "stale" ? "delete" : diff.action;
|
|
10115
|
+
let remoteSrc;
|
|
10116
|
+
let localSrc;
|
|
10117
|
+
if (diff.type === "component") {
|
|
10118
|
+
remoteSrc = remote.components.get(diff.name);
|
|
10119
|
+
localSrc = local.components.find((c) => c.name === diff.name);
|
|
10120
|
+
} else if (diff.type === "componentFolder") {
|
|
10121
|
+
remoteSrc = remote.componentFolders.get(diff.name);
|
|
10122
|
+
localSrc = local.componentFolders.find((f) => f.name === diff.name);
|
|
10123
|
+
} else if (diff.type === "datasource") {
|
|
10124
|
+
remoteSrc = remote.datasources.get(diff.name);
|
|
10125
|
+
localSrc = local.datasources.find((d) => d.name === diff.name);
|
|
10126
|
+
}
|
|
10127
|
+
changes.push({
|
|
10128
|
+
type: diff.type,
|
|
10129
|
+
name: diff.name,
|
|
10130
|
+
action,
|
|
10131
|
+
...remoteSrc && { before: { ...remoteSrc } },
|
|
10132
|
+
...localSrc && { after: { ...localSrc } }
|
|
10133
|
+
});
|
|
10134
|
+
}
|
|
10135
|
+
return changes;
|
|
10136
|
+
}
|
|
10137
|
+
|
|
10138
|
+
function findRemoteFolderByUuid(remoteFolders, uuid) {
|
|
10139
|
+
for (const folder of remoteFolders.values()) {
|
|
10140
|
+
if (folder.uuid === uuid) {
|
|
10141
|
+
return folder;
|
|
10142
|
+
}
|
|
10143
|
+
}
|
|
10144
|
+
return void 0;
|
|
10145
|
+
}
|
|
10146
|
+
function resolveFolderReferences(local, remote) {
|
|
10147
|
+
const localUuidToName = /* @__PURE__ */ new Map();
|
|
10148
|
+
for (const folder of local.componentFolders) {
|
|
10149
|
+
if (folder.uuid) {
|
|
10150
|
+
localUuidToName.set(folder.uuid, folder.name);
|
|
10151
|
+
}
|
|
10152
|
+
}
|
|
10153
|
+
const pendingFolderAssignments = /* @__PURE__ */ new Map();
|
|
10154
|
+
const resolvedComponents = local.components.map((comp) => {
|
|
10155
|
+
if (!comp.component_group_uuid) {
|
|
10156
|
+
return comp;
|
|
10157
|
+
}
|
|
10158
|
+
const folderName = localUuidToName.get(comp.component_group_uuid);
|
|
10159
|
+
if (!folderName) {
|
|
10160
|
+
return comp;
|
|
10161
|
+
}
|
|
10162
|
+
const remoteByUuid = findRemoteFolderByUuid(remote.componentFolders, comp.component_group_uuid);
|
|
10163
|
+
if (remoteByUuid) {
|
|
10164
|
+
return comp;
|
|
10165
|
+
}
|
|
10166
|
+
const remoteByName = remote.componentFolders.get(folderName);
|
|
10167
|
+
if (remoteByName) {
|
|
10168
|
+
return { ...comp, component_group_uuid: remoteByName.uuid };
|
|
10169
|
+
}
|
|
10170
|
+
const pending = pendingFolderAssignments.get(folderName) ?? [];
|
|
10171
|
+
pending.push(comp.name);
|
|
10172
|
+
pendingFolderAssignments.set(folderName, pending);
|
|
10173
|
+
return comp;
|
|
10174
|
+
});
|
|
10175
|
+
return {
|
|
10176
|
+
resolved: { ...local, components: resolvedComponents },
|
|
10177
|
+
pendingFolderAssignments
|
|
10178
|
+
};
|
|
10179
|
+
}
|
|
10180
|
+
|
|
10181
|
+
async function ensureDir(dir) {
|
|
10182
|
+
await mkdir(dir, { recursive: true });
|
|
10183
|
+
}
|
|
10184
|
+
async function saveChangeset(basePath, data) {
|
|
10185
|
+
const dir = join(basePath, "schema", "changesets");
|
|
10186
|
+
await ensureDir(dir);
|
|
10187
|
+
const fileName = `${fileTimestamp(data.timestamp)}.json`;
|
|
10188
|
+
const filePath = join(dir, fileName);
|
|
10189
|
+
await writeFile(filePath, JSON.stringify(data, null, 2), "utf-8");
|
|
10190
|
+
return filePath;
|
|
10191
|
+
}
|
|
10192
|
+
|
|
10193
|
+
const SENTINEL_FIELDS = /* @__PURE__ */ new Set(["_uid", "component"]);
|
|
10194
|
+
function classifyFieldChanges(remoteSchema, localSchema) {
|
|
10195
|
+
const removed = [];
|
|
10196
|
+
const added = [];
|
|
10197
|
+
const typeChanged = [];
|
|
10198
|
+
const requiredAdded = [];
|
|
10199
|
+
const requiredChanged = [];
|
|
10200
|
+
for (const [field, remoteField] of Object.entries(remoteSchema)) {
|
|
10201
|
+
if (SENTINEL_FIELDS.has(field)) {
|
|
10202
|
+
continue;
|
|
10203
|
+
}
|
|
10204
|
+
if (typeof remoteField.type !== "string") {
|
|
10205
|
+
continue;
|
|
10206
|
+
}
|
|
10207
|
+
if (!(field in localSchema)) {
|
|
10208
|
+
removed.push({ field, type: remoteField.type });
|
|
10209
|
+
}
|
|
10210
|
+
}
|
|
10211
|
+
for (const [field, localField] of Object.entries(localSchema)) {
|
|
10212
|
+
if (SENTINEL_FIELDS.has(field)) {
|
|
10213
|
+
continue;
|
|
10214
|
+
}
|
|
10215
|
+
if (typeof localField.type !== "string") {
|
|
10216
|
+
continue;
|
|
10217
|
+
}
|
|
10218
|
+
if (!(field in remoteSchema)) {
|
|
10219
|
+
if (localField.required) {
|
|
10220
|
+
requiredAdded.push({ field, type: localField.type });
|
|
10221
|
+
} else {
|
|
10222
|
+
added.push({ field, type: localField.type, required: false });
|
|
10223
|
+
}
|
|
10224
|
+
} else {
|
|
10225
|
+
const remoteField = remoteSchema[field];
|
|
10226
|
+
if (typeof remoteField?.type !== "string") {
|
|
10227
|
+
continue;
|
|
10228
|
+
}
|
|
10229
|
+
if (remoteField.type !== localField.type) {
|
|
10230
|
+
typeChanged.push({ field, oldType: remoteField.type, newType: localField.type });
|
|
10231
|
+
}
|
|
10232
|
+
if (localField.required && !remoteField.required) {
|
|
10233
|
+
requiredChanged.push({ field, type: localField.type });
|
|
10234
|
+
}
|
|
10235
|
+
}
|
|
10236
|
+
}
|
|
10237
|
+
return { removed, added, typeChanged, requiredAdded, requiredChanged };
|
|
10238
|
+
}
|
|
10239
|
+
function longestCommonSubstring(a, b) {
|
|
10240
|
+
let maxLen = 0;
|
|
10241
|
+
for (let i = 0; i < a.length; i++) {
|
|
10242
|
+
for (let j = 0; j < b.length; j++) {
|
|
10243
|
+
let len = 0;
|
|
10244
|
+
while (i + len < a.length && j + len < b.length && a[i + len] === b[j + len]) {
|
|
10245
|
+
len++;
|
|
10246
|
+
}
|
|
10247
|
+
if (len > maxLen) {
|
|
10248
|
+
maxLen = len;
|
|
10249
|
+
}
|
|
10250
|
+
}
|
|
10251
|
+
}
|
|
10252
|
+
return maxLen;
|
|
10253
|
+
}
|
|
10254
|
+
function nameSimilarity(a, b) {
|
|
10255
|
+
const longer = Math.max(a.length, b.length);
|
|
10256
|
+
if (longer === 0) {
|
|
10257
|
+
return 1;
|
|
10258
|
+
}
|
|
10259
|
+
return longestCommonSubstring(a, b) / longer;
|
|
10260
|
+
}
|
|
10261
|
+
function detectRenames(removed, added) {
|
|
10262
|
+
const renames = [];
|
|
10263
|
+
const usedRemoved = /* @__PURE__ */ new Set();
|
|
10264
|
+
const usedAdded = /* @__PURE__ */ new Set();
|
|
10265
|
+
const addedByType = /* @__PURE__ */ new Map();
|
|
10266
|
+
for (const addedField of added) {
|
|
10267
|
+
if (!addedByType.has(addedField.type)) {
|
|
10268
|
+
addedByType.set(addedField.type, []);
|
|
10269
|
+
}
|
|
10270
|
+
addedByType.get(addedField.type).push(addedField);
|
|
10271
|
+
}
|
|
10272
|
+
const isSinglePair = removed.length === 1 && added.length === 1;
|
|
10273
|
+
for (const removedField of removed) {
|
|
10274
|
+
const candidates = addedByType.get(removedField.type) ?? [];
|
|
10275
|
+
const availableCandidates = candidates.filter((c) => !usedAdded.has(c.field));
|
|
10276
|
+
if (availableCandidates.length === 0) {
|
|
10277
|
+
continue;
|
|
10278
|
+
}
|
|
10279
|
+
let bestCandidate = availableCandidates[0];
|
|
10280
|
+
let bestScore = nameSimilarity(removedField.field, bestCandidate.field);
|
|
10281
|
+
for (let i = 1; i < availableCandidates.length; i++) {
|
|
10282
|
+
const score = nameSimilarity(removedField.field, availableCandidates[i].field);
|
|
10283
|
+
if (score > bestScore) {
|
|
10284
|
+
bestScore = score;
|
|
10285
|
+
bestCandidate = availableCandidates[i];
|
|
10286
|
+
}
|
|
10287
|
+
}
|
|
10288
|
+
if (!isSinglePair && bestScore < 0.3) {
|
|
10289
|
+
continue;
|
|
10290
|
+
}
|
|
10291
|
+
renames.push({ oldField: removedField.field, newField: bestCandidate.field, fieldType: removedField.type });
|
|
10292
|
+
usedRemoved.add(removedField.field);
|
|
10293
|
+
usedAdded.add(bestCandidate.field);
|
|
10294
|
+
}
|
|
10295
|
+
const unmatchedRemoved = removed.filter((r) => !usedRemoved.has(r.field));
|
|
10296
|
+
const unmatchedAdded = added.filter((a) => !usedAdded.has(a.field));
|
|
10297
|
+
return { renames, unmatchedRemoved, unmatchedAdded };
|
|
10298
|
+
}
|
|
10299
|
+
function analyzeBreakingChanges(diffResult, local, remote) {
|
|
10300
|
+
const results = [];
|
|
10301
|
+
const updatedComponents = diffResult.diffs.filter(
|
|
10302
|
+
(d) => d.type === "component" && d.action === "update"
|
|
10303
|
+
);
|
|
10304
|
+
for (const diff of updatedComponents) {
|
|
10305
|
+
const localComp = local.components.find((c) => c.name === diff.name);
|
|
10306
|
+
const remoteComp = remote.components.get(diff.name);
|
|
10307
|
+
if (!localComp?.schema || !remoteComp?.schema) {
|
|
10308
|
+
continue;
|
|
10309
|
+
}
|
|
10310
|
+
const classification = classifyFieldChanges(
|
|
10311
|
+
remoteComp.schema,
|
|
10312
|
+
localComp.schema
|
|
10313
|
+
);
|
|
10314
|
+
const changes = [];
|
|
10315
|
+
const { renames, unmatchedRemoved } = detectRenames(classification.removed, classification.added);
|
|
10316
|
+
for (const rename of renames) {
|
|
10317
|
+
changes.push({ kind: "rename", field: rename.newField, oldField: rename.oldField });
|
|
10318
|
+
}
|
|
10319
|
+
for (const removed of unmatchedRemoved) {
|
|
10320
|
+
changes.push({ kind: "removed", field: removed.field });
|
|
10321
|
+
}
|
|
10322
|
+
for (const tc of classification.typeChanged) {
|
|
10323
|
+
changes.push({ kind: "type_changed", field: tc.field, oldType: tc.oldType, newType: tc.newType });
|
|
10324
|
+
}
|
|
10325
|
+
for (const ra of classification.requiredAdded) {
|
|
10326
|
+
changes.push({ kind: "required_added", field: ra.field, fieldType: ra.type });
|
|
10327
|
+
}
|
|
10328
|
+
for (const rc of classification.requiredChanged) {
|
|
10329
|
+
changes.push({ kind: "required_changed", field: rc.field, fieldType: rc.type });
|
|
10330
|
+
}
|
|
10331
|
+
if (changes.length > 0) {
|
|
10332
|
+
results.push({ componentName: diff.name, changes });
|
|
10333
|
+
}
|
|
10334
|
+
}
|
|
10335
|
+
return results;
|
|
10336
|
+
}
|
|
10337
|
+
|
|
10338
|
+
const COMPATIBLE_TYPES = /* @__PURE__ */ new Set(["text:textarea", "textarea:text"]);
|
|
10339
|
+
function defaultForType(fieldType) {
|
|
10340
|
+
switch (fieldType) {
|
|
10341
|
+
case "text":
|
|
10342
|
+
case "textarea":
|
|
10343
|
+
case "markdown":
|
|
10344
|
+
return `''`;
|
|
10345
|
+
case "number":
|
|
10346
|
+
return "0";
|
|
10347
|
+
case "boolean":
|
|
10348
|
+
return "false";
|
|
10349
|
+
default:
|
|
10350
|
+
return null;
|
|
10351
|
+
}
|
|
10352
|
+
}
|
|
10353
|
+
function typeConversion(field, oldType, newType) {
|
|
10354
|
+
const key = `${oldType}:${newType}`;
|
|
10355
|
+
if (COMPATIBLE_TYPES.has(key)) {
|
|
10356
|
+
return null;
|
|
10357
|
+
}
|
|
10358
|
+
const accessor = `block.${field}`;
|
|
10359
|
+
switch (key) {
|
|
10360
|
+
case "text:number":
|
|
10361
|
+
return `${accessor} = Number(${accessor}) || 0;`;
|
|
10362
|
+
case "number:text":
|
|
10363
|
+
return `${accessor} = String(${accessor});`;
|
|
10364
|
+
case "text:boolean":
|
|
10365
|
+
return `${accessor} = !!${accessor};`;
|
|
10366
|
+
case "boolean:text":
|
|
10367
|
+
return `${accessor} = String(${accessor});`;
|
|
10368
|
+
default:
|
|
10369
|
+
return `${accessor}; // TODO: convert from ${oldType} to ${newType}`;
|
|
10370
|
+
}
|
|
10371
|
+
}
|
|
10372
|
+
function renderMigrationCode(changes) {
|
|
10373
|
+
const lines = [];
|
|
10374
|
+
lines.push(" // Review this migration before running it against your space.");
|
|
10375
|
+
lines.push(" // Generated migrations are scaffolds and may need manual adjustments.");
|
|
10376
|
+
lines.push(" // Example rename migration:");
|
|
10377
|
+
lines.push(" // block.new_field = block.old_field;");
|
|
10378
|
+
lines.push(" // delete block.old_field;");
|
|
10379
|
+
lines.push("");
|
|
10380
|
+
for (const change of changes) {
|
|
10381
|
+
switch (change.kind) {
|
|
10382
|
+
case "rename":
|
|
10383
|
+
lines.push(` // Rename: ${change.oldField} \u2192 ${change.field}`);
|
|
10384
|
+
lines.push(` if ('${change.oldField}' in block) {`);
|
|
10385
|
+
lines.push(` block.${change.field} = block.${change.oldField};`);
|
|
10386
|
+
lines.push(` delete block.${change.oldField};`);
|
|
10387
|
+
lines.push(` }`);
|
|
10388
|
+
break;
|
|
10389
|
+
case "removed":
|
|
10390
|
+
if (change.renameHint) {
|
|
10391
|
+
lines.push(` // If '${change.field}' was renamed to '${change.renameHint.newField}', uncomment:`);
|
|
10392
|
+
lines.push(` // block.${change.renameHint.newField} = block.${change.field};`);
|
|
10393
|
+
} else {
|
|
10394
|
+
lines.push(` // Removed field: ${change.field}`);
|
|
10395
|
+
}
|
|
10396
|
+
lines.push(` delete block.${change.field};`);
|
|
10397
|
+
break;
|
|
10398
|
+
case "type_changed": {
|
|
10399
|
+
const conversion = typeConversion(change.field, change.oldType, change.newType);
|
|
10400
|
+
if (conversion) {
|
|
10401
|
+
lines.push(` // Type change: ${change.field} (${change.oldType} \u2192 ${change.newType})`);
|
|
10402
|
+
lines.push(` ${conversion}`);
|
|
10403
|
+
}
|
|
10404
|
+
break;
|
|
10405
|
+
}
|
|
10406
|
+
case "required_added": {
|
|
10407
|
+
const defaultValue = defaultForType(change.fieldType);
|
|
10408
|
+
lines.push(` // New required field: ${change.field} (${change.fieldType})`);
|
|
10409
|
+
if (defaultValue !== null) {
|
|
10410
|
+
lines.push(` // TODO: provide a meaningful default value`);
|
|
10411
|
+
lines.push(` block.${change.field} = block.${change.field} ?? ${defaultValue};`);
|
|
10412
|
+
} else {
|
|
10413
|
+
lines.push(` // TODO: provide a default value appropriate for the '${change.fieldType}' type`);
|
|
10414
|
+
lines.push(` // block.${change.field} = block.${change.field} ?? <default>;`);
|
|
10415
|
+
}
|
|
10416
|
+
break;
|
|
10417
|
+
}
|
|
10418
|
+
case "required_changed": {
|
|
10419
|
+
const defaultValue = defaultForType(change.fieldType);
|
|
10420
|
+
lines.push(` // Field is now required: ${change.field} (${change.fieldType})`);
|
|
10421
|
+
lines.push(` // Existing stories may have null/undefined values \u2014 provide a default for those.`);
|
|
10422
|
+
if (defaultValue !== null) {
|
|
10423
|
+
lines.push(` // TODO: provide a meaningful default value`);
|
|
10424
|
+
lines.push(` block.${change.field} = block.${change.field} ?? ${defaultValue};`);
|
|
10425
|
+
} else {
|
|
10426
|
+
lines.push(` // TODO: provide a default value appropriate for the '${change.fieldType}' type`);
|
|
10427
|
+
lines.push(` // block.${change.field} = block.${change.field} ?? <default>;`);
|
|
10428
|
+
}
|
|
10429
|
+
break;
|
|
10430
|
+
}
|
|
10431
|
+
}
|
|
10432
|
+
lines.push("");
|
|
10433
|
+
}
|
|
10434
|
+
const body = lines.length > 0 ? `
|
|
10435
|
+
${lines.join("\n")}` : "\n";
|
|
10436
|
+
return `export default function (block) {${body} return block;
|
|
10437
|
+
}
|
|
10438
|
+
`;
|
|
10439
|
+
}
|
|
10440
|
+
async function writeMigrationFile(options) {
|
|
10441
|
+
const { spaceId, componentName, code, timestamp, basePath } = options;
|
|
10442
|
+
const dir = resolvePath(basePath, `migrations/${spaceId}`);
|
|
10443
|
+
await mkdir(dir, { recursive: true });
|
|
10444
|
+
const fileName = `${componentName}.${fileTimestamp(timestamp)}.js`;
|
|
10445
|
+
const filePath = join(dir, fileName);
|
|
10446
|
+
await writeFile(filePath, code, "utf-8");
|
|
10447
|
+
return filePath;
|
|
10448
|
+
}
|
|
10449
|
+
|
|
10450
|
+
const DEFAULT_GROUPS_FILENAME = "groups.json";
|
|
10451
|
+
const CONSOLIDATED_COMPONENTS_FILENAME = "components.json";
|
|
10452
|
+
async function writeLocalComponents({
|
|
10453
|
+
space,
|
|
10454
|
+
basePath,
|
|
10455
|
+
resolved,
|
|
10456
|
+
diffResult,
|
|
10457
|
+
deleteRemoved,
|
|
10458
|
+
ui,
|
|
10459
|
+
logger
|
|
10460
|
+
}) {
|
|
10461
|
+
const componentsDir = resolveCommandPath(directories.components, space, basePath);
|
|
10462
|
+
const consolidatedPath = join(componentsDir, CONSOLIDATED_COMPONENTS_FILENAME);
|
|
10463
|
+
if (await fileExists(consolidatedPath)) {
|
|
10464
|
+
ui.warn(
|
|
10465
|
+
`A consolidated ${CONSOLIDATED_COMPONENTS_FILENAME} exists at ${componentsDir}. Per-component files will still be written, but the consolidated file may shadow them when stories push validates schemas. Delete it or run \`storyblok components pull --separate-files\` to regenerate.`
|
|
10466
|
+
);
|
|
10467
|
+
}
|
|
10468
|
+
for (const component of resolved.components) {
|
|
10469
|
+
const filePath = join(componentsDir, `${sanitizeFilename(component.name || "")}.json`);
|
|
10470
|
+
await saveToFile(filePath, JSON.stringify(component, null, 2));
|
|
10471
|
+
}
|
|
10472
|
+
const groupsPath = join(componentsDir, DEFAULT_GROUPS_FILENAME);
|
|
10473
|
+
if (resolved.componentFolders.length > 0) {
|
|
10474
|
+
await saveToFile(groupsPath, JSON.stringify(resolved.componentFolders, null, 2));
|
|
10475
|
+
} else if (await fileExists(groupsPath)) {
|
|
10476
|
+
try {
|
|
10477
|
+
await unlink(groupsPath);
|
|
10478
|
+
logger.info("Removed stale local groups file", { path: groupsPath });
|
|
10479
|
+
} catch (error) {
|
|
10480
|
+
if (error.code !== "ENOENT") {
|
|
10481
|
+
throw error;
|
|
10482
|
+
}
|
|
10483
|
+
}
|
|
10484
|
+
}
|
|
10485
|
+
if (deleteRemoved) {
|
|
10486
|
+
const staleComponents = diffResult.diffs.filter(
|
|
10487
|
+
(d) => d.type === "component" && d.action === "stale"
|
|
10488
|
+
);
|
|
10489
|
+
for (const stale of staleComponents) {
|
|
10490
|
+
const filePath = join(componentsDir, `${sanitizeFilename(stale.name)}.json`);
|
|
10491
|
+
try {
|
|
10492
|
+
await unlink(filePath);
|
|
10493
|
+
logger.info("Removed stale local component file", { path: filePath });
|
|
10494
|
+
} catch (error) {
|
|
10495
|
+
if (error.code !== "ENOENT") {
|
|
10496
|
+
throw error;
|
|
10497
|
+
}
|
|
10498
|
+
}
|
|
10499
|
+
}
|
|
10500
|
+
}
|
|
10501
|
+
logger.info("Wrote local component files", {
|
|
10502
|
+
space,
|
|
10503
|
+
componentsWritten: resolved.components.length,
|
|
10504
|
+
groupsWritten: resolved.componentFolders.length
|
|
10505
|
+
});
|
|
10506
|
+
}
|
|
10507
|
+
|
|
10508
|
+
schemaCommand.command("push <entry-file>").description("Push local TypeScript schema and datasource definitions to a Storyblok space").option("-s, --space <space>", "space ID").option("-p, --path <path>", "path for file storage").option("--dry-run", "Show diffs without applying changes", false).option("--delete", "Delete remote entities not present in local schema", false).option("--migrations", "Generate scaffold migration files for breaking changes", true).addOption(new Option("--no-migrations", "Skip migration generation for breaking changes")).option("--write-components", "Write component schemas as local JSON files after push (also removes local files for components deleted via --delete)", true).addOption(new Option("--no-write-components", "Skip writing local component files")).action(async (entryFile, options, command) => {
|
|
10509
|
+
const ui = getUI();
|
|
10510
|
+
const logger = getLogger();
|
|
10511
|
+
const reporter = getReporter();
|
|
10512
|
+
const { space, path: basePath, verbose } = command.optsWithGlobals();
|
|
10513
|
+
const { state } = session();
|
|
10514
|
+
ui.title(commands.SCHEMA, colorPalette.SCHEMA, "Pushing schema...");
|
|
10515
|
+
logger.info("Schema push started", { entryFile, space });
|
|
10516
|
+
if (!requireAuthentication(state, verbose)) {
|
|
10517
|
+
return;
|
|
10518
|
+
}
|
|
10519
|
+
if (!space) {
|
|
10520
|
+
handleError(new CommandError("Please provide the space as argument --space SPACE_ID."), verbose);
|
|
10521
|
+
return;
|
|
10522
|
+
}
|
|
10523
|
+
const summary = { total: 0, succeeded: 0, failed: 0 };
|
|
10524
|
+
try {
|
|
10525
|
+
const loadSpinner = ui.createSpinner("Resolving schema...");
|
|
10526
|
+
let local;
|
|
10527
|
+
try {
|
|
10528
|
+
local = await loadSchema(entryFile);
|
|
10529
|
+
} catch (maybeError) {
|
|
10530
|
+
loadSpinner.failed("Failed to resolve schema");
|
|
10531
|
+
handleError(toError(maybeError), verbose);
|
|
10532
|
+
return;
|
|
10533
|
+
}
|
|
10534
|
+
loadSpinner.succeed(`Found: ${local.components.length} components, ${local.componentFolders.length} component folders, ${local.datasources.length} datasources`);
|
|
10535
|
+
const totalLocal = local.components.length + local.componentFolders.length + local.datasources.length;
|
|
10536
|
+
if (totalLocal === 0) {
|
|
10537
|
+
ui.warn("No components, folders, or datasources found in the entry file. Verify the file exports schema definitions.");
|
|
10538
|
+
return;
|
|
10539
|
+
}
|
|
10540
|
+
const remoteSpinner = ui.createSpinner(`Fetching remote state from space ${space}...`);
|
|
10541
|
+
let remoteResult;
|
|
10542
|
+
try {
|
|
10543
|
+
remoteResult = await fetchRemoteSchema(space);
|
|
10544
|
+
} catch (maybeError) {
|
|
10545
|
+
remoteSpinner.failed("Failed to fetch remote schema");
|
|
10546
|
+
handleError(toError(maybeError), verbose);
|
|
10547
|
+
return;
|
|
10548
|
+
}
|
|
10549
|
+
const { remote, rawComponents, rawComponentFolders, rawDatasources } = remoteResult;
|
|
10550
|
+
remoteSpinner.succeed(`Remote: ${remote.components.size} components, ${remote.componentFolders.size} component folders, ${remote.datasources.size} datasources`);
|
|
10551
|
+
const { resolved, pendingFolderAssignments } = resolveFolderReferences(local, remote);
|
|
10552
|
+
const diffResult = diffSchema(resolved, remote);
|
|
10553
|
+
ui.br();
|
|
10554
|
+
ui.log(formatDiffOutput(diffResult, { delete: options.delete }));
|
|
10555
|
+
if (options.migrations) {
|
|
10556
|
+
const breakingChanges = analyzeBreakingChanges(diffResult, resolved, remote);
|
|
10557
|
+
if (breakingChanges.length > 0) {
|
|
10558
|
+
const totalChanges = breakingChanges.reduce((sum, c) => sum + c.changes.length, 0);
|
|
10559
|
+
ui.br();
|
|
10560
|
+
ui.warn(`${totalChanges} breaking change(s) detected in ${breakingChanges.length} component(s).`);
|
|
10561
|
+
ui.info("Generated migrations are scaffolds. Review and adjust them before running `storyblok migrations run`.");
|
|
10562
|
+
if (!options.dryRun) {
|
|
10563
|
+
const explicitMigrations = command.getOptionValueSource("migrations") === "cli";
|
|
10564
|
+
const shouldGenerate = explicitMigrations || await confirm({
|
|
10565
|
+
message: "Generate migration files for breaking changes?",
|
|
10566
|
+
default: true
|
|
10567
|
+
});
|
|
10568
|
+
if (shouldGenerate) {
|
|
10569
|
+
const migrationTimestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
10570
|
+
const resolvedBase = resolvePath(basePath, "");
|
|
10571
|
+
for (const comp of breakingChanges) {
|
|
10572
|
+
const renames = comp.changes.filter((c) => c.kind === "rename");
|
|
10573
|
+
if (renames.length > 0 && explicitMigrations) {
|
|
10574
|
+
for (const r of renames) {
|
|
10575
|
+
if (r.kind === "rename") {
|
|
10576
|
+
ui.log(` Assumed rename in '${comp.componentName}': ${r.oldField} \u2192 ${r.field}`);
|
|
10577
|
+
}
|
|
10578
|
+
}
|
|
10579
|
+
}
|
|
10580
|
+
if (renames.length > 0 && !explicitMigrations) {
|
|
10581
|
+
ui.br();
|
|
10582
|
+
ui.log(`Detected renames in '${comp.componentName}':`);
|
|
10583
|
+
for (const r of renames) {
|
|
10584
|
+
if (r.kind === "rename") {
|
|
10585
|
+
ui.log(` ${r.oldField} \u2192 ${r.field}`);
|
|
10586
|
+
}
|
|
10587
|
+
}
|
|
10588
|
+
const renameConfirmed = await confirm({
|
|
10589
|
+
message: "Are these renames correct?",
|
|
10590
|
+
default: true
|
|
10591
|
+
});
|
|
10592
|
+
if (!renameConfirmed) {
|
|
10593
|
+
comp.changes = comp.changes.map((c) => {
|
|
10594
|
+
if (c.kind === "rename") {
|
|
10595
|
+
return { kind: "removed", field: c.oldField, renameHint: { newField: c.field } };
|
|
10596
|
+
}
|
|
10597
|
+
return c;
|
|
10598
|
+
});
|
|
10599
|
+
}
|
|
10600
|
+
}
|
|
10601
|
+
const code = renderMigrationCode(comp.changes);
|
|
10602
|
+
const path = await writeMigrationFile({
|
|
10603
|
+
spaceId: space,
|
|
10604
|
+
componentName: comp.componentName,
|
|
10605
|
+
code,
|
|
10606
|
+
timestamp: migrationTimestamp,
|
|
10607
|
+
basePath: resolvedBase
|
|
10608
|
+
});
|
|
10609
|
+
ui.log(` Generated: ${path}`);
|
|
10610
|
+
}
|
|
10611
|
+
ui.br();
|
|
10612
|
+
ui.info(`Run migrations when ready: storyblok migrations run --space ${space}`);
|
|
10613
|
+
}
|
|
10614
|
+
}
|
|
10615
|
+
}
|
|
10616
|
+
}
|
|
10617
|
+
if (options.delete) {
|
|
10618
|
+
const deletedComponents = diffResult.diffs.filter((d) => d.type === "component" && d.action === "stale");
|
|
10619
|
+
for (const comp of deletedComponents) {
|
|
10620
|
+
ui.warn(`Component '${comp.name}' will be deleted. Stories using it will have out-of-schema content.`);
|
|
10621
|
+
}
|
|
10622
|
+
}
|
|
10623
|
+
if (diffResult.stale > 0 && !options.delete) {
|
|
10624
|
+
ui.warn(`${diffResult.stale} stale entity(s) exist remotely but not in schema. Use --delete to remove.`);
|
|
10625
|
+
}
|
|
10626
|
+
if (options.dryRun) {
|
|
10627
|
+
ui.info("Dry run \u2014 no changes applied.");
|
|
10628
|
+
logger.info("Dry run completed", { creates: diffResult.creates, updates: diffResult.updates });
|
|
10629
|
+
return;
|
|
10630
|
+
}
|
|
10631
|
+
const resolvedPath = resolvePath(basePath, "");
|
|
10632
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
10633
|
+
const changesetPath = await saveChangeset(resolvedPath, {
|
|
10634
|
+
timestamp,
|
|
10635
|
+
spaceId: Number(space),
|
|
10636
|
+
remote: { components: rawComponents, componentFolders: rawComponentFolders, datasources: rawDatasources },
|
|
10637
|
+
changes: buildChangesetEntries(diffResult, resolved, remote, { delete: options.delete })
|
|
10638
|
+
});
|
|
10639
|
+
logger.info("Changeset saved", { path: changesetPath });
|
|
10640
|
+
const nothingToPush = diffResult.creates === 0 && diffResult.updates === 0 && (!options.delete || diffResult.stale === 0);
|
|
10641
|
+
if (nothingToPush) {
|
|
10642
|
+
ui.ok("Everything up to date \u2014 nothing to push.");
|
|
10643
|
+
} else {
|
|
10644
|
+
const pushSpinner = ui.createSpinner("Pushing schema...");
|
|
10645
|
+
let result;
|
|
10646
|
+
try {
|
|
10647
|
+
result = await executePush(space, resolved, remote, diffResult, { delete: options.delete, pendingFolderAssignments });
|
|
10648
|
+
} catch (error) {
|
|
10649
|
+
pushSpinner.failed("Failed to push schema");
|
|
10650
|
+
throw error;
|
|
10651
|
+
}
|
|
10652
|
+
summary.total = result.created + result.updated + result.deleted;
|
|
10653
|
+
summary.succeeded = summary.total;
|
|
10654
|
+
pushSpinner.succeed(`Pushed ${result.created} creations, ${result.updated} updates${result.deleted > 0 ? `, ${result.deleted} deletions` : ""}.`);
|
|
10655
|
+
}
|
|
10656
|
+
if (options.writeComponents) {
|
|
10657
|
+
try {
|
|
10658
|
+
await writeLocalComponents({
|
|
10659
|
+
space,
|
|
10660
|
+
basePath,
|
|
10661
|
+
resolved,
|
|
10662
|
+
diffResult,
|
|
10663
|
+
deleteRemoved: options.delete,
|
|
10664
|
+
ui,
|
|
10665
|
+
logger
|
|
10666
|
+
});
|
|
10667
|
+
} catch (writeError) {
|
|
10668
|
+
ui.warn(`Failed to write local component files: ${toError(writeError).message}`);
|
|
10669
|
+
logger.warn("Failed to write local component files", { error: toError(writeError).message });
|
|
10670
|
+
}
|
|
10671
|
+
}
|
|
10672
|
+
} catch (maybeError) {
|
|
10673
|
+
summary.failed += 1;
|
|
10674
|
+
handleError(toError(maybeError), verbose);
|
|
10675
|
+
} finally {
|
|
10676
|
+
logger.info("Schema push finished", { summary });
|
|
10677
|
+
reporter.addSummary("schemaPushResults", summary);
|
|
10678
|
+
reporter.finalize();
|
|
10679
|
+
}
|
|
10680
|
+
});
|
|
10681
|
+
|
|
10682
|
+
const FIELD_STRIP_KEYS = /* @__PURE__ */ new Set(["id", "pos"]);
|
|
10683
|
+
function toCamelCase(str) {
|
|
10684
|
+
return str.toLowerCase().replace(/[\s_-]+(.)/g, (_, char) => char.toUpperCase());
|
|
10685
|
+
}
|
|
10686
|
+
function toKebabCase(str) {
|
|
10687
|
+
return str.replace(/[\s_]+/g, "-").replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
|
|
10688
|
+
}
|
|
10689
|
+
function componentVarName(name) {
|
|
10690
|
+
return `${toCamelCase(name)}Block`;
|
|
10691
|
+
}
|
|
10692
|
+
function folderVarName(name) {
|
|
10693
|
+
return `${toCamelCase(name)}Folder`;
|
|
10694
|
+
}
|
|
10695
|
+
function datasourceVarName(name) {
|
|
10696
|
+
return `${toCamelCase(name)}Datasource`;
|
|
10697
|
+
}
|
|
10698
|
+
function componentFileName(name) {
|
|
10699
|
+
return toKebabCase(name);
|
|
10700
|
+
}
|
|
10701
|
+
function folderFileName(name) {
|
|
10702
|
+
return toKebabCase(name);
|
|
10703
|
+
}
|
|
10704
|
+
function datasourceFileName(datasource) {
|
|
10705
|
+
return toKebabCase(datasource.slug || datasource.name);
|
|
10706
|
+
}
|
|
10707
|
+
function generateFieldCode(fieldName, fieldData, depth) {
|
|
10708
|
+
const clean = stripKeys(fieldData, FIELD_STRIP_KEYS);
|
|
10709
|
+
return `defineField('${fieldName.replace(/'/g, "\\'")}', ${formatValue(clean, depth + 1)})`;
|
|
10710
|
+
}
|
|
10711
|
+
function sortSchemaByPos(schema) {
|
|
10712
|
+
return Object.entries(schema).filter(([key]) => key !== "_uid" && key !== "component").sort(([, a], [, b]) => {
|
|
10713
|
+
const posA = typeof a.pos === "number" ? a.pos : Infinity;
|
|
10714
|
+
const posB = typeof b.pos === "number" ? b.pos : Infinity;
|
|
10715
|
+
return posA - posB;
|
|
10716
|
+
});
|
|
10717
|
+
}
|
|
10718
|
+
function generateComponentFile(component, componentFolders) {
|
|
10719
|
+
const lines = [];
|
|
10720
|
+
let matchedFolder;
|
|
10721
|
+
if (component.component_group_uuid && componentFolders) {
|
|
10722
|
+
matchedFolder = componentFolders.find((f) => f.uuid === component.component_group_uuid);
|
|
10723
|
+
}
|
|
10724
|
+
lines.push("import {");
|
|
10725
|
+
lines.push(" defineBlock,");
|
|
10726
|
+
lines.push(" defineField,");
|
|
10727
|
+
lines.push("} from '@storyblok/schema';");
|
|
10728
|
+
if (matchedFolder) {
|
|
10729
|
+
lines.push("");
|
|
10730
|
+
const fVarName = folderVarName(matchedFolder.name);
|
|
10731
|
+
const fFileName = folderFileName(matchedFolder.name);
|
|
10732
|
+
lines.push(`import { ${fVarName} } from './folders/${fFileName}';`);
|
|
10733
|
+
}
|
|
10734
|
+
lines.push("");
|
|
10735
|
+
const varName = componentVarName(component.name);
|
|
10736
|
+
lines.push(`export const ${varName} = defineBlock({`);
|
|
10737
|
+
const clean = stripKeys(component, COMPONENT_STRIP_KEYS);
|
|
10738
|
+
if (matchedFolder) {
|
|
10739
|
+
delete clean.component_group_uuid;
|
|
10740
|
+
}
|
|
10741
|
+
const orderedKeys = [];
|
|
10742
|
+
if (clean.name !== void 0) {
|
|
10743
|
+
orderedKeys.push("name");
|
|
10744
|
+
}
|
|
10745
|
+
if (clean.display_name !== void 0) {
|
|
10746
|
+
orderedKeys.push("display_name");
|
|
10747
|
+
}
|
|
10748
|
+
if (clean.is_root !== void 0) {
|
|
10749
|
+
orderedKeys.push("is_root");
|
|
10750
|
+
}
|
|
10751
|
+
if (clean.is_nestable !== void 0) {
|
|
10752
|
+
orderedKeys.push("is_nestable");
|
|
10753
|
+
}
|
|
10754
|
+
const handled = /* @__PURE__ */ new Set(["name", "display_name", "is_root", "is_nestable", "schema"]);
|
|
10755
|
+
for (const key of Object.keys(clean).sort()) {
|
|
10756
|
+
if (!handled.has(key)) {
|
|
10757
|
+
orderedKeys.push(key);
|
|
10758
|
+
}
|
|
10759
|
+
}
|
|
10760
|
+
if (matchedFolder) {
|
|
10761
|
+
const fVarName = folderVarName(matchedFolder.name);
|
|
10762
|
+
lines.push(`${INDENT}component_group_uuid: ${fVarName}.uuid,`);
|
|
10763
|
+
}
|
|
10764
|
+
for (const key of orderedKeys) {
|
|
10765
|
+
lines.push(`${INDENT}${key}: ${formatValue(clean[key], 1)},`);
|
|
10766
|
+
}
|
|
10767
|
+
if (clean.schema && typeof clean.schema === "object") {
|
|
10768
|
+
const schema = clean.schema;
|
|
10769
|
+
const sortedFields = sortSchemaByPos(schema);
|
|
10770
|
+
if (sortedFields.length > 0) {
|
|
10771
|
+
lines.push(`${INDENT}schema: [`);
|
|
10772
|
+
for (const [fieldName, fieldData] of sortedFields) {
|
|
10773
|
+
const fieldCode = generateFieldCode(fieldName, fieldData, 2);
|
|
10774
|
+
lines.push(`${INDENT}${INDENT}${fieldCode},`);
|
|
10775
|
+
}
|
|
10776
|
+
lines.push(`${INDENT}],`);
|
|
10777
|
+
} else {
|
|
10778
|
+
lines.push(`${INDENT}schema: [],`);
|
|
10779
|
+
}
|
|
10780
|
+
}
|
|
10781
|
+
lines.push("});");
|
|
10782
|
+
lines.push("");
|
|
10783
|
+
return lines.join("\n");
|
|
10784
|
+
}
|
|
10785
|
+
function generateFolderFile(folder) {
|
|
10786
|
+
const lines = [];
|
|
10787
|
+
lines.push("import { defineBlockFolder } from '@storyblok/schema';");
|
|
10788
|
+
lines.push("");
|
|
10789
|
+
const varName = folderVarName(folder.name);
|
|
10790
|
+
lines.push(`export const ${varName} = defineBlockFolder({`);
|
|
10791
|
+
const clean = stripKeys(folder, FOLDER_INIT_STRIP_KEYS);
|
|
10792
|
+
if (clean.name !== void 0) {
|
|
10793
|
+
lines.push(`${INDENT}name: ${formatValue(clean.name, 1)},`);
|
|
10794
|
+
}
|
|
10795
|
+
const handled = /* @__PURE__ */ new Set(["name"]);
|
|
10796
|
+
for (const [key, value] of Object.entries(clean).sort(([a], [b]) => a.localeCompare(b))) {
|
|
10797
|
+
if (!handled.has(key)) {
|
|
10798
|
+
lines.push(`${INDENT}${key}: ${formatValue(value, 1)},`);
|
|
10799
|
+
}
|
|
10800
|
+
}
|
|
10801
|
+
lines.push("});");
|
|
10802
|
+
lines.push("");
|
|
10803
|
+
return lines.join("\n");
|
|
10804
|
+
}
|
|
10805
|
+
function generateDatasourceFile(datasource) {
|
|
10806
|
+
const lines = [];
|
|
10807
|
+
lines.push("import { defineDatasource } from '@storyblok/schema';");
|
|
10808
|
+
lines.push("");
|
|
10809
|
+
const varName = datasourceVarName(datasource.name);
|
|
10810
|
+
lines.push(`export const ${varName} = defineDatasource({`);
|
|
10811
|
+
const clean = stripKeys(datasource, DATASOURCE_STRIP_KEYS);
|
|
10812
|
+
if (clean.name !== void 0) {
|
|
10813
|
+
lines.push(`${INDENT}name: ${formatValue(clean.name, 1)},`);
|
|
10814
|
+
}
|
|
10815
|
+
if (clean.slug !== void 0) {
|
|
10816
|
+
lines.push(`${INDENT}slug: ${formatValue(clean.slug, 1)},`);
|
|
10817
|
+
}
|
|
10818
|
+
const handled = /* @__PURE__ */ new Set(["name", "slug"]);
|
|
10819
|
+
for (const [key, value] of Object.entries(clean).sort(([a], [b]) => a.localeCompare(b))) {
|
|
10820
|
+
if (!handled.has(key)) {
|
|
10821
|
+
lines.push(`${INDENT}${key}: ${formatValue(value, 1)},`);
|
|
10822
|
+
}
|
|
10823
|
+
}
|
|
10824
|
+
lines.push("});");
|
|
10825
|
+
lines.push("");
|
|
10826
|
+
return lines.join("\n");
|
|
10827
|
+
}
|
|
10828
|
+
function generateSchemaFile(components, componentFolders, datasources) {
|
|
10829
|
+
const lines = [];
|
|
10830
|
+
lines.push("import type { Schema as InferSchema, Story as InferStory } from '@storyblok/schema';");
|
|
10831
|
+
lines.push("import type { MapiStory as InferStoryMapi } from '@storyblok/schema';");
|
|
10832
|
+
lines.push("");
|
|
10833
|
+
for (const component of components) {
|
|
10834
|
+
const varName = componentVarName(component.name);
|
|
10835
|
+
const fileName = componentFileName(component.name);
|
|
10836
|
+
lines.push(`import { ${varName} } from './components/${fileName}';`);
|
|
10837
|
+
}
|
|
10838
|
+
for (const folder of componentFolders) {
|
|
10839
|
+
const varName = folderVarName(folder.name);
|
|
10840
|
+
const fileName = folderFileName(folder.name);
|
|
10841
|
+
lines.push(`import { ${varName} } from './components/folders/${fileName}';`);
|
|
10842
|
+
}
|
|
10843
|
+
for (const datasource of datasources) {
|
|
10844
|
+
const varName = datasourceVarName(datasource.name);
|
|
10845
|
+
const fileName = datasourceFileName(datasource);
|
|
10846
|
+
lines.push(`import { ${varName} } from './datasources/${fileName}';`);
|
|
10847
|
+
}
|
|
10848
|
+
lines.push("");
|
|
10849
|
+
lines.push("export const schema = {");
|
|
10850
|
+
if (components.length > 0) {
|
|
10851
|
+
lines.push(" blocks: {");
|
|
10852
|
+
for (const component of components) {
|
|
10853
|
+
const varName = componentVarName(component.name);
|
|
10854
|
+
lines.push(` ${varName},`);
|
|
10855
|
+
}
|
|
10856
|
+
lines.push(" },");
|
|
10857
|
+
}
|
|
10858
|
+
if (componentFolders.length > 0) {
|
|
10859
|
+
lines.push(" blockFolders: {");
|
|
10860
|
+
for (const folder of componentFolders) {
|
|
10861
|
+
const varName = folderVarName(folder.name);
|
|
10862
|
+
lines.push(` ${varName},`);
|
|
10863
|
+
}
|
|
10864
|
+
lines.push(" },");
|
|
10865
|
+
}
|
|
10866
|
+
if (datasources.length > 0) {
|
|
10867
|
+
lines.push(" datasources: {");
|
|
10868
|
+
for (const datasource of datasources) {
|
|
10869
|
+
const varName = datasourceVarName(datasource.name);
|
|
10870
|
+
lines.push(` ${varName},`);
|
|
10871
|
+
}
|
|
10872
|
+
lines.push(" },");
|
|
10873
|
+
}
|
|
10874
|
+
lines.push("};");
|
|
10875
|
+
lines.push("");
|
|
10876
|
+
lines.push("export type Schema = InferSchema<typeof schema>;");
|
|
10877
|
+
lines.push("export type Blocks = Schema['blocks'];");
|
|
10878
|
+
lines.push("export type Story = InferStory<Blocks>;");
|
|
10879
|
+
lines.push("export type StoryMapi = InferStoryMapi<Blocks>;");
|
|
10880
|
+
lines.push("");
|
|
10881
|
+
return lines.join("\n");
|
|
10882
|
+
}
|
|
10883
|
+
|
|
10884
|
+
async function writeFileWithDirs(filePath, content) {
|
|
10885
|
+
const dir = dirname(filePath);
|
|
10886
|
+
await mkdir(dir, { recursive: true });
|
|
10887
|
+
await writeFile(filePath, content, "utf-8");
|
|
10888
|
+
}
|
|
10889
|
+
async function writeSchemaFiles(targetPath, components, componentFolders, datasources) {
|
|
10890
|
+
const writtenFiles = [];
|
|
10891
|
+
for (const comp of components) {
|
|
10892
|
+
const fileName = componentFileName(comp.name);
|
|
10893
|
+
const filePath = join(targetPath, "components", `${fileName}.ts`);
|
|
10894
|
+
await writeFileWithDirs(filePath, generateComponentFile(comp, componentFolders));
|
|
10895
|
+
writtenFiles.push(filePath);
|
|
10896
|
+
}
|
|
10897
|
+
for (const folder of componentFolders) {
|
|
10898
|
+
const fileName = folderFileName(folder.name);
|
|
10899
|
+
const filePath = join(targetPath, "components", "folders", `${fileName}.ts`);
|
|
10900
|
+
await writeFileWithDirs(filePath, generateFolderFile(folder));
|
|
10901
|
+
writtenFiles.push(filePath);
|
|
10902
|
+
}
|
|
10903
|
+
for (const ds of datasources) {
|
|
10904
|
+
const fileName = datasourceFileName(ds);
|
|
10905
|
+
const filePath = join(targetPath, "datasources", `${fileName}.ts`);
|
|
10906
|
+
await writeFileWithDirs(filePath, generateDatasourceFile(ds));
|
|
10907
|
+
writtenFiles.push(filePath);
|
|
10908
|
+
}
|
|
10909
|
+
const schemaPath = join(targetPath, "schema.ts");
|
|
10910
|
+
await writeFileWithDirs(schemaPath, generateSchemaFile(components, componentFolders, datasources));
|
|
10911
|
+
writtenFiles.push(schemaPath);
|
|
10912
|
+
return writtenFiles;
|
|
10913
|
+
}
|
|
10914
|
+
|
|
10915
|
+
async function isTargetEmpty(targetPath) {
|
|
10916
|
+
try {
|
|
10917
|
+
const entries = await readdir(targetPath);
|
|
10918
|
+
return entries.every((entry) => entry.startsWith("."));
|
|
10919
|
+
} catch (maybeError) {
|
|
10920
|
+
const error = maybeError;
|
|
10921
|
+
if (error?.code === "ENOENT") {
|
|
10922
|
+
return true;
|
|
10923
|
+
}
|
|
10924
|
+
throw error;
|
|
10925
|
+
}
|
|
10926
|
+
}
|
|
10927
|
+
schemaCommand.command("init").description("Initialize a local code-driven schema workspace from an existing Storyblok space (one-time bootstrap)").option("-s, --space <space>", "space ID").option("--out-dir <dir>", "Output directory for generated bootstrap files", ".storyblok/schema").action(async (options, command) => {
|
|
10928
|
+
const ui = getUI();
|
|
10929
|
+
const logger = getLogger();
|
|
10930
|
+
const reporter = getReporter();
|
|
10931
|
+
const { space, verbose } = command.optsWithGlobals();
|
|
10932
|
+
const { state } = session();
|
|
10933
|
+
ui.title(commands.SCHEMA, colorPalette.SCHEMA, "Initializing schema...");
|
|
10934
|
+
logger.info("Schema init started", { space });
|
|
10935
|
+
if (!requireAuthentication(state, verbose)) {
|
|
10936
|
+
return;
|
|
10937
|
+
}
|
|
10938
|
+
if (!space) {
|
|
10939
|
+
handleError(new CommandError("Please provide the space as argument --space SPACE_ID."), verbose);
|
|
10940
|
+
return;
|
|
10941
|
+
}
|
|
10942
|
+
const targetPath = resolve(options.outDir);
|
|
10943
|
+
if (!await isTargetEmpty(targetPath)) {
|
|
10944
|
+
handleError(
|
|
10945
|
+
new CommandError(`Target directory ${targetPath} is not empty. \`schema init\` is a one-time bootstrap and refuses to overwrite existing files. Use \`schema push\` for ongoing changes, or remove the directory to re-bootstrap.`),
|
|
10946
|
+
verbose
|
|
10947
|
+
);
|
|
10948
|
+
return;
|
|
10949
|
+
}
|
|
10950
|
+
const summary = { total: 0, succeeded: 0, failed: 0 };
|
|
10951
|
+
try {
|
|
10952
|
+
const fetchSpinner = ui.createSpinner(`Fetching schema from space ${space}...`);
|
|
10953
|
+
let fetchResult;
|
|
10954
|
+
try {
|
|
10955
|
+
fetchResult = await fetchRemoteSchema(space);
|
|
10956
|
+
} catch (maybeError) {
|
|
10957
|
+
fetchSpinner.failed("Failed to fetch remote schema");
|
|
10958
|
+
handleError(toError(maybeError), verbose);
|
|
10959
|
+
return;
|
|
10960
|
+
}
|
|
10961
|
+
const { rawComponents, rawComponentFolders, rawDatasources } = fetchResult;
|
|
10962
|
+
fetchSpinner.succeed(`Found: ${rawComponents.length} components, ${rawComponentFolders.length} component folders, ${rawDatasources.length} datasources`);
|
|
10963
|
+
const writeSpinner = ui.createSpinner(`Generating TypeScript files to ${targetPath}...`);
|
|
10964
|
+
const writtenFiles = await writeSchemaFiles(targetPath, rawComponents, rawComponentFolders, rawDatasources);
|
|
10965
|
+
summary.total = writtenFiles.length;
|
|
10966
|
+
summary.succeeded = writtenFiles.length;
|
|
10967
|
+
writeSpinner.succeed(`Generated ${writtenFiles.length} files`);
|
|
10968
|
+
ui.list(writtenFiles);
|
|
10969
|
+
ui.warn("`schema init` is a one-time bootstrap step for adopting an existing space. Review generated files before continuing.");
|
|
10970
|
+
ui.info("After bootstrapping, keep your local schema as the source of truth and use `schema push` for ongoing changes.");
|
|
10971
|
+
ui.info("Make sure `@storyblok/schema` is installed in the project that imports these files (e.g. `pnpm add @storyblok/schema`).");
|
|
10972
|
+
} catch (maybeError) {
|
|
10973
|
+
summary.failed += 1;
|
|
10974
|
+
handleError(toError(maybeError), verbose);
|
|
10975
|
+
} finally {
|
|
10976
|
+
logger.info("Schema init finished", { summary });
|
|
10977
|
+
reporter.addSummary("schemaInitResults", summary);
|
|
10978
|
+
reporter.finalize();
|
|
10979
|
+
}
|
|
10980
|
+
});
|
|
10981
|
+
|
|
10982
|
+
const API_ASSIGNED_FIELDS = [
|
|
10983
|
+
"id",
|
|
10984
|
+
"created_at",
|
|
10985
|
+
"updated_at",
|
|
10986
|
+
"real_name",
|
|
10987
|
+
"all_presets",
|
|
10988
|
+
"image",
|
|
10989
|
+
"uuid"
|
|
10990
|
+
];
|
|
10991
|
+
function stripApiFields(payload) {
|
|
10992
|
+
const result = { ...payload };
|
|
10993
|
+
for (const field of API_ASSIGNED_FIELDS) {
|
|
10994
|
+
delete result[field];
|
|
10995
|
+
}
|
|
10996
|
+
return result;
|
|
10997
|
+
}
|
|
10998
|
+
async function listChangesets(basePath) {
|
|
10999
|
+
const dir = join(basePath, "schema", "changesets");
|
|
11000
|
+
if (!await fileExists(dir)) {
|
|
11001
|
+
return [];
|
|
11002
|
+
}
|
|
11003
|
+
const files = await readDirectory(dir);
|
|
11004
|
+
return files.filter((f) => f.endsWith(".json")).sort().reverse().map((f) => join(dir, f));
|
|
11005
|
+
}
|
|
11006
|
+
async function loadChangeset(filePath) {
|
|
11007
|
+
const content = await readFile$1(filePath, "utf-8");
|
|
11008
|
+
return JSON.parse(content);
|
|
11009
|
+
}
|
|
11010
|
+
function buildRollbackOps(changeset) {
|
|
11011
|
+
if (changeset.changes.length === 0) {
|
|
11012
|
+
return [];
|
|
11013
|
+
}
|
|
11014
|
+
return changeset.changes.map((entry) => {
|
|
11015
|
+
switch (entry.action) {
|
|
11016
|
+
case "create":
|
|
11017
|
+
return { type: entry.type, name: entry.name, action: "delete", payload: {} };
|
|
11018
|
+
case "update":
|
|
11019
|
+
return { type: entry.type, name: entry.name, action: "update", payload: entry.before ?? {} };
|
|
11020
|
+
case "delete":
|
|
11021
|
+
return { type: entry.type, name: entry.name, action: "create", payload: entry.before ?? {} };
|
|
11022
|
+
default:
|
|
11023
|
+
return { type: entry.type, name: entry.name, action: entry.action, payload: {} };
|
|
11024
|
+
}
|
|
11025
|
+
});
|
|
11026
|
+
}
|
|
11027
|
+
function rollbackAction(original) {
|
|
11028
|
+
switch (original) {
|
|
11029
|
+
case "create":
|
|
11030
|
+
return "delete";
|
|
11031
|
+
case "update":
|
|
11032
|
+
return "update";
|
|
11033
|
+
case "delete":
|
|
11034
|
+
return "create";
|
|
11035
|
+
}
|
|
11036
|
+
}
|
|
11037
|
+
function formatRollbackOutput(changes) {
|
|
11038
|
+
const byType = {
|
|
11039
|
+
component: [],
|
|
11040
|
+
componentFolder: [],
|
|
11041
|
+
datasource: []
|
|
11042
|
+
};
|
|
11043
|
+
for (const entry of changes) {
|
|
11044
|
+
byType[entry.type]?.push(entry);
|
|
11045
|
+
}
|
|
11046
|
+
const icons = {
|
|
11047
|
+
create: chalk.green("+"),
|
|
11048
|
+
update: chalk.yellow("~"),
|
|
11049
|
+
delete: chalk.red("-")
|
|
11050
|
+
};
|
|
11051
|
+
const lines = [];
|
|
11052
|
+
const sections = [
|
|
11053
|
+
["Components", byType.component],
|
|
11054
|
+
["Component Folders", byType.componentFolder],
|
|
11055
|
+
["Datasources", byType.datasource]
|
|
11056
|
+
];
|
|
11057
|
+
for (const [label, entries] of sections) {
|
|
11058
|
+
if (entries.length === 0) {
|
|
11059
|
+
continue;
|
|
11060
|
+
}
|
|
11061
|
+
lines.push(chalk.bold(label));
|
|
11062
|
+
for (const entry of entries) {
|
|
11063
|
+
const action = rollbackAction(entry.action);
|
|
11064
|
+
const icon = icons[action] ?? " ";
|
|
11065
|
+
const name = action === "delete" ? chalk.red(entry.name) : entry.name;
|
|
11066
|
+
lines.push(` ${icon} ${name} ${chalk.dim(`(${action})`)}`);
|
|
11067
|
+
if (entry.action === "update" && entry.before && entry.after) {
|
|
11068
|
+
let fromStr;
|
|
11069
|
+
let toStr;
|
|
11070
|
+
if (entry.type === "component") {
|
|
11071
|
+
fromStr = serializeComponent(applyDefaults(entry.after, COMPONENT_DEFAULTS));
|
|
11072
|
+
toStr = serializeComponent(applyDefaults(entry.before, COMPONENT_DEFAULTS));
|
|
11073
|
+
} else if (entry.type === "componentFolder") {
|
|
11074
|
+
fromStr = serializeComponentFolder(entry.after);
|
|
11075
|
+
toStr = serializeComponentFolder(entry.before);
|
|
11076
|
+
} else {
|
|
11077
|
+
fromStr = serializeDatasource(entry.after);
|
|
11078
|
+
toStr = serializeDatasource(entry.before);
|
|
11079
|
+
}
|
|
11080
|
+
if (fromStr !== toStr) {
|
|
11081
|
+
const patch = createTwoFilesPatch(
|
|
11082
|
+
`current/${entry.name}`,
|
|
11083
|
+
`restore/${entry.name}`,
|
|
11084
|
+
fromStr,
|
|
11085
|
+
toStr,
|
|
11086
|
+
"current",
|
|
11087
|
+
"restore"
|
|
11088
|
+
);
|
|
11089
|
+
for (const line of patch.split("\n")) {
|
|
11090
|
+
if (line.startsWith("+") && !line.startsWith("+++")) {
|
|
11091
|
+
lines.push(` ${chalk.green(line)}`);
|
|
11092
|
+
} else if (line.startsWith("-") && !line.startsWith("---")) {
|
|
11093
|
+
lines.push(` ${chalk.red(line)}`);
|
|
11094
|
+
}
|
|
11095
|
+
}
|
|
11096
|
+
}
|
|
11097
|
+
}
|
|
11098
|
+
}
|
|
11099
|
+
lines.push("");
|
|
11100
|
+
}
|
|
11101
|
+
return lines.join("\n").trimEnd();
|
|
11102
|
+
}
|
|
11103
|
+
async function executeRollback(spaceId, ops, remote) {
|
|
11104
|
+
const client = getMapiClient();
|
|
11105
|
+
const spaceIdNum = Number(spaceId);
|
|
11106
|
+
let created = 0;
|
|
11107
|
+
let updated = 0;
|
|
11108
|
+
let deleted = 0;
|
|
11109
|
+
const folderOps = ops.filter((op) => op.type === "componentFolder");
|
|
11110
|
+
const componentOps = ops.filter((op) => op.type === "component");
|
|
11111
|
+
const datasourceOps = ops.filter((op) => op.type === "datasource");
|
|
11112
|
+
const folderUuidRemap = /* @__PURE__ */ new Map();
|
|
11113
|
+
for (const op of folderOps.filter((o) => o.action !== "delete")) {
|
|
11114
|
+
if (op.action === "create") {
|
|
11115
|
+
const oldUuid = op.payload.uuid;
|
|
11116
|
+
const payload = toComponentFolderCreate(stripApiFields(op.payload));
|
|
11117
|
+
try {
|
|
11118
|
+
const response = await client.componentFolders.create({
|
|
11119
|
+
path: { space_id: spaceIdNum },
|
|
11120
|
+
body: { component_group: payload },
|
|
11121
|
+
throwOnError: true
|
|
11122
|
+
});
|
|
11123
|
+
const remoteUuid = response.data?.component_group?.uuid;
|
|
11124
|
+
if (remoteUuid && typeof oldUuid === "string") {
|
|
11125
|
+
folderUuidRemap.set(oldUuid, remoteUuid);
|
|
11126
|
+
}
|
|
11127
|
+
created++;
|
|
11128
|
+
} catch (error) {
|
|
11129
|
+
handleAPIError("push_component_group", error, `Failed to create folder ${op.name}`);
|
|
11130
|
+
}
|
|
11131
|
+
} else if (op.action === "update") {
|
|
11132
|
+
const existing = remote.componentFolders.get(op.name);
|
|
11133
|
+
if (existing?.id) {
|
|
11134
|
+
const payload = toComponentFolderCreate(stripApiFields(op.payload));
|
|
11135
|
+
try {
|
|
11136
|
+
await client.componentFolders.update(existing.id, {
|
|
11137
|
+
path: { space_id: spaceIdNum },
|
|
11138
|
+
body: { component_group: payload },
|
|
11139
|
+
throwOnError: true
|
|
11140
|
+
});
|
|
11141
|
+
updated++;
|
|
11142
|
+
} catch (error) {
|
|
11143
|
+
handleAPIError("update_component_group", error, `Failed to update folder ${op.name}`);
|
|
11144
|
+
}
|
|
11145
|
+
}
|
|
11146
|
+
}
|
|
11147
|
+
}
|
|
11148
|
+
for (const op of componentOps) {
|
|
11149
|
+
const oldUuid = op.payload.component_group_uuid;
|
|
11150
|
+
if (typeof oldUuid !== "string") {
|
|
11151
|
+
continue;
|
|
11152
|
+
}
|
|
11153
|
+
const newUuid = folderUuidRemap.get(oldUuid);
|
|
11154
|
+
if (newUuid) {
|
|
11155
|
+
op.payload.component_group_uuid = newUuid;
|
|
11156
|
+
}
|
|
11157
|
+
}
|
|
11158
|
+
for (const op of componentOps.filter((o) => o.action !== "delete")) {
|
|
11159
|
+
if (op.action === "create") {
|
|
11160
|
+
const payload = toComponentCreate(stripApiFields(op.payload));
|
|
11161
|
+
try {
|
|
11162
|
+
await client.components.create({
|
|
11163
|
+
path: { space_id: spaceIdNum },
|
|
11164
|
+
body: { component: payload },
|
|
11165
|
+
throwOnError: true
|
|
11166
|
+
});
|
|
11167
|
+
created++;
|
|
11168
|
+
} catch (error) {
|
|
11169
|
+
handleAPIError("push_component", error, `Failed to create component ${op.name}`);
|
|
11170
|
+
}
|
|
11171
|
+
} else if (op.action === "update") {
|
|
11172
|
+
const existing = remote.components.get(op.name);
|
|
11173
|
+
if (existing?.id) {
|
|
11174
|
+
const payload = toComponentUpdate(stripApiFields(op.payload));
|
|
11175
|
+
try {
|
|
11176
|
+
await client.components.update(existing.id, {
|
|
11177
|
+
path: { space_id: spaceIdNum },
|
|
11178
|
+
body: { component: payload },
|
|
11179
|
+
throwOnError: true
|
|
11180
|
+
});
|
|
11181
|
+
updated++;
|
|
11182
|
+
} catch (error) {
|
|
11183
|
+
handleAPIError("update_component", error, `Failed to update component ${op.name}`);
|
|
11184
|
+
}
|
|
11185
|
+
}
|
|
11186
|
+
}
|
|
11187
|
+
}
|
|
11188
|
+
for (const op of datasourceOps.filter((o) => o.action !== "delete")) {
|
|
11189
|
+
if (op.action === "create") {
|
|
11190
|
+
const payload = toDatasourceCreate(stripApiFields(op.payload));
|
|
11191
|
+
try {
|
|
11192
|
+
await client.datasources.create({
|
|
11193
|
+
path: { space_id: spaceIdNum },
|
|
11194
|
+
body: { datasource: payload },
|
|
11195
|
+
throwOnError: true
|
|
11196
|
+
});
|
|
11197
|
+
created++;
|
|
11198
|
+
} catch (error) {
|
|
11199
|
+
handleAPIError("push_datasource", error, `Failed to create datasource ${op.name}`);
|
|
11200
|
+
}
|
|
11201
|
+
} else if (op.action === "update") {
|
|
11202
|
+
const existing = remote.datasources.get(op.name);
|
|
11203
|
+
if (existing?.id) {
|
|
11204
|
+
const payload = toDatasourceUpdate(stripApiFields(op.payload), existing);
|
|
11205
|
+
try {
|
|
11206
|
+
await client.datasources.update(existing.id, {
|
|
11207
|
+
path: { space_id: spaceIdNum },
|
|
11208
|
+
body: { datasource: payload },
|
|
11209
|
+
throwOnError: true
|
|
11210
|
+
});
|
|
11211
|
+
updated++;
|
|
11212
|
+
} catch (error) {
|
|
11213
|
+
handleAPIError("update_datasource", error, `Failed to update datasource ${op.name}`);
|
|
11214
|
+
}
|
|
11215
|
+
}
|
|
11216
|
+
}
|
|
11217
|
+
}
|
|
11218
|
+
for (const op of datasourceOps.filter((o) => o.action === "delete")) {
|
|
11219
|
+
const existing = remote.datasources.get(op.name);
|
|
11220
|
+
if (existing?.id) {
|
|
11221
|
+
try {
|
|
11222
|
+
await client.datasources.delete(existing.id, {
|
|
11223
|
+
path: { space_id: spaceIdNum },
|
|
11224
|
+
throwOnError: true
|
|
11225
|
+
});
|
|
11226
|
+
deleted++;
|
|
11227
|
+
} catch (error) {
|
|
11228
|
+
handleAPIError("delete_datasource", error, `Failed to delete datasource ${op.name}`);
|
|
11229
|
+
}
|
|
11230
|
+
}
|
|
11231
|
+
}
|
|
11232
|
+
for (const op of componentOps.filter((o) => o.action === "delete")) {
|
|
11233
|
+
const existing = remote.components.get(op.name);
|
|
11234
|
+
if (existing?.id) {
|
|
11235
|
+
try {
|
|
11236
|
+
await client.components.delete(existing.id, {
|
|
11237
|
+
path: { space_id: spaceIdNum },
|
|
11238
|
+
throwOnError: true
|
|
11239
|
+
});
|
|
11240
|
+
deleted++;
|
|
11241
|
+
} catch (error) {
|
|
11242
|
+
handleAPIError("push_component", error, `Failed to delete component ${op.name}`);
|
|
11243
|
+
}
|
|
11244
|
+
}
|
|
11245
|
+
}
|
|
11246
|
+
for (const op of folderOps.filter((o) => o.action === "delete")) {
|
|
11247
|
+
const existing = remote.componentFolders.get(op.name);
|
|
11248
|
+
if (existing?.id) {
|
|
11249
|
+
try {
|
|
11250
|
+
await client.componentFolders.delete(existing.id, {
|
|
11251
|
+
path: { space_id: spaceIdNum },
|
|
11252
|
+
throwOnError: true
|
|
11253
|
+
});
|
|
11254
|
+
deleted++;
|
|
11255
|
+
} catch (error) {
|
|
11256
|
+
handleAPIError("push_component_group", error, `Failed to delete folder ${op.name}`);
|
|
11257
|
+
}
|
|
11258
|
+
}
|
|
11259
|
+
}
|
|
11260
|
+
return { created, updated, deleted };
|
|
11261
|
+
}
|
|
11262
|
+
|
|
11263
|
+
schemaCommand.command("rollback [changeset-file]").description("Roll back a Storyblok space to the state captured in a changeset").option("-s, --space <space>", "space ID").option("-p, --path <path>", "path for file storage").option("--dry-run", "Show what would be undone without applying changes", false).option("--yes", "Skip confirmation prompt", false).option("--latest", "Automatically select the most recent changeset", false).action(async (changesetFile, options, command) => {
|
|
11264
|
+
const ui = getUI();
|
|
11265
|
+
const logger = getLogger();
|
|
11266
|
+
const reporter = getReporter();
|
|
11267
|
+
const { space, path: basePath, verbose } = command.optsWithGlobals();
|
|
11268
|
+
const { state } = session();
|
|
11269
|
+
ui.title(commands.SCHEMA, colorPalette.SCHEMA, "Rolling back schema...");
|
|
11270
|
+
logger.info("Schema rollback started", { changesetFile, space });
|
|
11271
|
+
if (!requireAuthentication(state, verbose)) {
|
|
11272
|
+
return;
|
|
11273
|
+
}
|
|
11274
|
+
if (!space) {
|
|
11275
|
+
handleError(new CommandError("Please provide the space as argument --space SPACE_ID."), verbose);
|
|
11276
|
+
return;
|
|
11277
|
+
}
|
|
11278
|
+
const summary = { total: 0, succeeded: 0, failed: 0 };
|
|
11279
|
+
try {
|
|
11280
|
+
const resolvedBase = resolvePath(basePath, "");
|
|
11281
|
+
let resolvedFile;
|
|
11282
|
+
if (changesetFile) {
|
|
11283
|
+
resolvedFile = changesetFile;
|
|
11284
|
+
} else if (options.latest) {
|
|
11285
|
+
const available = await listChangesets(resolvedBase);
|
|
11286
|
+
if (available.length === 0) {
|
|
11287
|
+
ui.warn("No changesets found. Run `schema push` first to create one.");
|
|
11288
|
+
return;
|
|
11289
|
+
}
|
|
11290
|
+
resolvedFile = available[0];
|
|
11291
|
+
ui.info(`Using latest changeset: ${basename(resolvedFile)}`);
|
|
11292
|
+
} else {
|
|
11293
|
+
const available = await listChangesets(resolvedBase);
|
|
11294
|
+
if (available.length === 0) {
|
|
11295
|
+
ui.warn("No changesets found. Run `schema push` first to create one.");
|
|
11296
|
+
return;
|
|
11297
|
+
}
|
|
11298
|
+
resolvedFile = await select({
|
|
11299
|
+
message: "Select a changeset to roll back:",
|
|
11300
|
+
choices: available.map((f) => ({ name: basename(f), value: f }))
|
|
11301
|
+
});
|
|
11302
|
+
}
|
|
11303
|
+
let changeset;
|
|
11304
|
+
try {
|
|
11305
|
+
changeset = await loadChangeset(resolvedFile);
|
|
11306
|
+
} catch (maybeError) {
|
|
11307
|
+
handleError(toError(maybeError), verbose);
|
|
11308
|
+
return;
|
|
11309
|
+
}
|
|
11310
|
+
logger.info("Changeset loaded", { file: resolvedFile, changes: changeset.changes.length });
|
|
11311
|
+
const ops = buildRollbackOps(changeset);
|
|
11312
|
+
if (ops.length === 0) {
|
|
11313
|
+
ui.ok("Changeset has no changes \u2014 nothing to roll back.");
|
|
11314
|
+
return;
|
|
11315
|
+
}
|
|
11316
|
+
ui.br();
|
|
11317
|
+
ui.log(formatRollbackOutput(changeset.changes));
|
|
11318
|
+
if (options.dryRun) {
|
|
11319
|
+
ui.info("Dry run \u2014 no changes applied.");
|
|
11320
|
+
logger.info("Dry run completed", { ops: ops.length });
|
|
11321
|
+
return;
|
|
11322
|
+
}
|
|
11323
|
+
if (!options.yes) {
|
|
11324
|
+
const confirmed = await confirm({
|
|
11325
|
+
message: `Apply rollback of ${ops.length} change(s) from ${basename(resolvedFile)}?`,
|
|
11326
|
+
default: false
|
|
11327
|
+
});
|
|
11328
|
+
if (!confirmed) {
|
|
11329
|
+
ui.info("Rollback cancelled.");
|
|
11330
|
+
return;
|
|
11331
|
+
}
|
|
11332
|
+
}
|
|
11333
|
+
const remoteSpinner = ui.createSpinner(`Fetching current remote state from space ${space}...`);
|
|
11334
|
+
let remoteResult;
|
|
11335
|
+
try {
|
|
11336
|
+
remoteResult = await fetchRemoteSchema(space);
|
|
11337
|
+
} catch (maybeError) {
|
|
11338
|
+
remoteSpinner.failed("Failed to fetch remote schema");
|
|
11339
|
+
handleError(toError(maybeError), verbose);
|
|
11340
|
+
return;
|
|
11341
|
+
}
|
|
11342
|
+
const { remote } = remoteResult;
|
|
11343
|
+
remoteSpinner.succeed(`Remote: ${remote.components.size} components, ${remote.componentFolders.size} component folders, ${remote.datasources.size} datasources`);
|
|
11344
|
+
const rollbackSpinner = ui.createSpinner("Applying rollback...");
|
|
11345
|
+
let result;
|
|
11346
|
+
try {
|
|
11347
|
+
result = await executeRollback(space, ops, remote);
|
|
11348
|
+
} catch (error) {
|
|
11349
|
+
rollbackSpinner.failed("Failed to apply rollback");
|
|
11350
|
+
throw error;
|
|
11351
|
+
}
|
|
11352
|
+
summary.total = result.created + result.updated + result.deleted;
|
|
11353
|
+
summary.succeeded = summary.total;
|
|
11354
|
+
rollbackSpinner.succeed(`Rolled back: ${result.created} created, ${result.updated} updated, ${result.deleted} deleted.`);
|
|
11355
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
11356
|
+
const rollbackChangesetPath = await saveChangeset(resolvedBase, {
|
|
11357
|
+
timestamp,
|
|
11358
|
+
spaceId: Number(space),
|
|
11359
|
+
remote: { components: remoteResult.rawComponents, componentFolders: remoteResult.rawComponentFolders, datasources: remoteResult.rawDatasources },
|
|
11360
|
+
changes: ops.map((op) => ({
|
|
11361
|
+
type: op.type,
|
|
11362
|
+
name: op.name,
|
|
11363
|
+
action: op.action,
|
|
11364
|
+
...Object.keys(op.payload).length > 0 && { after: op.payload }
|
|
11365
|
+
}))
|
|
11366
|
+
});
|
|
11367
|
+
logger.info("Rollback changeset saved", { path: rollbackChangesetPath });
|
|
11368
|
+
} catch (maybeError) {
|
|
11369
|
+
summary.failed += 1;
|
|
11370
|
+
handleError(toError(maybeError), verbose);
|
|
11371
|
+
} finally {
|
|
11372
|
+
logger.info("Schema rollback finished", { summary });
|
|
11373
|
+
reporter.addSummary("schemaRollbackResults", summary);
|
|
11374
|
+
reporter.finalize();
|
|
11375
|
+
}
|
|
11376
|
+
});
|
|
11377
|
+
|
|
9446
11378
|
const program = getProgram();
|
|
9447
11379
|
konsola.br();
|
|
9448
11380
|
konsola.br();
|