diff --git a/change/@fluentui-eslint-plugin-a8ff06ca-d0bd-4d68-96b5-54549bb1a758.json b/change/@fluentui-eslint-plugin-a8ff06ca-d0bd-4d68-96b5-54549bb1a758.json new file mode 100644 index 0000000000000..6759580956e71 --- /dev/null +++ b/change/@fluentui-eslint-plugin-a8ff06ca-d0bd-4d68-96b5-54549bb1a758.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "Turn off import/no-extraneous-dependencies rule for cypress component test files.", + "packageName": "@fluentui/eslint-plugin", + "email": "tristan.watanabe@gmail.com", + "dependentChangeType": "none" +} diff --git a/packages/eslint-plugin/src/configs/react.js b/packages/eslint-plugin/src/configs/react.js index 3e302383d984f..4b8d2936aed14 100644 --- a/packages/eslint-plugin/src/configs/react.js +++ b/packages/eslint-plugin/src/configs/react.js @@ -44,5 +44,11 @@ module.exports = { ], }, }, + { + files: ['**/*.cy.{ts,tsx,js}', 'isConformant.{ts,tsx,js}'], + rules: { + 'import/no-extraneous-dependencies': 'off', + }, + }, ], }; diff --git a/scripts/cypress/cypress.config.ts b/scripts/cypress/cypress.config.ts index 6244a55955939..8571305a55dc2 100644 --- a/scripts/cypress/cypress.config.ts +++ b/scripts/cypress/cypress.config.ts @@ -65,7 +65,7 @@ const cypressWebpackConfig = (): Configuration => { export default defineConfig({ video: false, component: { - specPattern: path.join(process.cwd(), '**/*.e2e.tsx'), + specPattern: [path.join(process.cwd(), '**/*.e2e.tsx'), path.join(process.cwd(), '**/*.cy.tsx')], devServer: { framework: 'react', bundler: 'webpack', diff --git a/scripts/tasks/eslint.ts b/scripts/tasks/eslint.ts index f796eb8520150..58f9691068040 100644 --- a/scripts/tasks/eslint.ts +++ b/scripts/tasks/eslint.ts @@ -1,10 +1,18 @@ import { eslintTask } from 'just-scripts'; import * as path from 'path'; import * as constants from './eslint-constants'; +import * as fs from 'fs'; + +const files = [path.join(process.cwd(), constants.directory)]; +const storiesPath = path.join(process.cwd(), 'stories'); + +if (fs.existsSync(storiesPath)) { + files.push(storiesPath); +} export const eslint = eslintTask({ // TODO: also lint config files? - files: [path.join(process.cwd(), constants.directory)], + files, extensions: constants.extensions, cache: true, // only lint files changed since last lint fix: process.argv.includes('--fix'), diff --git a/tools/generators/migrate-converged-pkg/index.spec.ts b/tools/generators/migrate-converged-pkg/index.spec.ts index b475859e55e7c..4e0502b28ef19 100644 --- a/tools/generators/migrate-converged-pkg/index.spec.ts +++ b/tools/generators/migrate-converged-pkg/index.spec.ts @@ -18,6 +18,7 @@ import { visitNotIgnoredFiles, writeJson, WorkspaceConfiguration, + joinPathFragments, } from '@nrwl/devkit'; import { PackageJson, TsConfig } from '../../types'; @@ -276,7 +277,7 @@ describe('migrate-converged-pkg generator', () => { lib: ['ES2019', 'dom'], types: ['static-assets', 'environment'], }, - exclude: ['./src/common/**', '**/*.spec.ts', '**/*.spec.tsx', '**/*.test.ts', '**/*.test.tsx'], + exclude: ['./src/testing/**', '**/*.spec.ts', '**/*.spec.tsx', '**/*.test.ts', '**/*.test.tsx'], include: ['./src/**/*.ts', './src/**/*.tsx'], }); expect(tsConfigTest).toEqual({ @@ -286,7 +287,15 @@ describe('migrate-converged-pkg generator', () => { outDir: 'dist', types: ['jest', 'node'], }, - include: ['**/*.spec.ts', '**/*.spec.tsx', '**/*.test.ts', '**/*.test.tsx', '**/*.d.ts'], + include: [ + '**/*.spec.ts', + '**/*.spec.tsx', + '**/*.test.ts', + '**/*.test.tsx', + '**/*.d.ts', + './src/testing/**/*.ts', + './src/testing/**/*.tsx', + ], }); }); @@ -322,6 +331,8 @@ describe('migrate-converged-pkg generator', () => { '**/*.test.js', '**/*.test.jsx', '**/*.d.ts', + './src/testing/**/*.js', + './src/testing/**/*.jsx', ]); }); @@ -498,8 +509,8 @@ describe('migrate-converged-pkg generator', () => { const normalizedProjectNameNamesVariants = names(normalizedProjectName); const paths = { - storyOne: `${projectConfig.root}/src/stories/${normalizedProjectNameNamesVariants.className}.stories.tsx`, - storyTwo: `${projectConfig.root}/src/stories/${normalizedProjectNameNamesVariants.className}Other.stories.tsx`, + storyOne: `${projectConfig.root}/stories/${normalizedProjectNameNamesVariants.className}.stories.tsx`, + storyTwo: `${projectConfig.root}/stories/${normalizedProjectNameNamesVariants.className}Other.stories.tsx`, tsconfig: { storybook: `${projectStorybookConfigPath}/tsconfig.json`, main: `${projectConfig.root}/tsconfig.json`, @@ -560,7 +571,7 @@ describe('migrate-converged-pkg generator', () => { outDir: '', types: ['static-assets', 'environment', 'storybook__addons'], }, - include: ['../src/**/*.stories.ts', '../src/**/*.stories.tsx', '*.js'], + include: ['../stories/**/*.stories.ts', '../stories/**/*.stories.tsx', '*.js'], }); expect(readJson(tree, paths.tsconfig.lib).exclude).toEqual( expect.arrayContaining(['**/*.stories.ts', '**/*.stories.tsx']), @@ -578,7 +589,7 @@ describe('migrate-converged-pkg generator', () => { module.exports = /** @type {Omit} */ ({ ...rootMain, - stories: [...rootMain.stories, '../src/**/*.stories.mdx', '../src/**/index.stories.@(ts|tsx)'], + stories: [...rootMain.stories, '../stories/**/*.stories.mdx', '../stories/**/index.stories.@(ts|tsx)'], addons: [...rootMain.addons], webpackFinal: (config, options) => { const localConfig = { ...rootMain.webpackFinal(config, options) }; @@ -632,7 +643,7 @@ describe('migrate-converged-pkg generator', () => { // artificially add stories globs to exclude writeJson(tree, paths.tsconfig.lib, { compilerOptions: {}, - exclude: ['../src/common/**', '**/*.test.ts', '**/*.test.tsx', '**/*.stories.ts', '**/*.stories.tsx'], + exclude: ['../src/testing/**', '**/*.test.ts', '**/*.test.tsx', '**/*.stories.ts', '**/*.stories.tsx'], }); // artificially create spec ts config writeJson(tree, paths.tsconfig.test, { @@ -674,11 +685,10 @@ describe('migrate-converged-pkg generator', () => { expect(tree.read(paths.storyOne)?.toString('utf-8')).toMatchSnapshot(); }); - it(`should move existing stories to the src/stories/ComponentName folder`, async () => { - const { projectConfig, normalizedProjectName } = setup({ createDummyStories: true }); - const componentName = names(normalizedProjectName).className.replace('React', ''); + it(`should move existing stories to the root stories subfolder`, async () => { + const { projectConfig } = setup({ createDummyStories: true }); const oldStoriesPath = `${projectConfig.root}/src/stories`; - const newStoriesPath = `${oldStoriesPath}/${componentName}`; + const newStoriesPath = `${projectConfig.root}/stories`; const storyFiles: string[] = []; visitNotIgnoredFiles(tree, oldStoriesPath, treePath => { @@ -707,29 +717,28 @@ describe('migrate-converged-pkg generator', () => { }); }); - describe(`e2e config`, () => { + describe(`cypress config`, () => { function setup(config: { projectName: string }) { const projectConfig = readProjectConfiguration(tree, config.projectName); const paths = { - e2eRoot: `${projectConfig.root}/e2e`, packageJson: `${projectConfig.root}/package.json`, tsconfig: { main: `${projectConfig.root}/tsconfig.json`, lib: `${projectConfig.root}/tsconfig.lib.json`, test: `${projectConfig.root}/tsconfig.spec.json`, - e2e: `${projectConfig.root}/e2e/tsconfig.json`, + cypress: `${projectConfig.root}/tsconfig.cy.json`, }, }; - function createE2eSetup() { - writeJson(tree, paths.tsconfig.e2e, { + function createCypressSetup() { + writeJson(tree, paths.tsconfig.cypress, { extends: '../../tsconfig.base.json', compilerOptions: {}, }); tree.write( - `${paths.e2eRoot}/index.e2e.ts`, + `${projectConfig.sourceRoot}/components/index.cy.ts`, stripIndents` - describe('E2E test', () => { + describe('Cypress test', () => { before(() => { cy.visitStorybook(); }); @@ -740,39 +749,41 @@ describe('migrate-converged-pkg generator', () => { return tree; } - return { projectConfig, paths, createE2eSetup }; + return { projectConfig, paths, createCypressSetup }; } - it(`should do nothing if e2e setup is missing`, async () => { + it(`should do nothing if cypress setup is missing`, async () => { const { paths } = setup({ projectName: options.name }); await generator(tree, { name: options.name }); - expect(tree.exists(paths.tsconfig.e2e)).toBeFalsy(); + expect(tree.exists(paths.tsconfig.cypress)).toBeFalsy(); }); - it(`should setup e2e if present`, async () => { - const { paths, createE2eSetup } = setup({ projectName: options.name }); + it(`should setup cypress if present`, async () => { + const { paths, createCypressSetup } = setup({ projectName: options.name }); - createE2eSetup(); + createCypressSetup(); - expect(tree.exists(paths.tsconfig.e2e)).toBeTruthy(); + expect(tree.exists(paths.tsconfig.cypress)).toBeTruthy(); await generator(tree, { name: options.name }); // // TS Updates - const e2eTsConfig: TsConfig = readJson(tree, paths.tsconfig.e2e); + const cypressTsConfig: TsConfig = readJson(tree, paths.tsconfig.cypress); const mainTsConfig: TsConfig = readJson(tree, paths.tsconfig.main); + const libTsConfig: TsConfig = readJson(tree, paths.tsconfig.lib); - expect(e2eTsConfig).toEqual({ - extends: '../tsconfig.json', + expect(cypressTsConfig).toEqual({ + extends: './tsconfig.json', compilerOptions: { isolatedModules: false, lib: ['ES2019', 'dom'], types: ['node', 'cypress', 'cypress-storybook/cypress', 'cypress-real-events'], }, - include: ['**/*.ts', '**/*.tsx'], + include: ['**/*.cy.ts', '**/*.cy.tsx'], }); - expect(mainTsConfig.references).toEqual(expect.arrayContaining([{ path: './e2e/tsconfig.json' }])); + expect(mainTsConfig.references).toEqual(expect.arrayContaining([{ path: './tsconfig.cy.json' }])); + expect(libTsConfig.exclude).toEqual(expect.arrayContaining(['**/*.cy.ts', '**/*.cy.tsx'])); // package.json updates const packageJson: PackageJson = readJson(tree, paths.packageJson); @@ -780,6 +791,54 @@ describe('migrate-converged-pkg generator', () => { expect.objectContaining({ e2e: 'cypress run --component', 'e2e:local': 'cypress open --component' }), ); }); + + it(`should migrate existing files in e2e folder to new setup`, async () => { + const { paths, projectConfig } = setup({ projectName: options.name }); + const sourceRoot = joinPathFragments(projectConfig.root, 'src'); + const e2eFolderPath = joinPathFragments(projectConfig.root, 'e2e'); + + function createOldE2eSetup() { + writeJson(tree, joinPathFragments(e2eFolderPath, 'tsconfig.json'), { + extends: '../../tsconfig.base.json', + compilerOptions: {}, + }); + tree.write( + `${e2eFolderPath}/Dummy.e2e.ts`, + stripIndents` + describe('Cypress test', () => { + before(() => { + cy.visitStorybook(); + }); + }); + `, + ); + + tree.write( + `${e2eFolderPath}/selectors.ts`, + stripIndents` + export const dummySelector = '[role="dummy"]'; + `, + ); + + return tree; + } + + createOldE2eSetup(); + + expect(tree.exists(joinPathFragments(e2eFolderPath, 'tsconfig.json'))).toBeTruthy(); + expect(tree.exists(joinPathFragments(e2eFolderPath, 'selectors.ts'))).toBeTruthy(); + expect(tree.exists(joinPathFragments(e2eFolderPath, 'Dummy.e2e.ts'))).toBeTruthy(); + + await generator(tree, { name: options.name }); + + expect(tree.exists(joinPathFragments(e2eFolderPath, 'tsconfig.json'))).toBeFalsy(); + expect(tree.exists(joinPathFragments(e2eFolderPath, 'selectors.ts'))).toBeFalsy(); + expect(tree.exists(joinPathFragments(e2eFolderPath, 'Dummy.e2e.ts'))).toBeFalsy(); + + expect(tree.exists(paths.tsconfig.cypress)).toBeTruthy(); + expect(tree.exists(joinPathFragments(sourceRoot, 'components', 'Dummy', 'Dummy.cy.ts'))).toBeTruthy(); + expect(tree.exists(joinPathFragments(sourceRoot, 'testing', 'selectors.ts'))).toBeTruthy(); + }); }); describe(`api-extractor.json updates`, () => { @@ -1025,10 +1084,11 @@ describe('migrate-converged-pkg generator', () => { bundle-size/ config/ coverage/ - e2e/ + docs/ etc/ node_modules/ src/ + stories/ dist/types/ temp/ __fixtures__ @@ -1038,7 +1098,7 @@ describe('migrate-converged-pkg generator', () => { *.api.json *.log *.spec.* - *.stories.* + *.cy.* *.test.* *.yml @@ -1271,6 +1331,76 @@ describe('migrate-converged-pkg generator', () => { expect(content).toContain(`packages/react-dummy @org/team-awesome`); }); }); + + describe(`common folder migration`, () => { + function setup(config: { projectName: string }) { + const projectConfig = readProjectConfiguration(tree, config.projectName); + const sourceRoot = projectConfig.sourceRoot ?? joinPathFragments(projectConfig.root, 'src'); + const paths = { + packageJson: `${projectConfig.root}/package.json`, + commonFolder: joinPathFragments(sourceRoot, 'common'), + testingFolder: joinPathFragments(sourceRoot, 'testing'), + components: joinPathFragments(sourceRoot, 'components'), + }; + + function createCommonFolderTestSetup() { + tree.write( + `${paths.commonFolder}/isConformant.ts`, + stripIndents` + export const isConformant(){} + `, + ); + tree.write( + `${paths.commonFolder}/mockDummy.ts`, + stripIndents` + export const mockDummy(){} + `, + ); + tree.write( + `${paths.components}/Dummy/Dummy.test.tsx`, + stripIndents` + import { isConformant } from "../../common/isConformant" + import { mockDummy } from "../../common/mockDummy" + `, + ); + + return tree; + } + + return { projectConfig, paths, createCommonFolderTestSetup }; + } + + it(`should move all files from src/common to src/testing`, async () => { + const { paths, createCommonFolderTestSetup } = setup({ projectName: options.name }); + + createCommonFolderTestSetup(); + + expect(tree.exists(joinPathFragments(paths.commonFolder, 'isConformant.ts'))).toBeTruthy(); + expect(tree.exists(joinPathFragments(paths.commonFolder, 'mockDummy.ts'))).toBeTruthy(); + + await generator(tree, options); + + expect(tree.exists(joinPathFragments(paths.commonFolder, 'isConformant.ts'))).toBeFalsy(); + expect(tree.exists(joinPathFragments(paths.commonFolder, 'mockDummy.ts'))).toBeFalsy(); + + expect(tree.exists(joinPathFragments(paths.testingFolder, 'isConformant.ts'))).toBeTruthy(); + expect(tree.exists(joinPathFragments(paths.testingFolder, 'mockDummy.ts'))).toBeTruthy(); + }); + + it(`should update imports of files from common/ to testing/ correctly `, async () => { + const { paths, createCommonFolderTestSetup } = setup({ projectName: options.name }); + + createCommonFolderTestSetup(); + const testFilePath = joinPathFragments(paths.components, 'Dummy', 'Dummy.test.tsx'); + + await generator(tree, options); + + expect(tree.read(testFilePath)?.toString('utf-8')).toMatchInlineSnapshot(` + "import { isConformant } from \\"../../testing/isConformant\\" + import { mockDummy } from \\"../../testing/mockDummy\\"" + `); + }); + }); }); // ==== helpers ==== @@ -1421,7 +1551,7 @@ function setupDummyPackage( function addConformanceSetup(tree: Tree, projectConfig: ReadProjectConfiguration) { // this is needed to stop TS parsing static imports and evaluating them in nx dep graph tree as true dependency - https://github.com/nrwl/nx/issues/8938 const template = fs.readFileSync(path.join(__dirname, '__fixtures__', 'conformance-setup.ts__tmpl__'), 'utf-8'); - tree.write(`${projectConfig.root}/src/common/isConformant.ts`, stripIndents`${template}`); + tree.write(`${projectConfig.root}/src/testing/isConformant.ts`, stripIndents`${template}`); } function addUnstableSetup(tree: Tree, projectConfig: ReadProjectConfiguration) { diff --git a/tools/generators/migrate-converged-pkg/index.ts b/tools/generators/migrate-converged-pkg/index.ts index 13be8ec87be6c..9cb649c547745 100644 --- a/tools/generators/migrate-converged-pkg/index.ts +++ b/tools/generators/migrate-converged-pkg/index.ts @@ -13,7 +13,6 @@ import { updateProjectConfiguration, serializeJson, offsetFromRoot, - names, } from '@nrwl/devkit'; import * as path from 'path'; import * as os from 'os'; @@ -117,6 +116,8 @@ function runMigrationOnProject(tree: Tree, schema: AssertedSchema, _userLog: Use ); return; } + // Perform common folder migration first then update TsConfig files accordingly afterwards. + migrateCommonFolderToTesting(tree, options); // 1. update TsConfigs const { configs } = updatedLocalTsConfig(tree, options); @@ -134,12 +135,14 @@ function runMigrationOnProject(tree: Tree, schema: AssertedSchema, _userLog: Use // setup storybook setupStorybook(tree, options); - setupE2E(tree, options); + migrateE2ESetupToCypress(tree, options); + setupCypress(tree, options); setupNpmIgnoreConfig(tree, options); setupBabel(tree, options); updateNxWorkspace(tree, options); + moveDocsToSubfolder(tree, options); } // ==== helpers ==== @@ -224,7 +227,7 @@ const templates = { tsConfig.compilerOptions.lib?.push('dom'); } if (options.hasConformance) { - tsConfig.exclude.unshift('./src/common/**'); + tsConfig.exclude.unshift('./src/testing/**'); } if (options.js) { tsConfig.include = globsToJs(tsConfig.include); @@ -241,7 +244,15 @@ const templates = { outDir: 'dist', types: ['jest', 'node'], } as TsConfig['compilerOptions'], - include: ['**/*.spec.ts', '**/*.spec.tsx', '**/*.test.ts', '**/*.test.tsx', '**/*.d.ts'], + include: [ + '**/*.spec.ts', + '**/*.spec.tsx', + '**/*.test.ts', + '**/*.test.tsx', + '**/*.d.ts', + './src/testing/**/*.ts', + './src/testing/**/*.tsx', + ], }; if (options.js) { @@ -300,7 +311,7 @@ const templates = { module.exports = /** @type {Omit} */ ({ ...rootMain, - stories: [...rootMain.stories, '../src/**/*.stories.mdx', '../src/**/index.stories.@(ts|tsx)'], + stories: [...rootMain.stories, '../stories/**/*.stories.mdx', '../stories/**/index.stories.@(ts|tsx)'], addons: [...rootMain.addons], webpackFinal: (config, options) => { const localConfig = { ...rootMain.webpackFinal(config, options) }; @@ -327,18 +338,18 @@ const templates = { allowJs: true, checkJs: true, }, - include: ['../src/**/*.stories.ts', '../src/**/*.stories.tsx', '*.js'], + include: ['../stories/**/*.stories.ts', '../stories/**/*.stories.tsx', '*.js'], }, }, - e2e: { + cypress: { tsconfig: { - extends: '../tsconfig.json', + extends: './tsconfig.json', compilerOptions: { isolatedModules: false, types: ['node', 'cypress', 'cypress-storybook/cypress', 'cypress-real-events'], lib: ['ES2019', 'dom'], }, - include: ['**/*.ts', '**/*.tsx'], + include: ['**/*.cy.ts', '**/*.cy.tsx'], }, }, npmIgnoreConfig: @@ -348,10 +359,11 @@ const templates = { bundle-size/ config/ coverage/ - e2e/ + docs/ etc/ node_modules/ src/ + stories/ dist/types/ temp/ __fixtures__ @@ -361,7 +373,7 @@ const templates = { *.api.json *.log *.spec.* - *.stories.* + *.cy.* *.test.* *.yml @@ -691,7 +703,7 @@ function setupStorybook(tree: Tree, options: NormalizedSchema) { return json; }); - moveStories(tree, options); + moveStoriesToPackageRoot(tree, options); } if (sbAction === 'remove') { @@ -730,14 +742,14 @@ function setupStorybook(tree: Tree, options: NormalizedSchema) { function removeTsIgnorePragmas() { const stories: string[] = []; - visitNotIgnoredFiles(tree, options.paths.sourceRoot, treePath => { + visitNotIgnoredFiles(tree, options.projectConfig.root, treePath => { if (treePath.includes('.stories.')) { stories.push(treePath); } }); stories.forEach(storyPath => { - const content = tree.read(storyPath)?.toString('utf-8'); + const content = tree.read(storyPath, 'utf8'); if (!content) { throw new Error('story file has no code'); @@ -761,42 +773,115 @@ function setupStorybook(tree: Tree, options: NormalizedSchema) { return tree; } +/** + * TODO: Remove function after migration is complete. + */ +function migrateE2ESetupToCypress(tree: Tree, options: NormalizedSchema) { + const e2ePath = joinPathFragments(options.projectConfig.root, 'e2e'); + const e2eFolderExists = tree.exists(e2ePath); -function moveStories(tree: Tree, options: NormalizedSchema) { - const componentName = names(options.normalizedPkgName).className.replace('React', ''); - const sourceRoot = options.projectConfig.sourceRoot ?? ''; - const oldStoriesPath = joinPathFragments(sourceRoot, 'stories'); - const newStoriesPath = joinPathFragments(oldStoriesPath, componentName); - const storiesExistInNewPath = tree.exists(newStoriesPath); + if (!e2eFolderExists) { + return; + } - if (storiesExistInNewPath) { + visitNotIgnoredFiles(tree, e2ePath, treePath => { + if (treePath.includes('selectors.ts')) { + const newFilePath = joinPathFragments(options.paths.sourceRoot, 'testing', path.basename(treePath)); + + // Move testing helper file to src/testing. + tree.rename(treePath, newFilePath); + return; + } + + if (treePath.includes('.e2e.')) { + const content = tree.read(treePath, 'utf8'); + const fileName = path.basename(treePath).replace('e2e', 'cy'); + const componentName = fileName.split('.')[0]; + const newCypressTestPath = joinPathFragments(options.paths.sourceRoot, 'components', componentName, fileName); + // Move cypress component test file to appropriate src/components/{ComponentName} location. + tree.rename(treePath, newCypressTestPath); + + //Update file imports of cypress component test file. + if (content && content.includes('./selectors')) { + const newContent = content.replace('./selectors', '../../testing/selectors'); + tree.write(newCypressTestPath, newContent); + } + return; + } + + if (treePath.includes('tsconfig.json')) { + const newCypressTSConfigPath = joinPathFragments(options.projectConfig.root, 'tsconfig.cy.json'); + // Move e2e folder tsconfig.json to root + tree.rename(treePath, newCypressTSConfigPath); + return; + } + }); +} + +/** + * TODO: Remove function after migration is complete. + */ +function migrateCommonFolderToTesting(tree: Tree, options: NormalizedSchema) { + const sourceRoot = options.paths.sourceRoot; + const commonFolderPath = joinPathFragments(sourceRoot, 'common'); + const commonFolderExists = tree.exists(commonFolderPath); + + if (!commonFolderExists) { return; } - visitNotIgnoredFiles(tree, oldStoriesPath, treePath => { - if (treePath.includes('.stories.') || treePath.includes('.md')) { - const storyFileName = path.basename(treePath); - const shouldBeMigratedToIndexFile = storyFileName.toLowerCase() === `${componentName.toLowerCase()}.stories.tsx`; + // Move any files in src/common/ to src/testing/ + visitNotIgnoredFiles(tree, commonFolderPath, treePath => { + const fileName = path.basename(treePath); + const newPath = joinPathFragments(sourceRoot, 'testing', fileName); + tree.rename(treePath, newPath); + + // Update files that import moved file to reflect file location change from common/ to testing/ + visitNotIgnoredFiles(tree, joinPathFragments(sourceRoot, 'components'), nestedTreePath => { + const fileContent = tree.read(nestedTreePath, 'utf8'); + if (fileContent && fileContent.includes('common/')) { + const newContent = fileContent.replace('common/', 'testing/'); + tree.write(nestedTreePath, newContent); + } + }); + }); +} - const newStoryPath = joinPathFragments( - newStoriesPath, - shouldBeMigratedToIndexFile ? 'index.stories.tsx' : storyFileName, - ); +function moveDocsToSubfolder(tree: Tree, options: NormalizedSchema) { + const root = options.projectConfig.root; - tree.rename(treePath, newStoryPath); - updateStoryFileImports(tree, options, newStoryPath); + visitNotIgnoredFiles(tree, root, treePath => { + const currPath = treePath.toLowerCase(); + if (currPath.includes('.md') && (currPath.includes('spec') || currPath.includes('migration'))) { + const fileName = path.basename(treePath); + const newPath = joinPathFragments(root, 'docs', fileName); + + !tree.exists(newPath) && tree.rename(treePath, newPath); } }); } -function updateStoryFileImports(tree: Tree, options: NormalizedSchema, storyPath: string) { - if (!tree.exists(storyPath)) { +/** + * TODO: Remove function after migration is complete. + */ +function moveStoriesToPackageRoot(tree: Tree, options: NormalizedSchema) { + const oldStoriesPath = joinPathFragments(options.paths.sourceRoot, 'stories'); + const storiesExistInNewPath = tree.exists(options.paths.stories); + + if (storiesExistInNewPath) { return; } - const storyFile = tree.read(storyPath, 'utf8') as string; - const updatedStoryFile = storyFile.replace('../index', options.name); - tree.write(storyPath, updatedStoryFile); + visitNotIgnoredFiles(tree, oldStoriesPath, treePath => { + if (treePath.includes('.stories.') || treePath.includes('.md')) { + const newStoryPath = treePath + .split('/') + .filter(str => str !== 'src') + .join('/'); + + tree.rename(treePath, newStoryPath); + } + }); } function shouldSetupStorybook(tree: Tree, options: NormalizedSchema) { @@ -821,23 +906,34 @@ function shouldSetupStorybook(tree: Tree, options: NormalizedSchema) { } } -function setupE2E(tree: Tree, options: NormalizedSchema) { - if (!shouldSetupE2E(tree, options)) { +function setupCypress(tree: Tree, options: NormalizedSchema) { + const template = { + exclude: ['**/*.cy.ts', '**/*.cy.tsx'], + }; + + if (!shouldSetupCypress(tree, options)) { return tree; } - tree.rename(joinPathFragments(options.paths.e2e.rootFolder, 'tsconfig.json'), options.paths.e2e.tsconfig); - - writeJson(tree, options.paths.e2e.tsconfig, templates.e2e.tsconfig); + writeJson(tree, options.paths.tsconfig.cypress, templates.cypress.tsconfig); updateJson(tree, options.paths.tsconfig.main, (json: TsConfig) => { json.references?.push({ - path: `./${path.basename(options.paths.e2e.rootFolder)}/${path.basename(options.paths.e2e.tsconfig)}`, + path: `./${path.basename(options.paths.tsconfig.cypress)}`, }); return json; }); + // update lib ts with new exclude globs + updateJson(tree, options.paths.tsconfig.lib, (json: TsConfig) => { + json.exclude = json.exclude || []; + json.exclude.push(...template.exclude); + json.exclude = uniqueArray(json.exclude); + + return json; + }); + updateJson(tree, options.paths.packageJson, (json: PackageJson) => { json.scripts = json.scripts ?? {}; json.scripts.e2e = 'cypress run --component'; @@ -849,11 +945,8 @@ function setupE2E(tree: Tree, options: NormalizedSchema) { return tree; } -function shouldSetupE2E(tree: Tree, options: NormalizedSchema) { - return ( - tree.exists(joinPathFragments(options.paths.e2e.rootFolder, 'tsconfig.json')) || - tree.exists(options.paths.e2e.tsconfig) - ); +function shouldSetupCypress(tree: Tree, options: NormalizedSchema) { + return tree.exists(options.paths.tsconfig.cypress); } function updateLocalJestConfig(tree: Tree, options: NormalizedSchema) { @@ -915,7 +1008,7 @@ function updateTsGlobalTypes(tree: Tree, options: NormalizedSchema) { // update test TS config updateJson(tree, options.paths.tsconfig.test, (json: TsConfig) => { if (tree.exists(options.paths.jestSetupFile)) { - const jestSetupFile = tree.read(options.paths.jestSetupFile)?.toString('utf-8')!; + const jestSetupFile = tree.read(options.paths.jestSetupFile, 'utf8')!; if (jestSetupFile.includes(`require('@testing-library/jest-dom')`)) { json.compilerOptions.types = json.compilerOptions.types ?? []; diff --git a/tools/utils.ts b/tools/utils.ts index ff2ededc93b3d..63842dfa0b031 100644 --- a/tools/utils.ts +++ b/tools/utils.ts @@ -92,13 +92,14 @@ export function getProjectConfig(tree: Tree, options: { packageName: string }) { main: joinPathFragments(projectConfig.root, 'tsconfig.json'), lib: joinPathFragments(projectConfig.root, 'tsconfig.lib.json'), test: joinPathFragments(projectConfig.root, 'tsconfig.spec.json'), + cypress: joinPathFragments(projectConfig.root, 'tsconfig.cy.json'), }, sourceRoot: joinPathFragments(projectConfig.root, 'src'), unstable: { sourceRoot: joinPathFragments(projectConfig.root, 'src', 'unstable'), rootPackageJson: joinPathFragments(projectConfig.root, 'src', 'unstable', 'package.json__tmpl__'), }, - conformanceSetup: joinPathFragments(projectConfig.root, 'src', 'common', 'isConformant.ts'), + conformanceSetup: joinPathFragments(projectConfig.root, 'src', 'testing', 'isConformant.ts'), babelConfig: joinPathFragments(projectConfig.root, '.babelrc.json'), jestConfig: joinPathFragments(projectConfig.root, 'jest.config.js'), jestSetupFile: joinPathFragments(projectConfig.root, 'config', 'tests.js'), @@ -107,17 +108,13 @@ export function getProjectConfig(tree: Tree, options: { packageName: string }) { rootJestPreset: '/jest.preset.js', rootJestConfig: '/jest.config.js', npmConfig: joinPathFragments(projectConfig.root, '.npmignore'), + stories: joinPathFragments(projectConfig.root, 'stories'), storybook: { rootFolder: joinPathFragments(projectConfig.root, '.storybook'), tsconfig: joinPathFragments(projectConfig.root, '.storybook/tsconfig.json'), main: joinPathFragments(projectConfig.root, '.storybook/main.js'), preview: joinPathFragments(projectConfig.root, '.storybook/preview.js'), }, - e2e: { - rootFolder: joinPathFragments(projectConfig.root, 'e2e'), - support: joinPathFragments(projectConfig.root, 'e2e', 'support.js'), - tsconfig: joinPathFragments(projectConfig.root, 'e2e', 'tsconfig.json'), - }, }; return {