From 171169609ae754ff6e9d99727b4336f5d8fc807d Mon Sep 17 00:00:00 2001 From: Martin Hochel Date: Mon, 22 Apr 2024 17:45:38 +0200 Subject: [PATCH 01/14] feat(workspace-plugin): make prepare-initial-release generator work with new split /library /stories react projects --- .../prepare-initial-release/index.spec.ts | 332 +++++++++++++++++- .../prepare-initial-release/index.ts | 147 ++++++-- 2 files changed, 445 insertions(+), 34 deletions(-) diff --git a/tools/workspace-plugin/src/generators/prepare-initial-release/index.spec.ts b/tools/workspace-plugin/src/generators/prepare-initial-release/index.spec.ts index cd0fb7d08877f9..72a8cc1c7c1331 100644 --- a/tools/workspace-plugin/src/generators/prepare-initial-release/index.spec.ts +++ b/tools/workspace-plugin/src/generators/prepare-initial-release/index.spec.ts @@ -16,11 +16,11 @@ import childProcess from 'child_process'; import generator from './index'; import { PackageJson, TsConfig } from '../../types'; -const blankGraphMock = { +const getBlankGraphMock = () => ({ dependencies: {}, nodes: {}, externalNodes: {}, -}; +}); let graphMock: ProjectGraph; const codeownersPath = joinPathFragments('.github', 'CODEOWNERS'); @@ -51,14 +51,15 @@ describe('prepare-initial-release generator', () => { ); // installPackagesTaskSpy = jest.spyOn(devkit, 'installPackagesTask').mockImplementation(noop); graphMock = { - ...blankGraphMock, + ...getBlankGraphMock(), }; tree = createTreeWithEmptyWorkspace(); - tree.write(codeownersPath, `@proj/foo @org/all`); + tree.write(codeownersPath, `foo/bar @org/all\n`); writeJson(tree, 'tsconfig.base.v8.json', { compilerOptions: { paths: {} } }); writeJson(tree, 'tsconfig.base.v0.json', { compilerOptions: { paths: {} } }); writeJson(tree, 'tsconfig.base.all.json', { compilerOptions: { paths: {} } }); }); + afterEach(() => { jest.resetAllMocks(); }); @@ -178,6 +179,48 @@ describe('prepare-initial-release generator', () => { }); }); + describe(`compat - isSplit`, () => { + it(`should prepare compat package for initial release`, async () => { + const utils = { + project: createSplitProject(tree, 'react-one-compat', { + root: 'packages/react-one-compat', + pkgJson: { + version: '0.0.0', + private: true, + }, + renameRoot: false, + }), + docsite: createProject(tree, 'public-docsite-v9', { + root: 'apps/public-docsite-v9', + pkgJson: { version: '9.0.123', private: true }, + renameRoot: false, + }), + }; + + await generator(tree, { project: '@proj/react-one-compat', phase: 'compat' }); + + expect(utils.project.library.pkgJson()).toMatchInlineSnapshot(` + Object { + "name": "@proj/react-one-compat", + "version": "0.0.0", + } + `); + expect(utils.project.stories.pkgJson()).toMatchInlineSnapshot(` + Object { + "name": "@proj/react-one-compat-stories", + "private": true, + "version": "0.0.0", + } + `); + + expect(utils.docsite.pkgJson().dependencies).toEqual( + expect.objectContaining({ + '@proj/react-one-compat': '*', + }), + ); + }); + }); + describe(`preview`, () => { it(`should prepare preview package for initial release`, async () => { const utils = { @@ -232,6 +275,48 @@ describe('prepare-initial-release generator', () => { }); }); + describe(`preview - isSplit`, () => { + it(`should prepare preview package for initial release`, async () => { + const utils = { + project: createSplitProject(tree, 'react-one-preview', { + root: 'packages/react-one-preview', + pkgJson: { + version: '0.0.0', + private: true, + }, + renameRoot: false, + }), + docsite: createProject(tree, 'public-docsite-v9', { + root: 'apps/public-docsite-v9', + pkgJson: { version: '9.0.123', private: true }, + renameRoot: false, + }), + }; + + await generator(tree, { project: '@proj/react-one-preview', phase: 'preview' }); + + expect(utils.project.library.pkgJson()).toMatchInlineSnapshot(` + Object { + "name": "@proj/react-one-preview", + "version": "0.0.0", + } + `); + expect(utils.project.stories.pkgJson()).toMatchInlineSnapshot(` + Object { + "name": "@proj/react-one-preview-stories", + "private": true, + "version": "0.0.0", + } + `); + + expect(utils.docsite.pkgJson().dependencies).toEqual( + expect.objectContaining({ + '@proj/react-one-preview': '*', + }), + ); + }); + }); + describe(`stable`, () => { const projectName = '@proj/react-one-preview'; type Utils = ReturnType; @@ -518,9 +603,248 @@ describe('prepare-initial-release generator', () => { ); }); }); + + describe(`stable - isSplit`, () => { + const projectName = '@proj/react-one-preview'; + + type Utils = ReturnType; + const utils = { + project: { library: {} as Utils, stories: {} as Utils }, + suite: {} as Utils, + docsite: {} as Utils, + vrTest: {} as Utils, + }; + + beforeEach(() => { + utils.project = createSplitProject(tree, 'react-one-preview', { + root: 'packages/react-one-preview', + pkgJson: { + version: '0.12.33', + }, + files: { + library: [], + stories: [ + { + filePath: 'packages/react-one-preview/stories/src/One.stories.tsx', + content: stripIndents` + import { One } from '@proj/react-one-preview'; + + export const Default = () =>
+ `, + }, + { + filePath: 'packages/react-one-preview/stories/src/index.stories.tsx', + content: stripIndents` + import { One } from '@proj/react-one-preview'; + + const metadata: ComponentMeta = { + title: 'Preview Components/One', + component: One, + } + + export default metadata; + `, + }, + ], + }, + }); + + utils.suite = createProject(tree, 'react-components', { + root: 'packages/react-components/react-components', + pkgJson: { version: '9.0.1' }, + }); + utils.docsite = createProject(tree, 'public-docsite-v9', { + root: 'apps/public-docsite-v9', + pkgJson: { version: '9.0.123', private: true }, + files: [ + { + filePath: 'apps/public-docsite-v9/src/example.stories.tsx', + content: stripIndents` + import { One } from '${projectName}'; + import * as suite from '@proj/react-components'; + + export const Example = () => { return ; } + `, + }, + ], + }); + utils.vrTest = createProject(tree, 'vr-tests-react-components', { + root: 'apps/vr-tests-react-components', + pkgJson: { version: '9.0.77', private: true }, + files: [ + { + filePath: 'apps/vr-tests-react-components/src/stories/One.stories.tsx', + content: stripIndents` + import { One } from '${projectName}'; + import * as suite from '@proj/react-components'; + + export const VrTest = () => { return ; } + `, + }, + ], + }); + }); + + it(`should prepare preview package for stable release`, async () => { + const treeStructureBefore = { + host: tree.children('packages/react-one-preview'), + library: tree.children('packages/react-one-preview/library'), + stories: tree.children('packages/react-one-preview/stories'), + }; + expect(treeStructureBefore.host).toEqual(['library', 'stories']); + + await generator(tree, { project: projectName, phase: 'stable' }); + + const treeStructureAfter = { + host: tree.children('packages/react-one'), + library: tree.children('packages/react-one/library'), + stories: tree.children('packages/react-one/stories'), + }; + + expect(treeStructureAfter).toEqual(treeStructureBefore); + expect(tree.children('packages/react-one-preview')).toEqual([]); + + expect(utils.project.library.projectJson()).toEqual( + expect.objectContaining({ + name: '@proj/react-one', + sourceRoot: 'packages/react-one/library/src', + }), + ); + expect(utils.project.stories.projectJson()).toEqual( + expect.objectContaining({ + name: '@proj/react-one-stories', + sourceRoot: 'packages/react-one/stories/src', + }), + ); + + expect(utils.project.library.md.readme()).toMatchInlineSnapshot(` + "# @proj/react-one + + **React Tags components for [Fluent UI React](https://react.fluentui.dev/)** + + These are not production-ready components and **should never be used in product**. This space is useful for testing new components whose APIs might change before final release. + " + `); + expect(utils.project.stories.md.readme()).toMatchInlineSnapshot(` + "# @fluentui/react-one-stories + + Storybook stories for packages/react-components/react-one-stories + + ## Usage + + To include within storybook specify stories globs: + + \\\\\`\\\\\`\\\\\`js + module.exports = { + stories: ['../packages/react-components/react-one-stories/stories/src/**/*.stories.mdx', '../packages/react-components/react-one-stories/stories/src/**/index.stories.@(ts|tsx)'], + } + \\\\\`\\\\\`\\\\\` + " + `); + + expect(tree.read('packages/react-one/stories/src/One.stories.tsx', 'utf-8')).toMatchInlineSnapshot(` + "import { One } from '@proj/react-components'; + + export const Default = () => ( +
+ +
+ ); + " + `); + expect(tree.read('packages/react-one/stories/src/index.stories.tsx', 'utf-8')).toMatchInlineSnapshot(` + "import { One } from '@proj/react-components'; + + const metadata: ComponentMeta = { + title: 'Components/One', + component: One, + }; + + export default metadata; + " + `); + + expect(utils.project.library.global.codeowners()).toEqual( + expect.stringContaining(stripIndents` + packages/react-one/library @org/universe @johnwick + packages/react-one/stories @org/universe @johnwick + `), + ); + expect(utils.project.library.global.tsBase().compilerOptions.paths).toEqual( + expect.objectContaining({ + '@proj/react-one': ['packages/react-one/library/src/index.ts'], + '@proj/react-one-stories': ['packages/react-one/stories/src/index.ts'], + }), + ); + expect(utils.project.library.global.tsBaseAll().compilerOptions.paths).toEqual( + expect.objectContaining({ + '@proj/react-one': ['packages/react-one/library/src/index.ts'], + '@proj/react-one-stories': ['packages/react-one/stories/src/index.ts'], + }), + ); + }); + }); }); }); +function createSplitProject( + tree: Tree, + projectName: string, + options: { + root: string; + pkgJson: Partial; + files?: { + library: Array<{ filePath: string; content: string }>; + stories: Array<{ filePath: string; content: string }>; + }; + tags?: string[]; + renameRoot?: boolean; + }, +) { + // library + const libraryProject = { root: options.root + '/library' }; + const library = createProject(tree, projectName, { + ...options, + root: libraryProject.root, + files: options.files?.library, + }); + + // stories + const storiesProjectName = `${projectName}-stories`; + const storiesProject = { root: options.root + '/stories' }; + + const stories = createProject(tree, storiesProjectName, { + ...options, + root: storiesProject.root, + files: [ + ...(options.files?.stories ?? []), + { + filePath: joinPathFragments(storiesProject.root, 'README.md'), + content: stripIndents` + # @fluentui/${storiesProjectName} + + Storybook stories for packages/react-components/${storiesProjectName} + + ## Usage + + To include within storybook specify stories globs: + + \`\`\`js + module.exports = { + stories: ['../packages/react-components/${storiesProjectName}/stories/src/**/*.stories.mdx', '../packages/react-components/${storiesProjectName}/stories/src/**/index.stories.@(ts|tsx)'], + } + \`\`\` + `, + }, + ], + }); + + return { + library, + stories, + }; +} + function createProject( tree: Tree, projectName: string, diff --git a/tools/workspace-plugin/src/generators/prepare-initial-release/index.ts b/tools/workspace-plugin/src/generators/prepare-initial-release/index.ts index 7e0404432981a7..de1ad39af6f8f2 100644 --- a/tools/workspace-plugin/src/generators/prepare-initial-release/index.ts +++ b/tools/workspace-plugin/src/generators/prepare-initial-release/index.ts @@ -16,7 +16,7 @@ import { import * as tsquery from '@phenomnomnominal/tsquery'; -import { getProjectConfig, workspacePaths } from '../../utils'; +import { getProjectConfig, getProjectNameWithoutScope, workspacePaths } from '../../utils'; import { PackageJson, TsConfig } from '../../types'; @@ -30,7 +30,9 @@ interface NormalizedSchema extends ReturnType {} export default async function (tree: Tree, schema: ReleasePackageGeneratorSchema) { const options = normalizeOptions(tree, schema); - assertProject(tree, options); + const isSplitProject = tree.exists(joinPathFragments(options.projectConfig.root, '../stories/project.json')); + + assertProject(tree, { isSplitProject, ...options }); const tasks: Array<(tree: Tree) => void> = []; @@ -38,7 +40,7 @@ export default async function (tree: Tree, schema: ReleasePackageGeneratorSchema tasks.push(initialRelease(tree, options)); } if (options.phase === 'stable') { - tasks.push(await stableRelease(tree, options)); + tasks.push(await stableRelease(tree, { isSplitProject, ...options })); } await formatFiles(tree); @@ -89,9 +91,10 @@ function initialRelease(tree: Tree, options: NormalizedSchema) { }; } -async function stableRelease(tree: Tree, options: NormalizedSchema) { +async function stableRelease(tree: Tree, options: NormalizedSchema & { isSplitProject: boolean }) { const suitePackageName = '@' + options.workspaceConfig.npmScope + '/react-components'; const currentPackageName = options.projectConfig.name as string; + const newPackage = { name: currentPackageName.replace('-preview', ''), normalizedName: options.normalizedPkgName.replace('-preview', ''), @@ -100,6 +103,15 @@ async function stableRelease(tree: Tree, options: NormalizedSchema) { sourceRoot: options.projectConfig.sourceRoot?.replace('-preview', '') as string, }; + const contentNameUpdater = (content: string) => { + const regexp = new RegExp(options.normalizedPkgName, 'g'); + return content.replace(regexp, newPackage.normalizedName); + }; + const contentNameToSuiteUpdater = (content: string) => { + const regexp = new RegExp(options.normalizedPkgName, 'g'); + return content.replace(regexp, 'react-components'); + }; + updateJson(tree, options.paths.packageJson, json => { delete json.private; json.name = newPackage.name; @@ -113,15 +125,6 @@ async function stableRelease(tree: Tree, options: NormalizedSchema) { return json; }); - const contentNameUpdater = (content: string) => { - const regexp = new RegExp(options.normalizedPkgName, 'g'); - return content.replace(regexp, newPackage.normalizedName); - }; - const contentNameToSuiteUpdater = (content: string) => { - const regexp = new RegExp(options.normalizedPkgName, 'g'); - return content.replace(regexp, 'react-components'); - }; - updateFileContent(tree, { filePath: options.paths.jestConfig, updater: contentNameUpdater }); const bundleSizeFixturesRoot = joinPathFragments(options.projectConfig.root, 'bundle-size'); @@ -146,21 +149,12 @@ async function stableRelease(tree: Tree, options: NormalizedSchema) { }); updateFileContent(tree, { filePath: mdFilePath.api, newFilePath: mdFilePath.apiNew, updater: contentNameUpdater }); - // update stories - visitNotIgnoredFiles(tree, options.paths.stories, filePath => { - updateFileContent(tree, { - filePath, - updater: content => { - let newContent = contentNameToSuiteUpdater(content); - - if (filePath.indexOf('index.stories.tsx') !== -1) { - newContent = newContent.replace(`'Preview `, `'`); - } - - return newContent; - }, - }); - }); + if (options.isSplitProject) { + const { storiesProjectPaths } = stableReleaseForSplitProject(tree, options); + updateStories(tree, { storiesSourcePath: storiesProjectPaths.sourceRoot, contentNameToSuiteUpdater }); + } else { + updateStories(tree, { storiesSourcePath: options.paths.stories, contentNameToSuiteUpdater }); + } // global updates updateJson(tree, options.paths.rootTsconfig, json => { @@ -256,7 +250,12 @@ async function stableRelease(tree: Tree, options: NormalizedSchema) { }); // AFTER updates are done - rename project folder - tree.rename(options.projectConfig.root, newPackage.root); + if (options.isSplitProject) { + const hostFolder = joinPathFragments(options.projectConfig.root, '..'); + tree.rename(hostFolder, hostFolder.replace('-preview', '')); + } else { + tree.rename(options.projectConfig.root, newPackage.root); + } return (_tree: Tree) => { installPackagesTask(tree, true); @@ -269,6 +268,65 @@ async function stableRelease(tree: Tree, options: NormalizedSchema) { }; } +function stableReleaseForSplitProject(tree: Tree, options: NormalizedSchema) { + const storiesProjectRoot = joinPathFragments(options.projectConfig.root, '../stories'); + const currentStoriesPackageName = options.projectConfig.name + '-stories'; + const currentStoriesNormalizedPackageName = getProjectNameWithoutScope(currentStoriesPackageName); + const storiesProjectPaths = { + root: storiesProjectRoot, + sourceRoot: joinPathFragments(storiesProjectRoot, 'src'), + packageJson: joinPathFragments(storiesProjectRoot, 'package.json'), + projectJson: joinPathFragments(storiesProjectRoot, 'project.json'), + readme: joinPathFragments(storiesProjectRoot, 'README.md'), + }; + const newStoriesProject = { + name: currentStoriesPackageName.replace('-preview', ''), + normalizedName: currentStoriesNormalizedPackageName.replace('-preview', ''), + root: storiesProjectPaths.root.replace('-preview', ''), + sourceRoot: storiesProjectPaths.sourceRoot.replace('-preview', ''), + }; + + const contentNameUpdaterStories = (content: string) => { + const regexp = new RegExp(currentStoriesNormalizedPackageName, 'g'); + return content.replace(regexp, newStoriesProject.normalizedName); + }; + + updateJson(tree, storiesProjectPaths.packageJson, json => { + json.name = newStoriesProject.name; + + return json; + }); + + updateJson(tree, storiesProjectPaths.projectJson, json => { + json.name = newStoriesProject.name; + json.sourceRoot = newStoriesProject.sourceRoot; + + return json; + }); + + updateFileContent(tree, { + filePath: storiesProjectPaths.readme, + updater: contentNameUpdaterStories, + }); + + // global updates + updateJson(tree, options.paths.rootTsconfig, json => { + json.compilerOptions.paths = json.compilerOptions.paths ?? {}; + + delete json.compilerOptions.paths[currentStoriesPackageName]; + json.compilerOptions.paths[newStoriesProject.name] = [joinPathFragments(newStoriesProject.sourceRoot, 'index.ts')]; + + return json; + }); + + updateFileContent(tree, { + filePath: workspacePaths.github.codeowners, + updater: contentNameUpdaterStories, + }); + + return { storiesProjectPaths }; +} + function updateFileContent( tree: Tree, options: { @@ -319,7 +377,7 @@ function generateApiMarkdownTask(tree: Tree, projectName: string) { return execSync(cmd, { cwd: workspaceRoot, stdio: 'inherit' }); } -function assertProject(tree: Tree, options: NormalizedSchema) { +function assertProject(tree: Tree, options: NormalizedSchema & { isSplitProject: boolean }) { const pkgJson = readJson(tree, options.paths.packageJson); const isVnextPackage = options.projectConfig.tags?.includes('vNext'); @@ -351,6 +409,15 @@ function assertProject(tree: Tree, options: NormalizedSchema) { if (isStableAlready) { throw new Error(`${options.project} is already released as stable.`); } + + if (options.isSplitProject && options.name.endsWith('-stories')) { + throw new Error( + `This generator can be invoked only against library project. Please run it against "${options.name.replace( + '-stories', + '', + )}" library project.`, + ); + } } function createExportsInSuite(content: string, packageName: string) { @@ -374,3 +441,23 @@ function createExportsInSuite(content: string, packageName: string) { export type { ${exportTypeExpression} } from '${packageName}'; `; } + +function updateStories( + tree: Tree, + options: { storiesSourcePath: string; contentNameToSuiteUpdater: (content: string) => string }, +) { + visitNotIgnoredFiles(tree, options.storiesSourcePath, filePath => { + updateFileContent(tree, { + filePath, + updater: content => { + let newContent = options.contentNameToSuiteUpdater(content); + + if (filePath.indexOf('index.stories.tsx') !== -1) { + newContent = newContent.replace(`'Preview `, `'`); + } + + return newContent; + }, + }); + }); +} From bd2a5fbd8000e4bd33eb19b2a4847481a1e76c17 Mon Sep 17 00:00:00 2001 From: Martin Hochel Date: Mon, 22 Apr 2024 17:55:06 +0200 Subject: [PATCH 02/14] feat(workspace-plugin): make bundle-size-configuration generator work with new split /library /stories react projects --- .../bundle-size-configuration/generator.ts | 28 +++++++++++++++---- 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/tools/workspace-plugin/src/generators/bundle-size-configuration/generator.ts b/tools/workspace-plugin/src/generators/bundle-size-configuration/generator.ts index 4464a112dc3f7f..985837cfb18f86 100644 --- a/tools/workspace-plugin/src/generators/bundle-size-configuration/generator.ts +++ b/tools/workspace-plugin/src/generators/bundle-size-configuration/generator.ts @@ -15,15 +15,20 @@ import { BundleSizeConfigurationGeneratorSchema } from './schema'; export async function bundleSizeConfigurationGenerator(tree: Tree, schema: BundleSizeConfigurationGeneratorSchema) { const options = normalizeOptions(tree, schema); - const config = readProjectConfiguration(tree, options.name); + const project = readProjectConfiguration(tree, options.name); + + const isSplitProject = tree.exists(joinPathFragments(project.root, '../stories/project.json')); + + assertOptions(tree, { isSplitProject, ...options }); + const configPaths = { - bundleSizeRoot: joinPathFragments(config.root, 'bundle-size'), - bundleSizeConfig: joinPathFragments(config.root, 'monosize.config.mjs'), + bundleSizeRoot: joinPathFragments(project.root, 'bundle-size'), + bundleSizeConfig: joinPathFragments(project.root, 'monosize.config.mjs'), }; - generateFiles(tree, path.join(__dirname, 'files'), config.root, { + generateFiles(tree, path.join(__dirname, 'files'), project.root, { packageName: options.name, - rootOffset: offsetFromRoot(config.root), + rootOffset: offsetFromRoot(project.root), }); let hasFixtures = false; @@ -41,7 +46,7 @@ export async function bundleSizeConfigurationGenerator(tree: Tree, schema: Bundl tree.delete(configPaths.bundleSizeConfig); } - updateJson(tree, joinPathFragments(config.root, 'package.json'), (json: PackageJson) => { + updateJson(tree, joinPathFragments(project.root, 'package.json'), (json: PackageJson) => { json.scripts = json.scripts ?? {}; json.scripts['bundle-size'] = 'monosize measure'; return json; @@ -57,4 +62,15 @@ function normalizeOptions(tree: Tree, schema: BundleSizeConfigurationGeneratorSc }; } +function assertOptions(tree: Tree, options: ReturnType & { isSplitProject: boolean }) { + if (options.isSplitProject && options.name.endsWith('-stories')) { + throw new Error( + `This generator can be invoked only against library project. Please run it against "${options.name.replace( + '-stories', + '', + )}" library project.`, + ); + } +} + export default bundleSizeConfigurationGenerator; From 98a5a5f819151ec9c80b98613221d57c871623c8 Mon Sep 17 00:00:00 2001 From: Martin Hochel Date: Mon, 22 Apr 2024 17:55:36 +0200 Subject: [PATCH 03/14] feat(workspace-plugin): make cypress-component-configuration generator work with new split /library /stories react projects --- .../cypress-component-configuration/index.ts | 32 ++++++++++++++++--- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/tools/workspace-plugin/src/generators/cypress-component-configuration/index.ts b/tools/workspace-plugin/src/generators/cypress-component-configuration/index.ts index b07eb3622a98d4..89bf835fa4d764 100644 --- a/tools/workspace-plugin/src/generators/cypress-component-configuration/index.ts +++ b/tools/workspace-plugin/src/generators/cypress-component-configuration/index.ts @@ -1,4 +1,4 @@ -import { Tree, formatFiles, names } from '@nx/devkit'; +import { Tree, formatFiles, names, joinPathFragments } from '@nx/devkit'; import { getProjectConfig, printUserLogs, UserLog } from '../../utils'; @@ -10,12 +10,13 @@ import { addFiles } from './lib/add-files'; interface _NormalizedSchema extends ReturnType {} export default async function (tree: Tree, schema: CypressComponentConfigurationGeneratorSchema) { - const userLog: UserLog = []; const normalizedOptions = normalizeOptions(tree, schema); - if (normalizedOptions.projectConfig.projectType === 'application') { - userLog.push({ type: 'warn', message: 'we dont support cypress component tests for applications' }); - printUserLogs(userLog); + const isSplitProject = tree.exists( + joinPathFragments(normalizedOptions.projectConfig.root, '../stories/project.json'), + ); + + if (!assertOptions(tree, { isSplitProject, ...normalizedOptions })) { return; } @@ -33,3 +34,24 @@ function normalizeOptions(tree: Tree, options: CypressComponentConfigurationGene ...names(options.project), }; } + +function assertOptions(tree: Tree, options: ReturnType & { isSplitProject: boolean }) { + if (options.projectConfig.projectType === 'application') { + const userLog: UserLog = []; + userLog.push({ type: 'warn', message: `We don't support cypress component tests for applications` }); + printUserLogs(userLog); + + return false; + } + + if (options.isSplitProject && options.name.endsWith('-stories')) { + throw new Error( + `This generator can be invoked only against library project. Please run it against "${options.name.replace( + '-stories', + '', + )}" library project.`, + ); + } + + return true; +} From 2cb2bfffe98ee3bc2c5ec233cb5e0cc94ce12fb7 Mon Sep 17 00:00:00 2001 From: Martin Hochel Date: Mon, 22 Apr 2024 17:58:11 +0200 Subject: [PATCH 04/14] fix(workspace-plugin): add proper devDependencies when invoking cypress-component-configuration --- .../cypress-component-configuration/index.spec.ts | 9 ++++++++- .../cypress-component-configuration/lib/add-files.ts | 3 +++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/tools/workspace-plugin/src/generators/cypress-component-configuration/index.spec.ts b/tools/workspace-plugin/src/generators/cypress-component-configuration/index.spec.ts index 1e15afb5699c2d..9de7f6f4135b32 100644 --- a/tools/workspace-plugin/src/generators/cypress-component-configuration/index.spec.ts +++ b/tools/workspace-plugin/src/generators/cypress-component-configuration/index.spec.ts @@ -71,12 +71,19 @@ describe(`cypress-component-configuration`, () => { } `); - expect(readJson(tree, 'packages/one/package.json').scripts).toEqual( + const pkgJson = readJson(tree, 'packages/one/package.json'); + + expect(pkgJson.scripts).toEqual( expect.objectContaining({ e2e: 'cypress run --component', 'e2e:local': 'cypress open --component', }), ); + expect(pkgJson.devDependencies).toEqual( + expect.objectContaining({ + '@fluentui/scripts-cypress': '*', + }), + ); }); }); diff --git a/tools/workspace-plugin/src/generators/cypress-component-configuration/lib/add-files.ts b/tools/workspace-plugin/src/generators/cypress-component-configuration/lib/add-files.ts index b395c08ec4f2dc..e30cd17afdfcb3 100644 --- a/tools/workspace-plugin/src/generators/cypress-component-configuration/lib/add-files.ts +++ b/tools/workspace-plugin/src/generators/cypress-component-configuration/lib/add-files.ts @@ -32,6 +32,9 @@ export function addFiles(tree: Tree, options: Options) { json.scripts.e2e = 'cypress run --component'; json.scripts['e2e:local'] = 'cypress open --component'; + json.devDependencies = json.devDependencies ?? {}; + json.devDependencies['@fluentui/scripts-cypress'] = '*'; + return json; }); From 068f0735d45aafe64722c2cc1907600d99911968 Mon Sep 17 00:00:00 2001 From: Martin Hochel Date: Mon, 22 Apr 2024 18:56:08 +0200 Subject: [PATCH 05/14] feat(workspace-plugin): make react-component generator work with new split /library /stories react projects --- .../generators/react-component/index.spec.ts | 230 +++++++++++------- .../src/generators/react-component/index.ts | 30 ++- 2 files changed, 159 insertions(+), 101 deletions(-) diff --git a/tools/workspace-plugin/src/generators/react-component/index.spec.ts b/tools/workspace-plugin/src/generators/react-component/index.spec.ts index f2bb8734a5dd5c..fd2e381619412b 100644 --- a/tools/workspace-plugin/src/generators/react-component/index.spec.ts +++ b/tools/workspace-plugin/src/generators/react-component/index.spec.ts @@ -5,11 +5,9 @@ import generator from './index'; describe('react-component generator', () => { let tree: Tree; - let metadata: ReturnType['metadata']; beforeEach(() => { tree = createTreeWithEmptyWorkspace(); - metadata = createLibrary(tree, 'react-one').metadata; }); describe(`assertions`, () => { @@ -25,6 +23,7 @@ describe('react-component generator', () => { }); it(`should throw error if component already exists`, async () => { + createLibrary(tree, 'react-one'); await generator(tree, { project: '@proj/react-one', name: 'MyOne' }); try { @@ -35,18 +34,36 @@ describe('react-component generator', () => { }); }); - it('should create component', async () => { - await generator(tree, { project: '@proj/react-one', name: 'MyOne' }); + describe(`component`, () => { + shouldCreateComponent('old'); + shouldCreateComponent('split'); - const projectSourceRootPath = 'packages/react-components/react-one/src'; - const componentRootPath = `${projectSourceRootPath}/components/MyOne`; + shouldUpdateBarrelFile('old'); + shouldUpdateBarrelFile('split'); - expect(tree.read(joinPathFragments(projectSourceRootPath, 'MyOne.ts'), 'utf-8')).toMatchInlineSnapshot(` + function shouldCreateComponent(type: 'old' | 'split') { + it(`should create component - ${type}`, async () => { + if (type === 'old') { + createLibrary(tree, 'react-one'); + } + if (type === 'split') { + createSplitProject(tree, 'react-one'); + } + + await generator(tree, { project: '@proj/react-one', name: 'MyOne' }); + + const projectSourceRootPath = + type === 'old' + ? 'packages/react-components/react-one/src' + : 'packages/react-components/react-one/library/src'; + const componentRootPath = `${projectSourceRootPath}/components/MyOne`; + + expect(tree.read(joinPathFragments(projectSourceRootPath, 'MyOne.ts'), 'utf-8')).toMatchInlineSnapshot(` "export * from './components/MyOne/index'; " `); - expect(tree.children(componentRootPath)).toMatchInlineSnapshot(` + expect(tree.children(componentRootPath)).toMatchInlineSnapshot(` Array [ "MyOne.test.tsx", "MyOne.tsx", @@ -58,7 +75,7 @@ describe('react-component generator', () => { ] `); - expect(tree.read(joinPathFragments(componentRootPath, 'MyOne.tsx'), 'utf-8')).toMatchInlineSnapshot(` + expect(tree.read(joinPathFragments(componentRootPath, 'MyOne.tsx'), 'utf-8')).toMatchInlineSnapshot(` "import * as React from 'react'; import type { ForwardRefComponent } from '@fluentui/react-utilities'; import { useCustomStyleHook_unstable } from '@fluentui/react-shared-contexts'; @@ -86,7 +103,8 @@ describe('react-component generator', () => { " `); - expect(tree.read(joinPathFragments(componentRootPath, 'useMyOneStyles.styles.ts'), 'utf-8')).toMatchInlineSnapshot(` + expect(tree.read(joinPathFragments(componentRootPath, 'useMyOneStyles.styles.ts'), 'utf-8')) + .toMatchInlineSnapshot(` "import { makeStyles, mergeClasses } from '@griffel/react'; import type { SlotClassNames } from '@fluentui/react-utilities'; import type { MyOneSlots, MyOneState } from './MyOne.types'; @@ -126,31 +144,46 @@ describe('react-component generator', () => { }; " `); - }); + }); + } - it(`should update barrel file`, async () => { - await generator(tree, { project: '@proj/react-one', name: 'MyOne' }); + function shouldUpdateBarrelFile(type: 'old' | 'split') { + it(`should update barrel file - ${type}`, async () => { + if (type === 'old') { + createLibrary(tree, 'react-one'); + } + if (type === 'split') { + createSplitProject(tree, 'react-one'); + } - const projectSourceRootPath = 'packages/react-components/react-one/src'; - const barrelPath = joinPathFragments(projectSourceRootPath, 'index.ts'); + await generator(tree, { project: '@proj/react-one', name: 'MyOne' }); + + const projectSourceRootPath = + type === 'old' + ? 'packages/react-components/react-one/src' + : 'packages/react-components/react-one/library/src'; + const barrelPath = joinPathFragments(projectSourceRootPath, 'index.ts'); - expect(tree.read(barrelPath, 'utf-8')).toMatchInlineSnapshot(` + expect(tree.read(barrelPath, 'utf-8')).toMatchInlineSnapshot(` "export * from './MyOne'; " `); - await generator(tree, { project: '@proj/react-one', name: 'MyTwo' }); + await generator(tree, { project: '@proj/react-one', name: 'MyTwo' }); - expect(tree.read(barrelPath, 'utf-8')).toMatchInlineSnapshot(` + expect(tree.read(barrelPath, 'utf-8')).toMatchInlineSnapshot(` "export * from './MyOne'; export * from './MyTwo'; " `); + }); + } }); describe(`stories`, () => { - it(`should remove stories/.gitkeep`, async () => { - const gitkeepPath = joinPathFragments(metadata.paths.storiesRoot, '.gitkeep'); + it(`should remove stories/.gitkeep - old`, async () => { + const { metadata } = createLibrary(tree, 'react-one'); + const gitkeepPath = joinPathFragments(joinPathFragments(metadata.paths.root, 'stories'), '.gitkeep'); expect(tree.exists(gitkeepPath)).toBe(true); await generator(tree, { project: '@proj/react-one', name: 'MyOne' }); @@ -158,11 +191,34 @@ describe('react-component generator', () => { expect(tree.exists(gitkeepPath)).toBe(false); }); - it('should create component story files', async () => { - const componentStoryRootPath = 'packages/react-components/react-one/stories/MyOne'; - await generator(tree, { project: '@proj/react-one', name: 'MyOne' }); + shouldCreateComponentStoryFiles('old'); + shouldCreateComponentStoryFiles('split'); + + shouldCreateComponentStoryForPackagePhase('stable', 'old'); + shouldCreateComponentStoryForPackagePhase('stable', 'split'); + + shouldCreateComponentStoryForPackagePhase('preview', 'old'); + shouldCreateComponentStoryForPackagePhase('preview', 'split'); + + shouldCreateComponentStoryForPackagePhase('compat', 'old'); + shouldCreateComponentStoryForPackagePhase('compat', 'split'); + + function shouldCreateComponentStoryFiles(type: 'old' | 'split') { + it(`should create component story files - ${type}`, async () => { + if (type === 'old') { + createLibrary(tree, 'react-one'); + } + if (type === 'split') { + createSplitProject(tree, 'react-one'); + } + + const componentStoryRootPath = + type === 'old' + ? 'packages/react-components/react-one/stories/MyOne' + : 'packages/react-components/react-one/stories/src/MyOne'; + await generator(tree, { project: '@proj/react-one', name: 'MyOne' }); - expect(tree.children(componentStoryRootPath)).toMatchInlineSnapshot(` + expect(tree.children(componentStoryRootPath)).toMatchInlineSnapshot(` Array [ "MyOneBestPractices.md", "MyOneDefault.stories.tsx", @@ -170,74 +226,44 @@ describe('react-component generator', () => { "index.stories.tsx", ] `); - }); - - it('should create component story for STABLE package', async () => { - const componentStoryRootPath = 'packages/react-components/react-one/stories/MyOne'; - await generator(tree, { project: '@proj/react-one', name: 'MyOne' }); - - expect(tree.read(joinPathFragments(componentStoryRootPath, 'index.stories.tsx'), 'utf-8')).toMatchInlineSnapshot(` - "import { MyOne } from '@proj/react-one'; - - import descriptionMd from './MyOneDescription.md'; - import bestPracticesMd from './MyOneBestPractices.md'; - - export { Default } from './MyOneDefault.stories'; + }); + } - export default { - title: 'Components/MyOne', - component: MyOne, - parameters: { - docs: { - description: { - component: [descriptionMd, bestPracticesMd].join('\\\\n'), - }, - }, - }, + function shouldCreateComponentStoryForPackagePhase(phase: 'stable' | 'preview' | 'compat', type: 'old' | 'split') { + const packageFolderName = { + stable: 'react-one', + preview: 'react-one-preview', + compat: 'react-one-compat', }; - " - `); - }); - - it('should create component story for PREVIEW package', async () => { - createLibrary(tree, 'react-one-preview'); - - const componentStoryRootPath = 'packages/react-components/react-one-preview/stories/MyOne'; - - await generator(tree, { project: '@proj/react-one-preview', name: 'MyOne' }); - - expect(tree.read(joinPathFragments(componentStoryRootPath, 'index.stories.tsx'), 'utf-8')).toMatchInlineSnapshot(` - "import { MyOne } from '@proj/react-one-preview'; - - import descriptionMd from './MyOneDescription.md'; - import bestPracticesMd from './MyOneBestPractices.md'; - - export { Default } from './MyOneDefault.stories'; - - export default { - title: 'Preview Components/MyOne', - component: MyOne, - parameters: { - docs: { - description: { - component: [descriptionMd, bestPracticesMd].join('\\\\n'), - }, - }, - }, + const titlePrefix = { + stable: '', + preview: 'Preview ', + compat: 'Compat ', + }; + const tags = { + stable: [], + preview: [], + compat: ['compat'], }; - " - `); - }); - it('should create component story for COMPAT package', async () => { - createLibrary(tree, 'react-one-compat', { tags: ['compat'] }); + it(`should create component story for ${phase.toUpperCase()} package - ${type}`, async () => { + if (type === 'old') { + createLibrary(tree, packageFolderName[phase], { tags: tags[phase] }); + } + if (type === 'split') { + createSplitProject(tree, packageFolderName[phase], { tags: tags[phase] }); + } - const componentStoryRootPath = 'packages/react-components/react-one-compat/stories/MyOne'; + const componentStoryRootPath = + type === 'old' + ? `packages/react-components/${packageFolderName[phase]}/stories/MyOne` + : `packages/react-components/${packageFolderName[phase]}/stories/src/MyOne`; - await generator(tree, { project: '@proj/react-one-compat', name: 'MyOne' }); + await generator(tree, { project: `@proj/${packageFolderName[phase]}`, name: 'MyOne' }); - expect(tree.read(joinPathFragments(componentStoryRootPath, 'index.stories.tsx'), 'utf-8')).toMatchInlineSnapshot(` - "import { MyOne } from '@proj/react-one-compat'; + expect(tree.read(joinPathFragments(componentStoryRootPath, 'index.stories.tsx'), 'utf-8')) + .toMatchInlineSnapshot(` + "import { MyOne } from '@proj/${packageFolderName[phase]}'; import descriptionMd from './MyOneDescription.md'; import bestPracticesMd from './MyOneBestPractices.md'; @@ -245,7 +271,7 @@ describe('react-component generator', () => { export { Default } from './MyOneDefault.stories'; export default { - title: 'Compat Components/MyOne', + title: '${titlePrefix[phase]}Components/MyOne', component: MyOne, parameters: { docs: { @@ -257,14 +283,36 @@ describe('react-component generator', () => { }; " `); - }); + }); + } }); }); -function createLibrary(tree: Tree, name: string, options: Partial<{ version: string; tags: string[] }> = {}) { - const _options = { version: '9.0.0', tags: ['vNext', ...(options.tags ?? [])], ...options }; +function createSplitProject( + tree: Tree, + name: string, + options: Partial<{ root: string; version: string; tags: string[] }> = {}, +) { + // library + createLibrary(tree, name, { ...options, root: `packages/react-components/${name}/library` }); + + // stories + createLibrary(tree, name + '-stories', { ...options, root: `packages/react-components/${name}/stories` }); +} + +function createLibrary( + tree: Tree, + name: string, + options: Partial<{ root: string; version: string; tags: string[] }> = {}, +) { + const _options = { + version: '9.0.0', + tags: ['vNext', ...(options.tags ?? [])], + ...options, + }; + const root = _options.root ?? `packages/react-components/${name}`; const projectName = '@proj/' + name; - const root = `packages/react-components/${name}`; + const sourceRoot = `${root}/src`; addProjectConfiguration(tree, projectName, { root, tags: _options.tags, sourceRoot }); writeJson(tree, joinPathFragments(root, 'package.json'), { @@ -276,7 +324,7 @@ function createLibrary(tree: Tree, name: string, options: Partial<{ version: str const metadata = { projectConfiguration: { name: projectName, root, tags: _options.tags, sourceRoot }, - paths: { root, sourceRoot, storiesRoot: joinPathFragments(root, 'stories') }, + paths: { root, sourceRoot }, }; return { tree, metadata }; diff --git a/tools/workspace-plugin/src/generators/react-component/index.ts b/tools/workspace-plugin/src/generators/react-component/index.ts index ca2ec22cb26e57..fe8f908725685a 100644 --- a/tools/workspace-plugin/src/generators/react-component/index.ts +++ b/tools/workspace-plugin/src/generators/react-component/index.ts @@ -43,6 +43,7 @@ export default async function (tree: Tree, schema: ReactComponentGeneratorSchema function normalizeOptions(tree: Tree, options: ReactComponentGeneratorSchema) { const project = getProjectConfig(tree, { packageName: options.project }); const nameCasings = names(options.name); + const isSplitProject = tree.exists(joinPathFragments(project.projectConfig.root, '../stories/project.json')); return { ...options, @@ -51,6 +52,7 @@ function normalizeOptions(tree: Tree, options: ReactComponentGeneratorSchema) { directory: 'components', componentName: nameCasings.className, npmPackageName: project.projectConfig.name as string, + isSplitProject, }; } @@ -98,16 +100,16 @@ function addFiles(tree: Tree, options: NormalizedSchema) { ); // story - generateFiles( - tree, - path.join(__dirname, 'files', 'story'), - path.join(options.paths.stories, options.componentName), - templateOptions, - ); - - const storiesGitkeep = path.join(options.paths.stories, '.gitkeep'); - if (tree.exists(storiesGitkeep)) { - tree.delete(storiesGitkeep); + const storiesPath = options.isSplitProject + ? path.join(options.projectConfig.root, '../stories/src', options.componentName) + : path.join(options.paths.stories, options.componentName); + generateFiles(tree, path.join(__dirname, 'files', 'story'), storiesPath, templateOptions); + + if (!options.isSplitProject) { + const storiesGitkeep = path.join(options.paths.stories, '.gitkeep'); + if (tree.exists(storiesGitkeep)) { + tree.delete(storiesGitkeep); + } } } @@ -129,5 +131,13 @@ function assertComponent(tree: Tree, options: NormalizedSchema) { if (tree.exists(componentDirPath)) { throw new Error(`The component "${options.componentName}" already exists`); } + if (options.isSplitProject && options.projectConfig.name?.endsWith('-stories')) { + throw new Error( + `This generator can be invoked only against library project. Please run it against "${options.projectConfig.name.replace( + '-stories', + '', + )}" library project.`, + ); + } return; } From a58784450e36b7729cf806c3778d6f48b35e65f4 Mon Sep 17 00:00:00 2001 From: Martin Hochel Date: Mon, 22 Apr 2024 19:28:53 +0200 Subject: [PATCH 06/14] feat(workspace-plugin): make react-library generator work with new split /library /stories react projects --- .../generators/react-library/index.spec.ts | 138 +++++++++++++----- .../src/generators/react-library/index.ts | 4 + 2 files changed, 103 insertions(+), 39 deletions(-) diff --git a/tools/workspace-plugin/src/generators/react-library/index.spec.ts b/tools/workspace-plugin/src/generators/react-library/index.spec.ts index a87c8cf77be5ef..eb189c4eb82c94 100644 --- a/tools/workspace-plugin/src/generators/react-library/index.spec.ts +++ b/tools/workspace-plugin/src/generators/react-library/index.spec.ts @@ -46,15 +46,16 @@ describe('react-library generator', () => { setup(tree); await generator(tree, { name: 'react-one', owner: '@org/chosen-one' }); - const config = readProjectConfiguration(tree, '@proj/react-one-preview'); - const rootPath = 'packages/react-components/react-one-preview'; - expect(tree.children(rootPath)).toMatchInlineSnapshot(` + const library = readProjectConfiguration(tree, '@proj/react-one-preview'); + const stories = readProjectConfiguration(tree, '@proj/react-one-preview-stories'); + + // library + expect(tree.children(library.root)).toMatchInlineSnapshot(` Array [ "project.json", ".babelrc.json", ".eslintrc.json", - ".storybook", ".swcrc", "LICENSE", "README.md", @@ -65,31 +66,25 @@ describe('react-library generator', () => { "just.config.ts", "package.json", "src", - "stories", "tsconfig.json", "tsconfig.lib.json", "tsconfig.spec.json", ] `); - expect(tree.children(joinPathFragments(rootPath, '.storybook'))).toEqual([ - 'main.js', - 'preview.js', - 'tsconfig.json', - ]); - expect(tree.children(joinPathFragments(rootPath, 'docs'))).toEqual(['Spec.md']); - expect(tree.children(joinPathFragments(rootPath, 'src'))).toEqual(['index.ts', 'testing']); - expect(tree.exists(joinPathFragments(rootPath, 'src', 'testing', 'isConformant.ts'))).toEqual(true); - expect(tree.children(joinPathFragments(rootPath, 'config'))).toEqual(['api-extractor.json', 'tests.js']); - expect(tree.children(joinPathFragments(rootPath, 'etc'))).toEqual(['react-one-preview.api.md']); - - expect(config).toMatchInlineSnapshot(` + expect(tree.children(joinPathFragments(library.root, 'docs'))).toEqual(['Spec.md']); + expect(tree.children(joinPathFragments(library.root, 'src'))).toEqual(['index.ts', 'testing']); + expect(tree.exists(joinPathFragments(library.root, 'src', 'testing', 'isConformant.ts'))).toEqual(true); + expect(tree.children(joinPathFragments(library.root, 'config'))).toEqual(['api-extractor.json', 'tests.js']); + expect(tree.children(joinPathFragments(library.root, 'etc'))).toEqual(['react-one-preview.api.md']); + + expect(library).toMatchInlineSnapshot(` Object { - "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "$schema": "../../../../node_modules/nx/schemas/project-schema.json", "implicitDependencies": Array [], "name": "@proj/react-one-preview", "projectType": "library", - "root": "packages/react-components/react-one-preview", - "sourceRoot": "packages/react-components/react-one-preview/src", + "root": "packages/react-components/react-one-preview/library", + "sourceRoot": "packages/react-components/react-one-preview/library/src", "tags": Array [ "platform:web", "vNext", @@ -97,7 +92,13 @@ describe('react-library generator', () => { } `); - expect(readJson(tree, `${rootPath}/package.json`)).toEqual( + expect(readJson(tree, `${library.root}/config/api-extractor.json`)).toEqual({ + $schema: 'https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json', + extends: '@fluentui/scripts-api-extractor/api-extractor.common.v-next.json', + mainEntryPointFilePath: + '/../../../../../../dist/out-tsc/types/packages/react-components//library/src/index.d.ts', + }); + expect(readJson(tree, `${library.root}/package.json`)).toEqual( expect.objectContaining({ name: '@proj/react-one-preview', private: true, @@ -113,21 +114,21 @@ describe('react-library generator', () => { }, }), ); - expect(readJson(tree, `${rootPath}/tsconfig.json`)).toEqual( - expect.objectContaining({ extends: '../../../tsconfig.base.json' }), + expect(readJson(tree, `${library.root}/tsconfig.json`)).toEqual( + expect.objectContaining({ extends: '../../../../tsconfig.base.json' }), ); - expect(readJson(tree, `${rootPath}/tsconfig.lib.json`)).toMatchInlineSnapshot(` + expect(readJson(tree, `${library.root}/tsconfig.lib.json`)).toMatchInlineSnapshot(` Object { "compilerOptions": Object { "declaration": true, - "declarationDir": "../../../dist/out-tsc/types", + "declarationDir": "../../../../dist/out-tsc/types", "inlineSources": true, "lib": Array [ "ES2019", "dom", ], "noEmit": false, - "outDir": "../../../dist/out-tsc", + "outDir": "../../../../dist/out-tsc", "types": Array [ "static-assets", "environment", @@ -149,13 +150,13 @@ describe('react-library generator', () => { ], } `); - expect(readJson(tree, `${rootPath}/.babelrc.json`)).toEqual( - expect.objectContaining({ extends: '../../../.babelrc-v9.json' }), + expect(readJson(tree, `${library.root}/.babelrc.json`)).toEqual( + expect.objectContaining({ extends: '../../../../.babelrc-v9.json' }), ); - expect(tree.read(`${rootPath}/jest.config.js`, 'utf-8')).toEqual( + expect(tree.read(`${library.root}/jest.config.js`, 'utf-8')).toEqual( expect.stringContaining(`displayName: 'react-one-preview',`), ); - expect(tree.read(`${rootPath}/README.md`, 'utf-8')).toEqual( + expect(tree.read(`${library.root}/README.md`, 'utf-8')).toEqual( expect.stringContaining(stripIndents` # @proj/react-one-preview @@ -163,10 +164,68 @@ describe('react-library generator', () => { `), ); - // global udtpates + // stories + + expect(tree.children(stories.root)).toMatchInlineSnapshot(` + Array [ + "src", + ".storybook", + "README.md", + "just.config.ts", + ".eslintrc.json", + "tsconfig.json", + "tsconfig.lib.json", + "package.json", + "project.json", + ] + `); + + expect(readJson(tree, `${stories.root}/package.json`)).toEqual({ + name: '@proj/react-one-preview-stories', + version: '0.0.0', + private: true, + devDependencies: { + '@fluentui/eslint-plugin': '*', + '@fluentui/react-storybook-addon': '*', + '@fluentui/react-storybook-addon-export-to-sandbox': '*', + '@fluentui/scripts-storybook': '*', + '@fluentui/scripts-tasks': '*', + }, + scripts: { + format: 'just-scripts prettier', + lint: 'eslint src/', + start: 'yarn storybook', + storybook: 'start-storybook', + 'type-check': 'just-scripts type-check', + }, + }); + + expect(stories).toMatchInlineSnapshot(` + Object { + "$schema": "../../../../node_modules/nx/schemas/project-schema.json", + "implicitDependencies": Array [], + "name": "@proj/react-one-preview-stories", + "projectType": "library", + "root": "packages/react-components/react-one-preview/stories", + "sourceRoot": "packages/react-components/react-one-preview/stories/src", + "tags": Array [ + "vNext", + "platform:web", + "type:stories", + ], + } + `); + + expect(tree.read(`${stories.root}/src/index.ts`, 'utf-8')).toMatchInlineSnapshot(` + "export {}; + " + `); + + // global updates const expectedPathAlias = { - ['@proj/react-one-preview']: ['packages/react-components/react-one-preview/src/index.ts'], + ['@proj/react-one-preview']: ['packages/react-components/react-one-preview/library/src/index.ts'], + ['@proj/react-one-preview-stories']: ['packages/react-components/react-one-preview/stories/src/index.ts'], }; expect(readJson(tree, `tsconfig.base.json`).compilerOptions.paths).toEqual( expect.objectContaining(expectedPathAlias), @@ -176,7 +235,10 @@ describe('react-library generator', () => { ); expect(tree.read('.github/CODEOWNERS', 'utf-8')).toEqual( - expect.stringContaining(`packages/react-components/react-one-preview @org/chosen-one`), + expect.stringContaining(stripIndents` + packages/react-components/react-one-preview/library @org/chosen-one + packages/react-components/react-one-preview/stories @org/chosen-one + `), ); }); @@ -185,13 +247,12 @@ describe('react-library generator', () => { await generator(tree, { name: 'react-one', owner: '@org/chosen-one', kind: 'compat' }); const config = readProjectConfiguration(tree, '@proj/react-one-compat'); - const rootPath = 'packages/react-components/react-one-compat'; - expect(tree.children(rootPath)).toMatchInlineSnapshot(` + + expect(tree.children(config.root)).toMatchInlineSnapshot(` Array [ "project.json", ".babelrc.json", ".eslintrc.json", - ".storybook", ".swcrc", "LICENSE", "README.md", @@ -202,7 +263,6 @@ describe('react-library generator', () => { "just.config.ts", "package.json", "src", - "stories", "tsconfig.json", "tsconfig.lib.json", "tsconfig.spec.json", @@ -210,8 +270,8 @@ describe('react-library generator', () => { `); expect(config).toEqual( expect.objectContaining({ - root: 'packages/react-components/react-one-compat', - sourceRoot: 'packages/react-components/react-one-compat/src', + root: 'packages/react-components/react-one-compat/library', + sourceRoot: 'packages/react-components/react-one-compat/library/src', tags: ['platform:web', 'vNext', 'compat'], }), ); diff --git a/tools/workspace-plugin/src/generators/react-library/index.ts b/tools/workspace-plugin/src/generators/react-library/index.ts index 861ac50deaeacd..6eed2f4b568db1 100644 --- a/tools/workspace-plugin/src/generators/react-library/index.ts +++ b/tools/workspace-plugin/src/generators/react-library/index.ts @@ -12,6 +12,8 @@ import { readJson, } from '@nx/devkit'; +import { splitLibraryInTwoGenerator } from '../split-library-in-two/generator'; + import { getProjectConfig, getWorkspaceConfig } from '../../utils'; import { ReactLibraryGeneratorSchema } from './schema'; @@ -32,6 +34,8 @@ export default async function (tree: Tree, schema: ReactLibraryGeneratorSchema) addCodeowner(tree, { packageName: options.projectConfig.name as string, owner: schema.owner }); + await splitLibraryInTwoGenerator(tree, { project: options.projectConfig.name }); + await formatFiles(tree); return () => { From 69b73932cc3e2c26cd0b685c2b585a7a50f92662 Mon Sep 17 00:00:00 2001 From: Martin Hochel Date: Tue, 23 Apr 2024 10:18:14 +0200 Subject: [PATCH 07/14] feat(workspace-plugin): enable split-library-in-two generator for compat and preview packages --- .../src/generators/split-library-in-two/generator.ts | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/tools/workspace-plugin/src/generators/split-library-in-two/generator.ts b/tools/workspace-plugin/src/generators/split-library-in-two/generator.ts index 5f4aabe527b8b7..a548f29c423e5a 100644 --- a/tools/workspace-plugin/src/generators/split-library-in-two/generator.ts +++ b/tools/workspace-plugin/src/generators/split-library-in-two/generator.ts @@ -388,16 +388,6 @@ function assertProject(tree: Tree, projectConfig: ProjectConfiguration, logger: return; } - if (projectConfig.name?.endsWith('-preview')) { - logger.warn({ title: 'preview projects are not supported YET, skipping...' }); - return; - } - - if (tags.includes('compat')) { - logger.warn({ title: 'compat projects are not supported YET, skipping...' }); - return; - } - if (projectConfig.root?.endsWith('/stories') || projectConfig.root?.endsWith('/library')) { logger.warn({ title: 'attempting to migrate already migrated projects, skipping...' }); return; From 4cdab5112032f5befdf471255a98a6859fc513ca Mon Sep 17 00:00:00 2001 From: Martin Hochel Date: Tue, 23 Apr 2024 10:51:37 +0200 Subject: [PATCH 08/14] feat(workspace-plugin): make split-library-in-two generator CLI logs configurable --- .../split-library-in-two/generator.spec.ts | 2 +- .../split-library-in-two/generator.ts | 70 +++++++++++++------ .../split-library-in-two/schema.d.ts | 5 ++ .../split-library-in-two/schema.json | 6 ++ 4 files changed, 59 insertions(+), 24 deletions(-) diff --git a/tools/workspace-plugin/src/generators/split-library-in-two/generator.spec.ts b/tools/workspace-plugin/src/generators/split-library-in-two/generator.spec.ts index 4578f4c88d37df..4440f932623f44 100644 --- a/tools/workspace-plugin/src/generators/split-library-in-two/generator.spec.ts +++ b/tools/workspace-plugin/src/generators/split-library-in-two/generator.spec.ts @@ -23,7 +23,7 @@ const noop = () => { describe('split-library-in-two generator', () => { let tree: Tree; - const options = { project: '@proj/react-hello' }; + const options = { project: '@proj/react-hello', logs: true }; beforeEach(() => { tree = createTreeWithEmptyWorkspace(); diff --git a/tools/workspace-plugin/src/generators/split-library-in-two/generator.ts b/tools/workspace-plugin/src/generators/split-library-in-two/generator.ts index a548f29c423e5a..1afd3d47e40fec 100644 --- a/tools/workspace-plugin/src/generators/split-library-in-two/generator.ts +++ b/tools/workspace-plugin/src/generators/split-library-in-two/generator.ts @@ -37,6 +37,19 @@ const noop = () => { return; }; +function createOutputLogger(options: SplitLibraryInTwoGeneratorSchema) { + if (options.logs) { + return output; + } + + return { + log: noop, + note: noop, + warn: noop, + error: noop, + } as unknown as typeof output; +} + export async function splitLibraryInTwoGenerator(tree: Tree, options: SplitLibraryInTwoGeneratorSchema) { if (options.project && options.all) { throw new Error('Cannot specify both project and all'); @@ -45,24 +58,26 @@ export async function splitLibraryInTwoGenerator(tree: Tree, options: SplitLibra throw new Error('missing `project` or `all` option'); } + const cliOutput = createOutputLogger(options); + if (options.all) { const projects = getProjects(tree); const projectsToSplit = Array.from(projects).filter(([_, project]) => assertProject(tree, project, { warn: noop } as unknown as typeof output), ); - output.log({ + cliOutput.log({ title: `Splitting ${projectsToSplit.length} libraries in two...`, bodyLines: projectsToSplit.map(([name]) => name), }); for (const [projectName, project] of projectsToSplit) { - splitLibraryInTwoInternal(tree, { projectName, project }); + splitLibraryInTwoInternal(tree, { projectName, project }, cliOutput); } } if (options.project) { - splitLibraryInTwoInternal(tree, { projectName: options.project }); + splitLibraryInTwoInternal(tree, { projectName: options.project }, cliOutput); } await tsConfigBaseAllGenerator(tree, { verify: false, skipFormat: true }); @@ -80,11 +95,15 @@ export async function splitLibraryInTwoGenerator(tree: Tree, options: SplitLibra }; } -function splitLibraryInTwoInternal(tree: Tree, options: { projectName: string; project?: ProjectConfiguration }) { +function splitLibraryInTwoInternal( + tree: Tree, + options: { projectName: string; project?: ProjectConfiguration }, + logger: typeof output, +) { const { projectName, project } = options; const projectConfig = project ?? readProjectConfiguration(tree, options.projectName); - output.log({ title: `Splitting library in two: ${projectConfig.name}`, color: 'magenta' }); + logger.log({ title: `Splitting library in two: ${projectConfig.name}`, color: 'magenta' }); if (!assertProject(tree, projectConfig, output)) { return; @@ -102,16 +121,16 @@ function splitLibraryInTwoInternal(tree: Tree, options: { projectName: string; p }, }; - cleanup(tree, normalizedOptions); + cleanup(tree, normalizedOptions, logger); - makeSrcLibrary(tree, normalizedOptions); - makeStoriesLibrary(tree, normalizedOptions); + makeSrcLibrary(tree, normalizedOptions, logger); + makeStoriesLibrary(tree, normalizedOptions, logger); } export default splitLibraryInTwoGenerator; -function cleanup(tree: Tree, options: Options) { - output.log({ title: 'Cleaning up build assets...' }); +function cleanup(tree: Tree, options: Options, logger: typeof output) { + logger.log({ title: 'Cleaning up build assets...' }); const oldProjectRoot = options.projectConfig.root; tree.delete(joinPathFragments(oldProjectRoot, 'dist')); tree.delete(joinPathFragments(oldProjectRoot, 'lib')); @@ -122,8 +141,8 @@ function cleanup(tree: Tree, options: Options) { tree.delete(joinPathFragments(oldProjectRoot, 'node_modules')); } -function makeSrcLibrary(tree: Tree, options: Options) { - output.log({ title: 'creating library/ project' }); +function makeSrcLibrary(tree: Tree, options: Options, logger: typeof output) { + logger.log({ title: 'creating library/ project' }); const oldProjectRoot = options.projectConfig.root; const newProjectRoot = joinPathFragments(oldProjectRoot, 'library'); @@ -165,11 +184,15 @@ function makeSrcLibrary(tree: Tree, options: Options) { json.scripts['test-ssr'] = `test-ssr \"../stories/src/**/*.stories.tsx\"`; } - const deps = getMissingDevDependenciesFromCypressAndJestFiles(tree, { - sourceRoot: newProjectSourceRoot, - projectName: options.projectConfig.name!, - dependencies: json.dependencies, - }); + const deps = getMissingDevDependenciesFromCypressAndJestFiles( + tree, + { + sourceRoot: newProjectSourceRoot, + projectName: options.projectConfig.name!, + dependencies: json.dependencies, + }, + logger, + ); json.devDependencies ??= {}; json.devDependencies = { ...deps, ...json.devDependencies }; @@ -230,8 +253,8 @@ function makeSrcLibrary(tree: Tree, options: Options) { updateCodeowners(tree, options); } -function makeStoriesLibrary(tree: Tree, options: Options) { - output.log({ title: 'creating stories/ project' }); +function makeStoriesLibrary(tree: Tree, options: Options, logger: typeof output) { + logger.log({ title: 'creating stories/ project' }); const oldProjectRoot = options.projectConfig.root; const newProjectRoot = joinPathFragments(oldProjectRoot, 'stories'); const newProjectSourceRoot = joinPathFragments(newProjectRoot, 'src'); @@ -487,6 +510,7 @@ function getWorkspaceDependencies(tree: Tree, imports: string[]) { function getMissingDevDependenciesFromCypressAndJestFiles( tree: Tree, options: { sourceRoot: string; projectName: string; dependencies: Record }, + logger: typeof output, ) { const { projectName, sourceRoot, dependencies } = options; @@ -518,7 +542,7 @@ function getMissingDevDependenciesFromCypressAndJestFiles( // don't add self to deps delete deps[projectName]; - output.warn({ + logger.warn({ title: 'Not adding self to dependencies', bodyLines: ['You should not import from you package absolute path within test files. Prefer relative imports.'], }); @@ -535,7 +559,7 @@ function getMissingDevDependenciesFromCypressAndJestFiles( }); if (log.length > 0) { - output.warn({ + logger.warn({ title: 'Not adding dependencies that are already present in package.json', bodyLines: log, }); @@ -543,7 +567,7 @@ function getMissingDevDependenciesFromCypressAndJestFiles( } if (deps['@fluentui/react-components']) { - output.error({ + logger.error({ title: 'react-components cannot be used within cypress or jest test files as it creates circular dependency.', bodyLines: [ 'Please remove/replace problematic imports from the test files and remove the dependency from "package.json#devDependencies".', @@ -551,7 +575,7 @@ function getMissingDevDependenciesFromCypressAndJestFiles( }); } - output.log({ title: 'Adding missing dependencies', bodyLines: Object.keys(deps) }); + logger.log({ title: 'Adding missing dependencies', bodyLines: Object.keys(deps) }); return deps; } diff --git a/tools/workspace-plugin/src/generators/split-library-in-two/schema.d.ts b/tools/workspace-plugin/src/generators/split-library-in-two/schema.d.ts index b50c5364303c15..a38944db9737e5 100644 --- a/tools/workspace-plugin/src/generators/split-library-in-two/schema.d.ts +++ b/tools/workspace-plugin/src/generators/split-library-in-two/schema.d.ts @@ -1,4 +1,9 @@ export interface SplitLibraryInTwoGeneratorSchema { project?: string; all?: string; + + /** + * @internal + */ + logs?: boolean; } diff --git a/tools/workspace-plugin/src/generators/split-library-in-two/schema.json b/tools/workspace-plugin/src/generators/split-library-in-two/schema.json index e1b3c491598eb8..aded31a717eac0 100644 --- a/tools/workspace-plugin/src/generators/split-library-in-two/schema.json +++ b/tools/workspace-plugin/src/generators/split-library-in-two/schema.json @@ -16,6 +16,12 @@ "all": { "type": "boolean", "description": "Run generator on all vNext packages" + }, + "logs": { + "type": "boolean", + "default": true, + "visible": false, + "x-priority": "internal" } }, "required": [] From 449bce5f2cef9e10eb4b923a4be7e858766af93f Mon Sep 17 00:00:00 2001 From: Martin Hochel Date: Tue, 23 Apr 2024 11:16:14 +0200 Subject: [PATCH 09/14] feat(workspace-plugin): make split-lib-in-two shared utils to be used in other related generators --- .../bundle-size-configuration/generator.ts | 20 ++++++++----------- .../cypress-component-configuration/index.ts | 11 +++------- .../prepare-initial-release/index.ts | 17 ++++++---------- .../src/generators/react-component/index.ts | 17 ++++++---------- .../split-library-in-two/generator.ts | 2 ++ .../generators/split-library-in-two/shared.ts | 15 ++++++++++++++ 6 files changed, 40 insertions(+), 42 deletions(-) create mode 100644 tools/workspace-plugin/src/generators/split-library-in-two/shared.ts diff --git a/tools/workspace-plugin/src/generators/bundle-size-configuration/generator.ts b/tools/workspace-plugin/src/generators/bundle-size-configuration/generator.ts index 985837cfb18f86..fdf7751c216635 100644 --- a/tools/workspace-plugin/src/generators/bundle-size-configuration/generator.ts +++ b/tools/workspace-plugin/src/generators/bundle-size-configuration/generator.ts @@ -3,13 +3,18 @@ import { generateFiles, joinPathFragments, offsetFromRoot, + ProjectConfiguration, readProjectConfiguration, Tree, updateJson, visitNotIgnoredFiles, } from '@nx/devkit'; + import * as path from 'path'; + import { PackageJson } from '../../types'; +import { assertStoriesProject, isSplitProject } from '../split-library-in-two/shared'; + import { BundleSizeConfigurationGeneratorSchema } from './schema'; export async function bundleSizeConfigurationGenerator(tree: Tree, schema: BundleSizeConfigurationGeneratorSchema) { @@ -17,9 +22,7 @@ export async function bundleSizeConfigurationGenerator(tree: Tree, schema: Bundl const project = readProjectConfiguration(tree, options.name); - const isSplitProject = tree.exists(joinPathFragments(project.root, '../stories/project.json')); - - assertOptions(tree, { isSplitProject, ...options }); + assertOptions(tree, { isSplitProject: isSplitProject(tree, project), project }); const configPaths = { bundleSizeRoot: joinPathFragments(project.root, 'bundle-size'), @@ -62,15 +65,8 @@ function normalizeOptions(tree: Tree, schema: BundleSizeConfigurationGeneratorSc }; } -function assertOptions(tree: Tree, options: ReturnType & { isSplitProject: boolean }) { - if (options.isSplitProject && options.name.endsWith('-stories')) { - throw new Error( - `This generator can be invoked only against library project. Please run it against "${options.name.replace( - '-stories', - '', - )}" library project.`, - ); - } +function assertOptions(tree: Tree, options: { project: ProjectConfiguration; isSplitProject: boolean }) { + assertStoriesProject(tree, options); } export default bundleSizeConfigurationGenerator; diff --git a/tools/workspace-plugin/src/generators/cypress-component-configuration/index.ts b/tools/workspace-plugin/src/generators/cypress-component-configuration/index.ts index 89bf835fa4d764..9e0b3467dbe437 100644 --- a/tools/workspace-plugin/src/generators/cypress-component-configuration/index.ts +++ b/tools/workspace-plugin/src/generators/cypress-component-configuration/index.ts @@ -4,6 +4,8 @@ import { getProjectConfig, printUserLogs, UserLog } from '../../utils'; import type { CypressComponentConfigurationGeneratorSchema } from './schema'; +import { assertStoriesProject } from '../split-library-in-two/shared'; + import { addFiles } from './lib/add-files'; // eslint-disable-next-line @typescript-eslint/naming-convention @@ -44,14 +46,7 @@ function assertOptions(tree: Tree, options: ReturnType return false; } - if (options.isSplitProject && options.name.endsWith('-stories')) { - throw new Error( - `This generator can be invoked only against library project. Please run it against "${options.name.replace( - '-stories', - '', - )}" library project.`, - ); - } + assertStoriesProject(tree, { isSplitProject: options.isSplitProject, project: options.projectConfig }); return true; } diff --git a/tools/workspace-plugin/src/generators/prepare-initial-release/index.ts b/tools/workspace-plugin/src/generators/prepare-initial-release/index.ts index de1ad39af6f8f2..2b4049d5fa34bc 100644 --- a/tools/workspace-plugin/src/generators/prepare-initial-release/index.ts +++ b/tools/workspace-plugin/src/generators/prepare-initial-release/index.ts @@ -1,3 +1,5 @@ +import { execSync } from 'child_process'; + import { Tree, formatFiles, @@ -13,7 +15,6 @@ import { stripIndents, workspaceRoot, } from '@nrwl/devkit'; - import * as tsquery from '@phenomnomnominal/tsquery'; import { getProjectConfig, getProjectNameWithoutScope, workspacePaths } from '../../utils'; @@ -22,15 +23,16 @@ import { PackageJson, TsConfig } from '../../types'; import tsConfigBaseAll from '../tsconfig-base-all'; +import { assertStoriesProject, isSplitProject as isSplitProjectFn } from '../split-library-in-two/shared'; + import { ReleasePackageGeneratorSchema } from './schema'; -import { execSync } from 'child_process'; interface NormalizedSchema extends ReturnType {} export default async function (tree: Tree, schema: ReleasePackageGeneratorSchema) { const options = normalizeOptions(tree, schema); - const isSplitProject = tree.exists(joinPathFragments(options.projectConfig.root, '../stories/project.json')); + const isSplitProject = isSplitProjectFn(tree, options.projectConfig); assertProject(tree, { isSplitProject, ...options }); @@ -410,14 +412,7 @@ function assertProject(tree: Tree, options: NormalizedSchema & { isSplitProject: throw new Error(`${options.project} is already released as stable.`); } - if (options.isSplitProject && options.name.endsWith('-stories')) { - throw new Error( - `This generator can be invoked only against library project. Please run it against "${options.name.replace( - '-stories', - '', - )}" library project.`, - ); - } + assertStoriesProject(tree, { isSplitProject: options.isSplitProject, project: options.projectConfig }); } function createExportsInSuite(content: string, packageName: string) { diff --git a/tools/workspace-plugin/src/generators/react-component/index.ts b/tools/workspace-plugin/src/generators/react-component/index.ts index fe8f908725685a..b5d09dcb3a8263 100644 --- a/tools/workspace-plugin/src/generators/react-component/index.ts +++ b/tools/workspace-plugin/src/generators/react-component/index.ts @@ -1,10 +1,11 @@ import path from 'path'; +import { execSync } from 'child_process'; import { Tree, formatFiles, names, generateFiles, joinPathFragments, workspaceRoot } from '@nx/devkit'; import { getProjectConfig, isPackageConverged } from '../../utils'; +import { assertStoriesProject, isSplitProject } from '../split-library-in-two/shared'; import { ReactComponentGeneratorSchema } from './schema'; -import { execSync } from 'child_process'; interface NormalizedSchema extends ReturnType {} @@ -43,7 +44,6 @@ export default async function (tree: Tree, schema: ReactComponentGeneratorSchema function normalizeOptions(tree: Tree, options: ReactComponentGeneratorSchema) { const project = getProjectConfig(tree, { packageName: options.project }); const nameCasings = names(options.name); - const isSplitProject = tree.exists(joinPathFragments(project.projectConfig.root, '../stories/project.json')); return { ...options, @@ -52,7 +52,7 @@ function normalizeOptions(tree: Tree, options: ReactComponentGeneratorSchema) { directory: 'components', componentName: nameCasings.className, npmPackageName: project.projectConfig.name as string, - isSplitProject, + isSplitProject: isSplitProject(tree, project.projectConfig), }; } @@ -131,13 +131,8 @@ function assertComponent(tree: Tree, options: NormalizedSchema) { if (tree.exists(componentDirPath)) { throw new Error(`The component "${options.componentName}" already exists`); } - if (options.isSplitProject && options.projectConfig.name?.endsWith('-stories')) { - throw new Error( - `This generator can be invoked only against library project. Please run it against "${options.projectConfig.name.replace( - '-stories', - '', - )}" library project.`, - ); - } + + assertStoriesProject(tree, { isSplitProject: options.isSplitProject, project: options.projectConfig }); + return; } diff --git a/tools/workspace-plugin/src/generators/split-library-in-two/generator.ts b/tools/workspace-plugin/src/generators/split-library-in-two/generator.ts index 1afd3d47e40fec..fa09b4ab3d1d33 100644 --- a/tools/workspace-plugin/src/generators/split-library-in-two/generator.ts +++ b/tools/workspace-plugin/src/generators/split-library-in-two/generator.ts @@ -25,6 +25,8 @@ import { TsConfig } from '../../types'; import { workspacePaths } from '../../utils'; import { SplitLibraryInTwoGeneratorSchema } from './schema'; +export { isSplitProject, assertStoriesProject } from './shared'; + interface Options extends SplitLibraryInTwoGeneratorSchema { projectConfig: ReturnType; projectOffsetFromRoot: { old: string; updated: string }; diff --git a/tools/workspace-plugin/src/generators/split-library-in-two/shared.ts b/tools/workspace-plugin/src/generators/split-library-in-two/shared.ts new file mode 100644 index 00000000000000..9f3ac2179ac6eb --- /dev/null +++ b/tools/workspace-plugin/src/generators/split-library-in-two/shared.ts @@ -0,0 +1,15 @@ +import { joinPathFragments, type ProjectConfiguration, type Tree } from '@nx/devkit'; + +export const isSplitProject = (tree: Tree, project: ProjectConfiguration) => + tree.exists(joinPathFragments(project.root, '../stories/project.json')); + +export function assertStoriesProject(tree: Tree, options: { isSplitProject: boolean; project: ProjectConfiguration }) { + if (options.isSplitProject && options.project.name?.endsWith('-stories')) { + throw new Error( + `This generator can be invoked only against library project. Please run it against "${options.project.name.replace( + '-stories', + '', + )}" library project.`, + ); + } +} From 261231b85cfed35c57c46eac96b13cf4f2462450 Mon Sep 17 00:00:00 2001 From: Martin Hochel Date: Tue, 23 Apr 2024 12:39:50 +0200 Subject: [PATCH 10/14] feat(workspace-plugin): make split-lib-in-two add compat tag if invoked on compat kind of project --- .../generators/react-library/index.spec.ts | 53 +++++++++++++++++-- .../split-library-in-two/generator.ts | 7 ++- 2 files changed, 56 insertions(+), 4 deletions(-) diff --git a/tools/workspace-plugin/src/generators/react-library/index.spec.ts b/tools/workspace-plugin/src/generators/react-library/index.spec.ts index eb189c4eb82c94..90804d5f23fbe7 100644 --- a/tools/workspace-plugin/src/generators/react-library/index.spec.ts +++ b/tools/workspace-plugin/src/generators/react-library/index.spec.ts @@ -221,6 +221,26 @@ describe('react-library generator', () => { " `); + expect(readJson(tree, `${stories.root}/.eslintrc.json`)).toMatchInlineSnapshot(` + Object { + "extends": Array [ + "plugin:@fluentui/eslint-plugin/react", + ], + "root": true, + "rules": Object { + "import/no-extraneous-dependencies": Array [ + "error", + Object { + "packageDir": Array [ + ".", + "../../../../", + ], + }, + ], + }, + } + `); + // global updates const expectedPathAlias = { @@ -246,9 +266,12 @@ describe('react-library generator', () => { setup(tree); await generator(tree, { name: 'react-one', owner: '@org/chosen-one', kind: 'compat' }); - const config = readProjectConfiguration(tree, '@proj/react-one-compat'); + const library = readProjectConfiguration(tree, '@proj/react-one-compat'); + const stories = readProjectConfiguration(tree, '@proj/react-one-compat-stories'); - expect(tree.children(config.root)).toMatchInlineSnapshot(` + // library + + expect(tree.children(library.root)).toMatchInlineSnapshot(` Array [ "project.json", ".babelrc.json", @@ -268,13 +291,37 @@ describe('react-library generator', () => { "tsconfig.spec.json", ] `); - expect(config).toEqual( + expect(library).toEqual( expect.objectContaining({ root: 'packages/react-components/react-one-compat/library', sourceRoot: 'packages/react-components/react-one-compat/library/src', tags: ['platform:web', 'vNext', 'compat'], }), ); + + // stories + + expect(tree.children(stories.root)).toMatchInlineSnapshot(` + Array [ + "src", + ".storybook", + "README.md", + "just.config.ts", + ".eslintrc.json", + "tsconfig.json", + "tsconfig.lib.json", + "package.json", + "project.json", + ] + `); + + expect(stories).toEqual( + expect.objectContaining({ + root: 'packages/react-components/react-one-compat/stories', + sourceRoot: 'packages/react-components/react-one-compat/stories/src', + tags: ['vNext', 'platform:web', 'compat', 'type:stories'], + }), + ); }); }); diff --git a/tools/workspace-plugin/src/generators/split-library-in-two/generator.ts b/tools/workspace-plugin/src/generators/split-library-in-two/generator.ts index fa09b4ab3d1d33..9917b09c4c675e 100644 --- a/tools/workspace-plugin/src/generators/split-library-in-two/generator.ts +++ b/tools/workspace-plugin/src/generators/split-library-in-two/generator.ts @@ -395,7 +395,12 @@ function makeStoriesLibrary(tree: Tree, options: Options, logger: typeof output) root: newProjectRoot, sourceRoot: newProjectSourceRoot, name: `${options.projectConfig.name}-stories`, - tags: ['vNext', 'platform:web', 'type:stories'], + tags: [ + 'vNext', + 'platform:web', + options.projectConfig.tags?.includes('compat') ? 'compat' : null, + 'type:stories', + ].filter(Boolean) as string[], }); updateJson(tree, '/tsconfig.base.json', (json: TsConfig) => { From 37bfa3b42811092d882e1ef65eb94446b61e83ed Mon Sep 17 00:00:00 2001 From: Martin Hochel Date: Fri, 3 May 2024 15:12:19 +0200 Subject: [PATCH 11/14] feat(workspace-plugin): add internal skipFormat flag to split-library-in-two --- .../src/generators/split-library-in-two/generator.ts | 4 +++- .../src/generators/split-library-in-two/schema.d.ts | 5 +++++ .../src/generators/split-library-in-two/schema.json | 6 ++++++ 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/tools/workspace-plugin/src/generators/split-library-in-two/generator.ts b/tools/workspace-plugin/src/generators/split-library-in-two/generator.ts index 9917b09c4c675e..76df4a83a57bc1 100644 --- a/tools/workspace-plugin/src/generators/split-library-in-two/generator.ts +++ b/tools/workspace-plugin/src/generators/split-library-in-two/generator.ts @@ -87,7 +87,9 @@ export async function splitLibraryInTwoGenerator(tree: Tree, options: SplitLibra // TODO: we don't wanna fail master build because formatting failed // - Nx is using await `prettier.format` under the hood which is for prettier v3, but we use prettier v2 ATM, while that unnecessary await should not cause harm it seems it does try { - await formatFiles(tree); + if (!options.skipFormat) { + await formatFiles(tree); + } } catch (err) { console.log(err); } diff --git a/tools/workspace-plugin/src/generators/split-library-in-two/schema.d.ts b/tools/workspace-plugin/src/generators/split-library-in-two/schema.d.ts index a38944db9737e5..1c0c1f36d21960 100644 --- a/tools/workspace-plugin/src/generators/split-library-in-two/schema.d.ts +++ b/tools/workspace-plugin/src/generators/split-library-in-two/schema.d.ts @@ -6,4 +6,9 @@ export interface SplitLibraryInTwoGeneratorSchema { * @internal */ logs?: boolean; + + /** + * @internal + */ + skipFormat?: boolean; } diff --git a/tools/workspace-plugin/src/generators/split-library-in-two/schema.json b/tools/workspace-plugin/src/generators/split-library-in-two/schema.json index aded31a717eac0..190ed113cbae55 100644 --- a/tools/workspace-plugin/src/generators/split-library-in-two/schema.json +++ b/tools/workspace-plugin/src/generators/split-library-in-two/schema.json @@ -22,6 +22,12 @@ "default": true, "visible": false, "x-priority": "internal" + }, + "skipFormat": { + "description": "Skip formatting files.", + "type": "boolean", + "default": false, + "x-priority": "internal" } }, "required": [] From 8cda7712c5b3197d8ceb61b922f454b8e1a23cbb Mon Sep 17 00:00:00 2001 From: Martin Hochel Date: Fri, 3 May 2024 15:13:44 +0200 Subject: [PATCH 12/14] feat(workspace-plugin): run formatFiles only once within react-library to avoid unhandled promises runtime issues --- tools/workspace-plugin/src/generators/react-library/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/workspace-plugin/src/generators/react-library/index.ts b/tools/workspace-plugin/src/generators/react-library/index.ts index 6eed2f4b568db1..a9dc88545a1aa3 100644 --- a/tools/workspace-plugin/src/generators/react-library/index.ts +++ b/tools/workspace-plugin/src/generators/react-library/index.ts @@ -34,7 +34,7 @@ export default async function (tree: Tree, schema: ReactLibraryGeneratorSchema) addCodeowner(tree, { packageName: options.projectConfig.name as string, owner: schema.owner }); - await splitLibraryInTwoGenerator(tree, { project: options.projectConfig.name }); + await splitLibraryInTwoGenerator(tree, { project: options.projectConfig.name, skipFormat: true }); await formatFiles(tree); From 4260ec00021b3190b16792b0b1a8cbcf660725d0 Mon Sep 17 00:00:00 2001 From: Martin Hochel Date: Fri, 3 May 2024 15:38:01 +0200 Subject: [PATCH 13/14] feat(workspace-plugin): move test-ssr task to /stories project to create proper dependency tree --- .../src/generators/react-library/index.spec.ts | 5 ++++- .../split-library-in-two/generator.spec.ts | 6 ++++-- .../generators/split-library-in-two/generator.ts | 15 ++++++++++++--- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/tools/workspace-plugin/src/generators/react-library/index.spec.ts b/tools/workspace-plugin/src/generators/react-library/index.spec.ts index 90804d5f23fbe7..e9d71d7575c454 100644 --- a/tools/workspace-plugin/src/generators/react-library/index.spec.ts +++ b/tools/workspace-plugin/src/generators/react-library/index.spec.ts @@ -98,7 +98,9 @@ describe('react-library generator', () => { mainEntryPointFilePath: '/../../../../../../dist/out-tsc/types/packages/react-components//library/src/index.d.ts', }); - expect(readJson(tree, `${library.root}/package.json`)).toEqual( + const libPackageJson = readJson(tree, `${library.root}/package.json`); + expect(libPackageJson.scripts['test-ssr']).toEqual(undefined); + expect(libPackageJson).toEqual( expect.objectContaining({ name: '@proj/react-one-preview', private: true, @@ -197,6 +199,7 @@ describe('react-library generator', () => { start: 'yarn storybook', storybook: 'start-storybook', 'type-check': 'just-scripts type-check', + 'test-ssr': 'test-ssr "./src/**/*.stories.tsx"', }, }); diff --git a/tools/workspace-plugin/src/generators/split-library-in-two/generator.spec.ts b/tools/workspace-plugin/src/generators/split-library-in-two/generator.spec.ts index 4440f932623f44..02d2e8338f34ba 100644 --- a/tools/workspace-plugin/src/generators/split-library-in-two/generator.spec.ts +++ b/tools/workspace-plugin/src/generators/split-library-in-two/generator.spec.ts @@ -119,11 +119,12 @@ describe('split-library-in-two generator', () => { }), ); - expect(readJson(tree, `${newConfig.root}/package.json`)).toEqual( + const newConfigPackageJSON = readJson(tree, `${newConfig.root}/package.json`); + expect(newConfigPackageJSON.scripts['test-ssr']).toEqual(undefined); + expect(newConfigPackageJSON).toEqual( expect.objectContaining({ name: '@proj/react-hello', scripts: expect.objectContaining({ - 'test-ssr': 'test-ssr "../stories/src/**/*.stories.tsx"', 'type-check': 'just-scripts type-check', storybook: 'yarn --cwd ../stories storybook', }), @@ -192,6 +193,7 @@ describe('split-library-in-two generator', () => { "lint": "eslint src/", "start": "yarn storybook", "storybook": "start-storybook", + "test-ssr": "test-ssr \\"./src/**/*.stories.tsx\\"", "type-check": "just-scripts type-check", }, "version": "0.0.0", diff --git a/tools/workspace-plugin/src/generators/split-library-in-two/generator.ts b/tools/workspace-plugin/src/generators/split-library-in-two/generator.ts index 76df4a83a57bc1..9d6874fc9caab8 100644 --- a/tools/workspace-plugin/src/generators/split-library-in-two/generator.ts +++ b/tools/workspace-plugin/src/generators/split-library-in-two/generator.ts @@ -32,6 +32,11 @@ interface Options extends SplitLibraryInTwoGeneratorSchema { projectOffsetFromRoot: { old: string; updated: string }; oldContent: { tsConfig: Record; + packageJSON: Record; + }; + + oldPackageMetadata: { + ssrTestsScript: string | undefined; }; } @@ -113,6 +118,7 @@ function splitLibraryInTwoInternal( return; } + const packageJSON = readJson(tree, joinPathFragments(projectConfig.root, 'package.json')); const normalizedOptions = { projectName, projectConfig, @@ -122,6 +128,10 @@ function splitLibraryInTwoInternal( }, oldContent: { tsConfig: readJson(tree, joinPathFragments(projectConfig.root, 'tsconfig.json')), + packageJSON, + }, + oldPackageMetadata: { + ssrTestsScript: packageJSON?.scripts?.['test-ssr'], }, }; @@ -184,9 +194,7 @@ function makeSrcLibrary(tree: Tree, options: Options, logger: typeof output) { json.scripts ??= {}; json.scripts.storybook = 'yarn --cwd ../stories storybook'; json.scripts['type-check'] = 'just-scripts type-check'; - if (json.scripts['test-ssr']) { - json.scripts['test-ssr'] = `test-ssr \"../stories/src/**/*.stories.tsx\"`; - } + delete json.scripts['test-ssr']; const deps = getMissingDevDependenciesFromCypressAndJestFiles( tree, @@ -315,6 +323,7 @@ function makeStoriesLibrary(tree: Tree, options: Options, logger: typeof output) 'type-check': 'just-scripts type-check', lint: 'eslint src/', format: 'just-scripts prettier', + ...(options.oldPackageMetadata.ssrTestsScript ? { 'test-ssr': `test-ssr "./src/**/*.stories.tsx"` } : null), }, devDependencies: { ...storiesWorkspaceDeps, From 9bc708407f6d20ac8c08488bcad4bb40e99729f2 Mon Sep 17 00:00:00 2001 From: Martin Hochel Date: Wed, 15 May 2024 12:14:40 +0200 Subject: [PATCH 14/14] test: accomodate bundle size changes within preview-initial-release if in split mode --- .../prepare-initial-release/index.spec.ts | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tools/workspace-plugin/src/generators/prepare-initial-release/index.spec.ts b/tools/workspace-plugin/src/generators/prepare-initial-release/index.spec.ts index 72a8cc1c7c1331..f1644da4e70544 100644 --- a/tools/workspace-plugin/src/generators/prepare-initial-release/index.spec.ts +++ b/tools/workspace-plugin/src/generators/prepare-initial-release/index.spec.ts @@ -624,6 +624,18 @@ describe('prepare-initial-release generator', () => { files: { library: [], stories: [ + { + filePath: 'packages/react-one-preview/library/bundle-size/index.fixture.js', + content: stripIndents` + import {One} from '@proj/react-one-preview'; + + console.log(One); + + export default { + name: '@proj/react-one-preview - package', + } + `, + }, { filePath: 'packages/react-one-preview/stories/src/One.stories.tsx', content: stripIndents` @@ -717,6 +729,18 @@ describe('prepare-initial-release generator', () => { }), ); + expect(utils.project.library.bundleSize()).toMatchInlineSnapshot(` + Object { + "packages/react-one/library/bundle-size/index.fixture.js": "import { One } from '@proj/react-one'; + + console.log(One); + + export default { + name: '@proj/react-one - package', + };", + } + `); + expect(utils.project.library.md.readme()).toMatchInlineSnapshot(` "# @proj/react-one