storyblok 4.0.4 → 4.1.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 +331 -56
- package/dist/index.mjs.map +1 -1
- package/package.json +6 -4
package/dist/index.mjs
CHANGED
|
@@ -7,24 +7,27 @@ import { Command } from 'commander';
|
|
|
7
7
|
import { readPackageUp } from 'read-package-up';
|
|
8
8
|
import { Spinner } from '@topcli/spinner';
|
|
9
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';
|
|
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
|
-
MIGRATIONS: "
|
|
27
|
-
TYPES: "
|
|
28
|
+
MIGRATIONS: "migrations",
|
|
29
|
+
TYPES: "types",
|
|
30
|
+
CREATE: "create"
|
|
28
31
|
};
|
|
29
32
|
const colorPalette = {
|
|
30
33
|
PRIMARY: "#8d60ff",
|
|
@@ -36,6 +39,7 @@ const colorPalette = {
|
|
|
36
39
|
LANGUAGES: "#f5c003",
|
|
37
40
|
MIGRATIONS: "#8CE2FF",
|
|
38
41
|
TYPES: "#3178C6",
|
|
42
|
+
CREATE: "#ffb3ba",
|
|
39
43
|
GROUPS: "#4ade80",
|
|
40
44
|
TAGS: "#fbbf24",
|
|
41
45
|
PRESETS: "#a855f7"
|
|
@@ -54,6 +58,13 @@ const regionsDomain = {
|
|
|
54
58
|
ca: "api-ca.storyblok.com",
|
|
55
59
|
ap: "api-ap.storyblok.com"
|
|
56
60
|
};
|
|
61
|
+
const appDomains = {
|
|
62
|
+
eu: "app.storyblok.com",
|
|
63
|
+
us: "app-us.storyblok.com",
|
|
64
|
+
cn: "app.storyblokchina.cn",
|
|
65
|
+
ca: "app-ca.storyblok.com",
|
|
66
|
+
ap: "app-ap.storyblok.com"
|
|
67
|
+
};
|
|
57
68
|
const regionNames = {
|
|
58
69
|
eu: "Europe",
|
|
59
70
|
us: "United States",
|
|
@@ -159,7 +170,9 @@ const API_ACTIONS = {
|
|
|
159
170
|
update_component_preset: "Failed to update component preset",
|
|
160
171
|
pull_stories: "Failed to pull stories",
|
|
161
172
|
pull_story: "Failed to pull story",
|
|
162
|
-
update_story: "Failed to update story"
|
|
173
|
+
update_story: "Failed to update story",
|
|
174
|
+
create_space: "Failed to create space",
|
|
175
|
+
fetch_blueprints: "Failed to fetch blueprints from GitHub"
|
|
163
176
|
};
|
|
164
177
|
const API_ERRORS = {
|
|
165
178
|
unauthorized: "The user is not authorized to access the API",
|
|
@@ -388,6 +401,9 @@ const toCamelCase = (str) => {
|
|
|
388
401
|
const capitalize = (str) => {
|
|
389
402
|
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
390
403
|
};
|
|
404
|
+
const toHumanReadable = (str) => {
|
|
405
|
+
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();
|
|
406
|
+
};
|
|
391
407
|
function maskToken(token) {
|
|
392
408
|
if (token.length <= 4) {
|
|
393
409
|
return token;
|
|
@@ -718,7 +734,7 @@ function session() {
|
|
|
718
734
|
return sessionInstance;
|
|
719
735
|
}
|
|
720
736
|
|
|
721
|
-
const program$
|
|
737
|
+
const program$f = getProgram();
|
|
722
738
|
const allRegionsText = Object.values(regions).join(",");
|
|
723
739
|
const loginStrategy = {
|
|
724
740
|
message: "How would you like to login?",
|
|
@@ -735,12 +751,12 @@ const loginStrategy = {
|
|
|
735
751
|
}
|
|
736
752
|
]
|
|
737
753
|
};
|
|
738
|
-
program$
|
|
754
|
+
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(
|
|
739
755
|
"-r, --region <region>",
|
|
740
756
|
`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
757
|
).action(async (options) => {
|
|
742
758
|
konsola.title(` ${commands.LOGIN} `, colorPalette.LOGIN);
|
|
743
|
-
const verbose = program$
|
|
759
|
+
const verbose = program$f.opts().verbose;
|
|
744
760
|
const { token, region } = options;
|
|
745
761
|
const { state, updateSession, persistCredentials, initializeSession } = session();
|
|
746
762
|
await initializeSession();
|
|
@@ -859,10 +875,10 @@ program$e.command(commands.LOGIN).description("Login to the Storyblok CLI").opti
|
|
|
859
875
|
konsola.br();
|
|
860
876
|
});
|
|
861
877
|
|
|
862
|
-
const program$
|
|
863
|
-
program$
|
|
878
|
+
const program$e = getProgram();
|
|
879
|
+
program$e.command(commands.LOGOUT).description("Logout from the Storyblok CLI").action(async () => {
|
|
864
880
|
konsola.title(` ${commands.LOGOUT} `, colorPalette.LOGOUT);
|
|
865
|
-
const verbose = program$
|
|
881
|
+
const verbose = program$e.opts().verbose;
|
|
866
882
|
try {
|
|
867
883
|
const { state, initializeSession } = session();
|
|
868
884
|
await initializeSession();
|
|
@@ -910,10 +926,10 @@ async function openSignupInBrowser(url) {
|
|
|
910
926
|
}
|
|
911
927
|
}
|
|
912
928
|
|
|
913
|
-
const program$
|
|
914
|
-
program$
|
|
929
|
+
const program$d = getProgram();
|
|
930
|
+
program$d.command(commands.SIGNUP).description("Sign up for Storyblok").action(async () => {
|
|
915
931
|
konsola.title(` ${commands.SIGNUP} `, colorPalette.SIGNUP);
|
|
916
|
-
const verbose = program$
|
|
932
|
+
const verbose = program$d.opts().verbose;
|
|
917
933
|
const { state, initializeSession } = session();
|
|
918
934
|
await initializeSession();
|
|
919
935
|
if (state.isLoggedIn && !state.envLogin) {
|
|
@@ -959,8 +975,8 @@ const getUser = async (token, region) => {
|
|
|
959
975
|
}
|
|
960
976
|
};
|
|
961
977
|
|
|
962
|
-
const program$
|
|
963
|
-
program$
|
|
978
|
+
const program$c = getProgram();
|
|
979
|
+
program$c.command(commands.USER).description("Get the current user").action(async () => {
|
|
964
980
|
konsola.title(` ${commands.USER} `, colorPalette.USER);
|
|
965
981
|
const { state, initializeSession } = session();
|
|
966
982
|
await initializeSession();
|
|
@@ -985,8 +1001,8 @@ program$b.command(commands.USER).description("Get the current user").action(asyn
|
|
|
985
1001
|
konsola.br();
|
|
986
1002
|
});
|
|
987
1003
|
|
|
988
|
-
const program$
|
|
989
|
-
const componentsCommand = program$
|
|
1004
|
+
const program$b = getProgram();
|
|
1005
|
+
const componentsCommand = program$b.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
1006
|
|
|
991
1007
|
let instance = null;
|
|
992
1008
|
const createMapiClient = (options) => {
|
|
@@ -1448,10 +1464,10 @@ async function readConsolidatedFiles(resolvedPath, suffix) {
|
|
|
1448
1464
|
};
|
|
1449
1465
|
}
|
|
1450
1466
|
|
|
1451
|
-
const program$
|
|
1467
|
+
const program$a = getProgram();
|
|
1452
1468
|
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
1469
|
konsola.title(` ${commands.COMPONENTS} `, colorPalette.COMPONENTS, componentName ? `Pulling component ${componentName}...` : "Pulling components...");
|
|
1454
|
-
const verbose = program$
|
|
1470
|
+
const verbose = program$a.opts().verbose;
|
|
1455
1471
|
const { space, path } = componentsCommand.opts();
|
|
1456
1472
|
const { separateFiles, suffix, filename = "components" } = options;
|
|
1457
1473
|
const { state, initializeSession } = session();
|
|
@@ -1568,9 +1584,16 @@ function buildDependencyGraph(context) {
|
|
|
1568
1584
|
const node = new ComponentNode(nodeId, component, targetComponent);
|
|
1569
1585
|
graph.nodes.set(nodeId, node);
|
|
1570
1586
|
});
|
|
1587
|
+
const componentMap = new Map(spaceState.local.components.map((c) => [c.id, c]));
|
|
1571
1588
|
spaceState.local.presets.forEach((preset) => {
|
|
1572
|
-
const nodeId = `preset:${preset.
|
|
1573
|
-
const
|
|
1589
|
+
const nodeId = `preset:${preset.id}`;
|
|
1590
|
+
const sourceComponent = componentMap.get(preset.component_id);
|
|
1591
|
+
if (!sourceComponent) {
|
|
1592
|
+
console.warn(`Warning: Preset "${preset.name}" (ID: ${preset.id}) references component ID ${preset.component_id} which is not available in local data. Skipping preset.`);
|
|
1593
|
+
return;
|
|
1594
|
+
}
|
|
1595
|
+
const compositeKey = `${sourceComponent.name}:${preset.name}`;
|
|
1596
|
+
const targetPreset = spaceState.target.presets.get(compositeKey);
|
|
1574
1597
|
const targetData = targetPreset ? {
|
|
1575
1598
|
resource: targetPreset,
|
|
1576
1599
|
id: targetPreset.id
|
|
@@ -1600,7 +1623,7 @@ function buildDependencyGraph(context) {
|
|
|
1600
1623
|
if (component.preset_id) {
|
|
1601
1624
|
const preset = spaceState.local.presets.find((p) => p.id === component.preset_id);
|
|
1602
1625
|
if (preset) {
|
|
1603
|
-
const presetId = `preset:${preset.
|
|
1626
|
+
const presetId = `preset:${preset.id}`;
|
|
1604
1627
|
addDependency(componentId, presetId);
|
|
1605
1628
|
}
|
|
1606
1629
|
}
|
|
@@ -1621,7 +1644,7 @@ function buildDependencyGraph(context) {
|
|
|
1621
1644
|
}
|
|
1622
1645
|
});
|
|
1623
1646
|
spaceState.local.presets.forEach((preset) => {
|
|
1624
|
-
const presetId = `preset:${preset.
|
|
1647
|
+
const presetId = `preset:${preset.id}`;
|
|
1625
1648
|
const component = spaceState.local.components.find((c) => c.id === preset.component_id);
|
|
1626
1649
|
if (component) {
|
|
1627
1650
|
const componentId = `component:${component.name}`;
|
|
@@ -1948,7 +1971,7 @@ class ComponentNode extends GraphNode {
|
|
|
1948
1971
|
if (this.sourceData.preset_id) {
|
|
1949
1972
|
const preset = this.findPresetById(this.sourceData.preset_id, graph);
|
|
1950
1973
|
if (preset) {
|
|
1951
|
-
const presetNodeId = `preset:${preset.
|
|
1974
|
+
const presetNodeId = `preset:${preset.id}`;
|
|
1952
1975
|
const presetNode = graph.nodes.get(presetNodeId);
|
|
1953
1976
|
if (presetNode?.targetData) {
|
|
1954
1977
|
updatedData.preset_id = presetNode.targetData.id;
|
|
@@ -2027,7 +2050,7 @@ class PresetNode {
|
|
|
2027
2050
|
constructor(preset, targetData) {
|
|
2028
2051
|
this.sourceData = preset;
|
|
2029
2052
|
this.targetData = targetData;
|
|
2030
|
-
this.id = `preset:${preset.
|
|
2053
|
+
this.id = `preset:${preset.id}`;
|
|
2031
2054
|
this.name = preset.name;
|
|
2032
2055
|
}
|
|
2033
2056
|
getName() {
|
|
@@ -2465,10 +2488,10 @@ async function pushWithDependencyGraph(space, spaceState, maxConcurrency = 5) {
|
|
|
2465
2488
|
return results;
|
|
2466
2489
|
}
|
|
2467
2490
|
|
|
2468
|
-
const program$
|
|
2491
|
+
const program$9 = getProgram();
|
|
2469
2492
|
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) => {
|
|
2470
2493
|
konsola.title(` ${commands.COMPONENTS} `, colorPalette.COMPONENTS, componentName ? `Pushing component ${componentName}...` : "Pushing components...");
|
|
2471
|
-
const verbose = program$
|
|
2494
|
+
const verbose = program$9.opts().verbose;
|
|
2472
2495
|
const { space, path } = componentsCommand.opts();
|
|
2473
2496
|
const { from, filter } = options;
|
|
2474
2497
|
const { state, initializeSession } = session();
|
|
@@ -2527,7 +2550,11 @@ componentsCommand.command("push [componentName]").description(`Push your space's
|
|
|
2527
2550
|
}
|
|
2528
2551
|
if (presets) {
|
|
2529
2552
|
presets.forEach((preset) => {
|
|
2530
|
-
|
|
2553
|
+
const targetComponent = components?.find((c) => c.id === preset.component_id);
|
|
2554
|
+
if (targetComponent) {
|
|
2555
|
+
const compositeKey = `${targetComponent.name}:${preset.name}`;
|
|
2556
|
+
spaceState.target.presets.set(compositeKey, preset);
|
|
2557
|
+
}
|
|
2531
2558
|
});
|
|
2532
2559
|
}
|
|
2533
2560
|
if (internalTags) {
|
|
@@ -2606,11 +2633,11 @@ const saveLanguagesToFile = async (space, internationalizationOptions, options)
|
|
|
2606
2633
|
}
|
|
2607
2634
|
};
|
|
2608
2635
|
|
|
2609
|
-
const program$
|
|
2610
|
-
const languagesCommand = program$
|
|
2636
|
+
const program$8 = getProgram();
|
|
2637
|
+
const languagesCommand = program$8.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");
|
|
2611
2638
|
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) => {
|
|
2612
2639
|
konsola.title(` ${commands.LANGUAGES} `, colorPalette.LANGUAGES);
|
|
2613
|
-
const verbose = program$
|
|
2640
|
+
const verbose = program$8.opts().verbose;
|
|
2614
2641
|
const { space, path } = languagesCommand.opts();
|
|
2615
2642
|
const { filename = "languages", suffix = options.space } = options;
|
|
2616
2643
|
const { state, initializeSession } = session();
|
|
@@ -2651,8 +2678,8 @@ languagesCommand.command("pull").description(`Download your space's languages sc
|
|
|
2651
2678
|
konsola.br();
|
|
2652
2679
|
});
|
|
2653
2680
|
|
|
2654
|
-
const program$
|
|
2655
|
-
const migrationsCommand = program$
|
|
2681
|
+
const program$7 = getProgram();
|
|
2682
|
+
const migrationsCommand = program$7.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");
|
|
2656
2683
|
|
|
2657
2684
|
const getMigrationTemplate = () => {
|
|
2658
2685
|
return `export default function (block) {
|
|
@@ -2680,10 +2707,10 @@ const generateMigration = async (space, path, component, suffix) => {
|
|
|
2680
2707
|
}
|
|
2681
2708
|
};
|
|
2682
2709
|
|
|
2683
|
-
const program$
|
|
2710
|
+
const program$6 = getProgram();
|
|
2684
2711
|
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) => {
|
|
2685
2712
|
konsola.title(` ${commands.MIGRATIONS} `, colorPalette.MIGRATIONS, componentName ? `Generating migration for component ${componentName}...` : "Generating migrations...");
|
|
2686
|
-
const verbose = program$
|
|
2713
|
+
const verbose = program$6.opts().verbose;
|
|
2687
2714
|
const { space, path } = migrationsCommand.opts();
|
|
2688
2715
|
const { suffix } = options;
|
|
2689
2716
|
if (!componentName) {
|
|
@@ -3099,10 +3126,10 @@ const isStoryWithUnpublishedChanges = (story) => {
|
|
|
3099
3126
|
return story.published && story.unpublished_changes;
|
|
3100
3127
|
};
|
|
3101
3128
|
|
|
3102
|
-
const program$
|
|
3129
|
+
const program$5 = getProgram();
|
|
3103
3130
|
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) => {
|
|
3104
3131
|
konsola.title(` ${commands.MIGRATIONS} `, colorPalette.MIGRATIONS, componentName ? `Running migrations for component ${componentName}...` : "Running migrations...");
|
|
3105
|
-
const verbose = program$
|
|
3132
|
+
const verbose = program$5.opts().verbose;
|
|
3106
3133
|
const { filter, dryRun = false, query, startsWith, publish } = options;
|
|
3107
3134
|
const { space, path } = migrationsCommand.opts();
|
|
3108
3135
|
const { state, initializeSession } = session();
|
|
@@ -3256,10 +3283,10 @@ migrationsCommand.command("run [componentName]").description("Run migrations").o
|
|
|
3256
3283
|
}
|
|
3257
3284
|
});
|
|
3258
3285
|
|
|
3259
|
-
const program$
|
|
3286
|
+
const program$4 = getProgram();
|
|
3260
3287
|
migrationsCommand.command("rollback [migrationFile]").description("Rollback a migration").action(async (migrationFile) => {
|
|
3261
3288
|
konsola.title(` ${commands.MIGRATIONS} `, colorPalette.MIGRATIONS, `Rolling back migration ${chalk.hex(colorPalette.MIGRATIONS)(migrationFile)}...`);
|
|
3262
|
-
const verbose = program$
|
|
3289
|
+
const verbose = program$4.opts().verbose;
|
|
3263
3290
|
const { space, path } = migrationsCommand.opts();
|
|
3264
3291
|
const { state, initializeSession } = session();
|
|
3265
3292
|
await initializeSession();
|
|
@@ -3298,8 +3325,8 @@ migrationsCommand.command("rollback [migrationFile]").description("Rollback a mi
|
|
|
3298
3325
|
}
|
|
3299
3326
|
});
|
|
3300
3327
|
|
|
3301
|
-
const program$
|
|
3302
|
-
const typesCommand = program$
|
|
3328
|
+
const program$3 = getProgram();
|
|
3329
|
+
const typesCommand = program$3.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");
|
|
3303
3330
|
|
|
3304
3331
|
const getAssetJSONSchema = (title) => ({
|
|
3305
3332
|
$id: "#/asset",
|
|
@@ -3998,20 +4025,11 @@ const generateStoryblokTypes = async (options = {}) => {
|
|
|
3998
4025
|
}
|
|
3999
4026
|
};
|
|
4000
4027
|
|
|
4001
|
-
const program$
|
|
4028
|
+
const program$2 = getProgram();
|
|
4002
4029
|
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) => {
|
|
4003
4030
|
konsola.title(` ${commands.TYPES} `, colorPalette.TYPES, "Generating types...");
|
|
4004
|
-
const verbose = program$
|
|
4031
|
+
const verbose = program$2.opts().verbose;
|
|
4005
4032
|
const { space, path } = typesCommand.opts();
|
|
4006
|
-
const { state, initializeSession } = session();
|
|
4007
|
-
await initializeSession();
|
|
4008
|
-
if (!requireAuthentication(state, verbose)) {
|
|
4009
|
-
return;
|
|
4010
|
-
}
|
|
4011
|
-
if (!space) {
|
|
4012
|
-
handleError(new CommandError(`Please provide the space as argument --space YOUR_SPACE_ID.`), verbose);
|
|
4013
|
-
return;
|
|
4014
|
-
}
|
|
4015
4033
|
const spinner = new Spinner({
|
|
4016
4034
|
verbose: !isVitest
|
|
4017
4035
|
});
|
|
@@ -4046,7 +4064,264 @@ typesCommand.command("generate").description("Generate types d.ts for your compo
|
|
|
4046
4064
|
}
|
|
4047
4065
|
});
|
|
4048
4066
|
|
|
4049
|
-
|
|
4067
|
+
let octokit;
|
|
4068
|
+
let lastToken;
|
|
4069
|
+
const createOctokit = (token) => {
|
|
4070
|
+
if (!octokit || token !== lastToken) {
|
|
4071
|
+
const options = {
|
|
4072
|
+
request: {
|
|
4073
|
+
fetch
|
|
4074
|
+
}
|
|
4075
|
+
};
|
|
4076
|
+
octokit = new Octokit(options);
|
|
4077
|
+
}
|
|
4078
|
+
return octokit;
|
|
4079
|
+
};
|
|
4080
|
+
|
|
4081
|
+
const generateProject = async (blueprint, projectName, targetPath = process.cwd()) => {
|
|
4082
|
+
try {
|
|
4083
|
+
const projectPath = path.join(targetPath, projectName);
|
|
4084
|
+
const templateRepo = `storyblok/blueprint-core-${blueprint}`;
|
|
4085
|
+
try {
|
|
4086
|
+
await fs.access(projectPath);
|
|
4087
|
+
const existsError = new Error(`Directory ${projectName} already exists`);
|
|
4088
|
+
existsError.code = "ENOTEMPTY";
|
|
4089
|
+
existsError.path = projectPath;
|
|
4090
|
+
throw new FileSystemError("directory_not_empty", "mkdir", existsError, `Directory ${projectName} already exists`);
|
|
4091
|
+
} catch (error) {
|
|
4092
|
+
const fsError = error;
|
|
4093
|
+
if (fsError.code === "ENOENT") {
|
|
4094
|
+
} else {
|
|
4095
|
+
handleFileSystemError("read", fsError);
|
|
4096
|
+
}
|
|
4097
|
+
}
|
|
4098
|
+
const degitProcess = spawn("npx", ["degit", templateRepo, projectPath], {
|
|
4099
|
+
stdio: "inherit",
|
|
4100
|
+
shell: true
|
|
4101
|
+
});
|
|
4102
|
+
return new Promise((resolve, reject) => {
|
|
4103
|
+
degitProcess.on("close", (code) => {
|
|
4104
|
+
if (code === 0) {
|
|
4105
|
+
resolve();
|
|
4106
|
+
} else {
|
|
4107
|
+
reject(new Error(`Failed to clone template. Process exited with code ${code}`));
|
|
4108
|
+
}
|
|
4109
|
+
});
|
|
4110
|
+
degitProcess.on("error", (error) => {
|
|
4111
|
+
reject(new Error(`Failed to spawn degit process: ${error.message}`));
|
|
4112
|
+
});
|
|
4113
|
+
});
|
|
4114
|
+
} catch (error) {
|
|
4115
|
+
handleFileSystemError("read", error);
|
|
4116
|
+
}
|
|
4117
|
+
};
|
|
4118
|
+
const createEnvFile = async (projectPath, accessToken, additionalVars) => {
|
|
4119
|
+
try {
|
|
4120
|
+
const envPath = path.join(projectPath, ".env");
|
|
4121
|
+
let envContent = `# Storyblok Configuration
|
|
4122
|
+
STORYBLOK_DELIVERY_API_TOKEN=${accessToken}
|
|
4123
|
+
`;
|
|
4124
|
+
if (additionalVars && Object.keys(additionalVars).length > 0) ;
|
|
4125
|
+
await saveToFile(envPath, envContent);
|
|
4126
|
+
} catch (error) {
|
|
4127
|
+
throw new Error(`Failed to create .env file: ${error.message}`);
|
|
4128
|
+
}
|
|
4129
|
+
};
|
|
4130
|
+
const generateSpaceUrl = (spaceId, region) => {
|
|
4131
|
+
const domain = appDomains[region];
|
|
4132
|
+
return `https://${domain}/#/me/spaces/${spaceId}/dashboard`;
|
|
4133
|
+
};
|
|
4134
|
+
const openSpaceInBrowser = async (spaceId, region) => {
|
|
4135
|
+
try {
|
|
4136
|
+
const spaceUrl = generateSpaceUrl(spaceId, region);
|
|
4137
|
+
await open(spaceUrl);
|
|
4138
|
+
} catch (error) {
|
|
4139
|
+
throw new Error(`Failed to open space in browser: ${error.message}`);
|
|
4140
|
+
}
|
|
4141
|
+
};
|
|
4142
|
+
const extractPortFromTopics = (topics) => {
|
|
4143
|
+
const portTopic = topics.find((topic) => topic.startsWith("port-"));
|
|
4144
|
+
if (portTopic) {
|
|
4145
|
+
const port = portTopic.replace("port-", "");
|
|
4146
|
+
if (/^\d+$/.test(port) && Number.parseInt(port) > 0 && Number.parseInt(port) <= 65535) {
|
|
4147
|
+
return port;
|
|
4148
|
+
}
|
|
4149
|
+
}
|
|
4150
|
+
return "3000";
|
|
4151
|
+
};
|
|
4152
|
+
const repositoryToBlueprint = (repo) => {
|
|
4153
|
+
const technology = repo.name.replace("blueprint-core-", "");
|
|
4154
|
+
const port = extractPortFromTopics(repo.topics || []);
|
|
4155
|
+
return {
|
|
4156
|
+
name: technology.charAt(0).toUpperCase() + technology.slice(1),
|
|
4157
|
+
value: technology,
|
|
4158
|
+
template: repo.clone_url,
|
|
4159
|
+
location: port ? `https://localhost:${port}/` : "https://localhost:3000/",
|
|
4160
|
+
description: repo.description,
|
|
4161
|
+
updated_at: repo.updated_at
|
|
4162
|
+
};
|
|
4163
|
+
};
|
|
4164
|
+
const fetchBlueprintRepositories = async () => {
|
|
4165
|
+
try {
|
|
4166
|
+
const octokit = createOctokit();
|
|
4167
|
+
const { data } = await octokit.rest.search.repos({
|
|
4168
|
+
q: "org:storyblok blueprint-core-",
|
|
4169
|
+
sort: "updated",
|
|
4170
|
+
order: "desc",
|
|
4171
|
+
per_page: 100
|
|
4172
|
+
});
|
|
4173
|
+
const blueprints = data.items.filter((repo) => repo.name.startsWith("blueprint-core-")).map(repositoryToBlueprint).sort((a, b) => a.name.localeCompare(b.name));
|
|
4174
|
+
return blueprints;
|
|
4175
|
+
} catch (error) {
|
|
4176
|
+
handleAPIError("fetch_blueprints", error, "Failed to fetch blueprints from GitHub");
|
|
4177
|
+
}
|
|
4178
|
+
};
|
|
4179
|
+
|
|
4180
|
+
const createSpace = async (space) => {
|
|
4181
|
+
try {
|
|
4182
|
+
const client = mapiClient();
|
|
4183
|
+
const { data } = await client.post("spaces", {
|
|
4184
|
+
body: JSON.stringify(space)
|
|
4185
|
+
});
|
|
4186
|
+
return data.space;
|
|
4187
|
+
} catch (error) {
|
|
4188
|
+
handleAPIError("create_space", error, `Failed to create space ${space.name}`);
|
|
4189
|
+
}
|
|
4190
|
+
};
|
|
4191
|
+
|
|
4192
|
+
const program$1 = getProgram();
|
|
4193
|
+
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) => {
|
|
4194
|
+
konsola.title(` ${commands.CREATE} `, colorPalette.CREATE);
|
|
4195
|
+
const verbose = program$1.opts().verbose;
|
|
4196
|
+
const { blueprint } = options;
|
|
4197
|
+
const { state, initializeSession } = session();
|
|
4198
|
+
await initializeSession();
|
|
4199
|
+
if (!requireAuthentication(state, verbose)) {
|
|
4200
|
+
return;
|
|
4201
|
+
}
|
|
4202
|
+
const { password, region } = state;
|
|
4203
|
+
mapiClient({
|
|
4204
|
+
token: password,
|
|
4205
|
+
region
|
|
4206
|
+
});
|
|
4207
|
+
const spinnerBlueprints = new Spinner({
|
|
4208
|
+
verbose: !isVitest
|
|
4209
|
+
});
|
|
4210
|
+
const spinnerSpace = new Spinner({
|
|
4211
|
+
verbose: !isVitest
|
|
4212
|
+
});
|
|
4213
|
+
try {
|
|
4214
|
+
spinnerBlueprints.start("Fetching starter blueprints...");
|
|
4215
|
+
const blueprints = await fetchBlueprintRepositories();
|
|
4216
|
+
spinnerBlueprints.succeed("Starter blueprints fetched successfully");
|
|
4217
|
+
if (!blueprints) {
|
|
4218
|
+
spinnerBlueprints.failed();
|
|
4219
|
+
konsola.warn("No starter blueprints found. Please contact support@storyblok.com");
|
|
4220
|
+
konsola.br();
|
|
4221
|
+
return;
|
|
4222
|
+
}
|
|
4223
|
+
let technologyBlueprint = blueprint;
|
|
4224
|
+
if (blueprint) {
|
|
4225
|
+
const validBlueprints = blueprints;
|
|
4226
|
+
const isValidBlueprint = validBlueprints.find((bp) => bp.value === blueprint);
|
|
4227
|
+
if (!isValidBlueprint) {
|
|
4228
|
+
const validOptions = validBlueprints.map((bp) => bp.value).join(", ");
|
|
4229
|
+
konsola.warn(`Invalid blueprint "${chalk.hex(colorPalette.CREATE)(blueprint)}". Valid options are: ${chalk.hex(colorPalette.CREATE)(validOptions)}`);
|
|
4230
|
+
konsola.br();
|
|
4231
|
+
technologyBlueprint = void 0;
|
|
4232
|
+
}
|
|
4233
|
+
}
|
|
4234
|
+
if (!technologyBlueprint) {
|
|
4235
|
+
technologyBlueprint = await select({
|
|
4236
|
+
message: "Please select the technology you would like to use:",
|
|
4237
|
+
choices: blueprints.map((blueprint2) => ({
|
|
4238
|
+
name: blueprint2.name,
|
|
4239
|
+
value: blueprint2.value
|
|
4240
|
+
}))
|
|
4241
|
+
});
|
|
4242
|
+
}
|
|
4243
|
+
let finalProjectPath = projectPath;
|
|
4244
|
+
if (!projectPath) {
|
|
4245
|
+
finalProjectPath = await input({
|
|
4246
|
+
message: "What is the path for your project?",
|
|
4247
|
+
default: `./my-${technologyBlueprint}-project`,
|
|
4248
|
+
validate: (value) => {
|
|
4249
|
+
if (!value.trim()) {
|
|
4250
|
+
return "Project path is required";
|
|
4251
|
+
}
|
|
4252
|
+
const projectName2 = path.basename(value);
|
|
4253
|
+
if (!/^[\w-]+$/.test(projectName2)) {
|
|
4254
|
+
return "Project name (last part of the path) can only contain letters, numbers, hyphens, and underscores";
|
|
4255
|
+
}
|
|
4256
|
+
return true;
|
|
4257
|
+
}
|
|
4258
|
+
});
|
|
4259
|
+
}
|
|
4260
|
+
const resolvedPath = path.resolve(finalProjectPath);
|
|
4261
|
+
const targetDirectory = path.dirname(resolvedPath);
|
|
4262
|
+
const projectName = path.basename(resolvedPath);
|
|
4263
|
+
konsola.br();
|
|
4264
|
+
konsola.info(`Scaffolding your project using the ${chalk.hex(colorPalette.CREATE)(technologyBlueprint)} blueprint...`);
|
|
4265
|
+
await generateProject(technologyBlueprint, projectName, targetDirectory);
|
|
4266
|
+
konsola.ok(`Project ${chalk.hex(colorPalette.PRIMARY)(projectName)} created successfully in ${chalk.hex(colorPalette.PRIMARY)(finalProjectPath)}`, true);
|
|
4267
|
+
let createdSpace;
|
|
4268
|
+
if (!options.skipSpace) {
|
|
4269
|
+
try {
|
|
4270
|
+
spinnerSpace.start(`Creating space "${toHumanReadable(projectName)}"`);
|
|
4271
|
+
const selectedBlueprint = blueprints.find((bp) => bp.value === technologyBlueprint);
|
|
4272
|
+
const blueprintDomain = selectedBlueprint?.location || "https://localhost:3000/";
|
|
4273
|
+
createdSpace = await createSpace({
|
|
4274
|
+
name: toHumanReadable(projectName),
|
|
4275
|
+
domain: blueprintDomain
|
|
4276
|
+
});
|
|
4277
|
+
spinnerSpace.succeed(`Space "${chalk.hex(colorPalette.PRIMARY)(toHumanReadable(projectName))}" created successfully`);
|
|
4278
|
+
} catch (error) {
|
|
4279
|
+
spinnerSpace.failed();
|
|
4280
|
+
konsola.br();
|
|
4281
|
+
handleError(error, verbose);
|
|
4282
|
+
return;
|
|
4283
|
+
}
|
|
4284
|
+
}
|
|
4285
|
+
if (createdSpace?.first_token) {
|
|
4286
|
+
try {
|
|
4287
|
+
await createEnvFile(resolvedPath, createdSpace.first_token);
|
|
4288
|
+
konsola.ok(`Created .env file with Storyblok access token`, true);
|
|
4289
|
+
} catch (error) {
|
|
4290
|
+
konsola.warn(`Failed to create .env file: ${error.message}`);
|
|
4291
|
+
konsola.info(`You can manually add this token to your .env file: ${createdSpace.first_token}`);
|
|
4292
|
+
}
|
|
4293
|
+
}
|
|
4294
|
+
if (createdSpace?.id) {
|
|
4295
|
+
try {
|
|
4296
|
+
await openSpaceInBrowser(createdSpace.id, region);
|
|
4297
|
+
konsola.info(`Opened space in your browser`);
|
|
4298
|
+
} catch (error) {
|
|
4299
|
+
konsola.warn(`Failed to open browser: ${error.message}`);
|
|
4300
|
+
const spaceUrl = generateSpaceUrl(createdSpace.id, region);
|
|
4301
|
+
konsola.info(`You can manually open your space at: ${chalk.hex(colorPalette.PRIMARY)(spaceUrl)}`);
|
|
4302
|
+
}
|
|
4303
|
+
}
|
|
4304
|
+
konsola.br();
|
|
4305
|
+
konsola.ok(`Your ${chalk.hex(colorPalette.PRIMARY)(technologyBlueprint)} project is ready \u{1F389} !`);
|
|
4306
|
+
if (createdSpace?.first_token) {
|
|
4307
|
+
konsola.ok(`Storyblok space created, preview url and .env configured automatically`);
|
|
4308
|
+
}
|
|
4309
|
+
konsola.br();
|
|
4310
|
+
konsola.info(`Next steps:
|
|
4311
|
+
cd ${finalProjectPath}
|
|
4312
|
+
npm install
|
|
4313
|
+
npm run dev
|
|
4314
|
+
`);
|
|
4315
|
+
} catch (error) {
|
|
4316
|
+
spinnerSpace.failed();
|
|
4317
|
+
spinnerBlueprints.failed();
|
|
4318
|
+
konsola.br();
|
|
4319
|
+
handleError(error, verbose);
|
|
4320
|
+
}
|
|
4321
|
+
konsola.br();
|
|
4322
|
+
});
|
|
4323
|
+
|
|
4324
|
+
const version = "4.1.0";
|
|
4050
4325
|
const pkg = {
|
|
4051
4326
|
version: version};
|
|
4052
4327
|
|