diff --git a/code/lib/core-events/src/index.ts b/code/lib/core-events/src/index.ts index 82978a994d07..ec6bd134be33 100644 --- a/code/lib/core-events/src/index.ts +++ b/code/lib/core-events/src/index.ts @@ -75,6 +75,8 @@ enum events { TELEMETRY_ERROR = 'telemetryError', FILE_COMPONENT_SEARCH = 'fileComponentSearch', FILE_COMPONENT_SEARCH_RESULT = 'fileComponentSearchResult', + CREATE_NEW_STORYFILE = 'createNewStoryfile', + CREATE_NEW_STORYFILE_RESULT = 'createNewStoryfileResult', } // Enables: `import Events from ...` @@ -86,6 +88,8 @@ export const { CHANNEL_WS_DISCONNECT, CHANNEL_CREATED, CONFIG_ERROR, + CREATE_NEW_STORYFILE, + CREATE_NEW_STORYFILE_RESULT, CURRENT_STORY_WAS_SET, DOCS_PREPARED, DOCS_RENDERED, diff --git a/code/lib/core-server/package.json b/code/lib/core-server/package.json index 391bfbda11fc..4116c6f9daa5 100644 --- a/code/lib/core-server/package.json +++ b/code/lib/core-server/package.json @@ -108,6 +108,7 @@ "@types/node-fetch": "^2.5.7", "@types/ws": "^8", "boxen": "^7.1.1", + "camelcase": "^8.0.0", "node-fetch": "^3.3.1", "slash": "^5.0.0", "typescript": "^5.3.2" diff --git a/code/lib/core-server/src/presets/common-preset.ts b/code/lib/core-server/src/presets/common-preset.ts index fbc39465aad4..f06e050f958d 100644 --- a/code/lib/core-server/src/presets/common-preset.ts +++ b/code/lib/core-server/src/presets/common-preset.ts @@ -35,6 +35,7 @@ import { parseStaticDir } from '../utils/server-statics'; import { defaultStaticDirs } from '../utils/constants'; import { sendTelemetryError } from '../withTelemetry'; import { initFileSearchChannel } from '../server-channel/file-search-channel'; +import { initCreateNewStoryChannel } from '../server-channel/create-new-story-channel'; const interpolate = (string: string, data: Record = {}) => Object.entries(data).reduce((acc, [k, v]) => acc.replace(new RegExp(`%${k}%`, 'g'), v), string); @@ -342,6 +343,7 @@ export const experimental_serverChannel = async ( }); initFileSearchChannel(channel, options); + initCreateNewStoryChannel(channel, options); return channel; }; diff --git a/code/lib/core-server/src/server-channel/create-new-story-channel.test.ts b/code/lib/core-server/src/server-channel/create-new-story-channel.test.ts new file mode 100644 index 000000000000..289756ea540b --- /dev/null +++ b/code/lib/core-server/src/server-channel/create-new-story-channel.test.ts @@ -0,0 +1,120 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { initCreateNewStoryChannel } from './create-new-story-channel'; +import path from 'path'; +import type { ChannelTransport } from '@storybook/channels'; +import { Channel } from '@storybook/channels'; +import { CREATE_NEW_STORYFILE, CREATE_NEW_STORYFILE_RESULT } from '@storybook/core-events'; + +vi.mock('@storybook/core-common', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + getProjectRoot: vi.fn().mockReturnValue(process.cwd()), + }; +}); + +const mockFs = vi.hoisted(() => { + return { + writeFile: vi.fn(), + }; +}); + +vi.mock('node:fs/promises', async (importOriginal) => { + const actual = await importOriginal(); + return { + default: { + ...actual, + writeFile: mockFs.writeFile, + }, + }; +}); + +describe('createNewStoryChannel', () => { + const transport = { setHandler: vi.fn(), send: vi.fn() } satisfies ChannelTransport; + const mockChannel = new Channel({ transport }); + const createNewStoryFileEventListener = vi.fn(); + + beforeEach(() => { + transport.setHandler.mockClear(); + transport.send.mockClear(); + createNewStoryFileEventListener.mockClear(); + }); + + describe('initCreateNewStoryChannel', () => { + it('should emit an event with a story id', async () => { + mockChannel.addListener(CREATE_NEW_STORYFILE_RESULT, createNewStoryFileEventListener); + const cwd = process.cwd(); + + initCreateNewStoryChannel(mockChannel, { + configDir: path.join(cwd, '.storybook'), + presets: { + apply: (val: string) => { + if (val === 'framework') { + return Promise.resolve('@storybook/nextjs'); + } + if (val === 'stories') { + return Promise.resolve(['../src/**/*.stories.@(js|jsx|mjs|ts|tsx)']); + } + }, + }, + } as any); + + mockChannel.emit(CREATE_NEW_STORYFILE, { + componentFilePath: 'src/components/Page.jsx', + componentExportName: 'Page', + componentIsDefaultExport: true, + }); + + await vi.waitFor(() => { + expect(createNewStoryFileEventListener).toHaveBeenCalled(); + }); + + expect(createNewStoryFileEventListener).toHaveBeenCalledWith({ + error: null, + result: { + storyId: 'components-page--default', + }, + success: true, + }); + }); + + it('should emit an error event if an error occurs', async () => { + mockChannel.addListener(CREATE_NEW_STORYFILE_RESULT, createNewStoryFileEventListener); + const cwd = process.cwd(); + + mockFs.writeFile.mockImplementation(() => { + throw new Error('Failed to write file'); + }); + + initCreateNewStoryChannel(mockChannel, { + configDir: path.join(cwd, '.storybook'), + presets: { + apply: (val: string) => { + if (val === 'framework') { + return Promise.resolve('@storybook/nextjs'); + } + if (val === 'stories') { + return Promise.resolve(['../src/**/*.stories.@(js|jsx|mjs|ts|tsx)']); + } + }, + }, + } as any); + + mockChannel.emit(CREATE_NEW_STORYFILE, { + componentFilePath: 'src/components/Page.jsx', + componentExportName: 'Page', + componentIsDefaultExport: true, + }); + + await vi.waitFor(() => { + expect(createNewStoryFileEventListener).toHaveBeenCalled(); + }); + + expect(createNewStoryFileEventListener).toHaveBeenCalledWith({ + error: 'An error occurred while creating a new story:\nFailed to write file', + result: null, + success: false, + }); + }); + }); +}); diff --git a/code/lib/core-server/src/server-channel/create-new-story-channel.ts b/code/lib/core-server/src/server-channel/create-new-story-channel.ts new file mode 100644 index 000000000000..7546c5910a70 --- /dev/null +++ b/code/lib/core-server/src/server-channel/create-new-story-channel.ts @@ -0,0 +1,51 @@ +import type { Options } from '@storybook/types'; +import type { Channel } from '@storybook/channels'; +import { CREATE_NEW_STORYFILE, CREATE_NEW_STORYFILE_RESULT } from '@storybook/core-events'; +import fs from 'node:fs/promises'; +import type { NewStoryData } from '../utils/get-new-story-file'; +import { getNewStoryFile } from '../utils/get-new-story-file'; +import { getStoryId } from '../utils/get-story-id'; + +interface CreateNewStoryPayload extends NewStoryData {} + +interface Result { + success: true | false; + result: null | { + storyId: string; + }; + error: null | string; +} + +export function initCreateNewStoryChannel(channel: Channel, options: Options) { + /** + * Listens for events to create a new storyfile + */ + channel.on(CREATE_NEW_STORYFILE, async (data: CreateNewStoryPayload) => { + try { + const { storyFilePath, exportedStoryName, storyFileContent } = await getNewStoryFile( + data, + options + ); + + await fs.writeFile(storyFilePath, storyFileContent, 'utf-8'); + + const storyId = await getStoryId({ storyFilePath, exportedStoryName }, options); + + channel.emit(CREATE_NEW_STORYFILE_RESULT, { + success: true, + result: { + storyId, + }, + error: null, + } satisfies Result); + } catch (e: any) { + channel.emit(CREATE_NEW_STORYFILE_RESULT, { + success: false, + result: null, + error: `An error occurred while creating a new story:\n${e?.message}`, + } satisfies Result); + } + }); + + return channel; +} diff --git a/code/lib/core-server/src/utils/get-component-variable-name.test.ts b/code/lib/core-server/src/utils/get-component-variable-name.test.ts new file mode 100644 index 000000000000..07b2fb5599aa --- /dev/null +++ b/code/lib/core-server/src/utils/get-component-variable-name.test.ts @@ -0,0 +1,11 @@ +import { describe, expect, it } from 'vitest'; +import { getComponentVariableName } from './get-component-variable-name'; + +describe('get-variable-name', () => { + it('should return a valid variable name for a given string', async () => { + await expect(getComponentVariableName('foo-bar')).resolves.toBe('FooBar'); + await expect(getComponentVariableName('foo bar')).resolves.toBe('FooBar'); + await expect(getComponentVariableName('0-foo-bar')).resolves.toBe('FooBar'); + await expect(getComponentVariableName('*Foo-bar-$')).resolves.toBe('FooBar$'); + }); +}); diff --git a/code/lib/core-server/src/utils/get-component-variable-name.ts b/code/lib/core-server/src/utils/get-component-variable-name.ts new file mode 100644 index 000000000000..bc29ff9e51d6 --- /dev/null +++ b/code/lib/core-server/src/utils/get-component-variable-name.ts @@ -0,0 +1,12 @@ +/** + * Get a valid variable name for a component. + * + * @param name The name of the component. + * @returns A valid variable name. + */ +export const getComponentVariableName = async (name: string) => { + const camelCase = await import('camelcase'); + const camelCased = camelCase.default(name.replace(/^[^a-zA-Z_$]*/, ''), { pascalCase: true }); + const sanitized = camelCased.replace(/[^a-zA-Z_$]+/, ''); + return sanitized; +}; diff --git a/code/lib/core-server/src/utils/get-new-story-file.test.ts b/code/lib/core-server/src/utils/get-new-story-file.test.ts new file mode 100644 index 000000000000..429801ad0186 --- /dev/null +++ b/code/lib/core-server/src/utils/get-new-story-file.test.ts @@ -0,0 +1,83 @@ +import { describe, expect, it, vi } from 'vitest'; +import { getNewStoryFile } from './get-new-story-file'; +import path from 'path'; + +vi.mock('@storybook/core-common', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + getProjectRoot: vi.fn().mockReturnValue(require('path').join(__dirname)), + }; +}); + +describe('get-new-story-file', () => { + it('should create a new story file (TypeScript)', async () => { + const { exportedStoryName, storyFileContent, storyFilePath } = await getNewStoryFile( + { + componentFilePath: 'src/components/Page.tsx', + componentExportName: 'Page', + componentIsDefaultExport: false, + }, + { + presets: { + apply: (val: string) => { + if (val === 'framework') { + return Promise.resolve('@storybook/nextjs'); + } + }, + }, + } as any + ); + + expect(exportedStoryName).toBe('Default'); + expect(storyFileContent).toMatchInlineSnapshot(` + "import type { Meta, StoryObj } from '@storybook/nextjs'; + + import { Page } from './Page'; + + const meta = { + component: Page, + } satisfies Meta; + + export default meta; + + type Story = StoryObj; + + export const Default: Story = {};" + `); + expect(storyFilePath).toBe(path.join(__dirname, 'src', 'components', 'Page.stories.tsx')); + }); + + it('should create a new story file (JavaScript)', async () => { + const { exportedStoryName, storyFileContent, storyFilePath } = await getNewStoryFile( + { + componentFilePath: 'src/components/Page.jsx', + componentExportName: 'Page', + componentIsDefaultExport: true, + }, + { + presets: { + apply: (val: string) => { + if (val === 'framework') { + return Promise.resolve('@storybook/nextjs'); + } + }, + }, + } as any + ); + + expect(exportedStoryName).toBe('Default'); + expect(storyFileContent).toMatchInlineSnapshot(` + "import Page from './Page'; + + const meta = { + component: Page, + }; + + export default meta; + + export const Default = {};" + `); + expect(storyFilePath).toBe(path.join(__dirname, 'src', 'components', 'Page.stories.jsx')); + }); +}); diff --git a/code/lib/core-server/src/utils/get-new-story-file.ts b/code/lib/core-server/src/utils/get-new-story-file.ts new file mode 100644 index 000000000000..cfec1dd52152 --- /dev/null +++ b/code/lib/core-server/src/utils/get-new-story-file.ts @@ -0,0 +1,59 @@ +import type { Options } from '@storybook/types'; +import { getFrameworkName, getProjectRoot } from '@storybook/core-common'; +import path from 'node:path'; +import fs from 'node:fs'; +import { getTypeScriptTemplateForNewStoryFile } from './new-story-templates/typescript'; +import { getJavaScriptTemplateForNewStoryFile } from './new-story-templates/javascript'; + +export interface NewStoryData { + // The filepath of the component for which the Story should be generated for (relative to the project root) + componentFilePath: string; + // The name of the exported component + componentExportName: string; + // is default export + componentIsDefaultExport: boolean; +} + +export async function getNewStoryFile( + { componentFilePath, componentExportName, componentIsDefaultExport }: NewStoryData, + options: Options +) { + const isTypescript = /\.(ts|tsx|mts|cts)$/.test(componentFilePath); + const cwd = getProjectRoot(); + + const frameworkPackageName = await getFrameworkName(options); + + const basename = path.basename(componentFilePath); + const extension = path.extname(componentFilePath); + const basenameWithoutExtension = basename.replace(extension, ''); + const dirname = path.dirname(componentFilePath); + + const storyFileExtension = isTypescript ? 'tsx' : 'jsx'; + const storyFileName = `${basenameWithoutExtension}.stories.${storyFileExtension}`; + const alternativeStoryFileName = `${basenameWithoutExtension}.${componentExportName}.stories.${storyFileExtension}`; + + const exportedStoryName = 'Default'; + + const storyFileContent = isTypescript + ? await getTypeScriptTemplateForNewStoryFile({ + basenameWithoutExtension, + componentExportName, + componentIsDefaultExport, + frameworkPackageName, + exportedStoryName, + }) + : await getJavaScriptTemplateForNewStoryFile({ + basenameWithoutExtension, + componentExportName, + componentIsDefaultExport, + exportedStoryName, + }); + + const doesStoryFileExist = fs.existsSync(path.join(cwd, componentFilePath)); + + const storyFilePath = doesStoryFileExist + ? path.join(cwd, dirname, alternativeStoryFileName) + : path.join(cwd, dirname, storyFileName); + + return { storyFilePath, exportedStoryName, storyFileContent }; +} diff --git a/code/lib/core-server/src/utils/get-story-id.test.ts b/code/lib/core-server/src/utils/get-story-id.test.ts new file mode 100644 index 000000000000..4fb0230bc21e --- /dev/null +++ b/code/lib/core-server/src/utils/get-story-id.test.ts @@ -0,0 +1,45 @@ +import path from 'path'; +import { describe, expect, it } from 'vitest'; +import { getStoryId } from './get-story-id'; + +describe('getStoryId', () => { + it('should return the storyId', async () => { + const cwd = process.cwd(); + const options = { + configDir: path.join(cwd, '.storybook'), + presets: { + apply: (val: string) => { + if (val === 'stories') { + return Promise.resolve(['../src/**/*.stories.@(js|jsx|mjs|ts|tsx)']); + } + }, + }, + } as any; + const storyFilePath = path.join(cwd, 'src', 'components', 'stories', 'Page1.stories.ts'); + const exportedStoryName = 'Default'; + + const storyId = await getStoryId({ storyFilePath, exportedStoryName }, options); + + expect(storyId).toBe('components-stories-page1--default'); + }); + + it('should throw an error if the storyId cannot be calculated', async () => { + const cwd = process.cwd(); + const options = { + configDir: path.join(cwd, '.storybook'), + presets: { + apply: (val: string) => { + if (val === 'stories') { + return Promise.resolve(['../src/**/*.stories.@(js|jsx|mjs|ts|tsx)']); + } + }, + }, + } as any; + const storyFilePath = path.join(cwd, 'not-covered-path', 'stories', 'Page1.stories.ts'); + const exportedStoryName = 'Default'; + + await expect(() => + getStoryId({ storyFilePath, exportedStoryName }, options) + ).rejects.toThrowError(); + }); +}); diff --git a/code/lib/core-server/src/utils/get-story-id.ts b/code/lib/core-server/src/utils/get-story-id.ts new file mode 100644 index 000000000000..acfbce990853 --- /dev/null +++ b/code/lib/core-server/src/utils/get-story-id.ts @@ -0,0 +1,44 @@ +import type { Options } from '@storybook/types'; +import dedent from 'ts-dedent'; +import { normalizeStories, normalizeStoryPath } from '@storybook/core-common'; +import path from 'path'; +import { storyNameFromExport, toId } from '@storybook/csf'; +import { userOrAutoTitleFromSpecifier } from '@storybook/preview-api'; +import { posix } from './posix'; + +interface StoryIdData { + storyFilePath: string; + exportedStoryName: string; +} + +export async function getStoryId(data: StoryIdData, options: Options) { + const stories = await options.presets.apply('stories', [], options); + + const workingDir = process.cwd(); + + const normalizedStories = normalizeStories(stories, { + configDir: options.configDir, + workingDir, + }); + + const relativePath = path.relative(workingDir, data.storyFilePath); + const importPath = posix(normalizeStoryPath(relativePath)); + + const autoTitle = normalizedStories + .map((normalizeStory) => userOrAutoTitleFromSpecifier(importPath, normalizeStory)) + .filter(Boolean)[0]; + + if (autoTitle === undefined) { + // eslint-disable-next-line local-rules/no-uncategorized-errors + throw new Error(dedent` + The generation of your new Story file was successful but it seems that we are unable to index it. + Please make sure that the new Story file is matched by the 'stories' glob pattern in your Storybook configuration. + The location of the new Story file is: ${relativePath} + `); + } + + const storyName = storyNameFromExport(data.exportedStoryName); + const storyId = toId(autoTitle as string, storyName); + + return storyId; +} diff --git a/code/lib/core-server/src/utils/new-story-templates/javascript.test.ts b/code/lib/core-server/src/utils/new-story-templates/javascript.test.ts new file mode 100644 index 000000000000..525c0fe25f4c --- /dev/null +++ b/code/lib/core-server/src/utils/new-story-templates/javascript.test.ts @@ -0,0 +1,46 @@ +import { describe, expect, it } from 'vitest'; +import { getJavaScriptTemplateForNewStoryFile } from './javascript'; + +describe('javascript', () => { + it('should return a TypeScript template with a default import', async () => { + const result = await getJavaScriptTemplateForNewStoryFile({ + basenameWithoutExtension: 'foo', + componentExportName: 'default', + componentIsDefaultExport: true, + exportedStoryName: 'Default', + }); + + expect(result).toMatchInlineSnapshot(` + "import Foo from './foo'; + + const meta = { + component: Foo, + }; + + export default meta; + + export const Default = {};" + `); + }); + + it('should return a TypeScript template with a named import', async () => { + const result = await getJavaScriptTemplateForNewStoryFile({ + basenameWithoutExtension: 'foo', + componentExportName: 'Example', + componentIsDefaultExport: false, + exportedStoryName: 'Default', + }); + + expect(result).toMatchInlineSnapshot(` + "import { Example } from './foo'; + + const meta = { + component: Example, + }; + + export default meta; + + export const Default = {};" + `); + }); +}); diff --git a/code/lib/core-server/src/utils/new-story-templates/javascript.ts b/code/lib/core-server/src/utils/new-story-templates/javascript.ts new file mode 100644 index 000000000000..e80e4d4b5a57 --- /dev/null +++ b/code/lib/core-server/src/utils/new-story-templates/javascript.ts @@ -0,0 +1,32 @@ +import dedent from 'ts-dedent'; +import { getComponentVariableName } from '../get-component-variable-name'; + +interface JavaScriptTemplateData { + /** The components file name without the extension */ + basenameWithoutExtension: string; + componentExportName: string; + componentIsDefaultExport: boolean; + /** The exported name of the default story */ + exportedStoryName: string; +} + +export async function getJavaScriptTemplateForNewStoryFile(data: JavaScriptTemplateData) { + const importName = data.componentIsDefaultExport + ? await getComponentVariableName(data.basenameWithoutExtension) + : data.componentExportName; + const importStatement = data.componentIsDefaultExport + ? `import ${importName} from './${data.basenameWithoutExtension}';` + : `import { ${importName} } from './${data.basenameWithoutExtension}';`; + + return dedent` + ${importStatement} + + const meta = { + component: ${importName}, + }; + + export default meta; + + export const ${data.exportedStoryName} = {}; + `; +} diff --git a/code/lib/core-server/src/utils/new-story-templates/typescript.test.ts b/code/lib/core-server/src/utils/new-story-templates/typescript.test.ts new file mode 100644 index 000000000000..f576a3d4ad2d --- /dev/null +++ b/code/lib/core-server/src/utils/new-story-templates/typescript.test.ts @@ -0,0 +1,56 @@ +import { describe, expect, it } from 'vitest'; +import { getTypeScriptTemplateForNewStoryFile } from './typescript'; + +describe('typescript', () => { + it('should return a TypeScript template with a default import', async () => { + const result = await getTypeScriptTemplateForNewStoryFile({ + basenameWithoutExtension: 'foo', + componentExportName: 'default', + componentIsDefaultExport: true, + frameworkPackageName: '@storybook/nextjs', + exportedStoryName: 'Default', + }); + + expect(result).toMatchInlineSnapshot(` + "import type { Meta, StoryObj } from '@storybook/nextjs'; + + import Foo from './foo'; + + const meta = { + component: Foo, + } satisfies Meta; + + export default meta; + + type Story = StoryObj; + + export const Default: Story = {};" + `); + }); + + it('should return a TypeScript template with a named import', async () => { + const result = await getTypeScriptTemplateForNewStoryFile({ + basenameWithoutExtension: 'foo', + componentExportName: 'Example', + componentIsDefaultExport: false, + frameworkPackageName: '@storybook/nextjs', + exportedStoryName: 'Default', + }); + + expect(result).toMatchInlineSnapshot(` + "import type { Meta, StoryObj } from '@storybook/nextjs'; + + import { Example } from './foo'; + + const meta = { + component: Example, + } satisfies Meta; + + export default meta; + + type Story = StoryObj; + + export const Default: Story = {};" + `); + }); +}); diff --git a/code/lib/core-server/src/utils/new-story-templates/typescript.ts b/code/lib/core-server/src/utils/new-story-templates/typescript.ts new file mode 100644 index 000000000000..cb44dfdfc9c4 --- /dev/null +++ b/code/lib/core-server/src/utils/new-story-templates/typescript.ts @@ -0,0 +1,38 @@ +import dedent from 'ts-dedent'; +import { getComponentVariableName } from '../get-component-variable-name'; + +interface TypeScriptTemplateData { + /** The components file name without the extension */ + basenameWithoutExtension: string; + componentExportName: string; + componentIsDefaultExport: boolean; + /** The framework package name, e.g. @storybook/nextjs */ + frameworkPackageName: string; + /** The exported name of the default story */ + exportedStoryName: string; +} + +export async function getTypeScriptTemplateForNewStoryFile(data: TypeScriptTemplateData) { + const importName = data.componentIsDefaultExport + ? await getComponentVariableName(data.basenameWithoutExtension) + : data.componentExportName; + const importStatement = data.componentIsDefaultExport + ? `import ${importName} from './${data.basenameWithoutExtension}'` + : `import { ${importName} } from './${data.basenameWithoutExtension}'`; + + return dedent` + import type { Meta, StoryObj } from '${data.frameworkPackageName}'; + + ${importStatement}; + + const meta = { + component: ${importName}, + } satisfies Meta; + + export default meta; + + type Story = StoryObj; + + export const ${data.exportedStoryName}: Story = {}; + `; +} diff --git a/code/lib/core-server/src/utils/posix.test.ts b/code/lib/core-server/src/utils/posix.test.ts new file mode 100644 index 000000000000..23c8d2ca3bec --- /dev/null +++ b/code/lib/core-server/src/utils/posix.test.ts @@ -0,0 +1,9 @@ +import { describe, expect, it } from 'vitest'; +import { posix } from './posix'; + +describe('posix', () => { + it('should replace backslashes with forward slashes', () => { + expect(posix('src\\components\\Page.tsx', '\\')).toBe('src/components/Page.tsx'); + expect(posix('src\\\\components\\\\Page.tsx', '\\\\')).toBe('src/components/Page.tsx'); + }); +}); diff --git a/code/lib/core-server/src/utils/posix.ts b/code/lib/core-server/src/utils/posix.ts new file mode 100644 index 000000000000..d9b8224cdde5 --- /dev/null +++ b/code/lib/core-server/src/utils/posix.ts @@ -0,0 +1,7 @@ +import path from 'node:path'; + +/** + * Replaces the path separator with forward slashes + */ +export const posix = (localPath: string, sep: string = path.sep) => + localPath.split(sep).filter(Boolean).join(path.posix.sep); diff --git a/code/ui/manager/src/globals/exports.ts b/code/ui/manager/src/globals/exports.ts index 794741a69e8b..d3da898e844c 100644 --- a/code/ui/manager/src/globals/exports.ts +++ b/code/ui/manager/src/globals/exports.ts @@ -133,6 +133,8 @@ export default { 'CHANNEL_CREATED', 'CHANNEL_WS_DISCONNECT', 'CONFIG_ERROR', + 'CREATE_NEW_STORYFILE', + 'CREATE_NEW_STORYFILE_RESULT', 'CURRENT_STORY_WAS_SET', 'DOCS_PREPARED', 'DOCS_RENDERED', diff --git a/code/yarn.lock b/code/yarn.lock index 722d3dafdab9..d3f6aa006a90 100644 --- a/code/yarn.lock +++ b/code/yarn.lock @@ -5788,6 +5788,7 @@ __metadata: "@types/ws": "npm:^8" better-opn: "npm:^3.0.2" boxen: "npm:^7.1.1" + camelcase: "npm:^8.0.0" chalk: "npm:^4.1.0" cjs-module-lexer: "npm:^1.2.3" cli-table3: "npm:^0.6.1" @@ -11258,6 +11259,13 @@ __metadata: languageName: node linkType: hard +"camelcase@npm:^8.0.0": + version: 8.0.0 + resolution: "camelcase@npm:8.0.0" + checksum: 10c0/56c5fe072f0523c9908cdaac21d4a3b3fb0f608fb2e9ba90a60e792b95dd3bb3d1f3523873ab17d86d146e94171305f73ef619e2f538bd759675bc4a14b4bff3 + languageName: node + linkType: hard + "can-symlink@npm:^1.0.0": version: 1.0.0 resolution: "can-symlink@npm:1.0.0"