diff --git a/e2e/qwik-nx-e2e/tests/storybook.spec.ts b/e2e/qwik-nx-e2e/tests/storybook.spec.ts new file mode 100644 index 0000000..b514bf8 --- /dev/null +++ b/e2e/qwik-nx-e2e/tests/storybook.spec.ts @@ -0,0 +1,113 @@ +import { + checkFilesExist, + ensureNxProject, + runNxCommandAsync, + uniq, +} from '@nrwl/nx-plugin/testing'; + +import { + runCommandUntil, + promisifiedTreeKill, + killPort, + killPorts, +} from '@qwikifiers/e2e/utils'; + +const STORYBOOK_PORT = 4400; + +describe('qwikNxVite plugin e2e', () => { + beforeAll(async () => { + await killPorts(STORYBOOK_PORT); + ensureNxProject('qwik-nx', 'dist/packages/qwik-nx'); + }, 10000); + + afterAll(async () => { + // `nx reset` kills the daemon, and performs + // some work which can help clean up e2e leftovers + await runNxCommandAsync('reset'); + }); + + describe('should be able to import components from libraries', () => { + const appProject = uniq('qwik-nx'); + const libProject = uniq('qwik-nx'); + const secondLibProject = uniq('qwik-nx'); + beforeAll(async () => { + await runNxCommandAsync( + `generate qwik-nx:app ${appProject} --e2eTestRunner=none --no-interactive` + ); + await runNxCommandAsync( + `generate qwik-nx:library ${libProject} --no-interactive` + ); + await runNxCommandAsync( + `generate qwik-nx:storybook-configuration ${appProject} --no-interactive` + ); + await runNxCommandAsync( + `generate qwik-nx:storybook-configuration ${libProject} --no-interactive` + ); + }, 200000); + + describe('Applying storybook for existing application', () => { + checkStorybookIsBuiltAndServed(appProject, 'apps', false); + }); + describe('Applying storybook for existing library', () => { + checkStorybookIsBuiltAndServed(libProject, 'libs', false); + }); + + describe('Generating a new library with storybook configuration', () => { + beforeAll(async () => { + await runNxCommandAsync( + `generate qwik-nx:library ${secondLibProject} --storybookConfiguration=true --no-interactive` + ); + }, 200000); + checkStorybookIsBuiltAndServed(secondLibProject, 'libs', true); + }); + }); +}); + +function checkStorybookIsBuiltAndServed( + projectName: string, + type: 'apps' | 'libs', + hasTsStories: boolean +) { + it(`should be able to build storybook for the "${projectName}"`, async () => { + const result = await runNxCommandAsync(`build-storybook ${projectName}`); + expect(result.stdout).toContain( + `Successfully ran target build-storybook for project ${projectName}` + ); + expect(() => + checkFilesExist(`dist/storybook/${projectName}/index.html`) + ).not.toThrow(); + }, 200000); + + it(`should serve storybook for the "${projectName}"`, async () => { + let resultOutput: string | undefined; + const p = await runCommandUntil( + `run ${projectName}:storybook`, + (output) => { + if ( + output.includes('Local:') && + output.includes(`:${STORYBOOK_PORT}`) + ) { + resultOutput = output; + return true; + } + return false; + } + ); + + // it is expected that projects won't have stories by default and storybook should recognize it. + expect(resultOutput).toContain( + `No story files found for the specified pattern: ${type}/${projectName}/**/*.stories.mdx` + ); + if (!hasTsStories) { + expect(resultOutput).toContain( + `No story files found for the specified pattern: ${type}/${projectName}/**/*.stories.@(js|jsx|ts|tsx)` + ); + } + try { + await promisifiedTreeKill(p.pid!, 'SIGKILL'); + await killPort(STORYBOOK_PORT); + } catch { + // ignore + } + }, 200000); +} diff --git a/package.json b/package.json index 78c87ba..5923c60 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "@nrwl/js": "15.8.2", "@nrwl/linter": "15.8.2", "@nrwl/nx-plugin": "15.8.2", + "@nrwl/storybook": "15.8.2", "@nrwl/vite": "15.8.2", "@nrwl/workspace": "15.8.2", "@nxkit/playwright": "^2.1.2", diff --git a/packages/qwik-nx/generators.json b/packages/qwik-nx/generators.json index e78bc4a..2337d0c 100644 --- a/packages/qwik-nx/generators.json +++ b/packages/qwik-nx/generators.json @@ -64,6 +64,11 @@ "factory": "./src/generators/remote/generator", "schema": "./src/generators/remote/schema.json", "description": "Generate a remote Qwik application for the micro frontend setup" + }, + "storybook-configuration": { + "factory": "./src/generators/storybook-configuration/generator", + "schema": "./src/generators/storybook-configuration/schema.json", + "description": "Adds Storybook configuration to a project." } } } diff --git a/packages/qwik-nx/src/generators/component/files/storybook/__fileName__.doc.mdx.template b/packages/qwik-nx/src/generators/component/files/storybook/__fileName__.doc.mdx.template new file mode 100644 index 0000000..91376be --- /dev/null +++ b/packages/qwik-nx/src/generators/component/files/storybook/__fileName__.doc.mdx.template @@ -0,0 +1,26 @@ +import { Canvas, Story } from '@storybook/addon-docs'; +import { <%- className %> } from './<%- fileName %>'; + +# <%- className %> Component + +## Purpose + +{/* Why the component is needed */} + +## Example + +{/* Common copy/paste example that people can throw into their templates and ts */} + +~~~tsx +<<%- className %> param="value" /> +~~~ + +## Use case examples + +{/* Examples based on use cases */} + +### Primary + + + + diff --git a/packages/qwik-nx/src/generators/component/files/storybook/__fileName__.stories.tsx.template b/packages/qwik-nx/src/generators/component/files/storybook/__fileName__.stories.tsx.template new file mode 100644 index 0000000..0cc867f --- /dev/null +++ b/packages/qwik-nx/src/generators/component/files/storybook/__fileName__.stories.tsx.template @@ -0,0 +1,21 @@ +import type { Meta } from 'storybook-framework-qwik'; +import { <%- className %> } from './<%- fileName %>'; +import doc from './<%- fileName %>.doc.mdx'; + +export default { + title: '<%- className %>', + tags: ['autodocs'], + parameters: { + docs: { + page: doc, + }, + }, + argTypes: { + // put component params here + }, + render(args) { + return <<%- className %> {...args}/>; + }, +} as Meta; + +export const Primary = {}; diff --git a/packages/qwik-nx/src/generators/component/generator.ts b/packages/qwik-nx/src/generators/component/generator.ts index 3debf69..d9fe110 100644 --- a/packages/qwik-nx/src/generators/component/generator.ts +++ b/packages/qwik-nx/src/generators/component/generator.ts @@ -9,6 +9,7 @@ import { Tree, } from '@nrwl/devkit'; import { addStyledModuleDependencies } from '../../utils/add-styled-dependencies'; +import { ensureMdxTypeInTsConfig } from '../../utils/ensure-file-utils'; import { ComponentGeneratorSchema } from './schema'; interface NormalizedSchema extends ComponentGeneratorSchema { @@ -95,6 +96,15 @@ function createComponentFiles(tree: Tree, options: NormalizedSchema) { templateOptions ); } + if (options.generateStories) { + generateFiles( + tree, + joinPathFragments(__dirname, 'files/storybook'), + componentDir, + templateOptions + ); + ensureMdxTypeInTsConfig(tree, options.project); + } } export async function componentGenerator( diff --git a/packages/qwik-nx/src/generators/component/schema.d.ts b/packages/qwik-nx/src/generators/component/schema.d.ts index 1045346..ab68c38 100644 --- a/packages/qwik-nx/src/generators/component/schema.d.ts +++ b/packages/qwik-nx/src/generators/component/schema.d.ts @@ -5,4 +5,5 @@ export interface ComponentGeneratorSchema { style?: 'none' | 'css' | 'scss' | 'styl' | 'less'; skipTests?: boolean; flat?: boolean; + generateStories?: boolean; } diff --git a/packages/qwik-nx/src/generators/component/schema.json b/packages/qwik-nx/src/generators/component/schema.json index 2cf3390..40996b5 100644 --- a/packages/qwik-nx/src/generators/component/schema.json +++ b/packages/qwik-nx/src/generators/component/schema.json @@ -68,6 +68,10 @@ "type": "boolean", "description": "Create component at the source root rather than its own directory.", "default": false + }, + "generateStories": { + "description": "Create Storybook stories for the component", + "type": "boolean" } }, "required": ["name", "project"] diff --git a/packages/qwik-nx/src/generators/e2e-project/generator.spec.ts b/packages/qwik-nx/src/generators/e2e-project/generator.spec.ts index 1f5b4ef..41aff1c 100644 --- a/packages/qwik-nx/src/generators/e2e-project/generator.spec.ts +++ b/packages/qwik-nx/src/generators/e2e-project/generator.spec.ts @@ -20,9 +20,9 @@ describe('e2e project', () => { .spyOn(getInstalledNxVersionModule, 'getInstalledNxVersion') .mockReturnValue('15.6.0'); - beforeEach(() => { + beforeEach(async () => { appTree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' }); - appGenerator(appTree, { + await appGenerator(appTree, { name: 'myapp', e2eTestRunner: 'none', }); diff --git a/packages/qwik-nx/src/generators/integrations/cloudflare-pages-integration/generator.spec.ts b/packages/qwik-nx/src/generators/integrations/cloudflare-pages-integration/generator.spec.ts index 40ebf4d..4bfd1a6 100644 --- a/packages/qwik-nx/src/generators/integrations/cloudflare-pages-integration/generator.spec.ts +++ b/packages/qwik-nx/src/generators/integrations/cloudflare-pages-integration/generator.spec.ts @@ -18,10 +18,10 @@ describe('cloudflare-pages-integration generator', () => { project: projectName, }; - beforeEach(() => { + beforeEach(async () => { appTree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' }); - applicationGenerator(appTree, { + await applicationGenerator(appTree, { name: projectName, e2eTestRunner: 'none', linter: Linter.None, diff --git a/packages/qwik-nx/src/generators/library/__snapshots__/generator.spec.ts.snap b/packages/qwik-nx/src/generators/library/__snapshots__/generator.spec.ts.snap index 4e23660..82ed1b6 100644 --- a/packages/qwik-nx/src/generators/library/__snapshots__/generator.spec.ts.snap +++ b/packages/qwik-nx/src/generators/library/__snapshots__/generator.spec.ts.snap @@ -67,8 +67,7 @@ export default defineConfig({ }), ], - mode: 'lib', - // Configuration for building your library. + // Configuration for building your library. // See: https://vitejs.dev/guide/build.html#library-mode build: { lib: { @@ -164,6 +163,10 @@ Array [ "path": "libs/mylib/vite.config.ts", "type": "CREATE", }, + Object { + "path": "libs/mylib/src/root.tsx", + "type": "CREATE", + }, Object { "path": "libs/mylib/src/lib/mylib.tsx", "type": "CREATE", @@ -261,6 +264,10 @@ Array [ "path": "libs/mylib/.eslintrc.json", "type": "CREATE", }, + Object { + "path": "libs/mylib/src/root.tsx", + "type": "CREATE", + }, Object { "path": "libs/mylib/src/lib/mylib.tsx", "type": "CREATE", @@ -404,6 +411,10 @@ Array [ "path": "libs/mylib/vite.config.ts", "type": "CREATE", }, + Object { + "path": "libs/mylib/src/root.tsx", + "type": "CREATE", + }, Object { "path": "libs/mylib/src/lib/mylib.tsx", "type": "CREATE", diff --git a/packages/qwik-nx/src/generators/library/files/tsconfig.json.template b/packages/qwik-nx/src/generators/library/files/tsconfig.json.template index 0482a45..06c49ec 100644 --- a/packages/qwik-nx/src/generators/library/files/tsconfig.json.template +++ b/packages/qwik-nx/src/generators/library/files/tsconfig.json.template @@ -19,7 +19,12 @@ "isolatedModules": true, "outDir": "tmp", "noEmit": true, - "types": ["node", "vite/client"<% if(setupVitest) { %> , "vitest" <% } %>] + "types": [ + "node", + "vite/client", + <% if(setupVitest) { %>"vitest", <% } %> + <% if(storybookConfiguration) { %>"mdx", <% } %> + ] }, "files": [], "include": [], diff --git a/packages/qwik-nx/src/generators/library/files/vite.config.ts.template b/packages/qwik-nx/src/generators/library/files/vite.config.ts.template index 060dc96..dcebac6 100644 --- a/packages/qwik-nx/src/generators/library/files/vite.config.ts.template +++ b/packages/qwik-nx/src/generators/library/files/vite.config.ts.template @@ -19,8 +19,7 @@ export default defineConfig({ }), <% } %> ], - <% if(buildable) { %> mode: 'lib', - // Configuration for building your library. + <% if(buildable) { %> // Configuration for building your library. // See: https://vitejs.dev/guide/build.html#library-mode build: { lib: { diff --git a/packages/qwik-nx/src/generators/library/generator.ts b/packages/qwik-nx/src/generators/library/generator.ts index 8a3a48e..8959bc5 100644 --- a/packages/qwik-nx/src/generators/library/generator.ts +++ b/packages/qwik-nx/src/generators/library/generator.ts @@ -22,6 +22,8 @@ import { initGenerator } from '@nrwl/vite'; import { addCommonQwikDependencies } from '../../utils/add-common-qwik-dependencies'; import { getQwikLibProjectTargets } from './utils/get-qwik-lib-project-params'; import { normalizeOptions } from './utils/normalize-options'; +import storybookConfigurationGenerator from '../storybook-configuration/generator'; +import { ensureRootTsxExists } from '../../utils/ensure-file-utils'; export async function libraryGenerator( tree: Tree, @@ -62,7 +64,6 @@ async function addLibrary( linter: Linter.None, importPath: options.importPath, strict: options.strict, - standaloneConfig: options.standaloneConfig, unitTestRunner: 'none', skipBabelrc: true, skipFormat: true, @@ -84,6 +85,8 @@ async function addLibrary( templateOptions ); + ensureRootTsxExists(tree, options.projectName); + if (!options.setupVitest) { tree.delete(`${options.projectRoot}/tsconfig.spec.json`); } @@ -95,11 +98,16 @@ async function addLibrary( } } + if (options.storybookConfiguration) { + tasks.push(await configureStorybook(tree, options)); + } + const componentGeneratorTask = await componentGenerator(tree, { name: options.name, skipTests: !options.setupVitest, style: options.style, project: options.projectName, + generateStories: options.storybookConfiguration, flat: true, }); @@ -126,4 +134,13 @@ async function configureVite(tree: Tree, options: NormalizedSchema) { return callback; } +async function configureStorybook( + tree: Tree, + options: NormalizedSchema +): Promise { + return storybookConfigurationGenerator(tree, { + name: options.projectName, + }); +} + export default libraryGenerator; diff --git a/packages/qwik-nx/src/generators/library/schema.d.ts b/packages/qwik-nx/src/generators/library/schema.d.ts index 594a743..beacb04 100644 --- a/packages/qwik-nx/src/generators/library/schema.d.ts +++ b/packages/qwik-nx/src/generators/library/schema.d.ts @@ -11,13 +11,14 @@ export interface LibraryGeneratorSchema { importPath?: string; strict?: boolean; buildable?: boolean; - standaloneConfig?: boolean; + storybookConfiguration?: boolean; } type NormalizedRequiredPropsNames = | 'style' | 'unitTestRunner' | 'linter' + | 'storybookConfiguration' | 'buildable'; type NormalizedRequiredProps = Required< Pick diff --git a/packages/qwik-nx/src/generators/library/schema.json b/packages/qwik-nx/src/generators/library/schema.json index fe0c6e5..defcb56 100644 --- a/packages/qwik-nx/src/generators/library/schema.json +++ b/packages/qwik-nx/src/generators/library/schema.json @@ -88,9 +88,10 @@ "description": "Whether to enable `tsconfig` strict mode or not.", "default": true }, - "standaloneConfig": { - "description": "Split the project configuration into `/project.json` rather than including it inside `workspace.json`.", - "type": "boolean" + "storybookConfiguration": { + "description": "Whether to include storybook configuration for the generated library.", + "type": "boolean", + "default": false } }, "required": ["name"] diff --git a/packages/qwik-nx/src/generators/library/utils/normalize-options.ts b/packages/qwik-nx/src/generators/library/utils/normalize-options.ts index d34b0e1..5ce5854 100644 --- a/packages/qwik-nx/src/generators/library/utils/normalize-options.ts +++ b/packages/qwik-nx/src/generators/library/utils/normalize-options.ts @@ -22,6 +22,7 @@ export function normalizeOptions( unitTestRunner: schema.unitTestRunner ?? 'vitest', linter: schema.linter ?? Linter.EsLint, buildable: !!schema.buildable, + storybookConfiguration: !!schema.storybookConfiguration, }; return { diff --git a/packages/qwik-nx/src/generators/storybook-configuration/__snapshots__/generator.spec.ts.snap b/packages/qwik-nx/src/generators/storybook-configuration/__snapshots__/generator.spec.ts.snap new file mode 100644 index 0000000..5d97d1f --- /dev/null +++ b/packages/qwik-nx/src/generators/storybook-configuration/__snapshots__/generator.spec.ts.snap @@ -0,0 +1,146 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`storybook-configuration generator should add required targets 1`] = ` +Array [ + Object { + "path": ".prettierrc", + "type": "CREATE", + }, + Object { + "path": "package.json", + "type": "CREATE", + }, + Object { + "path": "nx.json", + "type": "CREATE", + }, + Object { + "path": "tsconfig.base.json", + "type": "CREATE", + }, + Object { + "path": "apps/.gitignore", + "type": "CREATE", + }, + Object { + "path": "libs/.gitignore", + "type": "CREATE", + }, + Object { + "path": "apps/test-project/project.json", + "type": "CREATE", + }, + Object { + "path": "apps/test-project/.eslintrc.json", + "type": "CREATE", + }, + Object { + "path": "apps/test-project/.prettierignore", + "type": "CREATE", + }, + Object { + "path": "apps/test-project/README.md", + "type": "CREATE", + }, + Object { + "path": "apps/test-project/package.json", + "type": "CREATE", + }, + Object { + "path": "apps/test-project/public/favicon.svg", + "type": "CREATE", + }, + Object { + "path": "apps/test-project/public/manifest.json", + "type": "CREATE", + }, + Object { + "path": "apps/test-project/public/robots.txt", + "type": "CREATE", + }, + Object { + "path": "apps/test-project/src/components/header/header.css", + "type": "CREATE", + }, + Object { + "path": "apps/test-project/src/components/header/header.tsx", + "type": "CREATE", + }, + Object { + "path": "apps/test-project/src/components/icons/qwik.tsx", + "type": "CREATE", + }, + Object { + "path": "apps/test-project/src/components/router-head/router-head.tsx", + "type": "CREATE", + }, + Object { + "path": "apps/test-project/src/entry.dev.tsx", + "type": "CREATE", + }, + Object { + "path": "apps/test-project/src/entry.preview.tsx", + "type": "CREATE", + }, + Object { + "path": "apps/test-project/src/entry.ssr.tsx", + "type": "CREATE", + }, + Object { + "path": "apps/test-project/src/global.css", + "type": "CREATE", + }, + Object { + "path": "apps/test-project/src/root.tsx", + "type": "CREATE", + }, + Object { + "path": "apps/test-project/src/routes/flower/flower.css", + "type": "CREATE", + }, + Object { + "path": "apps/test-project/src/routes/flower/index.tsx", + "type": "CREATE", + }, + Object { + "path": "apps/test-project/src/routes/index.tsx", + "type": "CREATE", + }, + Object { + "path": "apps/test-project/src/routes/layout.tsx", + "type": "CREATE", + }, + Object { + "path": "apps/test-project/src/routes/service-worker.ts", + "type": "CREATE", + }, + Object { + "path": "apps/test-project/tsconfig.app.json", + "type": "CREATE", + }, + Object { + "path": "apps/test-project/tsconfig.json", + "type": "CREATE", + }, + Object { + "path": "apps/test-project/tsconfig.spec.json", + "type": "CREATE", + }, + Object { + "path": "apps/test-project/vite.config.ts", + "type": "CREATE", + }, + Object { + "path": "apps/test-project/.storybook/main.ts", + "type": "CREATE", + }, + Object { + "path": "apps/test-project/.storybook/preview.ts", + "type": "CREATE", + }, + Object { + "path": "apps/test-project/.storybook/tsconfig.json", + "type": "CREATE", + }, +] +`; diff --git a/packages/qwik-nx/src/generators/storybook-configuration/files/.storybook/main.__configExtension__.template b/packages/qwik-nx/src/generators/storybook-configuration/files/.storybook/main.__configExtension__.template new file mode 100644 index 0000000..866ce4f --- /dev/null +++ b/packages/qwik-nx/src/generators/storybook-configuration/files/.storybook/main.__configExtension__.template @@ -0,0 +1,16 @@ +import { mergeConfig, UserConfig } from 'vite'; +import viteConfig from './../vite.config'; + +const config = { + stories: [ + '../**/*.stories.mdx', + '../**/*.stories.@(js|jsx|ts|tsx)' + ], + addons: ['@storybook/addon-essentials'], + framework: { name: 'storybook-framework-qwik', }, + async viteFinal(config: UserConfig) { + return mergeConfig(config, viteConfig); + }, +}; + +export default config; diff --git a/packages/qwik-nx/src/generators/storybook-configuration/generator.spec.ts b/packages/qwik-nx/src/generators/storybook-configuration/generator.spec.ts new file mode 100644 index 0000000..36302de --- /dev/null +++ b/packages/qwik-nx/src/generators/storybook-configuration/generator.spec.ts @@ -0,0 +1,70 @@ +import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing'; +import { Tree, readProjectConfiguration } from '@nrwl/devkit'; + +import { storybookConfigurationGenerator } from './generator'; +import { StorybookConfigurationGeneratorSchema } from './schema'; +import appGenerator from '../application/generator'; +import { Linter } from '@nrwl/linter'; + +// eslint-disable-next-line @typescript-eslint/no-var-requires +const devkit = require('@nrwl/devkit'); +const getInstalledNxVersionModule = require('../../utils/get-installed-nx-version'); + +describe('storybook-configuration generator', () => { + let appTree: Tree; + const projectName = 'test-project'; + const options: StorybookConfigurationGeneratorSchema = { name: projectName }; + + jest.spyOn(devkit, 'ensurePackage').mockReturnValue(Promise.resolve()); + jest + .spyOn(getInstalledNxVersionModule, 'getInstalledNxVersion') + .mockReturnValue('15.6.0'); + + beforeEach(async () => { + appTree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' }); + + await appGenerator(appTree, { + name: projectName, + e2eTestRunner: 'none', + linter: Linter.None, + skipFormat: false, + strict: true, + style: 'css', + unitTestRunner: 'none', + }); + }); + + it('should add required targets', async () => { + await storybookConfigurationGenerator(appTree, options); + const config = readProjectConfiguration(appTree, projectName); + expect(config.targets!['storybook']).toEqual({ + executor: '@nrwl/storybook:storybook', + options: { + port: 4400, + configDir: `apps/${projectName}/.storybook`, + }, + configurations: { + ci: { + quiet: true, + }, + }, + }); + expect(config.targets!['build-storybook']).toEqual({ + executor: '@nrwl/storybook:build', + outputs: ['{options.outputDir}'], + options: { + configDir: `apps/${projectName}/.storybook`, + outputDir: `dist/storybook/${projectName}`, + }, + configurations: { + ci: { + quiet: true, + }, + }, + }); + + expect( + appTree.listChanges().map((c) => ({ path: c.path, type: c.type })) + ).toMatchSnapshot(); + }); +}); diff --git a/packages/qwik-nx/src/generators/storybook-configuration/generator.ts b/packages/qwik-nx/src/generators/storybook-configuration/generator.ts new file mode 100644 index 0000000..4d0e4ae --- /dev/null +++ b/packages/qwik-nx/src/generators/storybook-configuration/generator.ts @@ -0,0 +1,105 @@ +import { + addDependenciesToPackageJson, + ensurePackage, + formatFiles, + generateFiles, + GeneratorCallback, + names, + offsetFromRoot, + readProjectConfiguration, + Tree, +} from '@nrwl/devkit'; +import { Linter } from '@nrwl/linter'; +import * as path from 'path'; +import { + ensureMdxTypeInTsConfig, + ensureRootTsxExists, +} from '../../utils/ensure-file-utils'; +import { getInstalledNxVersion } from '../../utils/get-installed-nx-version'; +import { + storybookFrameworkQwikVersion, + storybookReactDOMVersion, + storybookReactVersion, + typesMdx, +} from '../../utils/versions'; +import { + NormalizedSchema, + StorybookConfigurationGeneratorSchema, +} from './schema'; + +function addFiles(tree: Tree, options: StorybookConfigurationGeneratorSchema) { + const { root } = readProjectConfiguration(tree, options.name); + + tree.delete(path.join(root, '.storybook/main.js')); + + const templateOptions = { + ...options, + ...names(options.name), + offsetFromRoot: offsetFromRoot(root), + projectRoot: root, + configExtension: options.tsConfiguration ? 'ts' : 'js', + }; + generateFiles(tree, path.join(__dirname, 'files'), root, templateOptions); + + ensureRootTsxExists(tree, options.name); + ensureMdxTypeInTsConfig(tree, options.name); +} + +function normalizeOptions( + options: StorybookConfigurationGeneratorSchema +): NormalizedSchema { + return { + ...options, + js: !!options.js, + linter: options.linter ?? Linter.EsLint, + tsConfiguration: options.tsConfiguration ?? true, + }; +} + +export async function storybookConfigurationGenerator( + tree: Tree, + options: StorybookConfigurationGeneratorSchema +): Promise { + const normalizedOptions = normalizeOptions(options); + ensurePackage('@nrwl/storybook', getInstalledNxVersion(tree)); + const { configurationGenerator } = await import('@nrwl/storybook'); + + await configurationGenerator(tree, { + uiFramework: '@storybook/html', + bundler: 'vite', + name: normalizedOptions.name, + js: normalizedOptions.js, + linter: normalizedOptions.linter, + tsConfiguration: normalizedOptions.tsConfiguration, + storybook7betaConfiguration: true, + configureCypress: false, + }); + + addFiles(tree, normalizedOptions); + await formatFiles(tree); + + return addStorybookDependencies(tree); +} + +async function addStorybookDependencies( + tree: Tree +): Promise { + const { storybook7Version } = await import( + '@nrwl/storybook/src/utils/versions' + ); + + return addDependenciesToPackageJson( + tree, + {}, + { + 'storybook-framework-qwik': storybookFrameworkQwikVersion, + '@storybook/builder-vite': storybook7Version, + '@storybook/addon-docs': storybook7Version, + react: storybookReactVersion, + 'react-dom': storybookReactDOMVersion, + '@types/mdx': typesMdx, + } + ); +} + +export default storybookConfigurationGenerator; diff --git a/packages/qwik-nx/src/generators/storybook-configuration/schema.d.ts b/packages/qwik-nx/src/generators/storybook-configuration/schema.d.ts new file mode 100644 index 0000000..178772b --- /dev/null +++ b/packages/qwik-nx/src/generators/storybook-configuration/schema.d.ts @@ -0,0 +1,19 @@ +import { Linter } from '@nrwl/linter'; + +export interface StorybookConfigurationGeneratorSchema { + name: string; + linter?: Linter; + js?: boolean; + tsConfiguration?: boolean; +} + +type NormalizedRequiredPropsNames = 'js' | 'linter' | 'tsConfiguration'; +type NormalizedRequiredProps = Required< + Pick +>; + +export type NormalizedSchema = Omit< + StorybookConfigurationGeneratorSchema, + NormalizedRequiredPropsNames +> & + NormalizedRequiredProps; diff --git a/packages/qwik-nx/src/generators/storybook-configuration/schema.json b/packages/qwik-nx/src/generators/storybook-configuration/schema.json new file mode 100644 index 0000000..82fc1e6 --- /dev/null +++ b/packages/qwik-nx/src/generators/storybook-configuration/schema.json @@ -0,0 +1,40 @@ +{ + "$schema": "http://json-schema.org/schema", + "cli": "nx", + "$id": "StorybookConfiguration", + "title": "Adds Storybook configuration to a project.", + "description": "Adds Storybook configuration to a project to be able to use and create stories.", + "type": "object", + "properties": { + "name": { + "type": "string", + "aliases": ["project", "projectName"], + "description": "Project for which to generate Storybook configuration.", + "$default": { + "$source": "argv", + "index": 0 + }, + "x-prompt": "For which project do you want to generate Storybook configuration?", + "x-dropdown": "projects", + "x-priority": "important" + }, + "linter": { + "description": "The tool to use for running lint checks.", + "type": "string", + "enum": ["eslint", "none"], + "default": "eslint" + }, + "js": { + "type": "boolean", + "description": "Generate JavaScript story files rather than TypeScript story files.", + "default": false + }, + "tsConfiguration": { + "type": "boolean", + "description": "Configure your project with TypeScript. Generate main.ts and preview.ts files, instead of main.js and preview.js.", + "default": true, + "x-priority": "important" + } + }, + "required": ["name"] +} diff --git a/packages/qwik-nx/src/utils/ensure-file-utils.ts b/packages/qwik-nx/src/utils/ensure-file-utils.ts new file mode 100644 index 0000000..94a9cf1 --- /dev/null +++ b/packages/qwik-nx/src/utils/ensure-file-utils.ts @@ -0,0 +1,32 @@ +import { readJson, readProjectConfiguration, Tree } from '@nrwl/devkit'; +import { join } from 'path'; + +/** Creates root.tsx if it is not found */ +export function ensureRootTsxExists(tree: Tree, projectName: string) { + const projectConfig = readProjectConfiguration(tree, projectName); + + const sourceRoot = + projectConfig.sourceRoot ?? join(projectConfig.root, 'src'); + const rootTsxPath = join(sourceRoot, 'root.tsx'); + + if (!tree.exists(rootTsxPath)) { + tree.write(rootTsxPath, rootTsxContent); + } +} + +const rootTsxContent = `// This is explicitly empty, but serves as a compilation entry-point for the client mode. +export default {};`; + +export function ensureMdxTypeInTsConfig(tree: Tree, projectName: string) { + const projectConfig = readProjectConfiguration(tree, projectName); + + const tsConfigPath = join(projectConfig.root, 'tsconfig.json'); + if (tree.exists(tsConfigPath)) { + const tsConfig = readJson(tree, tsConfigPath); + + if (!((tsConfig.compilerOptions ??= {}).types ??= []).includes('mdx')) { + tsConfig.compilerOptions.types.push('mdx'); + tree.write(tsConfigPath, JSON.stringify(tsConfig)); + } + } +} diff --git a/packages/qwik-nx/src/utils/versions.ts b/packages/qwik-nx/src/utils/versions.ts index 9e1b385..d6ca5fe 100644 --- a/packages/qwik-nx/src/utils/versions.ts +++ b/packages/qwik-nx/src/utils/versions.ts @@ -24,6 +24,12 @@ export const nxKitVersion = '^2.1.2'; export const wranglerVersion = '^2.8.0'; export const nxCloudflareWrangler = '^2.0.0'; +// storybook +export const storybookFrameworkQwikVersion = '^0.0.8'; +export const storybookReactVersion = '^18.0.0'; +export const storybookReactDOMVersion = '^18.0.0'; +export const typesMdx = '^2.0.3'; + // other export const eslintVersion = '~8.36.0'; export const tsEslintVersion = '~5.43.0'; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7452e0a..da0ff3a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -16,6 +16,7 @@ importers: '@nrwl/js': 15.8.2 '@nrwl/linter': 15.8.2 '@nrwl/nx-plugin': 15.8.2 + '@nrwl/storybook': 15.8.2 '@nrwl/vite': 15.8.2 '@nrwl/workspace': 15.8.2 '@nxkit/playwright': ^2.1.2 @@ -74,6 +75,7 @@ importers: '@nrwl/js': 15.8.2_f5tjcz6mhtzkzfqnengccsh7d4 '@nrwl/linter': 15.8.2_f5tjcz6mhtzkzfqnengccsh7d4 '@nrwl/nx-plugin': 15.8.2_bp5jfpqvuv6yvfesjqccwfqew4 + '@nrwl/storybook': 15.8.2_f5tjcz6mhtzkzfqnengccsh7d4 '@nrwl/vite': 15.8.2_rr7rokjmekkdx2xr3kv4tzxvui '@nrwl/workspace': 15.8.2_53wkrcrrxffq2surs33fwlwix4 '@nxkit/playwright': 2.1.2_@playwright+test@1.30.0 @@ -3292,6 +3294,32 @@ packages: dev: true optional: true + /@nrwl/storybook/15.8.2_f5tjcz6mhtzkzfqnengccsh7d4: + resolution: + { + integrity: sha512-rSA6utYeCn07+0Z/w0vlkNeF1eE4Slct5EMoZKhgvw+4z0ZLQMABOxj7rnsifakmMr4tXKEJK+VZiR33xHlmwQ==, + } + dependencies: + '@nrwl/cypress': 15.8.2_f5tjcz6mhtzkzfqnengccsh7d4 + '@nrwl/devkit': 15.8.2_nx@15.8.2+typescript@4.9.5 + '@nrwl/js': 15.8.2_f5tjcz6mhtzkzfqnengccsh7d4 + '@nrwl/linter': 15.8.2_f5tjcz6mhtzkzfqnengccsh7d4 + '@nrwl/workspace': 15.8.2_53wkrcrrxffq2surs33fwlwix4 + '@phenomnomnominal/tsquery': 4.1.1_typescript@4.9.5 + dotenv: 10.0.0 + semver: 7.3.4 + transitivePeerDependencies: + - '@swc-node/register' + - '@swc/core' + - cypress + - debug + - eslint + - nx + - prettier + - supports-color + - typescript + dev: true + /@nrwl/tao/15.8.2_lwc5ciab46qbgcufzept4zyhgi: resolution: { diff --git a/tsconfig.base.json b/tsconfig.base.json index 28fb8e4..5f1f818 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -18,7 +18,7 @@ "baseUrl": ".", "paths": { "@qwikifiers/e2e/utils": ["e2e/utils"], - "qwik-nx": ["packages/qwik-nx/src/index.ts"] + "qwik-nx": ["packages/qwik-nx/index.ts"] } }, "exclude": ["node_modules", "tmp"]