Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion packages/cli/build.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,13 @@ import { defineBuildConfig } from 'unbuild';

export default defineBuildConfig({
declaration: true,
entries: ['./src/index'],
entries: [
'./src/index',
{
input: './src/entrypoints/config',
name: 'config/index',
},
],
failOnWarn: false,
sourcemap: true,
});
12 changes: 12 additions & 0 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,18 @@
"node",
"javascript"
],
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.mjs"
},
"./config": {
"types": "./dist/config/index.d.ts",
"import": "./dist/config/index.mjs"
}
},
"main": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"bin": {
"storyblok": "./dist/index.mjs"
},
Expand All @@ -44,6 +55,7 @@
"@storyblok/region-helper": "workspace:*",
"@topcli/spinner": "^2.1.2",
"async-sema": "^3.1.1",
"c12": "^3.3.0",
"chalk": "^5.4.1",
"cli-progress": "^3.12.0",
"commander": "^13.1.0",
Expand Down
23 changes: 18 additions & 5 deletions packages/cli/src/api.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,22 @@
import { ManagementApiClient, type ManagementApiClientConfig } from '@storyblok/management-api-client';
import { RateLimit } from 'async-sema';
import { getActiveConfig } from './config';

let instance: ManagementApiClient | null = null;
let storedConfig: ManagementApiClientConfig | null = null;
const lim = RateLimit(6, {
uniformDistribution: true,
});

// Keep the limiter aligned with the currently resolved config (which can change per command run).
let currentLimiterCapacity = Math.max(1, getActiveConfig().api.maxConcurrency);
let limiter = RateLimit(currentLimiterCapacity, { uniformDistribution: true });

function resolveLimiter() {
const desiredCapacity = Math.max(1, getActiveConfig().api.maxConcurrency);
if (desiredCapacity !== currentLimiterCapacity) {
limiter = RateLimit(desiredCapacity, { uniformDistribution: true });
currentLimiterCapacity = desiredCapacity;
}
return limiter;
}

function configsAreEqual(config1: ManagementApiClientConfig, config2: ManagementApiClientConfig): boolean {
return JSON.stringify(config1) === JSON.stringify(config2);
Expand All @@ -15,7 +26,8 @@ export function mapiClient(options?: ManagementApiClientConfig) {
if (!instance && options) {
instance = new ManagementApiClient(options);
instance.interceptors.request.use(async (request) => {
await lim();
const limit = resolveLimiter();
await limit();
return request;
});
storedConfig = options;
Expand All @@ -27,7 +39,8 @@ export function mapiClient(options?: ManagementApiClientConfig) {
// Create new instance if options are different from stored config
instance = new ManagementApiClient(options);
instance.interceptors.request.use(async (request) => {
await lim();
const limit = resolveLimiter();
await limit();
return request;
});
storedConfig = options;
Expand Down
22 changes: 16 additions & 6 deletions packages/cli/src/commands/components/pull/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,16 @@ import { componentsCommand } from '../command';
import chalk from 'chalk';
import { getProgram } from '../../../program';
import { mapiClient } from '../../../api';
import { join } from 'pathe';
import { DEFAULT_STORAGE_DIR } from '../../../utils/filesystem';
import { parseOptionalBoolean } from '../../../config';

const program = getProgram();

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('-f, --filename <filename>', 'custom name to be used in file(s) name instead of space id', 'components')
.option('--sf, --separate-files [boolean]', 'Argument to create a single file for each component', parseOptionalBoolean, false)
.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: string | undefined, options: PullComponentsOptions) => {
Expand All @@ -25,7 +28,14 @@ componentsCommand

// Command options
const { space, path } = componentsCommand.opts();
const { separateFiles, suffix, filename = 'components' } = options;
const {
separateFiles = false,
suffix,
filename = 'components',
} = options;
// `--path` overrides remain command-scoped; fallback keeps the historic .storyblok output.
const resolvedBaseDir = path ?? DEFAULT_STORAGE_DIR;
const componentsOutputDir = join(resolvedBaseDir, 'components', space);

const { state, initializeSession } = session();
await initializeSession();
Expand Down Expand Up @@ -109,18 +119,18 @@ componentsCommand
if (filename !== 'components') {
konsola.warn(`The --filename option is ignored when using --separate-files`);
}
const filePath = path ? `${path}/components/${space}/` : `.storyblok/components/${space}/`;
const filePath = `${componentsOutputDir}/`;

konsola.ok(`Components downloaded successfully to ${chalk.hex(colorPalette.PRIMARY)(filePath)}`);
}
else if (componentName) {
const fileName = suffix ? `${filename}.${suffix}.json` : `${componentName}.json`;
const filePath = path ? `${path}/components/${space}/${fileName}` : `.storyblok/components/${space}/${fileName}`;
const filePath = join(componentsOutputDir, fileName);
konsola.ok(`Component ${chalk.hex(colorPalette.PRIMARY)(componentName)} downloaded successfully in ${chalk.hex(colorPalette.PRIMARY)(filePath)}`);
}
else {
const fileName = suffix ? `${filename}.${suffix}.json` : `${filename}.json`;
const filePath = path ? `${path}/components/${space}/${fileName}` : `.storyblok/components/${space}/${fileName}`;
const filePath = join(componentsOutputDir, fileName);

konsola.ok(`Components downloaded successfully to ${chalk.hex(colorPalette.PRIMARY)(filePath)}`);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { GraphBuildingContext, PushResults } from './types';

import { buildDependencyGraph, validateGraph } from './dependency-graph';
import { processAllResources } from './resource-processor';
import { getActiveConfig } from '../../../../config';

// Re-export commonly used utilities
export type { PushResults } from './types';
Expand All @@ -28,7 +29,8 @@ export type { PushResults } from './types';
export async function pushWithDependencyGraph(
space: string,
spaceState: SpaceComponentsDataState,
maxConcurrency: number = 5,

maxConcurrency: number = getActiveConfig().api.maxConcurrency,
): Promise<PushResults> {
// Build and validate the dependency graph with colocated target data
const context: GraphBuildingContext = { spaceState };
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { progressDisplay } from '../progress-display';
import { pushComponent } from '../actions';
import type { SpaceComponent } from '../../constants';
import chalk from 'chalk';
import { getActiveConfig } from '../../../../config';

// =============================================================================
// RESOURCE PROCESSING
Expand All @@ -16,7 +17,8 @@ import chalk from 'chalk';
export async function processAllResources(
graph: DependencyGraph,
space: string,
maxConcurrency: number = 5,

maxConcurrency: number = getActiveConfig().api.maxConcurrency,
): Promise<PushResults> {
const levels = determineProcessingOrder(graph);
const results: PushResults = { successful: [], failed: [] };
Expand Down
3 changes: 2 additions & 1 deletion packages/cli/src/commands/components/push/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import chalk from 'chalk';
import { mapiClient } from '../../../api';
import { fetchComponentGroups, fetchComponentInternalTags, fetchComponentPresets, fetchComponents } from '../actions';
import type { SpaceComponent, SpaceComponentFolder, SpaceComponentInternalTag, SpaceComponentPreset, SpaceComponentsData, SpaceComponentsDataState } from '../constants';
import { parseOptionalBoolean } from '../../../config';

const program = getProgram(); // Get the shared singleton instance

Expand All @@ -20,7 +21,7 @@ componentsCommand
.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('--sf, --separate-files [boolean]', 'Read from separate files instead of consolidated files', parseOptionalBoolean, false)
.option('--su, --suffix <suffix>', 'Suffix to add to the component name')

.action(async (componentName: string | undefined, options: PushComponentsOptions) => {
Expand Down
3 changes: 2 additions & 1 deletion packages/cli/src/commands/create/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { Spinner } from '@topcli/spinner';
import { mapiClient } from '../../api';
import type { User } from '../user/actions';
import { getUser } from '../user/actions';
import { parseOptionalBoolean } from '../../config';

const program = getProgram(); // Get the shared singleton instance

Expand All @@ -22,7 +23,7 @@ export const createCommand = program
.description(`Scaffold a new project using Storyblok`)
.option('-t, --template <template>', 'technology starter template')
.option('-b, --blueprint <blueprint>', '[DEPRECATED] use --template instead')
.option('--skip-space', 'skip space creation')
.option('--skip-space [boolean]', 'skip space creation', parseOptionalBoolean, false)
.action(async (projectPath: string, options: CreateOptions) => {
konsola.title(`${commands.CREATE}`, colorPalette.CREATE);
// Global options
Expand Down
3 changes: 2 additions & 1 deletion packages/cli/src/commands/datasources/delete/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,15 @@ import type { DeleteDatasourceOptions } from './constants';
import { mapiClient } from '../../../api';
import { fetchDatasource } from '../pull/actions';
import { confirm } from '@inquirer/prompts';
import { parseOptionalBoolean } from '../../../config';

// Register the delete command under datasources
// Usage: storyblok datasources delete <name> --space <SPACE_ID> [--id <ID>]
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)')
.option('--force [boolean]', 'Skip confirmation prompt for deletion (useful for CI)', parseOptionalBoolean, false)
.action(async (name: string, options: DeleteDatasourceOptions) => {
konsola.title(
`${commands.DATASOURCES}`,
Expand Down
22 changes: 16 additions & 6 deletions packages/cli/src/commands/datasources/pull/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,16 @@ import type { PullDatasourcesOptions } from './constants';
import { CommandError, handleError, isVitest, konsola, requireAuthentication } from '../../../utils';
import chalk from 'chalk';
import { fetchDatasource, fetchDatasources, saveDatasourcesToFiles } from './actions';
import { join } from 'pathe';
import { DEFAULT_STORAGE_DIR } from '../../../utils/filesystem';
import { parseOptionalBoolean } from '../../../config';

const program = getProgram();

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('-f, --filename <filename>', 'custom name to be used in file(s) name instead of space id', 'datasources')
.option('--sf, --separate-files [boolean]', 'Argument to create a single file for each datasource', parseOptionalBoolean, false)
.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: string | undefined, options: PullDatasourcesOptions) => {
Expand All @@ -26,7 +29,14 @@ datasourcesCommand

// Command options
const { space, path } = datasourcesCommand.opts();
const { separateFiles, suffix, filename = 'datasources' } = options;
const {
separateFiles = false,
suffix,
filename = 'datasources',
} = options;
// Keep writing under .storyblok unless a command-level --path explicitly overrides it.
const resolvedBaseDir = path ?? DEFAULT_STORAGE_DIR;
const datasourcesOutputDir = join(resolvedBaseDir, 'datasources', space);

const { state, initializeSession } = session();
await initializeSession();
Expand Down Expand Up @@ -84,17 +94,17 @@ datasourcesCommand
if (filename !== 'datasources') {
konsola.warn(`The --filename option is ignored when using --separate-files`);
}
const filePath = path ? `${path}/datasources/${space}/` : `.storyblok/datasources/${space}/`;
const filePath = `${datasourcesOutputDir}/`;
konsola.ok(`Datasources downloaded successfully to ${chalk.hex(colorPalette.PRIMARY)(filePath)}`);
}
else if (datasourceName) {
const fileName = suffix ? `${filename}.${suffix}.json` : `${datasourceName}.json`;
const filePath = path ? `${path}/datasources/${space}/${fileName}` : `.storyblok/datasources/${space}/${fileName}`;
const filePath = join(datasourcesOutputDir, fileName);
konsola.ok(`Datasource ${chalk.hex(colorPalette.PRIMARY)(datasourceName)} downloaded successfully in ${chalk.hex(colorPalette.PRIMARY)(filePath)}`);
}
else {
const fileName = suffix ? `${filename}.${suffix}.json` : `${filename}.json`;
const filePath = path ? `${path}/datasources/${space}/${fileName}` : `.storyblok/datasources/${space}/${fileName}`;
const filePath = join(datasourcesOutputDir, fileName);
konsola.ok(`Datasources downloaded successfully to ${chalk.hex(colorPalette.PRIMARY)(filePath)}`);
}
konsola.br();
Expand Down
3 changes: 2 additions & 1 deletion packages/cli/src/commands/datasources/push/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type { SpaceDatasource, SpaceDatasourcesDataState } from '../constants';
import { readDatasourcesFiles, upsertDatasource, upsertDatasourceEntry } from './actions';
import { fetchDatasources } from '../pull/actions';
import { Spinner } from '@topcli/spinner';
import { parseOptionalBoolean } from '../../../config';

const program = getProgram(); // Get the shared singleton instance

Expand All @@ -18,7 +19,7 @@ datasourcesCommand
.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('--sf, --separate-files [boolean]', 'Read from separate files instead of consolidated files', parseOptionalBoolean, false)
.option('--su, --suffix <suffix>', 'Suffix to add to the datasource name')
.action(async (datasourceName: string | undefined, options: PushDatasourcesOptions) => {
konsola.title(`${commands.DATASOURCES}`, colorPalette.DATASOURCES, datasourceName ? `Pushing datasource ${datasourceName}...` : 'Pushing datasources...');
Expand Down
3 changes: 2 additions & 1 deletion packages/cli/src/commands/migrations/run/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,14 @@ import { mapiClient } from '../../../api';
import { MultiBar, Presets } from 'cli-progress';
import { pipeline } from 'node:stream';
import chalk from 'chalk';
import { parseOptionalBoolean } from '../../../config';

const program = getProgram();

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('-d, --dry-run [boolean]', 'Preview changes without applying them to Storyblok', parseOptionalBoolean, false)
.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')
Expand Down
6 changes: 4 additions & 2 deletions packages/cli/src/commands/types/generate/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,20 @@ import { generateStoryblokTypes, generateTypes, saveTypesToComponentsFile } from
import { readDatasourcesFiles } from '../../datasources/push/actions';
import type { SpaceDatasourcesData } from '../../../commands/datasources/constants';
import type { ReadDatasourcesOptions } from './../../datasources/push/constants';
import { parseOptionalBoolean } from '../../../config';

const program = getProgram();

typesCommand
.command('generate')
.description('Generate types d.ts for your component schemas')
.option('--sf, --separate-files', 'Generate one .d.ts file per component instead of a single combined file')
.option(
'--filename <name>',
'Base file name for all component types when generating a single declarations file (e.g. components.d.ts). Ignored when using --separate-files.',
)
.option('--strict', 'strict mode, no loose typing')

.option('--sf, --separate-files [boolean]', '', parseOptionalBoolean, false)
.option('--strict [boolean]', 'strict mode, no loose typing', parseOptionalBoolean, false)
.option('--type-prefix <prefix>', 'prefix to be prepended to all generated component type names')
.option('--type-suffix <suffix>', 'suffix to be appended to all generated component type names')
.option('--suffix <suffix>', 'Components suffix')
Expand Down
Loading
Loading