Skip to content
Open
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
41 changes: 38 additions & 3 deletions packages/cli/src/commands/types/generate/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ const DEFAULT_TYPEDEFS_HEADER = [
'// This file was generated by the storyblok CLI.',
'// DO NOT MODIFY THIS FILE BY HAND.',
];
const getDatasourceTypeTitle = (slug: string) =>
`${toPascalCase(slug)}DataSource`;

const getPropertyTypeAnnotation = (property: ComponentPropertySchema, prefix?: string, suffix?: string) => {
// If a property type is one of the ones provided by Storyblok, return that type
Expand Down Expand Up @@ -191,7 +193,13 @@ const getComponentPropertiesTypeAnnotations = async (
const componentType = toPascalCase(propertyType);
propertyTypeAnnotation[key].tsType = `Storyblok${componentType}`;
}
if (spaceData.datasources.length > 0 && schema.source === 'internal' && schema?.datasource_slug) {
const type = getDatasourceTypeTitle(schema.datasource_slug);

// Assign type based on whether it's single or multiple selection
propertyTypeAnnotation[key].tsType
= propertyType === 'options' ? `${type}[]` : type;
}
if (propertyType === 'multilink') {
const excludedLinktypes: string[] = [
...(!schema.email_link_type ? ['{ linktype?: "email" }'] : []),
Expand Down Expand Up @@ -306,6 +314,7 @@ export const generateTypes = async (
try {
const typeDefs = [...DEFAULT_TYPEDEFS_HEADER];
const storyblokPropertyTypes = new Set<string>();
const contentTypeBloks = new Set<string>();
let customFieldsParser: ((key: string, value: Record<string, unknown>) => Record<string, unknown>) | undefined;
let compilerOptions: Record<string, unknown> | undefined;
// Custom fields parser
Expand All @@ -317,10 +326,13 @@ export const generateTypes = async (
if (options.compilerOptions) {
compilerOptions = await loadCompilerOptions(options.compilerOptions);
}

const schemas = await Promise.all(spaceData.components.map(async (component) => {
const componentsSchema = spaceData.components.map(async (component) => {
// Get the component type name with proper handling of numbers at the start
const type = getComponentType(component.name, options);
// Add all the Content Type and Universial Blok to contentTypeBloks
if (component.is_root) {
contentTypeBloks.add(type);
}
const componentPropertiesTypeAnnotations = await getComponentPropertiesTypeAnnotations(component, options, spaceData, customFieldsParser);
const requiredFields = Object.entries(component?.schema || {}).reduce(
(acc: string[], [key, value]) => {
Expand Down Expand Up @@ -363,7 +375,30 @@ export const generateTypes = async (
};

return componentSchema;
}));
});
const datasourcesSchema = spaceData.datasources.map(async (datasource) => {
const enumValues: string[] | undefined = datasource.entries
?.filter(d => d.value)
.map(d => d.value!);
const datasourceSchema: JSONSchema = {
$id: `#/${datasource.slug}`,
title: getDatasourceTypeTitle(datasource.slug),
type: 'string',
enum: enumValues,
};
return datasourceSchema;
});
const contentTypeSchema: JSONSchema = {
$id: `#/ContentType`,
title: 'ContentType',
type: 'string',
tsType: `${Array.from(contentTypeBloks).join(' | ')}`,
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Async race: empty ContentType from unawaited promises

The contentTypeSchema is created using contentTypeBloks before the async component mapping functions have executed. Since componentsSchema is an array of unawaited promises, the contentTypeBloks.add(type) calls at line 334 haven't run yet when contentTypeSchema reads from the set at line 395. This results in ContentType always being an empty string instead of a union of all content type components.

Fix in Cursor Fix in Web

const schemas = await Promise.all([
...componentsSchema,
...datasourcesSchema,
contentTypeSchema,
]);

const result = await Promise.all(schemas.map(async (schema) => {
// Use the title as the interface name
Expand Down
29 changes: 22 additions & 7 deletions packages/cli/src/commands/types/generate/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import type { GenerateTypesOptions } from './constants';
import type { ReadComponentsOptions } from '../../components/push/constants';
import { typesCommand } from '../command';
import { generateStoryblokTypes, generateTypes, saveTypesToComponentsFile } from './actions';
import { readDatasourcesFiles } from '../../datasources/push/actions';
import type { SpaceDatasourcesData } from '../../../commands/datasources/constants';
import type { ReadDatasourcesOptions } from './../../datasources/push/constants';

const program = getProgram();

Expand Down Expand Up @@ -38,23 +41,35 @@ typesCommand

try {
spinner.start(`Generating types...`);
const spaceData = await readComponentsFiles({
...options as ReadComponentsOptions,
const componentsData = await readComponentsFiles({
...(options as ReadComponentsOptions),
from: space,
path,
});

// Try to read datasources, but make it optional
let dataSourceData: SpaceDatasourcesData;
try {
dataSourceData = await readDatasourcesFiles({
...(options as ReadDatasourcesOptions),
from: space,
path,
});
}
catch {
// If no datasources found, use empty array
dataSourceData = { datasources: [] };
}
await generateStoryblokTypes({
path,
});

// Add empty datasources array to match expected type for generateTypes
const spaceDataWithDatasources: ComponentsData & { datasources: [] } = {
...spaceData,
datasources: [],
const spaceDataWithComponentsAndDatasources: ComponentsData & SpaceDatasourcesData = {
...componentsData,
...dataSourceData,
};

const typedefString = await generateTypes(spaceDataWithDatasources, {
const typedefString = await generateTypes(spaceDataWithComponentsAndDatasources, {
...options,
path,
});
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/types/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export interface ComponentPropertySchema {
restrict_components?: boolean;
restrict_type?: 'groups' | 'components' | 'tags' | '';
source?: 'internal' | 'external' | 'internal_stories' | 'internal_languages';
datasource_slug?: string;
type: ComponentPropertySchemaType;
use_uuid?: boolean;
};
Loading