diff --git a/code/builders/builder-vite/package.json b/code/builders/builder-vite/package.json index 5af8fa963c09..f97766fbc348 100644 --- a/code/builders/builder-vite/package.json +++ b/code/builders/builder-vite/package.json @@ -53,7 +53,9 @@ "es-module-lexer": "^1.5.0", "find-cache-dir": "^3.0.0", "glob": "^10.0.0", + "knitwork": "^1.1.0", "magic-string": "^0.30.0", + "pathe": "^1.1.2", "polka": "^1.0.0-next.28", "sirv": "^2.0.4", "slash": "^5.0.0", diff --git a/code/builders/builder-vite/src/codegen-importfn-script.test.ts b/code/builders/builder-vite/src/codegen-importfn-script.test.ts new file mode 100644 index 000000000000..4d3ce5bcb79a --- /dev/null +++ b/code/builders/builder-vite/src/codegen-importfn-script.test.ts @@ -0,0 +1,56 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import { toImportFn } from './codegen-importfn-script'; + +describe('toImportFn', () => { + it('should correctly map story paths to import functions for absolute paths on Linux', async () => { + const root = '/absolute/path'; + const stories = ['/absolute/path/to/story1.js', '/absolute/path/to/story2.js']; + + const result = await toImportFn(root, stories); + + expect(result).toMatchInlineSnapshot(` + "const importers = { + "./to/story1.js": () => import("/absolute/path/to/story1.js"), + "./to/story2.js": () => import("/absolute/path/to/story2.js") + }; + + export async function importFn(path) { + return await importers[path](); + }" + `); + }); + + it('should correctly map story paths to import functions for absolute paths on Windows', async () => { + const root = 'C:\\absolute\\path'; + const stories = ['C:\\absolute\\path\\to\\story1.js', 'C:\\absolute\\path\\to\\story2.js']; + + const result = await toImportFn(root, stories); + + expect(result).toMatchInlineSnapshot(` + "const importers = { + "./to/story1.js": () => import("C:/absolute/path/to/story1.js"), + "./to/story2.js": () => import("C:/absolute/path/to/story2.js") + }; + + export async function importFn(path) { + return await importers[path](); + }" + `); + }); + + it('should handle an empty array of stories', async () => { + const root = '/absolute/path'; + const stories: string[] = []; + + const result = await toImportFn(root, stories); + + expect(result).toMatchInlineSnapshot(` + "const importers = {}; + + export async function importFn(path) { + return await importers[path](); + }" + `); + }); +}); diff --git a/code/builders/builder-vite/src/codegen-importfn-script.ts b/code/builders/builder-vite/src/codegen-importfn-script.ts index 0853b0549628..4b118ad9cc51 100644 --- a/code/builders/builder-vite/src/codegen-importfn-script.ts +++ b/code/builders/builder-vite/src/codegen-importfn-script.ts @@ -1,7 +1,9 @@ -import { relative } from 'node:path'; - import type { Options } from 'storybook/internal/types'; +import { genDynamicImport, genImport, genObjectFromRawEntries } from 'knitwork'; +import { normalize, relative } from 'pathe'; +import { dedent } from 'ts-dedent'; + import { listStories } from './list-stories'; /** @@ -21,34 +23,33 @@ function toImportPath(relativePath: string) { /** * This function takes an array of stories and creates a mapping between the stories' relative paths * to the working directory and their dynamic imports. The import is done in an asynchronous - * function to delay loading. It then creates a function, `importFn(path)`, which resolves a path to - * an import function and this is called by Storybook to fetch a story dynamically when needed. + * function to delay loading and to allow Vite to split the code into smaller chunks. It then + * creates a function, `importFn(path)`, which resolves a path to an import function and this is + * called by Storybook to fetch a story dynamically when needed. * * @param stories An array of absolute story paths. */ -async function toImportFn(stories: string[]) { - const { normalizePath } = await import('vite'); +export async function toImportFn(root: string, stories: string[]) { const objectEntries = stories.map((file) => { - const relativePath = normalizePath(relative(process.cwd(), file)); + const relativePath = relative(root, file); - return ` '${toImportPath(relativePath)}': async () => import('/@fs/${file}')`; + return [toImportPath(relativePath), genDynamicImport(normalize(file))] as [string, string]; }); - return ` - const importers = { - ${objectEntries.join(',\n')} - }; + return dedent` + const importers = ${genObjectFromRawEntries(objectEntries)}; export async function importFn(path) { - return importers[path](); + return await importers[path](); } `; } -export async function generateImportFnScriptCode(options: Options) { +export async function generateImportFnScriptCode(options: Options): Promise { // First we need to get an array of stories and their absolute paths. const stories = await listStories(options); // We can then call toImportFn to create a function that can be used to load each story dynamically. - return (await toImportFn(stories)).trim(); + // eslint-disable-next-line @typescript-eslint/return-await + return await toImportFn(options.projectRoot || process.cwd(), stories); } diff --git a/code/builders/builder-vite/src/vite-config.ts b/code/builders/builder-vite/src/vite-config.ts index 8983dfc137b3..de8f12ed8822 100644 --- a/code/builders/builder-vite/src/vite-config.ts +++ b/code/builders/builder-vite/src/vite-config.ts @@ -53,18 +53,18 @@ export async function commonConfig( const { viteConfigPath } = await getBuilderOptions(options); - const projectRoot = resolve(options.configDir, '..'); + options.projectRoot = options.projectRoot || resolve(options.configDir, '..'); // I destructure away the `build` property from the user's config object // I do this because I can contain config that breaks storybook, such as we had in a lit project. // If the user needs to configure the `build` they need to do so in the viteFinal function in main.js. const { config: { build: buildProperty = undefined, ...userConfig } = {} } = - (await loadConfigFromFile(configEnv, viteConfigPath, projectRoot)) ?? {}; + (await loadConfigFromFile(configEnv, viteConfigPath, options.projectRoot)) ?? {}; const sbConfig: InlineConfig = { configFile: false, cacheDir: resolvePathInStorybookCache('sb-vite', options.cacheKey), - root: projectRoot, + root: options.projectRoot, // Allow storybook deployed as subfolder. See https://github.com/storybookjs/builder-vite/issues/238 base: './', plugins: await pluginConfig(options), diff --git a/code/core/src/types/modules/core-common.ts b/code/core/src/types/modules/core-common.ts index dbdabe22122c..07372bc308dd 100644 --- a/code/core/src/types/modules/core-common.ts +++ b/code/core/src/types/modules/core-common.ts @@ -194,6 +194,7 @@ export interface BuilderOptions { ignorePreview?: boolean; cache?: FileSystemCache; configDir: string; + projectRoot?: string; docsMode?: boolean; features?: StorybookConfigRaw['features']; versionCheck?: VersionCheck; diff --git a/code/sandbox/nuxt-vite-default-ts/project.json b/code/sandbox/nuxt-vite-default-ts/project.json new file mode 100644 index 000000000000..71bb3820c382 --- /dev/null +++ b/code/sandbox/nuxt-vite-default-ts/project.json @@ -0,0 +1,21 @@ +{ + "name": "nuxt-vite/default-ts", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "projectType": "application", + "implicitDependencies": [ + "storybook", + "core", + "addon-essentials", + "addon-interactions", + "addon-links", + "addon-onboarding", + "blocks", + "vue3-vite" + ], + "targets": { + "sandbox": {}, + "sb:dev": {}, + "sb:build": {} + }, + "tags": ["ci:normal", "ci:merged", "ci:daily"] +} diff --git a/code/yarn.lock b/code/yarn.lock index 42fd23469029..1bada105b2f1 100644 --- a/code/yarn.lock +++ b/code/yarn.lock @@ -6172,7 +6172,9 @@ __metadata: es-module-lexer: "npm:^1.5.0" find-cache-dir: "npm:^3.0.0" glob: "npm:^10.0.0" + knitwork: "npm:^1.1.0" magic-string: "npm:^0.30.0" + pathe: "npm:^1.1.2" polka: "npm:^1.0.0-next.28" sirv: "npm:^2.0.4" slash: "npm:^5.0.0" @@ -19330,6 +19332,13 @@ __metadata: languageName: node linkType: hard +"knitwork@npm:^1.1.0": + version: 1.1.0 + resolution: "knitwork@npm:1.1.0" + checksum: 10c0/e23c679d4ded01890ab2669ccde2e85e4d7e6ba327b1395ff657f8067c7d73dc134fc8cd8188c653de4a687be7fa9c130bd36c3e2f76d8685e8b97ff8b30779c + languageName: node + linkType: hard + "language-subtag-registry@npm:^0.3.20": version: 0.3.22 resolution: "language-subtag-registry@npm:0.3.22"