diff --git a/tools/workspace-plugin/src/executors/build/executor.spec.ts b/tools/workspace-plugin/src/executors/build/executor.spec.ts index 1e256f152ed6a..ea26f7c24e652 100644 --- a/tools/workspace-plugin/src/executors/build/executor.spec.ts +++ b/tools/workspace-plugin/src/executors/build/executor.spec.ts @@ -203,7 +203,7 @@ describe('Build Executor', () => { " `); expect(readFileSync(join(workspaceRoot, 'libs/proj/lib/greeter.js.map'), 'utf-8')).toMatchInlineSnapshot( - `"{\\"version\\":3,\\"sources\\":[\\"greeter.ts\\"],\\"sourcesContent\\":[\\"import { useStyles } from './greeter.styles';\\\\nexport function greeter(greeting: string, user: User): string {\\\\n const styles = useStyles();\\\\n return \`

\${greeting} \${user.name} from \${user.hometown?.name}

\`;\\\\n}\\\\n\\\\ntype User = {\\\\n name: string;\\\\n hometown?: {\\\\n name: string;\\\\n };\\\\n};\\\\n\\"],\\"names\\":[\\"useStyles\\",\\"greeter\\",\\"greeting\\",\\"user\\",\\"styles\\",\\"name\\",\\"hometown\\"],\\"rangeMappings\\":\\";;;;;\\",\\"mappings\\":\\"AAAA,SAASA,SAAS,QAAQ,mBAAmB;AAC7C,OAAO,SAASC,QAAQC,QAAgB,EAAEC,IAAU;QAEYA;IAD9D,MAAMC,SAASJ;IACf,OAAO,CAAC,WAAW,EAAEI,OAAO,EAAE,EAAEF,SAAS,CAAC,EAAEC,KAAKE,IAAI,CAAC,MAAM,GAAEF,iBAAAA,KAAKG,QAAQ,cAAbH,qCAAAA,eAAeE,IAAI,CAAC,KAAK,CAAC;AAC1F\\"}"`, + `"{\\"version\\":3,\\"sources\\":[\\"../src/greeter.ts\\"],\\"sourcesContent\\":[\\"import { useStyles } from './greeter.styles';\\\\nexport function greeter(greeting: string, user: User): string {\\\\n const styles = useStyles();\\\\n return \`

\${greeting} \${user.name} from \${user.hometown?.name}

\`;\\\\n}\\\\n\\\\ntype User = {\\\\n name: string;\\\\n hometown?: {\\\\n name: string;\\\\n };\\\\n};\\\\n\\"],\\"names\\":[\\"useStyles\\",\\"greeter\\",\\"greeting\\",\\"user\\",\\"styles\\",\\"name\\",\\"hometown\\"],\\"rangeMappings\\":\\";;;;;\\",\\"mappings\\":\\"AAAA,SAASA,SAAS,QAAQ,mBAAmB;AAC7C,OAAO,SAASC,QAAQC,QAAgB,EAAEC,IAAU;QAEYA;IAD9D,MAAMC,SAASJ;IACf,OAAO,CAAC,WAAW,EAAEI,OAAO,EAAE,EAAEF,SAAS,CAAC,EAAEC,KAAKE,IAAI,CAAC,MAAM,GAAEF,iBAAAA,KAAKG,QAAQ,cAAbH,qCAAAA,eAAeE,IAAI,CAAC,KAAK,CAAC;AAC1F\\"}"`, ); expect(readFileSync(join(workspaceRoot, 'libs/proj/lib-commonjs/greeter.js'), 'utf-8')).toMatchInlineSnapshot(` diff --git a/tools/workspace-plugin/src/executors/build/lib/babel.ts b/tools/workspace-plugin/src/executors/build/lib/babel.ts index 04576101348ab..4939d90f209f9 100644 --- a/tools/workspace-plugin/src/executors/build/lib/babel.ts +++ b/tools/workspace-plugin/src/executors/build/lib/babel.ts @@ -85,6 +85,7 @@ async function babel(esmModuleOutput: NormalizedOptions['moduleOutput'][number], const sourceCode = codeBuffer.toString().replace(EOL_REGEX, '\n'); const result = (await transformAsync(sourceCode, { + cwd: normalizedOptions.absoluteProjectRoot, ast: false, sourceMaps: true, diff --git a/tools/workspace-plugin/src/executors/build/lib/swc.ts b/tools/workspace-plugin/src/executors/build/lib/swc.ts index afd89dcaa018c..a3c2dd9f887b5 100644 --- a/tools/workspace-plugin/src/executors/build/lib/swc.ts +++ b/tools/workspace-plugin/src/executors/build/lib/swc.ts @@ -1,14 +1,20 @@ -import { readFileSync } from 'node:fs'; import { mkdir, writeFile } from 'node:fs/promises'; -import { basename, dirname, join } from 'node:path'; +import { dirname, join } from 'node:path'; import { globSync } from 'fast-glob'; import { isMatch } from 'micromatch'; -import { transform, type Config } from '@swc/core'; +import { transformFile, type Config } from '@swc/core'; import { logger, readJsonFile } from '@nx/devkit'; import { type NormalizedOptions } from './shared'; +// extend @swc/core types by missing apis +declare module '@swc/core' { + interface BaseModuleConfig { + resolveFully?: boolean; + } +} + interface Options { module: 'es6' | 'commonjs' | 'amd'; outputPath: string; @@ -35,15 +41,10 @@ export async function compileSwc(options: Options, normalizedOptions: Normalized continue; } - const sourceCode = readFileSync(srcFilePath, 'utf-8'); - - const result = await transform(sourceCode, { - filename: fileName, - sourceFileName: basename(fileName), - module: { type: module }, - outputPath, - // this is crucial in order to transpile with project config SWC - configFile: swcConfigPath, + const result = await transformFile(srcFilePath, { + module: { type: module, resolveFully: Boolean(swcConfig.jsc?.baseUrl) }, + // srcFilePath is absolute path so outputPath needs to be as well in order to properly emit relative path within .map (eg: `"sources":["../src/utils/createDarkTheme.ts"]`) + outputPath: join(normalizedOptions.absoluteProjectRoot, outputPath), }); // Strip @jsx comments, see https://github.com/microsoft/fluentui/issues/29126 diff --git a/tools/workspace-plugin/src/executors/build/schema.d.ts b/tools/workspace-plugin/src/executors/build/schema.d.ts index 1c099ab6cef01..26b5481c0e2cc 100644 --- a/tools/workspace-plugin/src/executors/build/schema.d.ts +++ b/tools/workspace-plugin/src/executors/build/schema.d.ts @@ -55,14 +55,7 @@ export interface BuildExecutorSchema { * Key-value pairs to replace in the output path */ substitutions?: { - /** - * The key to replace. - */ - key: string; - /** - * The value to replace. - */ - value: string; + [k: string]: string; }; } | string diff --git a/tools/workspace-plugin/src/executors/build/schema.json b/tools/workspace-plugin/src/executors/build/schema.json index bf4373b83626b..f8dabb4fd107e 100644 --- a/tools/workspace-plugin/src/executors/build/schema.json +++ b/tools/workspace-plugin/src/executors/build/schema.json @@ -47,12 +47,9 @@ "substitutions": { "type": "object", "description": "Key-value pairs to replace in the output path", - "properties": { - "key": { "type": "string", "description": "The key to replace." }, - "value": { "type": "string", "description": "The value to replace." } - }, - "additionalProperties": false, - "required": ["key", "value"] + "additionalProperties": { + "type": "string" + } } }, "additionalProperties": false, diff --git a/tools/workspace-plugin/src/executors/generate-api/executor.spec.ts b/tools/workspace-plugin/src/executors/generate-api/executor.spec.ts index 0bdead569d8d6..b68417b56b479 100644 --- a/tools/workspace-plugin/src/executors/generate-api/executor.spec.ts +++ b/tools/workspace-plugin/src/executors/generate-api/executor.spec.ts @@ -148,8 +148,10 @@ describe('GenerateApi Executor', () => { const output = await executor(options, context); + const projectRootAbsolutePath = `${__dirname}/__fixtures__/proj`; + expect(execSyncMock.mock.calls.flat()).toEqual([ - `tsc -p ${__dirname}/__fixtures__/proj/tsconfig.lib.json --pretty --emitDeclarationOnly --baseUrl .`, + `tsc -p ${projectRootAbsolutePath}/tsconfig.lib.json --pretty --emitDeclarationOnly --baseUrl ${projectRootAbsolutePath}`, { stdio: 'inherit' }, ]); diff --git a/tools/workspace-plugin/src/executors/generate-api/executor.ts b/tools/workspace-plugin/src/executors/generate-api/executor.ts index d878979c731ce..17bb673b99aa4 100644 --- a/tools/workspace-plugin/src/executors/generate-api/executor.ts +++ b/tools/workspace-plugin/src/executors/generate-api/executor.ts @@ -84,7 +84,7 @@ function generateTypeDeclarations(options: NormalizedOptions) { '--pretty', '--emitDeclarationOnly', // turn off path aliases. - '--baseUrl .', + `--baseUrl ${options.projectAbsolutePath}`, ].join(' '); verboseLog(`Emitting '.d.ts' files via: "${cmd}"`); diff --git a/tools/workspace-plugin/src/executors/type-check/executor.spec.ts b/tools/workspace-plugin/src/executors/type-check/executor.spec.ts index 22fd3495e896d..280e08803b990 100644 --- a/tools/workspace-plugin/src/executors/type-check/executor.spec.ts +++ b/tools/workspace-plugin/src/executors/type-check/executor.spec.ts @@ -75,8 +75,8 @@ describe('TypeCheck Executor', () => { const output = await executor(options, mockContext); expect(promisifyCallMock.mock.calls.flat()).toEqual([ - 'tsc -p /root/libs/my-lib/tsconfig.lib.json --pretty --noEmit --baseUrl .', - 'tsc -p /root/libs/my-lib/tsconfig.spec.json --pretty --noEmit --baseUrl .', + 'tsc -p /root/libs/my-lib/tsconfig.lib.json --pretty --noEmit --baseUrl /root/libs/my-lib', + 'tsc -p /root/libs/my-lib/tsconfig.spec.json --pretty --noEmit --baseUrl /root/libs/my-lib', ]); expect(output.success).toBe(true); @@ -93,7 +93,7 @@ describe('TypeCheck Executor', () => { const output = await executor({ ...options, excludeProject: { spec: true, e2e: false } }, mockContext); expect(promisifyCallMock.mock.calls.flat()).toEqual([ - 'tsc -p /root/libs/my-lib/tsconfig.lib.json --pretty --noEmit --baseUrl .', + 'tsc -p /root/libs/my-lib/tsconfig.lib.json --pretty --noEmit --baseUrl /root/libs/my-lib', ]); expect(output.success).toBe(true); diff --git a/tools/workspace-plugin/src/executors/type-check/executor.ts b/tools/workspace-plugin/src/executors/type-check/executor.ts index dfd0a6fdafc74..33673193983a0 100644 --- a/tools/workspace-plugin/src/executors/type-check/executor.ts +++ b/tools/workspace-plugin/src/executors/type-check/executor.ts @@ -43,7 +43,7 @@ async function runTypeCheck(options: NormalizedOptions, context: ExecutorContext const asyncQueue = []; for (const ref of tsConfigsRefs) { - const program = `tsc -p ${ref} --pretty --noEmit --baseUrl .`; + const program = `tsc -p ${ref} --pretty --noEmit --baseUrl ${projectRootAbsolutePath}`; verboseLog(`Running "${program}"`); diff --git a/tools/workspace-plugin/src/executors/verify-packaging/executor.ts b/tools/workspace-plugin/src/executors/verify-packaging/executor.ts index 4a48934f5da7b..37033028b6faa 100644 --- a/tools/workspace-plugin/src/executors/verify-packaging/executor.ts +++ b/tools/workspace-plugin/src/executors/verify-packaging/executor.ts @@ -122,7 +122,8 @@ function assertions( ); } - if (isV8package) { + // apply only for non cross domain v8 packages (eg: react-migration-* is v9/v8) + if (isV8package && !isV9package) { issues.push(assertEmpty(npmPackResult, '(lib|lib-commonjs)/**/*.d.ts', `ships dts`)); if (options.isProduction && shipsBundle) { diff --git a/tools/workspace-plugin/src/plugins/workspace-plugin.spec.ts b/tools/workspace-plugin/src/plugins/workspace-plugin.spec.ts index 4eef49a2c1c3b..819c0972eb4d0 100644 --- a/tools/workspace-plugin/src/plugins/workspace-plugin.spec.ts +++ b/tools/workspace-plugin/src/plugins/workspace-plugin.spec.ts @@ -136,32 +136,40 @@ describe(`workspace-plugin`, () => { describe(`v9 project nodes`, () => { it('should create default nodes for v9 library project', async () => { await tempFs.createFiles({ - 'proj/project.json': serializeJson({ + 'proj/library/project.json': serializeJson({ root: 'proj', + name: 'proj', projectType: 'library', tags: ['vNext'], } satisfies ProjectConfiguration), - 'proj/package.json': serializeJson({ name: '@proj/proj', private: true } satisfies Partial), + 'proj/library/package.json': serializeJson({ + name: '@proj/proj', + private: true, + } satisfies Partial), + 'proj/stories/project.json': serializeJson({ + root: 'proj/stories', + name: 'proj-stories', + } satisfies ProjectConfiguration), }); - const results = await createNodesFunction(['proj/project.json'], {}, context); + const results = await createNodesFunction(['proj/library/project.json'], {}, context); - expect(getTargetsNames(results)).toEqual([ + expect(getTargetsNames(results, 'proj/library')).toEqual([ 'clean', 'format', 'type-check', 'generate-api', 'build', - 'start', 'storybook', + 'start', ]); expect(results).toMatchInlineSnapshot(` Array [ Array [ - "proj/project.json", + "proj/library/project.json", Object { "projects": Object { - "proj": Object { + "proj/library": Object { "targets": Object { "build": Object { "cache": true, @@ -189,7 +197,7 @@ describe(`workspace-plugin`, () => { ], "metadata": Object { "help": Object { - "command": "yarn nx run undefined:build --help", + "command": "yarn nx run proj:build --help", "example": Object {}, }, "technologies": Array [ @@ -217,7 +225,7 @@ describe(`workspace-plugin`, () => { "{projectRoot}/lib-commonjs", "{projectRoot}/dist", "{projectRoot}/dist/index.d.ts", - "{projectRoot}/etc/undefined.api.md", + "{projectRoot}/etc/proj.api.md", ], }, "clean": Object { @@ -266,7 +274,7 @@ describe(`workspace-plugin`, () => { ], "metadata": Object { "help": Object { - "command": "yarn nx run undefined:generate-api --help", + "command": "yarn nx run proj:generate-api --help", "example": Object {}, }, "technologies": Array [ @@ -276,26 +284,16 @@ describe(`workspace-plugin`, () => { }, "outputs": Array [ "{projectRoot}/dist/index.d.ts", - "{projectRoot}/etc/undefined.api.md", + "{projectRoot}/etc/proj.api.md", ], }, "start": Object { - "command": "yarn storybook", - "options": Object { - "cwd": "proj", - }, + "cache": true, + "command": "nx run proj-stories:storybook", }, "storybook": Object { "cache": true, - "command": "yarn --cwd ../stories storybook", - "metadata": Object { - "technologies": Array [ - "storybook", - ], - }, - "options": Object { - "cwd": "proj", - }, + "command": "nx run proj-stories:storybook", }, "type-check": Object { "cache": true, @@ -323,6 +321,7 @@ describe(`workspace-plugin`, () => { it('should create default nodes for v9 stories project', async () => { await tempFs.createFiles({ + 'proj/stories/.storybook/main.js': '', 'proj/stories/project.json': serializeJson({ root: 'proj/stories', projectType: 'library', @@ -339,15 +338,16 @@ describe(`workspace-plugin`, () => { 'clean', 'format', 'type-check', + 'storybook', 'test-ssr', 'start', - 'storybook', ]); const targets = getTargets(results, 'proj/stories'); expect(targets?.storybook).toMatchInlineSnapshot(` Object { + "cache": true, "command": "yarn storybook dev", "inputs": Array [ "production", diff --git a/tools/workspace-plugin/src/plugins/workspace-plugin.ts b/tools/workspace-plugin/src/plugins/workspace-plugin.ts index a7f191c4870a9..27a2f365b69ca 100644 --- a/tools/workspace-plugin/src/plugins/workspace-plugin.ts +++ b/tools/workspace-plugin/src/plugins/workspace-plugin.ts @@ -22,34 +22,73 @@ import { assertProjectExists, projectConfigGlob } from './shared'; import { buildCleanTarget } from './clean-plugin'; import { buildFormatTarget } from './format-plugin'; import { buildTypeCheckTarget } from './type-check-plugin'; +import { measureStart, measureEnd } from '../utils'; -interface WorkspacePluginOptions {} +interface WorkspacePluginOptions { + testSSR?: TargetPluginOption; + verifyPackaging?: TargetPluginOption; +} +interface TargetPluginOption { + targetName?: string; + /** + * project names to exclude from adding target + */ + exclude?: string[]; + /** + * project names to include for adding target + */ + include?: string[]; +} export const createNodesV2: CreateNodesV2 = [ projectConfigGlob, - (configFiles, options, context) => { - return createNodesFromFiles( + async (configFiles, options, context) => { + const globalConfig: Pick = { pmc: getPackageManagerCommand('yarn') }; + + measureStart('workspace-plugin'); + const nodes = await createNodesFromFiles( (configFile, options, context) => { - return createNodesInternal(configFile, options ?? {}, context); + return createNodesInternal(configFile, options ?? {}, context, globalConfig); }, configFiles, options, context, ); + + measureEnd('workspace-plugin'); + + return nodes; }, ]; // =================================================================================================================== -function normalizeOptions(options: WorkspacePluginOptions | undefined): Required { +type NormalizedOptions = ReturnType; + +type DeepRequired = { + [K in keyof T]-?: NonNullable extends object ? DeepRequired : NonNullable; +}; + +function normalizeOptions(options: WorkspacePluginOptions | undefined): DeepRequired { options ??= {}; - return options as Required; + options.testSSR ??= {}; + options.testSSR.targetName ??= 'test-ssr'; + options.testSSR.include ??= []; + options.testSSR.exclude ??= []; + + options.verifyPackaging ??= {}; + options.verifyPackaging.targetName ??= 'verify-packaging'; + options.verifyPackaging.include ??= []; + options.verifyPackaging.exclude ??= []; + + return options as DeepRequired; } function createNodesInternal( configFilePath: string, options: WorkspacePluginOptions, context: CreateNodesContextV2, + globalConfig: Pick, ): CreateNodesResult { const projectRoot = dirname(configFilePath); @@ -58,7 +97,8 @@ function createNodesInternal( } const normalizedOptions = normalizeOptions(options); - const config = { pmc: getPackageManagerCommand('yarn') }; + + const config = { ...globalConfig }; const targetsConfig = buildWorkspaceTargets(projectRoot, normalizedOptions, context, config); @@ -78,7 +118,7 @@ interface TaskBuilderConfig { function buildWorkspaceTargets( projectRoot: string, - options: Required, + options: NormalizedOptions, context: CreateNodesContextV2, sharedConfig: Pick, ) { @@ -94,50 +134,33 @@ function buildWorkspaceTargets( targets.format = buildFormatTarget({}, context, config); targets['type-check'] = buildTypeCheckTarget({}, context, config); - const lintTarget = buildLintTarget(projectRoot, normalizeOptions, context, config); + const lintTarget = buildLintTarget(projectRoot, options, context, config); if (lintTarget) { targets.lint = lintTarget; } - const testTarget = buildTestTarget(projectRoot, normalizeOptions, context, config); + const testTarget = buildTestTarget(projectRoot, options, context, config); if (testTarget) { targets.test = testTarget; } + const storybookTarget = buildStorybookTarget(projectRoot, options, context, config); + if (storybookTarget) { + targets.storybook = storybookTarget; + } + // react v9 lib if (projectJSON.projectType === 'library' && tags.includes('vNext')) { // *-stories projects if (tags.includes('type:stories')) { - targets['test-ssr'] = { - cache: true, - command: `${config.pmc.exec} test-ssr "./src/**/*.stories.tsx"`, - options: { cwd: projectRoot }, - metadata: { - technologies: ['test-ssr'], - help: { - command: `${config.pmc.exec} test-ssr --help`, - example: {}, - }, - }, - }; - targets.start = { command: `${config.pmc.exec} storybook`, options: { cwd: projectRoot } }; - targets.storybook = { - command: `${config.pmc.exec} storybook dev`, - inputs: [ - 'production', - '{workspaceRoot}/.storybook/**', - '{projectRoot}/.storybook/**', - { externalDependencies: ['storybook'] }, - ], - options: { cwd: projectRoot }, - metadata: { - technologies: ['storybook'], - help: { - command: `${config.pmc.exec} storybook dev --help`, - example: {}, - }, - }, - }; + const testSsrTarget = buildTestSsrTarget(projectRoot, options, context, config); + if (testSsrTarget) { + targets[options.testSSR.targetName] = testSsrTarget; + } + + if (storybookTarget) { + targets.start = { command: `nx run ${config.projectJSON.name}:storybook`, cache: true }; + } return targets; } @@ -204,18 +227,11 @@ function buildWorkspaceTargets( }, }; - targets.start = { - command: `${config.pmc.exec} storybook`, - options: { cwd: projectRoot }, - }; - targets.storybook = { - cache: true, - command: `${config.pmc.exec} --cwd ../stories storybook`, - options: { cwd: projectRoot }, - metadata: { - technologies: ['storybook'], - }, - }; + if (existsSync(join(projectRoot, '../stories/project.json'))) { + const storybookTarget = { command: `nx run ${config.projectJSON.name}-stories:storybook`, cache: true }; + targets.storybook = storybookTarget; + targets.start = storybookTarget; + } const bundleSizeTarget = buildBundleSizeTarget(projectRoot, options, context, config); if (bundleSizeTarget) { @@ -227,9 +243,9 @@ function buildWorkspaceTargets( targets.e2e = e2eTarget; } - const verifyPackagingTarget = buildVerifyPackagingTarget(projectRoot, normalizeOptions, context, config); + const verifyPackagingTarget = buildVerifyPackagingTarget(projectRoot, options, context, config); if (verifyPackagingTarget) { - targets['verify-packaging'] = verifyPackagingTarget; + targets[options.verifyPackaging.targetName] = verifyPackagingTarget; } return targets; @@ -310,10 +326,17 @@ function buildLintTarget( function buildVerifyPackagingTarget( projectRoot: string, - options: Required, + options: NormalizedOptions, context: CreateNodesContextV2, config: TaskBuilderConfig, ): TargetConfiguration | null { + if (options.verifyPackaging.include.length && !options.verifyPackaging.include.includes(config.projectJSON.name!)) { + return null; + } + if (options.verifyPackaging.exclude.length && options.verifyPackaging.exclude.includes(config.projectJSON.name!)) { + return null; + } + if (config.packageJSON.private) { return null; } @@ -386,6 +409,7 @@ function buildE2eTarget( { externalDependencies: ['cypress', '@cypress/react'] }, ], metadata: { + technologies: ['cypress'], help: { command: `${config.pmc.exec} cypress run --help`, example: {}, @@ -408,9 +432,10 @@ function buildE2eTarget( '{projectRoot}/playwright.config.ts', '!{projectRoot}/**/?(*.)+spec.[jt]s?(x)?', '!{projectRoot}/**/?(*.)+spec-e2e.[jt]s?(x)?', - { externalDependencies: ['playwright'] }, + { externalDependencies: ['@playwright/test'] }, ], metadata: { + technologies: ['playwright'], help: { command: `${config.pmc.exec} playwright test --help`, example: {}, @@ -421,3 +446,60 @@ function buildE2eTarget( return null; } + +function buildTestSsrTarget( + projectRoot: string, + options: NormalizedOptions, + context: CreateNodesContextV2, + config: TaskBuilderConfig, +): TargetConfiguration | null { + if (options.testSSR.include.length && !options.testSSR.include.includes(config.projectJSON.name!)) { + return null; + } + if (options.testSSR.exclude.length && options.testSSR.exclude.includes(config.projectJSON.name!)) { + return null; + } + + return { + cache: true, + command: `${config.pmc.exec} test-ssr "./src/**/*.stories.tsx"`, + options: { cwd: projectRoot }, + metadata: { + technologies: ['test-ssr'], + help: { + command: `${config.pmc.exec} test-ssr --help`, + example: {}, + }, + }, + }; +} + +function buildStorybookTarget( + projectRoot: string, + options: NormalizedOptions, + context: CreateNodesContextV2, + config: TaskBuilderConfig, +): TargetConfiguration | null { + if (!existsSync(join(projectRoot, '.storybook/main.js'))) { + return null; + } + + return { + command: `${config.pmc.exec} storybook dev`, + cache: true, + inputs: [ + 'production', + '{workspaceRoot}/.storybook/**', + '{projectRoot}/.storybook/**', + { externalDependencies: ['storybook'] }, + ], + options: { cwd: projectRoot }, + metadata: { + technologies: ['storybook'], + help: { + command: `${config.pmc.exec} storybook dev --help`, + example: {}, + }, + }, + }; +}