Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
150 changes: 144 additions & 6 deletions packages/cli/src/commands/types/generate/actions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,19 @@ import { join, resolve } from 'node:path';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import type { SpaceComponent, SpaceComponentsData } from '../../../commands/components/constants';
import type { GenerateTypesOptions } from './constants';
import { generateStoryblokTypes, generateTypes, getComponentType, getStoryType } from './actions';
import { generateStoryblokTypes, generateTypes, getComponentType, getStoryType, saveTypesToFile } from './actions';

// Import the mocked functions
import { saveToFile } from '../../../utils/filesystem';

// Mock the filesystem module
vi.mock('../../../utils/filesystem', () => ({
saveToFile: vi.fn().mockResolvedValue(undefined),
resolvePath: vi.fn().mockReturnValue('/mocked/resolved/path'),
}));
vi.mock('../../../utils/filesystem', async (importOriginal) => {
const actual = await importOriginal<typeof import("../../../utils/filesystem")>();
return {
sanitizeFilename: actual.sanitizeFilename,
saveToFile: vi.fn().mockResolvedValue(undefined),
resolvePath: vi.fn().mockReturnValue('/mocked/resolved/path'),
};
});

// Mock the fs module
vi.mock('node:fs', () => ({
Expand Down Expand Up @@ -815,3 +818,138 @@ describe('generateStoryblokTypes', () => {
expect(savedContent).toContain('export interface StoryblokCustom');
});
});

describe('separate files mode', () => {
it('returns an array of files when options.separateFiles = true (one per component)', async () => {
const spaceData: SpaceComponentsData = {
components: [
{
name: 'Hero Section X',
display_name: 'Hero',
created_at: '2023-01-01T00:00:00Z',
updated_at: '2023-01-01T00:00:00Z',
id: 1,
schema: {
title: { type: 'text', required: true },
},
internal_tags_list: [],
internal_tag_ids: [],
},
{
name: 'cta-button',
display_name: 'CTA Button',
created_at: '2023-01-01T00:00:00Z',
updated_at: '2023-01-01T00:00:00Z',
id: 2,
schema: {
href: { type: 'multilink', required: false, email_link_type: true, asset_link_type: true },
},
internal_tags_list: [],
internal_tag_ids: [],
},
],
datasources: [],
groups: [],
presets: [],
internalTags: [],
};

const result = await generateTypes(spaceData, { separateFiles: true, strict: false });
expect(Array.isArray(result)).toBe(true);
const files = result as { name: string; content: string }[];
expect(files).toHaveLength(2);

expect(files[0].content).toContain('// This file was generated by the storyblok CLI.');
expect(files[0].content).toContain('// DO NOT MODIFY THIS FILE BY HAND.');
expect(files[0].name).toBe('Hero Section X')
expect(files.some(f => f.content.includes('export interface HeroSectionX'))).toBe(true);
expect(files[1].name).toBe('cta-button')
expect(files.some(f => f.content.includes('export interface CtaButton'))).toBe(true);
});

it('scopes Storyblok imports per file (only files that need Multilink import should have it)', async () => {
const withMultilink: SpaceComponent = {
name: 'link-card',
created_at: '2023-01-01T00:00:00Z',
updated_at: '2023-01-01T00:00:00Z',
id: 1,
schema: {
link: { type: 'multilink', required: false, email_link_type: true, asset_link_type: true },
},
};

const simpleText: SpaceComponent = {
name: 'plain-text',
created_at: '2023-01-01T00:00:00Z',
updated_at: '2023-01-01T00:00:00Z',
id: 2,
schema: {
text: { type: 'text', required: false },
},
};

const spaceData: SpaceComponentsData = {
components: [withMultilink, simpleText],
datasources: [],
groups: [],
presets: [],
internalTags: [],
};

const res = await generateTypes(spaceData, { separateFiles: true, strict: false });
const files = res as { name: string; content: string }[];

const fileWith = files.find(f => f.name.includes('link-card') || f.content.includes('export interface LinkCard'));
const fileWithout = files.find(f => f.name.includes('plain-text') || f.content.includes('export interface PlainText'));

expect(fileWith).toBeDefined();
expect(fileWithout).toBeDefined();

// Multilink import only in the file that needs it
expect(fileWith!.content).toMatch(/import type \{ StoryblokMultilink \} from '\.\.\/storyblok\.d\.ts';/);
expect(fileWithout!.content).not.toMatch(/import type \{ StoryblokMultilink \} from '\.\.\/storyblok\.d\.ts';/);
});

it('saveTypesToFile writes one file per component when separateFiles = true and ignores filename', async () => {
vi.clearAllMocks();

const files = [
{ name: 'Hero', content: '// hero' },
{ name: 'Button', content: '// button' },
];

await saveTypesToFile('12345', files, { separateFiles: true, filename: 'IGNORED' });

// Two writes
expect(saveToFile).toHaveBeenCalledTimes(2);

// join() receives the sanitized filenames (we only mocked return; assert args)
expect(join).toHaveBeenCalledWith(expect.any(String), 'Hero.d.ts');
expect(join).toHaveBeenCalledWith(expect.any(String), 'Button.d.ts');

// Should not try to use the single-file filename
expect(join).not.toHaveBeenCalledWith(expect.any(String), 'IGNORED.d.ts');
});

it('default behavior still returns a single string when separateFiles is not set', async () => {
const spaceData: SpaceComponentsData = {
components: [
{
name: 'simple',
created_at: '2023-01-01T00:00:00Z',
updated_at: '2023-01-01T00:00:00Z',
id: 1,
schema: { title: { type: 'text', required: true } },
},
],
datasources: [],
groups: [],
presets: [],
internalTags: [],
};

const result = await generateTypes(spaceData, {});
expect(typeof result).toBe('string');
expect(result).toContain('export interface Simple');
});
});
Loading