storyblok 4.0.5 → 4.2.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 +901 -86
- package/dist/index.mjs.map +1 -1
- package/package.json +6 -4
package/dist/index.mjs
CHANGED
|
@@ -6,25 +6,29 @@ import chalk from 'chalk';
|
|
|
6
6
|
import { Command } from 'commander';
|
|
7
7
|
import { readPackageUp } from 'read-package-up';
|
|
8
8
|
import { Spinner } from '@topcli/spinner';
|
|
9
|
-
import { select, password, input } from '@inquirer/prompts';
|
|
10
|
-
import { mkdir, writeFile, readFile as readFile$1, access, readdir } from 'node:fs/promises';
|
|
11
|
-
import { join, parse, resolve } from 'node:path';
|
|
12
|
-
import { exec } from 'node:child_process';
|
|
9
|
+
import { select, password, input, confirm } from '@inquirer/prompts';
|
|
10
|
+
import fs, { mkdir, writeFile, readFile as readFile$1, access, readdir } from 'node:fs/promises';
|
|
11
|
+
import path, { join, parse, resolve } from 'node:path';
|
|
12
|
+
import { exec, spawn } from 'node:child_process';
|
|
13
13
|
import { promisify } from 'node:util';
|
|
14
14
|
import { minimatch } from 'minimatch';
|
|
15
15
|
import { hash } from 'ohash';
|
|
16
16
|
import { compile } from 'json-schema-to-typescript';
|
|
17
17
|
import { readFileSync } from 'node:fs';
|
|
18
|
+
import open from 'open';
|
|
19
|
+
import { Octokit } from 'octokit';
|
|
18
20
|
|
|
19
21
|
const commands = {
|
|
20
22
|
LOGIN: "login",
|
|
21
23
|
LOGOUT: "logout",
|
|
22
24
|
SIGNUP: "signup",
|
|
23
25
|
USER: "user",
|
|
24
|
-
COMPONENTS: "
|
|
26
|
+
COMPONENTS: "components",
|
|
25
27
|
LANGUAGES: "languages",
|
|
26
28
|
MIGRATIONS: "Migrations",
|
|
27
|
-
TYPES: "Types"
|
|
29
|
+
TYPES: "Types",
|
|
30
|
+
DATASOURCES: "Datasources",
|
|
31
|
+
CREATE: "create"
|
|
28
32
|
};
|
|
29
33
|
const colorPalette = {
|
|
30
34
|
PRIMARY: "#8d60ff",
|
|
@@ -36,9 +40,11 @@ const colorPalette = {
|
|
|
36
40
|
LANGUAGES: "#f5c003",
|
|
37
41
|
MIGRATIONS: "#8CE2FF",
|
|
38
42
|
TYPES: "#3178C6",
|
|
43
|
+
CREATE: "#ffb3ba",
|
|
39
44
|
GROUPS: "#4ade80",
|
|
40
45
|
TAGS: "#fbbf24",
|
|
41
|
-
PRESETS: "#a855f7"
|
|
46
|
+
PRESETS: "#a855f7",
|
|
47
|
+
DATASOURCES: "#4ade80"
|
|
42
48
|
};
|
|
43
49
|
const regions = {
|
|
44
50
|
EU: "eu",
|
|
@@ -54,6 +60,13 @@ const regionsDomain = {
|
|
|
54
60
|
ca: "api-ca.storyblok.com",
|
|
55
61
|
ap: "api-ap.storyblok.com"
|
|
56
62
|
};
|
|
63
|
+
const appDomains = {
|
|
64
|
+
eu: "app.storyblok.com",
|
|
65
|
+
us: "app-us.storyblok.com",
|
|
66
|
+
cn: "app.storyblokchina.cn",
|
|
67
|
+
ca: "app-ca.storyblok.com",
|
|
68
|
+
ap: "app-ap.storyblok.com"
|
|
69
|
+
};
|
|
57
70
|
const regionNames = {
|
|
58
71
|
eu: "Europe",
|
|
59
72
|
us: "United States",
|
|
@@ -159,7 +172,13 @@ const API_ACTIONS = {
|
|
|
159
172
|
update_component_preset: "Failed to update component preset",
|
|
160
173
|
pull_stories: "Failed to pull stories",
|
|
161
174
|
pull_story: "Failed to pull story",
|
|
162
|
-
update_story: "Failed to update story"
|
|
175
|
+
update_story: "Failed to update story",
|
|
176
|
+
pull_datasources: "Failed to pull datasources",
|
|
177
|
+
push_datasource: "Failed to push datasource",
|
|
178
|
+
update_datasource: "Failed to update datasource",
|
|
179
|
+
delete_datasource: "Failed to delete datasource",
|
|
180
|
+
create_space: "Failed to create space",
|
|
181
|
+
fetch_blueprints: "Failed to fetch blueprints from GitHub"
|
|
163
182
|
};
|
|
164
183
|
const API_ERRORS = {
|
|
165
184
|
unauthorized: "The user is not authorized to access the API",
|
|
@@ -388,6 +407,9 @@ const toCamelCase = (str) => {
|
|
|
388
407
|
const capitalize = (str) => {
|
|
389
408
|
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
390
409
|
};
|
|
410
|
+
const toHumanReadable = (str) => {
|
|
411
|
+
return str.replace(/[-_]/g, " ").replace(/([a-z])([A-Z])/g, "$1 $2").split(" ").map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join(" ").replace(/\s+/g, " ").trim();
|
|
412
|
+
};
|
|
391
413
|
function maskToken(token) {
|
|
392
414
|
if (token.length <= 4) {
|
|
393
415
|
return token;
|
|
@@ -591,6 +613,18 @@ const resolvePath = (path, folder) => {
|
|
|
591
613
|
const getComponentNameFromFilename = (filename) => {
|
|
592
614
|
return filename.replace(/\.js$/, "");
|
|
593
615
|
};
|
|
616
|
+
async function readJsonFile(filePath) {
|
|
617
|
+
try {
|
|
618
|
+
const content = (await readFile(filePath)).toString();
|
|
619
|
+
if (!content) {
|
|
620
|
+
return { data: [] };
|
|
621
|
+
}
|
|
622
|
+
const parsed = JSON.parse(content);
|
|
623
|
+
return { data: Array.isArray(parsed) ? parsed : [parsed] };
|
|
624
|
+
} catch (error) {
|
|
625
|
+
return { data: [], error };
|
|
626
|
+
}
|
|
627
|
+
}
|
|
594
628
|
|
|
595
629
|
const getCredentials = async (filePath = join(getStoryblokGlobalPath(), "credentials.json")) => {
|
|
596
630
|
try {
|
|
@@ -718,7 +752,7 @@ function session() {
|
|
|
718
752
|
return sessionInstance;
|
|
719
753
|
}
|
|
720
754
|
|
|
721
|
-
const program$
|
|
755
|
+
const program$i = getProgram();
|
|
722
756
|
const allRegionsText = Object.values(regions).join(",");
|
|
723
757
|
const loginStrategy = {
|
|
724
758
|
message: "How would you like to login?",
|
|
@@ -735,12 +769,12 @@ const loginStrategy = {
|
|
|
735
769
|
}
|
|
736
770
|
]
|
|
737
771
|
};
|
|
738
|
-
program$
|
|
772
|
+
program$i.command(commands.LOGIN).description("Login to the Storyblok CLI").option("-t, --token <token>", "Token to login directly without questions, like for CI environments").option(
|
|
739
773
|
"-r, --region <region>",
|
|
740
774
|
`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}.`
|
|
741
775
|
).action(async (options) => {
|
|
742
776
|
konsola.title(` ${commands.LOGIN} `, colorPalette.LOGIN);
|
|
743
|
-
const verbose = program$
|
|
777
|
+
const verbose = program$i.opts().verbose;
|
|
744
778
|
const { token, region } = options;
|
|
745
779
|
const { state, updateSession, persistCredentials, initializeSession } = session();
|
|
746
780
|
await initializeSession();
|
|
@@ -859,10 +893,10 @@ program$e.command(commands.LOGIN).description("Login to the Storyblok CLI").opti
|
|
|
859
893
|
konsola.br();
|
|
860
894
|
});
|
|
861
895
|
|
|
862
|
-
const program$
|
|
863
|
-
program$
|
|
896
|
+
const program$h = getProgram();
|
|
897
|
+
program$h.command(commands.LOGOUT).description("Logout from the Storyblok CLI").action(async () => {
|
|
864
898
|
konsola.title(` ${commands.LOGOUT} `, colorPalette.LOGOUT);
|
|
865
|
-
const verbose = program$
|
|
899
|
+
const verbose = program$h.opts().verbose;
|
|
866
900
|
try {
|
|
867
901
|
const { state, initializeSession } = session();
|
|
868
902
|
await initializeSession();
|
|
@@ -910,10 +944,10 @@ async function openSignupInBrowser(url) {
|
|
|
910
944
|
}
|
|
911
945
|
}
|
|
912
946
|
|
|
913
|
-
const program$
|
|
914
|
-
program$
|
|
947
|
+
const program$g = getProgram();
|
|
948
|
+
program$g.command(commands.SIGNUP).description("Sign up for Storyblok").action(async () => {
|
|
915
949
|
konsola.title(` ${commands.SIGNUP} `, colorPalette.SIGNUP);
|
|
916
|
-
const verbose = program$
|
|
950
|
+
const verbose = program$g.opts().verbose;
|
|
917
951
|
const { state, initializeSession } = session();
|
|
918
952
|
await initializeSession();
|
|
919
953
|
if (state.isLoggedIn && !state.envLogin) {
|
|
@@ -959,8 +993,8 @@ const getUser = async (token, region) => {
|
|
|
959
993
|
}
|
|
960
994
|
};
|
|
961
995
|
|
|
962
|
-
const program$
|
|
963
|
-
program$
|
|
996
|
+
const program$f = getProgram();
|
|
997
|
+
program$f.command(commands.USER).description("Get the current user").action(async () => {
|
|
964
998
|
konsola.title(` ${commands.USER} `, colorPalette.USER);
|
|
965
999
|
const { state, initializeSession } = session();
|
|
966
1000
|
await initializeSession();
|
|
@@ -985,8 +1019,8 @@ program$b.command(commands.USER).description("Get the current user").action(asyn
|
|
|
985
1019
|
konsola.br();
|
|
986
1020
|
});
|
|
987
1021
|
|
|
988
|
-
const program$
|
|
989
|
-
const componentsCommand = program$
|
|
1022
|
+
const program$e = getProgram();
|
|
1023
|
+
const componentsCommand = program$e.command("components").alias("comp").description(`Manage your space's block schema`).option("-s, --space <space>", "space ID").option("-p, --path <path>", "path to save the file. Default is .storyblok/components");
|
|
990
1024
|
|
|
991
1025
|
let instance = null;
|
|
992
1026
|
const createMapiClient = (options) => {
|
|
@@ -1037,14 +1071,18 @@ const createMapiClient = (options) => {
|
|
|
1037
1071
|
...fetchOptions
|
|
1038
1072
|
});
|
|
1039
1073
|
let data;
|
|
1040
|
-
|
|
1041
|
-
data =
|
|
1042
|
-
}
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1074
|
+
if (res.status === 204 || res.headers.get("content-length") === "0") {
|
|
1075
|
+
data = null;
|
|
1076
|
+
} else {
|
|
1077
|
+
try {
|
|
1078
|
+
data = await res.json();
|
|
1079
|
+
} catch {
|
|
1080
|
+
throw new FetchError("Non-JSON response", {
|
|
1081
|
+
status: res.status,
|
|
1082
|
+
statusText: res.statusText,
|
|
1083
|
+
data: null
|
|
1084
|
+
});
|
|
1085
|
+
}
|
|
1048
1086
|
}
|
|
1049
1087
|
options?.onResponse?.({
|
|
1050
1088
|
path,
|
|
@@ -1111,11 +1149,15 @@ const createMapiClient = (options) => {
|
|
|
1111
1149
|
const put = async (path, fetchOptions) => {
|
|
1112
1150
|
return request(path, { ...fetchOptions, method: "PUT" });
|
|
1113
1151
|
};
|
|
1152
|
+
const _delete = async (path, fetchOptions) => {
|
|
1153
|
+
return request(path, { ...fetchOptions, method: "DELETE" });
|
|
1154
|
+
};
|
|
1114
1155
|
instance = {
|
|
1115
1156
|
uuid: state.uuid,
|
|
1116
1157
|
get,
|
|
1117
1158
|
post,
|
|
1118
1159
|
put,
|
|
1160
|
+
delete: _delete,
|
|
1119
1161
|
dispose: () => {
|
|
1120
1162
|
instance = null;
|
|
1121
1163
|
}
|
|
@@ -1332,20 +1374,8 @@ const upsertComponentInternalTag = async (space, tag, existingId) => {
|
|
|
1332
1374
|
return await pushComponentInternalTag(space, tag);
|
|
1333
1375
|
}
|
|
1334
1376
|
};
|
|
1335
|
-
async function readJsonFile(filePath) {
|
|
1336
|
-
try {
|
|
1337
|
-
const content = (await readFile$1(filePath)).toString();
|
|
1338
|
-
if (!content) {
|
|
1339
|
-
return { data: [] };
|
|
1340
|
-
}
|
|
1341
|
-
const parsed = JSON.parse(content);
|
|
1342
|
-
return { data: Array.isArray(parsed) ? parsed : [parsed] };
|
|
1343
|
-
} catch (error) {
|
|
1344
|
-
return { data: [], error };
|
|
1345
|
-
}
|
|
1346
|
-
}
|
|
1347
1377
|
const readComponentsFiles = async (options) => {
|
|
1348
|
-
const { from, path, separateFiles = false, suffix
|
|
1378
|
+
const { from, path, separateFiles = false, suffix } = options;
|
|
1349
1379
|
const resolvedPath = resolvePath(path, `components/${from}`);
|
|
1350
1380
|
try {
|
|
1351
1381
|
await readdir(resolvedPath);
|
|
@@ -1356,7 +1386,7 @@ const readComponentsFiles = async (options) => {
|
|
|
1356
1386
|
${chalk.cyan(`storyblok components pull --space ${from}`)}
|
|
1357
1387
|
|
|
1358
1388
|
2. Then try pushing again:
|
|
1359
|
-
${chalk.cyan(`storyblok components push --space
|
|
1389
|
+
${chalk.cyan(`storyblok components push --space <target_space> --from ${from}`)}`;
|
|
1360
1390
|
throw new FileSystemError(
|
|
1361
1391
|
"file_not_found",
|
|
1362
1392
|
"read",
|
|
@@ -1365,11 +1395,11 @@ const readComponentsFiles = async (options) => {
|
|
|
1365
1395
|
);
|
|
1366
1396
|
}
|
|
1367
1397
|
if (separateFiles) {
|
|
1368
|
-
return await readSeparateFiles(resolvedPath, suffix);
|
|
1398
|
+
return await readSeparateFiles$1(resolvedPath, suffix);
|
|
1369
1399
|
}
|
|
1370
|
-
return await readConsolidatedFiles(resolvedPath, suffix);
|
|
1400
|
+
return await readConsolidatedFiles$1(resolvedPath, suffix);
|
|
1371
1401
|
};
|
|
1372
|
-
async function readSeparateFiles(resolvedPath, suffix) {
|
|
1402
|
+
async function readSeparateFiles$1(resolvedPath, suffix) {
|
|
1373
1403
|
const files = await readdir(resolvedPath);
|
|
1374
1404
|
const components = [];
|
|
1375
1405
|
const presets = [];
|
|
@@ -1424,7 +1454,7 @@ async function readSeparateFiles(resolvedPath, suffix) {
|
|
|
1424
1454
|
internalTags
|
|
1425
1455
|
};
|
|
1426
1456
|
}
|
|
1427
|
-
async function readConsolidatedFiles(resolvedPath, suffix) {
|
|
1457
|
+
async function readConsolidatedFiles$1(resolvedPath, suffix) {
|
|
1428
1458
|
const componentsPath = join(resolvedPath, suffix ? `components.${suffix}.json` : "components.json");
|
|
1429
1459
|
const componentsResult = await readJsonFile(componentsPath);
|
|
1430
1460
|
if (componentsResult.error || !componentsResult.data.length) {
|
|
@@ -1448,10 +1478,10 @@ async function readConsolidatedFiles(resolvedPath, suffix) {
|
|
|
1448
1478
|
};
|
|
1449
1479
|
}
|
|
1450
1480
|
|
|
1451
|
-
const program$
|
|
1481
|
+
const program$d = getProgram();
|
|
1452
1482
|
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)").description(`Download your space's components schema as json. Optionally specify a component name to pull a single component.`).action(async (componentName, options) => {
|
|
1453
1483
|
konsola.title(` ${commands.COMPONENTS} `, colorPalette.COMPONENTS, componentName ? `Pulling component ${componentName}...` : "Pulling components...");
|
|
1454
|
-
const verbose = program$
|
|
1484
|
+
const verbose = program$d.opts().verbose;
|
|
1455
1485
|
const { space, path } = componentsCommand.opts();
|
|
1456
1486
|
const { separateFiles, suffix, filename = "components" } = options;
|
|
1457
1487
|
const { state, initializeSession } = session();
|
|
@@ -1539,6 +1569,153 @@ componentsCommand.command("pull [componentName]").option("-f, --filename <filena
|
|
|
1539
1569
|
}
|
|
1540
1570
|
});
|
|
1541
1571
|
|
|
1572
|
+
const pushDatasource = async (space, datasource) => {
|
|
1573
|
+
try {
|
|
1574
|
+
const client = mapiClient();
|
|
1575
|
+
const { data } = await client.post(`spaces/${space}/datasources`, {
|
|
1576
|
+
body: JSON.stringify(datasource)
|
|
1577
|
+
});
|
|
1578
|
+
return data.datasource;
|
|
1579
|
+
} catch (error) {
|
|
1580
|
+
handleAPIError("push_datasource", error, `Failed to push datasource ${datasource.name}`);
|
|
1581
|
+
}
|
|
1582
|
+
};
|
|
1583
|
+
const updateDatasource = async (space, datasourceId, datasource) => {
|
|
1584
|
+
try {
|
|
1585
|
+
const client = mapiClient();
|
|
1586
|
+
const { data } = await client.put(`spaces/${space}/datasources/${datasourceId}`, {
|
|
1587
|
+
body: JSON.stringify(datasource)
|
|
1588
|
+
});
|
|
1589
|
+
return data.datasource;
|
|
1590
|
+
} catch (error) {
|
|
1591
|
+
handleAPIError("update_datasource", error, `Failed to update datasource ${datasource.name}`);
|
|
1592
|
+
}
|
|
1593
|
+
};
|
|
1594
|
+
const upsertDatasource = async (space, datasource, existingId) => {
|
|
1595
|
+
if (existingId) {
|
|
1596
|
+
return await updateDatasource(space, existingId, datasource);
|
|
1597
|
+
} else {
|
|
1598
|
+
return await pushDatasource(space, datasource);
|
|
1599
|
+
}
|
|
1600
|
+
};
|
|
1601
|
+
const pushDatasourceEntry = async (space, datasourceId, entry) => {
|
|
1602
|
+
try {
|
|
1603
|
+
const client = mapiClient();
|
|
1604
|
+
const { data } = await client.post(`spaces/${space}/datasource_entries`, {
|
|
1605
|
+
body: JSON.stringify({
|
|
1606
|
+
datasource_entry: {
|
|
1607
|
+
...entry,
|
|
1608
|
+
datasource_id: datasourceId
|
|
1609
|
+
}
|
|
1610
|
+
})
|
|
1611
|
+
});
|
|
1612
|
+
return data.datasource_entry;
|
|
1613
|
+
} catch (error) {
|
|
1614
|
+
handleAPIError("push_datasource", error, `Failed to push datasource entry ${entry.name}`);
|
|
1615
|
+
}
|
|
1616
|
+
};
|
|
1617
|
+
const updateDatasourceEntry = async (space, entryId, entry) => {
|
|
1618
|
+
try {
|
|
1619
|
+
const client = mapiClient();
|
|
1620
|
+
await client.put(`spaces/${space}/datasource_entries/${entryId}`, {
|
|
1621
|
+
body: JSON.stringify({
|
|
1622
|
+
datasource_entry: entry
|
|
1623
|
+
})
|
|
1624
|
+
});
|
|
1625
|
+
} catch (error) {
|
|
1626
|
+
handleAPIError("update_datasource", error, `Failed to update datasource entry ${entry.name}`);
|
|
1627
|
+
}
|
|
1628
|
+
};
|
|
1629
|
+
const upsertDatasourceEntry = async (space, datasourceId, entry, existingId) => {
|
|
1630
|
+
if (existingId) {
|
|
1631
|
+
await updateDatasourceEntry(space, existingId, entry);
|
|
1632
|
+
return void 0;
|
|
1633
|
+
} else {
|
|
1634
|
+
return await pushDatasourceEntry(space, datasourceId, entry);
|
|
1635
|
+
}
|
|
1636
|
+
};
|
|
1637
|
+
const readDatasourcesFiles = async (options) => {
|
|
1638
|
+
const { from, path, separateFiles = false, suffix, space } = options;
|
|
1639
|
+
const resolvedPath = resolvePath(path, `datasources/${from}`);
|
|
1640
|
+
try {
|
|
1641
|
+
await readdir(resolvedPath);
|
|
1642
|
+
} catch (error) {
|
|
1643
|
+
const message = `No local datasources found for space ${chalk.bold(from)}. To push datasources, you need to pull them first:
|
|
1644
|
+
|
|
1645
|
+
1. Pull the datasources from your source space:
|
|
1646
|
+
${chalk.cyan(`storyblok datasources pull --space ${from}`)}
|
|
1647
|
+
|
|
1648
|
+
2. Then try pushing again:
|
|
1649
|
+
${chalk.cyan(`storyblok datasources push --space ${space} --from ${from}`)}`;
|
|
1650
|
+
throw new FileSystemError(
|
|
1651
|
+
"file_not_found",
|
|
1652
|
+
"read",
|
|
1653
|
+
error,
|
|
1654
|
+
message
|
|
1655
|
+
);
|
|
1656
|
+
}
|
|
1657
|
+
if (separateFiles) {
|
|
1658
|
+
return await readSeparateFiles(resolvedPath, suffix);
|
|
1659
|
+
}
|
|
1660
|
+
return await readConsolidatedFiles(resolvedPath, suffix);
|
|
1661
|
+
};
|
|
1662
|
+
async function readSeparateFiles(resolvedPath, suffix) {
|
|
1663
|
+
const files = await readdir(resolvedPath);
|
|
1664
|
+
const datasources = [];
|
|
1665
|
+
const filteredFiles = files.filter((file) => {
|
|
1666
|
+
if (suffix) {
|
|
1667
|
+
return file.endsWith(`.${suffix}.json`);
|
|
1668
|
+
} else {
|
|
1669
|
+
return !/\.\w+\.json$/.test(file);
|
|
1670
|
+
}
|
|
1671
|
+
});
|
|
1672
|
+
for (const file of filteredFiles) {
|
|
1673
|
+
const filePath = join(resolvedPath, file);
|
|
1674
|
+
if (file.endsWith(".json") || file.endsWith(`${suffix}.json`)) {
|
|
1675
|
+
if (file === "datasources.json" || /^datasources\.\w+\.json$/.test(file)) {
|
|
1676
|
+
continue;
|
|
1677
|
+
}
|
|
1678
|
+
const result = await readJsonFile(filePath);
|
|
1679
|
+
if (result.error) {
|
|
1680
|
+
handleFileSystemError("read", result.error);
|
|
1681
|
+
continue;
|
|
1682
|
+
}
|
|
1683
|
+
datasources.push(...result.data);
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
return {
|
|
1687
|
+
datasources
|
|
1688
|
+
};
|
|
1689
|
+
}
|
|
1690
|
+
async function readConsolidatedFiles(resolvedPath, suffix) {
|
|
1691
|
+
const datasourcesPath = join(resolvedPath, suffix ? `datasources.${suffix}.json` : "datasources.json");
|
|
1692
|
+
const datasourcesResult = await readJsonFile(datasourcesPath);
|
|
1693
|
+
if (datasourcesResult.error || !datasourcesResult.data.length) {
|
|
1694
|
+
throw new FileSystemError(
|
|
1695
|
+
"file_not_found",
|
|
1696
|
+
"read",
|
|
1697
|
+
datasourcesResult.error || new Error("Datasources file is empty"),
|
|
1698
|
+
`No datasources found in ${datasourcesPath}. Please make sure you have pulled the datasources first.`
|
|
1699
|
+
);
|
|
1700
|
+
}
|
|
1701
|
+
return {
|
|
1702
|
+
datasources: datasourcesResult.data
|
|
1703
|
+
};
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
function createStubDatasource(name) {
|
|
1707
|
+
return {
|
|
1708
|
+
id: 0,
|
|
1709
|
+
// Will be set by API
|
|
1710
|
+
name,
|
|
1711
|
+
slug: name,
|
|
1712
|
+
dimensions: [],
|
|
1713
|
+
entries: [],
|
|
1714
|
+
// Empty entries for stub
|
|
1715
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1716
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1717
|
+
};
|
|
1718
|
+
}
|
|
1542
1719
|
function buildDependencyGraph(context) {
|
|
1543
1720
|
const { spaceState } = context;
|
|
1544
1721
|
const graph = { nodes: /* @__PURE__ */ new Map() };
|
|
@@ -1550,6 +1727,22 @@ function buildDependencyGraph(context) {
|
|
|
1550
1727
|
dependency.dependents.add(dependentId);
|
|
1551
1728
|
}
|
|
1552
1729
|
}
|
|
1730
|
+
const referencedDatasources = /* @__PURE__ */ new Set();
|
|
1731
|
+
spaceState.local.components.forEach((component) => {
|
|
1732
|
+
if (component.schema) {
|
|
1733
|
+
const dependencies = collectWhitelistDependencies(component.schema);
|
|
1734
|
+
dependencies.datasourceNames.forEach((datasourceName) => {
|
|
1735
|
+
referencedDatasources.add(datasourceName);
|
|
1736
|
+
});
|
|
1737
|
+
}
|
|
1738
|
+
});
|
|
1739
|
+
referencedDatasources.forEach((datasourceName) => {
|
|
1740
|
+
const nodeId = `datasource:${datasourceName}`;
|
|
1741
|
+
const targetDatasource = spaceState.target.datasources?.get(datasourceName);
|
|
1742
|
+
const stubDatasource = createStubDatasource(datasourceName);
|
|
1743
|
+
const node = new DatasourceNode(nodeId, stubDatasource, targetDatasource);
|
|
1744
|
+
graph.nodes.set(nodeId, node);
|
|
1745
|
+
});
|
|
1553
1746
|
spaceState.local.internalTags.forEach((tag) => {
|
|
1554
1747
|
const nodeId = `tag:${tag.id}`;
|
|
1555
1748
|
const targetTag = spaceState.target.tags.get(tag.name);
|
|
@@ -1625,6 +1818,10 @@ function buildDependencyGraph(context) {
|
|
|
1625
1818
|
const dependencyId = `component:${componentName}`;
|
|
1626
1819
|
addDependency(componentId, dependencyId);
|
|
1627
1820
|
});
|
|
1821
|
+
dependencies.datasourceNames.forEach((datasourceName) => {
|
|
1822
|
+
const datasourceId = `datasource:${datasourceName}`;
|
|
1823
|
+
addDependency(componentId, datasourceId);
|
|
1824
|
+
});
|
|
1628
1825
|
}
|
|
1629
1826
|
});
|
|
1630
1827
|
spaceState.local.presets.forEach((preset) => {
|
|
@@ -1641,6 +1838,7 @@ function collectWhitelistDependencies(schema) {
|
|
|
1641
1838
|
const groupUuids = /* @__PURE__ */ new Set();
|
|
1642
1839
|
const tagIds = /* @__PURE__ */ new Set();
|
|
1643
1840
|
const componentNames = /* @__PURE__ */ new Set();
|
|
1841
|
+
const datasourceNames = /* @__PURE__ */ new Set();
|
|
1644
1842
|
function traverseField(field) {
|
|
1645
1843
|
if (field.type === "bloks") {
|
|
1646
1844
|
if (field.component_group_whitelist && Array.isArray(field.component_group_whitelist)) {
|
|
@@ -1653,6 +1851,11 @@ function collectWhitelistDependencies(schema) {
|
|
|
1653
1851
|
field.component_whitelist.forEach((name) => componentNames.add(name));
|
|
1654
1852
|
}
|
|
1655
1853
|
}
|
|
1854
|
+
if ((field.type === "option" || field.type === "options") && field.source === "internal") {
|
|
1855
|
+
if (field.datasource_slug && typeof field.datasource_slug === "string") {
|
|
1856
|
+
datasourceNames.add(field.datasource_slug);
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1656
1859
|
Object.values(field).forEach((value) => {
|
|
1657
1860
|
if (Array.isArray(value)) {
|
|
1658
1861
|
value.forEach((item) => {
|
|
@@ -1670,7 +1873,7 @@ function collectWhitelistDependencies(schema) {
|
|
|
1670
1873
|
traverseField(field);
|
|
1671
1874
|
}
|
|
1672
1875
|
});
|
|
1673
|
-
return { groupUuids, tagIds, componentNames };
|
|
1876
|
+
return { groupUuids, tagIds, componentNames, datasourceNames };
|
|
1674
1877
|
}
|
|
1675
1878
|
function detectProblematicCycles(graph) {
|
|
1676
1879
|
const problematicCycles = [];
|
|
@@ -2001,6 +2204,15 @@ class ComponentNode extends GraphNode {
|
|
|
2001
2204
|
});
|
|
2002
2205
|
}
|
|
2003
2206
|
}
|
|
2207
|
+
if ((resolvedField.type === "option" || resolvedField.type === "options") && resolvedField.source === "internal") {
|
|
2208
|
+
if (resolvedField.datasource_slug && typeof resolvedField.datasource_slug === "string") {
|
|
2209
|
+
const datasourceNodeId = `datasource:${resolvedField.datasource_slug}`;
|
|
2210
|
+
const datasourceNode = graph.nodes.get(datasourceNodeId);
|
|
2211
|
+
if (datasourceNode?.targetData) {
|
|
2212
|
+
resolvedField.datasource_slug = datasourceNode.targetData.resource.slug;
|
|
2213
|
+
}
|
|
2214
|
+
}
|
|
2215
|
+
}
|
|
2004
2216
|
Object.keys(resolvedField).forEach((key) => {
|
|
2005
2217
|
if (typeof resolvedField[key] === "object" && resolvedField[key] !== null) {
|
|
2006
2218
|
resolvedField[key] = resolveField(resolvedField[key]);
|
|
@@ -2072,6 +2284,23 @@ class PresetNode {
|
|
|
2072
2284
|
};
|
|
2073
2285
|
}
|
|
2074
2286
|
}
|
|
2287
|
+
class DatasourceNode extends GraphNode {
|
|
2288
|
+
constructor(id, data, targetDatasource) {
|
|
2289
|
+
super(id, "datasource", data.name, data, targetDatasource);
|
|
2290
|
+
}
|
|
2291
|
+
resolveReferences(_graph) {
|
|
2292
|
+
}
|
|
2293
|
+
async upsert(space) {
|
|
2294
|
+
const existingDatasource = this.targetData?.resource;
|
|
2295
|
+
const existingId = existingDatasource?.id;
|
|
2296
|
+
const { entries, ...datasourceDefinition } = this.sourceData;
|
|
2297
|
+
const result = await upsertDatasource(space, datasourceDefinition, existingId);
|
|
2298
|
+
if (!result) {
|
|
2299
|
+
throw new Error(`Failed to upsert datasource ${this.name}`);
|
|
2300
|
+
}
|
|
2301
|
+
return result;
|
|
2302
|
+
}
|
|
2303
|
+
}
|
|
2075
2304
|
|
|
2076
2305
|
function collectAllDependencies(components, allComponents, allGroups, allTags) {
|
|
2077
2306
|
const requiredComponents = /* @__PURE__ */ new Set();
|
|
@@ -2472,10 +2701,67 @@ async function pushWithDependencyGraph(space, spaceState, maxConcurrency = 5) {
|
|
|
2472
2701
|
return results;
|
|
2473
2702
|
}
|
|
2474
2703
|
|
|
2475
|
-
const
|
|
2704
|
+
const fetchDatasourceEntries = async (space, datasourceId) => {
|
|
2705
|
+
try {
|
|
2706
|
+
const client = mapiClient();
|
|
2707
|
+
const { data } = await client.get(`spaces/${space}/datasource_entries?datasource_id=${datasourceId}`);
|
|
2708
|
+
return data.datasource_entries;
|
|
2709
|
+
} catch (error) {
|
|
2710
|
+
handleAPIError("pull_datasources", error);
|
|
2711
|
+
}
|
|
2712
|
+
};
|
|
2713
|
+
const fetchDatasources = async (space) => {
|
|
2714
|
+
try {
|
|
2715
|
+
const client = mapiClient();
|
|
2716
|
+
const { data } = await client.get(`spaces/${space}/datasources`);
|
|
2717
|
+
const datasources = data.datasources;
|
|
2718
|
+
const datasourcesWithEntries = await Promise.all(
|
|
2719
|
+
datasources.map(async (ds) => {
|
|
2720
|
+
const entries = await fetchDatasourceEntries(space, ds.id);
|
|
2721
|
+
return { ...ds, entries };
|
|
2722
|
+
})
|
|
2723
|
+
);
|
|
2724
|
+
return datasourcesWithEntries;
|
|
2725
|
+
} catch (error) {
|
|
2726
|
+
handleAPIError("pull_datasources", error);
|
|
2727
|
+
}
|
|
2728
|
+
};
|
|
2729
|
+
const fetchDatasource = async (space, datasourceName) => {
|
|
2730
|
+
try {
|
|
2731
|
+
const client = mapiClient();
|
|
2732
|
+
const { data } = await client.get(`spaces/${space}/datasources?search=${encodeURIComponent(datasourceName)}`);
|
|
2733
|
+
const found = data.datasources?.find((d) => d.name === datasourceName);
|
|
2734
|
+
if (!found) {
|
|
2735
|
+
return void 0;
|
|
2736
|
+
}
|
|
2737
|
+
const entries = await fetchDatasourceEntries(space, found.id);
|
|
2738
|
+
return { ...found, entries };
|
|
2739
|
+
} catch (error) {
|
|
2740
|
+
handleAPIError("pull_datasources", error, `Failed to fetch datasource ${datasourceName}`);
|
|
2741
|
+
}
|
|
2742
|
+
};
|
|
2743
|
+
const saveDatasourcesToFiles = async (space, datasources, options) => {
|
|
2744
|
+
const { filename = "datasources", suffix, path, separateFiles } = options;
|
|
2745
|
+
const resolvedPath = path ? resolve(process.cwd(), path, "datasources", space) : resolvePath(path, `datasources/${space}`);
|
|
2746
|
+
try {
|
|
2747
|
+
if (separateFiles) {
|
|
2748
|
+
for (const datasource of datasources) {
|
|
2749
|
+
const datasourceFilePath = join(resolvedPath, suffix ? `${datasource.name}.${suffix}.json` : `${datasource.name}.json`);
|
|
2750
|
+
await saveToFile(datasourceFilePath, JSON.stringify(datasource, null, 2));
|
|
2751
|
+
}
|
|
2752
|
+
return;
|
|
2753
|
+
}
|
|
2754
|
+
const datasourcesFilePath = join(resolvedPath, suffix ? `${filename}.${suffix}.json` : `${filename}.json`);
|
|
2755
|
+
await saveToFile(datasourcesFilePath, JSON.stringify(datasources, null, 2));
|
|
2756
|
+
} catch (error) {
|
|
2757
|
+
handleFileSystemError("write", error);
|
|
2758
|
+
}
|
|
2759
|
+
};
|
|
2760
|
+
|
|
2761
|
+
const program$c = getProgram();
|
|
2476
2762
|
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").option("--su, --suffix <suffix>", "Suffix to add to the component name").action(async (componentName, options) => {
|
|
2477
2763
|
konsola.title(` ${commands.COMPONENTS} `, colorPalette.COMPONENTS, componentName ? `Pushing component ${componentName}...` : "Pushing components...");
|
|
2478
|
-
const verbose = program$
|
|
2764
|
+
const verbose = program$c.opts().verbose;
|
|
2479
2765
|
const { space, path } = componentsCommand.opts();
|
|
2480
2766
|
const { from, filter } = options;
|
|
2481
2767
|
const { state, initializeSession } = session();
|
|
@@ -2502,26 +2788,33 @@ componentsCommand.command("push [componentName]").description(`Push your space's
|
|
|
2502
2788
|
}
|
|
2503
2789
|
});
|
|
2504
2790
|
try {
|
|
2791
|
+
const componentsData = await readComponentsFiles({
|
|
2792
|
+
...options,
|
|
2793
|
+
path,
|
|
2794
|
+
space
|
|
2795
|
+
});
|
|
2796
|
+
const localData = {
|
|
2797
|
+
...componentsData,
|
|
2798
|
+
datasources: []
|
|
2799
|
+
};
|
|
2505
2800
|
const spaceState = {
|
|
2506
|
-
local:
|
|
2507
|
-
...options,
|
|
2508
|
-
path,
|
|
2509
|
-
space
|
|
2510
|
-
}),
|
|
2801
|
+
local: localData,
|
|
2511
2802
|
target: {
|
|
2512
2803
|
components: /* @__PURE__ */ new Map(),
|
|
2513
2804
|
groups: /* @__PURE__ */ new Map(),
|
|
2514
2805
|
tags: /* @__PURE__ */ new Map(),
|
|
2515
|
-
presets: /* @__PURE__ */ new Map()
|
|
2806
|
+
presets: /* @__PURE__ */ new Map(),
|
|
2807
|
+
datasources: /* @__PURE__ */ new Map()
|
|
2516
2808
|
}
|
|
2517
2809
|
};
|
|
2518
2810
|
const promises = [
|
|
2519
2811
|
fetchComponents(space),
|
|
2520
2812
|
fetchComponentGroups(space),
|
|
2521
2813
|
fetchComponentPresets(space),
|
|
2522
|
-
fetchComponentInternalTags(space)
|
|
2814
|
+
fetchComponentInternalTags(space),
|
|
2815
|
+
fetchDatasources(space)
|
|
2523
2816
|
];
|
|
2524
|
-
const [components, groups, presets, internalTags] = await Promise.all(promises);
|
|
2817
|
+
const [components, groups, presets, internalTags, datasources] = await Promise.all(promises);
|
|
2525
2818
|
if (components) {
|
|
2526
2819
|
components.forEach((component) => {
|
|
2527
2820
|
spaceState.target.components.set(component.name, component);
|
|
@@ -2546,6 +2839,11 @@ componentsCommand.command("push [componentName]").description(`Push your space's
|
|
|
2546
2839
|
spaceState.target.tags.set(tag.name, tag);
|
|
2547
2840
|
});
|
|
2548
2841
|
}
|
|
2842
|
+
if (datasources) {
|
|
2843
|
+
datasources.forEach((datasource) => {
|
|
2844
|
+
spaceState.target.datasources.set(datasource.name, datasource);
|
|
2845
|
+
});
|
|
2846
|
+
}
|
|
2549
2847
|
if (componentName) {
|
|
2550
2848
|
spaceState.local = filterSpaceDataByComponent(spaceState.local, componentName);
|
|
2551
2849
|
if (!spaceState.local.components.length) {
|
|
@@ -2617,11 +2915,11 @@ const saveLanguagesToFile = async (space, internationalizationOptions, options)
|
|
|
2617
2915
|
}
|
|
2618
2916
|
};
|
|
2619
2917
|
|
|
2620
|
-
const program$
|
|
2621
|
-
const languagesCommand = program$
|
|
2918
|
+
const program$b = getProgram();
|
|
2919
|
+
const languagesCommand = program$b.command("languages").alias("lang").description(`Manage your space's languages`).option("-s, --space <space>", "space ID").option("-p, --path <path>", "path to save the file. Default is .storyblok/languages");
|
|
2622
2920
|
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.").action(async (options) => {
|
|
2623
2921
|
konsola.title(` ${commands.LANGUAGES} `, colorPalette.LANGUAGES);
|
|
2624
|
-
const verbose = program$
|
|
2922
|
+
const verbose = program$b.opts().verbose;
|
|
2625
2923
|
const { space, path } = languagesCommand.opts();
|
|
2626
2924
|
const { filename = "languages", suffix = options.space } = options;
|
|
2627
2925
|
const { state, initializeSession } = session();
|
|
@@ -2662,8 +2960,8 @@ languagesCommand.command("pull").description(`Download your space's languages sc
|
|
|
2662
2960
|
konsola.br();
|
|
2663
2961
|
});
|
|
2664
2962
|
|
|
2665
|
-
const program$
|
|
2666
|
-
const migrationsCommand = program$
|
|
2963
|
+
const program$a = getProgram();
|
|
2964
|
+
const migrationsCommand = program$a.command("migrations").alias("mig").description(`Manage your space's migrations`).option("-s, --space <space>", "space ID").option("-p, --path <path>", "path to save the file. Default is .storyblok/migrations");
|
|
2667
2965
|
|
|
2668
2966
|
const getMigrationTemplate = () => {
|
|
2669
2967
|
return `export default function (block) {
|
|
@@ -2691,10 +2989,10 @@ const generateMigration = async (space, path, component, suffix) => {
|
|
|
2691
2989
|
}
|
|
2692
2990
|
};
|
|
2693
2991
|
|
|
2694
|
-
const program$
|
|
2992
|
+
const program$9 = getProgram();
|
|
2695
2993
|
migrationsCommand.command("generate [componentName]").description("Generate a migration file").option("--su, --suffix <suffix>", "suffix to add to the file name (e.g. {component-name}.<suffix>.js)").action(async (componentName, options) => {
|
|
2696
2994
|
konsola.title(` ${commands.MIGRATIONS} `, colorPalette.MIGRATIONS, componentName ? `Generating migration for component ${componentName}...` : "Generating migrations...");
|
|
2697
|
-
const verbose = program$
|
|
2995
|
+
const verbose = program$9.opts().verbose;
|
|
2698
2996
|
const { space, path } = migrationsCommand.opts();
|
|
2699
2997
|
const { suffix } = options;
|
|
2700
2998
|
if (!componentName) {
|
|
@@ -3110,10 +3408,10 @@ const isStoryWithUnpublishedChanges = (story) => {
|
|
|
3110
3408
|
return story.published && story.unpublished_changes;
|
|
3111
3409
|
};
|
|
3112
3410
|
|
|
3113
|
-
const program$
|
|
3411
|
+
const program$8 = getProgram();
|
|
3114
3412
|
migrationsCommand.command("run [componentName]").description("Run migrations").option("--fi, --filter <filter>", "glob filter to apply to the components before pushing").option("-d, --dry-run", "Preview changes without applying them to Storyblok").option("-q, --query <query>", 'Filter stories by content attributes using Storyblok filter query syntax. Example: --query="[highlighted][in]=true"').option("--starts-with <path>", 'Filter stories by path. Example: --starts-with="/en/blog/"').option("--publish <publish>", "Options for publication mode: all | published | published-with-changes").action(async (componentName, options) => {
|
|
3115
3413
|
konsola.title(` ${commands.MIGRATIONS} `, colorPalette.MIGRATIONS, componentName ? `Running migrations for component ${componentName}...` : "Running migrations...");
|
|
3116
|
-
const verbose = program$
|
|
3414
|
+
const verbose = program$8.opts().verbose;
|
|
3117
3415
|
const { filter, dryRun = false, query, startsWith, publish } = options;
|
|
3118
3416
|
const { space, path } = migrationsCommand.opts();
|
|
3119
3417
|
const { state, initializeSession } = session();
|
|
@@ -3267,10 +3565,10 @@ migrationsCommand.command("run [componentName]").description("Run migrations").o
|
|
|
3267
3565
|
}
|
|
3268
3566
|
});
|
|
3269
3567
|
|
|
3270
|
-
const program$
|
|
3568
|
+
const program$7 = getProgram();
|
|
3271
3569
|
migrationsCommand.command("rollback [migrationFile]").description("Rollback a migration").action(async (migrationFile) => {
|
|
3272
3570
|
konsola.title(` ${commands.MIGRATIONS} `, colorPalette.MIGRATIONS, `Rolling back migration ${chalk.hex(colorPalette.MIGRATIONS)(migrationFile)}...`);
|
|
3273
|
-
const verbose = program$
|
|
3571
|
+
const verbose = program$7.opts().verbose;
|
|
3274
3572
|
const { space, path } = migrationsCommand.opts();
|
|
3275
3573
|
const { state, initializeSession } = session();
|
|
3276
3574
|
await initializeSession();
|
|
@@ -3309,8 +3607,8 @@ migrationsCommand.command("rollback [migrationFile]").description("Rollback a mi
|
|
|
3309
3607
|
}
|
|
3310
3608
|
});
|
|
3311
3609
|
|
|
3312
|
-
const program$
|
|
3313
|
-
const typesCommand = program$
|
|
3610
|
+
const program$6 = getProgram();
|
|
3611
|
+
const typesCommand = program$6.command(commands.TYPES).alias("ts").description(`Generate types d.ts for your component schemas`).option("-s, --space <space>", "space ID").option("-p, --path <path>", "path to save the file. Default is .storyblok/types");
|
|
3314
3612
|
|
|
3315
3613
|
const getAssetJSONSchema = (title) => ({
|
|
3316
3614
|
$id: "#/asset",
|
|
@@ -4009,20 +4307,11 @@ const generateStoryblokTypes = async (options = {}) => {
|
|
|
4009
4307
|
}
|
|
4010
4308
|
};
|
|
4011
4309
|
|
|
4012
|
-
const program$
|
|
4310
|
+
const program$5 = getProgram();
|
|
4013
4311
|
typesCommand.command("generate").description("Generate types d.ts for your component schemas").option("--sf, --separate-files", "").option("--strict", "strict mode, no loose typing").option("--type-prefix <prefix>", "prefix to be prepended to all generated component type names").option("--suffix <suffix>", "Components suffix").option("--custom-fields-parser <path>", "Path to the parser file for Custom Field Types").option("--compiler-options <options>", "path to the compiler options from json-schema-to-typescript").action(async (options) => {
|
|
4014
4312
|
konsola.title(` ${commands.TYPES} `, colorPalette.TYPES, "Generating types...");
|
|
4015
|
-
const verbose = program$
|
|
4313
|
+
const verbose = program$5.opts().verbose;
|
|
4016
4314
|
const { space, path } = typesCommand.opts();
|
|
4017
|
-
const { state, initializeSession } = session();
|
|
4018
|
-
await initializeSession();
|
|
4019
|
-
if (!requireAuthentication(state, verbose)) {
|
|
4020
|
-
return;
|
|
4021
|
-
}
|
|
4022
|
-
if (!space) {
|
|
4023
|
-
handleError(new CommandError(`Please provide the space as argument --space YOUR_SPACE_ID.`), verbose);
|
|
4024
|
-
return;
|
|
4025
|
-
}
|
|
4026
4315
|
const spinner = new Spinner({
|
|
4027
4316
|
verbose: !isVitest
|
|
4028
4317
|
});
|
|
@@ -4037,7 +4326,11 @@ typesCommand.command("generate").description("Generate types d.ts for your compo
|
|
|
4037
4326
|
...options,
|
|
4038
4327
|
path
|
|
4039
4328
|
});
|
|
4040
|
-
const
|
|
4329
|
+
const spaceDataWithDatasources = {
|
|
4330
|
+
...spaceData,
|
|
4331
|
+
datasources: []
|
|
4332
|
+
};
|
|
4333
|
+
const typedefString = await generateTypes(spaceDataWithDatasources, {
|
|
4041
4334
|
...options,
|
|
4042
4335
|
path
|
|
4043
4336
|
});
|
|
@@ -4057,7 +4350,529 @@ typesCommand.command("generate").description("Generate types d.ts for your compo
|
|
|
4057
4350
|
}
|
|
4058
4351
|
});
|
|
4059
4352
|
|
|
4060
|
-
const
|
|
4353
|
+
const program$4 = getProgram();
|
|
4354
|
+
const datasourcesCommand = program$4.command("datasources").alias("ds").description(`Manage your space's datasources`).option("-s, --space <space>", "space ID").option("-p, --path <path>", "path to save the file. Default is .storyblok/datasources");
|
|
4355
|
+
|
|
4356
|
+
const program$3 = getProgram();
|
|
4357
|
+
datasourcesCommand.command("pull [datasourceName]").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 datasource").option("--su, --suffix <suffix>", "suffix to add to the file name (e.g. datasources.<suffix>.json)").description("Pull datasources from your space").action(async (datasourceName, options) => {
|
|
4358
|
+
konsola.title(` ${commands.DATASOURCES} `, colorPalette.DATASOURCES, datasourceName ? `Pulling datasource ${datasourceName}...` : "Pulling datasources...");
|
|
4359
|
+
const verbose = program$3.opts().verbose;
|
|
4360
|
+
const { space, path } = datasourcesCommand.opts();
|
|
4361
|
+
const { separateFiles, suffix, filename = "datasources" } = options;
|
|
4362
|
+
const { state, initializeSession } = session();
|
|
4363
|
+
await initializeSession();
|
|
4364
|
+
if (!requireAuthentication(state, verbose)) {
|
|
4365
|
+
return;
|
|
4366
|
+
}
|
|
4367
|
+
if (!space) {
|
|
4368
|
+
handleError(new CommandError(`Please provide the space as argument --space YOUR_SPACE_ID.`), verbose);
|
|
4369
|
+
return;
|
|
4370
|
+
}
|
|
4371
|
+
const { password, region } = state;
|
|
4372
|
+
mapiClient({
|
|
4373
|
+
token: password,
|
|
4374
|
+
region
|
|
4375
|
+
});
|
|
4376
|
+
const spinnerDatasources = new Spinner({
|
|
4377
|
+
verbose: !isVitest
|
|
4378
|
+
});
|
|
4379
|
+
try {
|
|
4380
|
+
spinnerDatasources.start(`Fetching ${chalk.hex(colorPalette.DATASOURCES)("datasources")}`);
|
|
4381
|
+
let datasources;
|
|
4382
|
+
if (datasourceName) {
|
|
4383
|
+
const datasource = await fetchDatasource(space, datasourceName);
|
|
4384
|
+
if (!datasource) {
|
|
4385
|
+
konsola.warn(`No datasource found with name "${datasourceName}"`);
|
|
4386
|
+
return;
|
|
4387
|
+
}
|
|
4388
|
+
datasources = [datasource];
|
|
4389
|
+
} else {
|
|
4390
|
+
datasources = await fetchDatasources(space);
|
|
4391
|
+
if (!datasources || datasources.length === 0) {
|
|
4392
|
+
konsola.warn(`No datasources found in the space ${space}`);
|
|
4393
|
+
return;
|
|
4394
|
+
}
|
|
4395
|
+
}
|
|
4396
|
+
spinnerDatasources.succeed(`${chalk.hex(colorPalette.DATASOURCES)("Datasources")} - Completed in ${spinnerDatasources.elapsedTime.toFixed(2)}ms`);
|
|
4397
|
+
await saveDatasourcesToFiles(
|
|
4398
|
+
space,
|
|
4399
|
+
datasources,
|
|
4400
|
+
{ ...options, path, separateFiles: separateFiles || !!datasourceName }
|
|
4401
|
+
);
|
|
4402
|
+
konsola.br();
|
|
4403
|
+
if (separateFiles) {
|
|
4404
|
+
if (filename !== "datasources") {
|
|
4405
|
+
konsola.warn(`The --filename option is ignored when using --separate-files`);
|
|
4406
|
+
}
|
|
4407
|
+
const filePath = path ? `${path}/datasources/${space}/` : `.storyblok/datasources/${space}/`;
|
|
4408
|
+
konsola.ok(`Datasources downloaded successfully to ${chalk.hex(colorPalette.PRIMARY)(filePath)}`);
|
|
4409
|
+
} else if (datasourceName) {
|
|
4410
|
+
const fileName = suffix ? `${filename}.${suffix}.json` : `${datasourceName}.json`;
|
|
4411
|
+
const filePath = path ? `${path}/datasources/${space}/${fileName}` : `.storyblok/datasources/${space}/${fileName}`;
|
|
4412
|
+
konsola.ok(`Datasource ${chalk.hex(colorPalette.PRIMARY)(datasourceName)} downloaded successfully in ${chalk.hex(colorPalette.PRIMARY)(filePath)}`);
|
|
4413
|
+
} else {
|
|
4414
|
+
const fileName = suffix ? `${filename}.${suffix}.json` : `${filename}.json`;
|
|
4415
|
+
const filePath = path ? `${path}/datasources/${space}/${fileName}` : `.storyblok/datasources/${space}/${fileName}`;
|
|
4416
|
+
konsola.ok(`Datasources downloaded successfully to ${chalk.hex(colorPalette.PRIMARY)(filePath)}`);
|
|
4417
|
+
}
|
|
4418
|
+
konsola.br();
|
|
4419
|
+
} catch (error) {
|
|
4420
|
+
spinnerDatasources.failed(`Fetching ${chalk.hex(colorPalette.DATASOURCES)("Datasources")} - Failed`);
|
|
4421
|
+
konsola.br();
|
|
4422
|
+
handleError(error, verbose);
|
|
4423
|
+
}
|
|
4424
|
+
});
|
|
4425
|
+
|
|
4426
|
+
const program$2 = getProgram();
|
|
4427
|
+
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").action(async (datasourceName, options) => {
|
|
4428
|
+
konsola.title(` ${commands.DATASOURCES} `, colorPalette.DATASOURCES, datasourceName ? `Pushing datasource ${datasourceName}...` : "Pushing datasources...");
|
|
4429
|
+
const verbose = program$2.opts().verbose;
|
|
4430
|
+
const { space, path } = datasourcesCommand.opts();
|
|
4431
|
+
const { from, filter } = options;
|
|
4432
|
+
const { state, initializeSession } = session();
|
|
4433
|
+
await initializeSession();
|
|
4434
|
+
if (!requireAuthentication(state, verbose)) {
|
|
4435
|
+
return;
|
|
4436
|
+
}
|
|
4437
|
+
if (!space) {
|
|
4438
|
+
handleError(new CommandError(`Please provide the target space as argument --space TARGET_SPACE_ID.`), verbose);
|
|
4439
|
+
return;
|
|
4440
|
+
}
|
|
4441
|
+
if (!from) {
|
|
4442
|
+
options.from = space;
|
|
4443
|
+
}
|
|
4444
|
+
konsola.info(`Attempting to push datasources ${chalk.bold("from")} space ${chalk.hex(colorPalette.DATASOURCES)(options.from || space)} ${chalk.bold("to")} ${chalk.hex(colorPalette.DATASOURCES)(space)}`);
|
|
4445
|
+
konsola.br();
|
|
4446
|
+
const { password, region } = state;
|
|
4447
|
+
mapiClient({
|
|
4448
|
+
token: password,
|
|
4449
|
+
region
|
|
4450
|
+
});
|
|
4451
|
+
try {
|
|
4452
|
+
const spaceState = {
|
|
4453
|
+
local: await readDatasourcesFiles({
|
|
4454
|
+
...options,
|
|
4455
|
+
path,
|
|
4456
|
+
space
|
|
4457
|
+
}),
|
|
4458
|
+
target: {
|
|
4459
|
+
datasources: /* @__PURE__ */ new Map()
|
|
4460
|
+
}
|
|
4461
|
+
};
|
|
4462
|
+
const targetSpaceDatasources = await fetchDatasources(space);
|
|
4463
|
+
if (targetSpaceDatasources) {
|
|
4464
|
+
targetSpaceDatasources.forEach((datasource) => {
|
|
4465
|
+
spaceState.target.datasources.set(datasource.name, datasource);
|
|
4466
|
+
});
|
|
4467
|
+
}
|
|
4468
|
+
if (datasourceName) {
|
|
4469
|
+
spaceState.local = {
|
|
4470
|
+
datasources: [spaceState.local.datasources.find((datasource) => datasource.name === datasourceName) || []]
|
|
4471
|
+
};
|
|
4472
|
+
if (!spaceState.local.datasources.length) {
|
|
4473
|
+
handleError(new CommandError(`Datasource "${datasourceName}" not found.`), verbose);
|
|
4474
|
+
return;
|
|
4475
|
+
}
|
|
4476
|
+
} else if (filter) {
|
|
4477
|
+
spaceState.local.datasources = spaceState.local.datasources.filter((datasource) => datasource.name.includes(filter));
|
|
4478
|
+
if (!spaceState.local.datasources.length) {
|
|
4479
|
+
handleError(new CommandError(`No datasources found matching pattern "${filter}".`), verbose);
|
|
4480
|
+
return;
|
|
4481
|
+
}
|
|
4482
|
+
konsola.info(`Filter applied: ${filter}`);
|
|
4483
|
+
}
|
|
4484
|
+
if (!spaceState.local.datasources.length) {
|
|
4485
|
+
konsola.warn("No datasources found. Please make sure you have pulled the datasources first.");
|
|
4486
|
+
return;
|
|
4487
|
+
}
|
|
4488
|
+
const results = {
|
|
4489
|
+
successful: [],
|
|
4490
|
+
failed: []
|
|
4491
|
+
};
|
|
4492
|
+
for (const datasource of spaceState.local.datasources) {
|
|
4493
|
+
const spinner = new Spinner({
|
|
4494
|
+
verbose: !isVitest
|
|
4495
|
+
});
|
|
4496
|
+
spinner.start(`Pushing ${chalk.hex(colorPalette.DATASOURCES)(datasource.name)}`);
|
|
4497
|
+
const existingDatasource = spaceState.target.datasources.get(datasource.name);
|
|
4498
|
+
const existingId = existingDatasource?.id;
|
|
4499
|
+
const { entries, ...datasourceDefinition } = datasource;
|
|
4500
|
+
const result = await upsertDatasource(space, datasourceDefinition, existingId);
|
|
4501
|
+
if (result) {
|
|
4502
|
+
results.successful.push(datasource.name);
|
|
4503
|
+
if (entries && entries.length > 0) {
|
|
4504
|
+
for (const entry of entries) {
|
|
4505
|
+
const existingEntryId = existingDatasource?.entries?.find((e) => e.name === entry.name)?.id;
|
|
4506
|
+
try {
|
|
4507
|
+
const { id, ...entryData } = entry;
|
|
4508
|
+
await upsertDatasourceEntry(space, result.id, entryData, existingEntryId);
|
|
4509
|
+
} catch (entryError) {
|
|
4510
|
+
results.failed.push({ name: datasource.name, error: entryError });
|
|
4511
|
+
spinner.failed(`${chalk.hex(colorPalette.DATASOURCES)(datasource.name)} - Failed in ${spinner.elapsedTime.toFixed(2)}ms`);
|
|
4512
|
+
}
|
|
4513
|
+
}
|
|
4514
|
+
}
|
|
4515
|
+
spinner.succeed(`${chalk.hex(colorPalette.DATASOURCES)(datasource.name)} - Completed in ${spinner.elapsedTime.toFixed(2)}ms`);
|
|
4516
|
+
} else {
|
|
4517
|
+
results.failed.push({ name: datasource.name, error: result });
|
|
4518
|
+
spinner.failed(`${chalk.hex(colorPalette.DATASOURCES)(datasource.name)} - Failed in ${spinner.elapsedTime.toFixed(2)}ms`);
|
|
4519
|
+
}
|
|
4520
|
+
}
|
|
4521
|
+
if (results.failed.length > 0) {
|
|
4522
|
+
if (!verbose) {
|
|
4523
|
+
konsola.br();
|
|
4524
|
+
konsola.info("For more information about the error, run the command with the `--verbose` flag");
|
|
4525
|
+
} else {
|
|
4526
|
+
results.failed.forEach((failed) => {
|
|
4527
|
+
handleError(failed.error, verbose);
|
|
4528
|
+
});
|
|
4529
|
+
}
|
|
4530
|
+
}
|
|
4531
|
+
} catch (error) {
|
|
4532
|
+
handleError(error, verbose);
|
|
4533
|
+
}
|
|
4534
|
+
});
|
|
4535
|
+
|
|
4536
|
+
async function deleteDatasource(space, id) {
|
|
4537
|
+
try {
|
|
4538
|
+
const client = mapiClient();
|
|
4539
|
+
await client.delete(`spaces/${space}/datasources/${id}`);
|
|
4540
|
+
} catch (error) {
|
|
4541
|
+
handleAPIError("delete_datasource", error, `Datasource with id '${id}' not found in space ${space}.`);
|
|
4542
|
+
}
|
|
4543
|
+
}
|
|
4544
|
+
|
|
4545
|
+
datasourcesCommand.command("delete [name]").description("Delete a datasource from your space by name or id").option("--id <id>", "Delete by datasource id instead of name").option("--force", "Skip confirmation prompt for deletion (useful for CI)").action(async (name, options) => {
|
|
4546
|
+
konsola.title(
|
|
4547
|
+
` ${commands.DATASOURCES} `,
|
|
4548
|
+
colorPalette.DATASOURCES,
|
|
4549
|
+
options.id ? `Deleting datasource with id ${options.id}...` : `Deleting datasource with name ${name}...`
|
|
4550
|
+
);
|
|
4551
|
+
if (name && options.id) {
|
|
4552
|
+
konsola.warn(
|
|
4553
|
+
"Both a datasource name and an id were provided. Only one is required. The id will be used as the source of truth."
|
|
4554
|
+
);
|
|
4555
|
+
}
|
|
4556
|
+
const { space } = datasourcesCommand.opts();
|
|
4557
|
+
const verbose = datasourcesCommand.parent?.opts().verbose;
|
|
4558
|
+
const { state, initializeSession } = session();
|
|
4559
|
+
await initializeSession();
|
|
4560
|
+
if (!requireAuthentication(state, verbose)) {
|
|
4561
|
+
return;
|
|
4562
|
+
}
|
|
4563
|
+
if (!space) {
|
|
4564
|
+
handleError(new CommandError("Please provide the space as argument --space YOUR_SPACE_ID."), verbose);
|
|
4565
|
+
return;
|
|
4566
|
+
}
|
|
4567
|
+
const { password, region } = state;
|
|
4568
|
+
mapiClient({
|
|
4569
|
+
token: password,
|
|
4570
|
+
region
|
|
4571
|
+
});
|
|
4572
|
+
const spinner = new Spinner({
|
|
4573
|
+
verbose: !isVitest
|
|
4574
|
+
});
|
|
4575
|
+
try {
|
|
4576
|
+
if (options.id) {
|
|
4577
|
+
spinner.start(`Deleting datasource...`);
|
|
4578
|
+
await deleteDatasource(space, options.id);
|
|
4579
|
+
spinner.succeed();
|
|
4580
|
+
konsola.ok(`Datasource ${chalk.hex(colorPalette.DATASOURCES)(options.id)} deleted successfully from space ${space}.`);
|
|
4581
|
+
} else {
|
|
4582
|
+
const datasource = await fetchDatasource(space, name);
|
|
4583
|
+
if (!datasource) {
|
|
4584
|
+
throw new CommandError(`Datasource with name '${name}' not found in space ${space}.`);
|
|
4585
|
+
}
|
|
4586
|
+
if (!options.force) {
|
|
4587
|
+
konsola.info(`Datasource details:`);
|
|
4588
|
+
console.log(` Name: ${chalk.hex(colorPalette.DATASOURCES)(datasource.name)}`);
|
|
4589
|
+
console.log(` ID: ${chalk.hex(colorPalette.DATASOURCES)(datasource.id)}`);
|
|
4590
|
+
console.log(` Space: ${chalk.hex(colorPalette.DATASOURCES)(space)}`);
|
|
4591
|
+
console.log(` Slug: ${chalk.hex(colorPalette.DATASOURCES)(datasource.slug)}`);
|
|
4592
|
+
console.log(` Created at: ${chalk.hex(colorPalette.DATASOURCES)(datasource.created_at)}`);
|
|
4593
|
+
console.log(` Updated at: ${chalk.hex(colorPalette.DATASOURCES)(datasource.updated_at)}`);
|
|
4594
|
+
konsola.br();
|
|
4595
|
+
const confirmed = await confirm({
|
|
4596
|
+
message: `\u26A0\uFE0F ${chalk.yellow(` Are you sure you want to delete the ${datasource.name} datasource from space ${space}? This action cannot be undone.`)} `,
|
|
4597
|
+
default: false
|
|
4598
|
+
});
|
|
4599
|
+
if (!confirmed) {
|
|
4600
|
+
spinner.failed("Deletion aborted by user.");
|
|
4601
|
+
konsola.warn("Deletion aborted by user.");
|
|
4602
|
+
return;
|
|
4603
|
+
}
|
|
4604
|
+
}
|
|
4605
|
+
spinner.start(`Deleting datasource...`);
|
|
4606
|
+
await deleteDatasource(space, datasource.id.toString());
|
|
4607
|
+
spinner.succeed();
|
|
4608
|
+
konsola.ok(`Datasource ${chalk.hex(colorPalette.DATASOURCES)(name)} deleted successfully from space ${space}.`);
|
|
4609
|
+
}
|
|
4610
|
+
} catch (error) {
|
|
4611
|
+
spinner.failed(
|
|
4612
|
+
`Failed to delete datasource ${chalk.hex(colorPalette.DATASOURCES)(options.id ? options.id : name)}`
|
|
4613
|
+
);
|
|
4614
|
+
handleError(error, verbose);
|
|
4615
|
+
}
|
|
4616
|
+
});
|
|
4617
|
+
|
|
4618
|
+
let octokit;
|
|
4619
|
+
let lastToken;
|
|
4620
|
+
const createOctokit = (token) => {
|
|
4621
|
+
if (!octokit || token !== lastToken) {
|
|
4622
|
+
const options = {
|
|
4623
|
+
request: {
|
|
4624
|
+
fetch
|
|
4625
|
+
}
|
|
4626
|
+
};
|
|
4627
|
+
octokit = new Octokit(options);
|
|
4628
|
+
}
|
|
4629
|
+
return octokit;
|
|
4630
|
+
};
|
|
4631
|
+
|
|
4632
|
+
const generateProject = async (blueprint, projectName, targetPath = process.cwd()) => {
|
|
4633
|
+
try {
|
|
4634
|
+
const projectPath = path.join(targetPath, projectName);
|
|
4635
|
+
const templateRepo = `storyblok/blueprint-core-${blueprint}`;
|
|
4636
|
+
try {
|
|
4637
|
+
await fs.access(projectPath);
|
|
4638
|
+
const existsError = new Error(`Directory ${projectName} already exists`);
|
|
4639
|
+
existsError.code = "ENOTEMPTY";
|
|
4640
|
+
existsError.path = projectPath;
|
|
4641
|
+
throw new FileSystemError("directory_not_empty", "mkdir", existsError, `Directory ${projectName} already exists`);
|
|
4642
|
+
} catch (error) {
|
|
4643
|
+
const fsError = error;
|
|
4644
|
+
if (fsError.code === "ENOENT") {
|
|
4645
|
+
} else {
|
|
4646
|
+
handleFileSystemError("read", fsError);
|
|
4647
|
+
}
|
|
4648
|
+
}
|
|
4649
|
+
const degitProcess = spawn("npx", ["degit", templateRepo, projectPath], {
|
|
4650
|
+
stdio: "inherit",
|
|
4651
|
+
shell: true
|
|
4652
|
+
});
|
|
4653
|
+
return new Promise((resolve, reject) => {
|
|
4654
|
+
degitProcess.on("close", (code) => {
|
|
4655
|
+
if (code === 0) {
|
|
4656
|
+
resolve();
|
|
4657
|
+
} else {
|
|
4658
|
+
reject(new Error(`Failed to clone template. Process exited with code ${code}`));
|
|
4659
|
+
}
|
|
4660
|
+
});
|
|
4661
|
+
degitProcess.on("error", (error) => {
|
|
4662
|
+
reject(new Error(`Failed to spawn degit process: ${error.message}`));
|
|
4663
|
+
});
|
|
4664
|
+
});
|
|
4665
|
+
} catch (error) {
|
|
4666
|
+
handleFileSystemError("read", error);
|
|
4667
|
+
}
|
|
4668
|
+
};
|
|
4669
|
+
const createEnvFile = async (projectPath, accessToken, additionalVars) => {
|
|
4670
|
+
try {
|
|
4671
|
+
const envPath = path.join(projectPath, ".env");
|
|
4672
|
+
let envContent = `# Storyblok Configuration
|
|
4673
|
+
STORYBLOK_DELIVERY_API_TOKEN=${accessToken}
|
|
4674
|
+
`;
|
|
4675
|
+
if (additionalVars && Object.keys(additionalVars).length > 0) ;
|
|
4676
|
+
await saveToFile(envPath, envContent);
|
|
4677
|
+
} catch (error) {
|
|
4678
|
+
throw new Error(`Failed to create .env file: ${error.message}`);
|
|
4679
|
+
}
|
|
4680
|
+
};
|
|
4681
|
+
const generateSpaceUrl = (spaceId, region) => {
|
|
4682
|
+
const domain = appDomains[region];
|
|
4683
|
+
return `https://${domain}/#/me/spaces/${spaceId}/dashboard`;
|
|
4684
|
+
};
|
|
4685
|
+
const openSpaceInBrowser = async (spaceId, region) => {
|
|
4686
|
+
try {
|
|
4687
|
+
const spaceUrl = generateSpaceUrl(spaceId, region);
|
|
4688
|
+
await open(spaceUrl);
|
|
4689
|
+
} catch (error) {
|
|
4690
|
+
throw new Error(`Failed to open space in browser: ${error.message}`);
|
|
4691
|
+
}
|
|
4692
|
+
};
|
|
4693
|
+
const extractPortFromTopics = (topics) => {
|
|
4694
|
+
const portTopic = topics.find((topic) => topic.startsWith("port-"));
|
|
4695
|
+
if (portTopic) {
|
|
4696
|
+
const port = portTopic.replace("port-", "");
|
|
4697
|
+
if (/^\d+$/.test(port) && Number.parseInt(port) > 0 && Number.parseInt(port) <= 65535) {
|
|
4698
|
+
return port;
|
|
4699
|
+
}
|
|
4700
|
+
}
|
|
4701
|
+
return "3000";
|
|
4702
|
+
};
|
|
4703
|
+
const repositoryToBlueprint = (repo) => {
|
|
4704
|
+
const technology = repo.name.replace("blueprint-core-", "");
|
|
4705
|
+
const port = extractPortFromTopics(repo.topics || []);
|
|
4706
|
+
return {
|
|
4707
|
+
name: technology.charAt(0).toUpperCase() + technology.slice(1),
|
|
4708
|
+
value: technology,
|
|
4709
|
+
template: repo.clone_url,
|
|
4710
|
+
location: port ? `https://localhost:${port}/` : "https://localhost:3000/",
|
|
4711
|
+
description: repo.description,
|
|
4712
|
+
updated_at: repo.updated_at
|
|
4713
|
+
};
|
|
4714
|
+
};
|
|
4715
|
+
const fetchBlueprintRepositories = async () => {
|
|
4716
|
+
try {
|
|
4717
|
+
const octokit = createOctokit();
|
|
4718
|
+
const { data } = await octokit.rest.search.repos({
|
|
4719
|
+
q: "org:storyblok blueprint-core-",
|
|
4720
|
+
sort: "updated",
|
|
4721
|
+
order: "desc",
|
|
4722
|
+
per_page: 100
|
|
4723
|
+
});
|
|
4724
|
+
const blueprints = data.items.filter((repo) => repo.name.startsWith("blueprint-core-")).map(repositoryToBlueprint).sort((a, b) => a.name.localeCompare(b.name));
|
|
4725
|
+
return blueprints;
|
|
4726
|
+
} catch (error) {
|
|
4727
|
+
handleAPIError("fetch_blueprints", error, "Failed to fetch blueprints from GitHub");
|
|
4728
|
+
}
|
|
4729
|
+
};
|
|
4730
|
+
|
|
4731
|
+
const createSpace = async (space) => {
|
|
4732
|
+
try {
|
|
4733
|
+
const client = mapiClient();
|
|
4734
|
+
const { data } = await client.post("spaces", {
|
|
4735
|
+
body: JSON.stringify(space)
|
|
4736
|
+
});
|
|
4737
|
+
return data.space;
|
|
4738
|
+
} catch (error) {
|
|
4739
|
+
handleAPIError("create_space", error, `Failed to create space ${space.name}`);
|
|
4740
|
+
}
|
|
4741
|
+
};
|
|
4742
|
+
|
|
4743
|
+
const program$1 = getProgram();
|
|
4744
|
+
program$1.command(`${commands.CREATE} [project-path]`).alias("c").description(`Scaffold a new project using Storyblok`).option("-b, --blueprint <blueprint>", "technology starter blueprint").option("--skip-space", "skip space creation").action(async (projectPath, options) => {
|
|
4745
|
+
konsola.title(` ${commands.CREATE} `, colorPalette.CREATE);
|
|
4746
|
+
const verbose = program$1.opts().verbose;
|
|
4747
|
+
const { blueprint } = options;
|
|
4748
|
+
const { state, initializeSession } = session();
|
|
4749
|
+
await initializeSession();
|
|
4750
|
+
if (!requireAuthentication(state, verbose)) {
|
|
4751
|
+
return;
|
|
4752
|
+
}
|
|
4753
|
+
const { password, region } = state;
|
|
4754
|
+
mapiClient({
|
|
4755
|
+
token: password,
|
|
4756
|
+
region
|
|
4757
|
+
});
|
|
4758
|
+
const spinnerBlueprints = new Spinner({
|
|
4759
|
+
verbose: !isVitest
|
|
4760
|
+
});
|
|
4761
|
+
const spinnerSpace = new Spinner({
|
|
4762
|
+
verbose: !isVitest
|
|
4763
|
+
});
|
|
4764
|
+
try {
|
|
4765
|
+
spinnerBlueprints.start("Fetching starter blueprints...");
|
|
4766
|
+
const blueprints = await fetchBlueprintRepositories();
|
|
4767
|
+
spinnerBlueprints.succeed("Starter blueprints fetched successfully");
|
|
4768
|
+
if (!blueprints) {
|
|
4769
|
+
spinnerBlueprints.failed();
|
|
4770
|
+
konsola.warn("No starter blueprints found. Please contact support@storyblok.com");
|
|
4771
|
+
konsola.br();
|
|
4772
|
+
return;
|
|
4773
|
+
}
|
|
4774
|
+
let technologyBlueprint = blueprint;
|
|
4775
|
+
if (blueprint) {
|
|
4776
|
+
const validBlueprints = blueprints;
|
|
4777
|
+
const isValidBlueprint = validBlueprints.find((bp) => bp.value === blueprint);
|
|
4778
|
+
if (!isValidBlueprint) {
|
|
4779
|
+
const validOptions = validBlueprints.map((bp) => bp.value).join(", ");
|
|
4780
|
+
konsola.warn(`Invalid blueprint "${chalk.hex(colorPalette.CREATE)(blueprint)}". Valid options are: ${chalk.hex(colorPalette.CREATE)(validOptions)}`);
|
|
4781
|
+
konsola.br();
|
|
4782
|
+
technologyBlueprint = void 0;
|
|
4783
|
+
}
|
|
4784
|
+
}
|
|
4785
|
+
if (!technologyBlueprint) {
|
|
4786
|
+
technologyBlueprint = await select({
|
|
4787
|
+
message: "Please select the technology you would like to use:",
|
|
4788
|
+
choices: blueprints.map((blueprint2) => ({
|
|
4789
|
+
name: blueprint2.name,
|
|
4790
|
+
value: blueprint2.value
|
|
4791
|
+
}))
|
|
4792
|
+
});
|
|
4793
|
+
}
|
|
4794
|
+
let finalProjectPath = projectPath;
|
|
4795
|
+
if (!projectPath) {
|
|
4796
|
+
finalProjectPath = await input({
|
|
4797
|
+
message: "What is the path for your project?",
|
|
4798
|
+
default: `./my-${technologyBlueprint}-project`,
|
|
4799
|
+
validate: (value) => {
|
|
4800
|
+
if (!value.trim()) {
|
|
4801
|
+
return "Project path is required";
|
|
4802
|
+
}
|
|
4803
|
+
const projectName2 = path.basename(value);
|
|
4804
|
+
if (!/^[\w-]+$/.test(projectName2)) {
|
|
4805
|
+
return "Project name (last part of the path) can only contain letters, numbers, hyphens, and underscores";
|
|
4806
|
+
}
|
|
4807
|
+
return true;
|
|
4808
|
+
}
|
|
4809
|
+
});
|
|
4810
|
+
}
|
|
4811
|
+
const resolvedPath = path.resolve(finalProjectPath);
|
|
4812
|
+
const targetDirectory = path.dirname(resolvedPath);
|
|
4813
|
+
const projectName = path.basename(resolvedPath);
|
|
4814
|
+
konsola.br();
|
|
4815
|
+
konsola.info(`Scaffolding your project using the ${chalk.hex(colorPalette.CREATE)(technologyBlueprint)} blueprint...`);
|
|
4816
|
+
await generateProject(technologyBlueprint, projectName, targetDirectory);
|
|
4817
|
+
konsola.ok(`Project ${chalk.hex(colorPalette.PRIMARY)(projectName)} created successfully in ${chalk.hex(colorPalette.PRIMARY)(finalProjectPath)}`, true);
|
|
4818
|
+
let createdSpace;
|
|
4819
|
+
if (!options.skipSpace) {
|
|
4820
|
+
try {
|
|
4821
|
+
spinnerSpace.start(`Creating space "${toHumanReadable(projectName)}"`);
|
|
4822
|
+
const selectedBlueprint = blueprints.find((bp) => bp.value === technologyBlueprint);
|
|
4823
|
+
const blueprintDomain = selectedBlueprint?.location || "https://localhost:3000/";
|
|
4824
|
+
createdSpace = await createSpace({
|
|
4825
|
+
name: toHumanReadable(projectName),
|
|
4826
|
+
domain: blueprintDomain
|
|
4827
|
+
});
|
|
4828
|
+
spinnerSpace.succeed(`Space "${chalk.hex(colorPalette.PRIMARY)(toHumanReadable(projectName))}" created successfully`);
|
|
4829
|
+
} catch (error) {
|
|
4830
|
+
spinnerSpace.failed();
|
|
4831
|
+
konsola.br();
|
|
4832
|
+
handleError(error, verbose);
|
|
4833
|
+
return;
|
|
4834
|
+
}
|
|
4835
|
+
}
|
|
4836
|
+
if (createdSpace?.first_token) {
|
|
4837
|
+
try {
|
|
4838
|
+
await createEnvFile(resolvedPath, createdSpace.first_token);
|
|
4839
|
+
konsola.ok(`Created .env file with Storyblok access token`, true);
|
|
4840
|
+
} catch (error) {
|
|
4841
|
+
konsola.warn(`Failed to create .env file: ${error.message}`);
|
|
4842
|
+
konsola.info(`You can manually add this token to your .env file: ${createdSpace.first_token}`);
|
|
4843
|
+
}
|
|
4844
|
+
}
|
|
4845
|
+
if (createdSpace?.id) {
|
|
4846
|
+
try {
|
|
4847
|
+
await openSpaceInBrowser(createdSpace.id, region);
|
|
4848
|
+
konsola.info(`Opened space in your browser`);
|
|
4849
|
+
} catch (error) {
|
|
4850
|
+
konsola.warn(`Failed to open browser: ${error.message}`);
|
|
4851
|
+
const spaceUrl = generateSpaceUrl(createdSpace.id, region);
|
|
4852
|
+
konsola.info(`You can manually open your space at: ${chalk.hex(colorPalette.PRIMARY)(spaceUrl)}`);
|
|
4853
|
+
}
|
|
4854
|
+
}
|
|
4855
|
+
konsola.br();
|
|
4856
|
+
konsola.ok(`Your ${chalk.hex(colorPalette.PRIMARY)(technologyBlueprint)} project is ready \u{1F389} !`);
|
|
4857
|
+
if (createdSpace?.first_token) {
|
|
4858
|
+
konsola.ok(`Storyblok space created, preview url and .env configured automatically`);
|
|
4859
|
+
}
|
|
4860
|
+
konsola.br();
|
|
4861
|
+
konsola.info(`Next steps:
|
|
4862
|
+
cd ${finalProjectPath}
|
|
4863
|
+
npm install
|
|
4864
|
+
npm run dev
|
|
4865
|
+
`);
|
|
4866
|
+
} catch (error) {
|
|
4867
|
+
spinnerSpace.failed();
|
|
4868
|
+
spinnerBlueprints.failed();
|
|
4869
|
+
konsola.br();
|
|
4870
|
+
handleError(error, verbose);
|
|
4871
|
+
}
|
|
4872
|
+
konsola.br();
|
|
4873
|
+
});
|
|
4874
|
+
|
|
4875
|
+
const version = "4.2.0";
|
|
4061
4876
|
const pkg = {
|
|
4062
4877
|
version: version};
|
|
4063
4878
|
|