From 47956db3280bf8c36b55755357715ed714fb6f10 Mon Sep 17 00:00:00 2001 From: Nicholas Cunningham Date: Wed, 4 Dec 2024 11:11:36 -0700 Subject: [PATCH] fix(core): React workspaces that use pnpm & the new TS solution should be declared correctly so that it can be referenced. --- e2e/react/src/react.test.ts | 635 ++++++++++-------- e2e/utils/create-project-utils.ts | 12 + .../src/generators/application/application.ts | 8 + .../expo/src/generators/library/library.ts | 5 + .../src/generators/application/application.ts | 8 + .../library/lib/normalize-options.ts | 3 + .../next/src/generators/library/library.ts | 5 + .../src/generators/application/application.ts | 8 + .../src/generators/library/library.ts | 5 + packages/react/package.json | 3 +- .../src/generators/application/application.ts | 15 +- .../library/lib/normalize-options.ts | 1 - .../react/src/generators/library/library.ts | 4 + .../src/utils/add-app-to-pnpm-workspace.ts | 33 + .../application/application.impl.ts | 8 + .../new/generate-workspace-files.spec.ts | 4 +- .../new/generate-workspace-files.ts | 2 +- 17 files changed, 481 insertions(+), 278 deletions(-) create mode 100644 packages/react/src/utils/add-app-to-pnpm-workspace.ts diff --git a/e2e/react/src/react.test.ts b/e2e/react/src/react.test.ts index 8e5db024e982ea..fb10609860f25e 100644 --- a/e2e/react/src/react.test.ts +++ b/e2e/react/src/react.test.ts @@ -8,6 +8,7 @@ import { listFiles, newProject, readFile, + readJson, runCLI, runCLIAsync, runE2ETests, @@ -17,147 +18,238 @@ import { } from '@nx/e2e/utils'; import { readFileSync } from 'fs-extra'; import { join } from 'path'; +const { load, dump } = require('@zkochan/js-yaml'); describe('React Applications', () => { let proj: string; describe('Crystal Supported Tests', () => { - beforeAll(() => { - proj = newProject({ packages: ['@nx/react'] }); - ensureCypressInstallation(); + describe('useTsSolution (PM=npm)', () => { + beforeAll(() => { + proj = newProject({ + packages: ['@nx/react'], + packageManager: 'npm', + workspaces: true, + }); + ensureCypressInstallation(); + }); + + afterAll(() => cleanupProject()); + it('None buildable libs using (workspaces = true) should be excluded from js/ts plugin', async () => { + const appName = uniq('app'); + const libName = uniq('lib'); + + runCLI( + `generate @nx/react:app apps/${appName} --name=${appName} --useTsSolution=true --bundler=vite --no-interactive --skipFormat --linter=eslint --unitTestRunner=vitest` + ); + runCLI( + `generate @nx/react:lib ${libName} --bundler=none --no-interactive --unit-test-runner=vitest --skipFormat --linter=eslint` + ); + + const nxJson = JSON.parse(readFile('nx.json')); + + const jsTypescriptPlugin = nxJson.plugins.find( + (plugin) => plugin.plugin === '@nx/js/typescript' + ); + expect(jsTypescriptPlugin).toBeDefined(); + + expect( + jsTypescriptPlugin.exclude.includes(`${libName}/*`) + ).toBeTruthy(); + }, 250_000); + + it('Apps/libs using (workspaces = true) should be added to workspaces', async () => { + const appName = uniq('app'); + const libName = uniq('lib'); + + runCLI( + `generate @nx/react:app apps/${appName} --name=${appName} --useTsSolution true --bundler=vite --no-interactive --skipFormat --linter=eslint --unitTestRunner=vitest` + ); + runCLI( + `generate @nx/react:lib ${libName} --bundler=vite --no-interactive --unit-test-runner=vitest --skipFormat --linter=eslint` + ); + + const packageJson = readJson('package.json'); + expect(packageJson.workspaces).toContain(`apps/${appName}`); + expect(packageJson.workspaces).toContain(`${libName}/*`); + }); }); - afterAll(() => cleanupProject()); + describe('useTsSolution (PM=pnpm)', () => { + beforeAll(() => { + proj = newProject({ + packages: ['@nx/react'], + packageManager: 'pnpm', + workspaces: true, + }); + ensureCypressInstallation(); + }); - it('should be able to use Vite to build and test apps', async () => { - const appName = uniq('app'); - const libName = uniq('lib'); + afterAll(() => cleanupProject()); - runCLI( - `generate @nx/react:app apps/${appName} --name=${appName} --bundler=vite --no-interactive --skipFormat --linter=eslint --unitTestRunner=vitest` - ); - runCLI( - `generate @nx/react:lib libs/${libName} --bundler=none --no-interactive --unit-test-runner=vitest --skipFormat --linter=eslint` - ); + it('None buildable libs using (workspaces = true) should be excluded from js/ts plugin', async () => { + const appName = uniq('app'); + const libName = uniq('lib'); - // Library generated with Vite - checkFilesExist(`libs/${libName}/vite.config.ts`); + runCLI( + `generate @nx/react:app apps/${appName} --name=${appName} --useTsSolution=true --bundler=vite --no-interactive --skipFormat --linter=eslint --unitTestRunner=vitest` + ); + runCLI( + `generate @nx/react:lib ${libName} --bundler=none --no-interactive --unit-test-runner=vitest --skipFormat --linter=eslint` + ); - const mainPath = `apps/${appName}/src/main.tsx`; - updateFile( - mainPath, - ` - import '@${proj}/${libName}'; - ${readFile(mainPath)} - ` - ); + const nxJson = JSON.parse(readFile('nx.json')); - runCLI(`build ${appName}`); + const jsTypescriptPlugin = nxJson.plugins.find( + (plugin) => plugin.plugin === '@nx/js/typescript' + ); + expect(jsTypescriptPlugin).toBeDefined(); - checkFilesExist(`dist/apps/${appName}/index.html`); + expect( + jsTypescriptPlugin.exclude.includes(`${libName}/*`) + ).toBeTruthy(); + }, 250_000); - if (runE2ETests()) { - const e2eResults = runCLI(`e2e ${appName}-e2e`); - expect(e2eResults).toContain('Successfully ran target e2e for project'); - expect(await killPorts()).toBeTruthy(); - } - }, 250_000); + it('Apps/libs using (useTsSolution = true) should be added to workspaces', async () => { + const appName = uniq('app'); + const libName = uniq('lib'); - it('None buildable libs using (useTsSolution = true) should be excluded from js/ts plugin', async () => { - const appName = uniq('app'); - const libName = uniq('lib'); + runCLI( + `generate @nx/react:app apps/${appName} --name=${appName} --useTsSolution=true --bundler=vite --no-interactive --skipFormat --linter=eslint --unitTestRunner=vitest` + ); + runCLI( + `generate @nx/react:lib ${libName} --bundler=vite --no-interactive --unit-test-runner=vitest --skipFormat --linter=eslint` + ); - runCLI( - `generate @nx/react:app apps/${appName} --name=${appName} --useTsSolution true --bundler=vite --no-interactive --skipFormat --linter=eslint --unitTestRunner=vitest` - ); - runCLI( - `generate @nx/react:lib ${libName} --bundler=none --no-interactive --unit-test-runner=vitest --skipFormat --linter=eslint` - ); + const pnpmWorkspaceContent = readFile('pnpm-workspace.yaml'); + const pnmpWorkspaceYaml = load(pnpmWorkspaceContent); + expect(pnmpWorkspaceYaml.packages).toContain(`apps/${appName}`); + expect(pnmpWorkspaceYaml.packages).toContain(`${libName}/*`); + }); + }); - const nxJson = JSON.parse(readFile('nx.json')); + describe('other Crystal tests', () => { + beforeAll(() => { + proj = newProject({ packages: ['@nx/react'] }); + ensureCypressInstallation(); + }); - const jsTypescriptPlugin = nxJson.plugins.find( - (plugin) => plugin.plugin === '@nx/js/typescript' - ); - expect(jsTypescriptPlugin).toBeDefined(); + afterAll(() => cleanupProject()); + it('should be able to use Vite to build and test apps', async () => { + const appName = uniq('app'); + const libName = uniq('lib'); - expect(jsTypescriptPlugin.exclude.includes(`${libName}/*`)).toBeTruthy(); - }, 250_000); + runCLI( + `generate @nx/react:app apps/${appName} --name=${appName} --bundler=vite --no-interactive --skipFormat --linter=eslint --unitTestRunner=vitest` + ); + runCLI( + `generate @nx/react:lib libs/${libName} --bundler=none --no-interactive --unit-test-runner=vitest --skipFormat --linter=eslint` + ); - it('should be able to use Rspack to build and test apps', async () => { - const appName = uniq('app'); - const libName = uniq('lib'); + // Library generated with Vite + checkFilesExist(`libs/${libName}/vite.config.ts`); - runCLI( - `generate @nx/react:app ${appName} --bundler=rspack --unit-test-runner=vitest --no-interactive --skipFormat --linter=eslint` - ); - runCLI( - `generate @nx/react:lib ${libName} --bundler=none --no-interactive --unit-test-runner=vitest --skipFormat --linter=eslint` - ); + const mainPath = `apps/${appName}/src/main.tsx`; + updateFile( + mainPath, + ` + import '@${proj}/${libName}'; + ${readFile(mainPath)} + ` + ); - // Library generated with Vite - checkFilesExist(`${libName}/vite.config.ts`); + runCLI(`build ${appName}`); - const mainPath = `${appName}/src/main.tsx`; - updateFile( - mainPath, - ` + checkFilesExist(`dist/apps/${appName}/index.html`); + + if (runE2ETests()) { + const e2eResults = runCLI(`e2e ${appName}-e2e`); + expect(e2eResults).toContain( + 'Successfully ran target e2e for project' + ); + expect(await killPorts()).toBeTruthy(); + } + }, 250_000); + + it('should be able to use Rspack to build and test apps', async () => { + const appName = uniq('app'); + const libName = uniq('lib'); + + runCLI( + `generate @nx/react:app ${appName} --bundler=rspack --unit-test-runner=vitest --no-interactive --skipFormat --linter=eslint` + ); + runCLI( + `generate @nx/react:lib ${libName} --bundler=none --no-interactive --unit-test-runner=vitest --skipFormat --linter=eslint` + ); + + // Library generated with Vite + checkFilesExist(`${libName}/vite.config.ts`); + + const mainPath = `${appName}/src/main.tsx`; + updateFile( + mainPath, + ` import '@${proj}/${libName}'; ${readFile(mainPath)} ` - ); + ); - runCLI(`build ${appName}`, { verbose: true }); + runCLI(`build ${appName}`, { verbose: true }); - checkFilesExist(`dist/${appName}/index.html`); + checkFilesExist(`dist/${appName}/index.html`); - if (runE2ETests()) { - // TODO(Colum): investigate why webkit is failing - const e2eResults = runCLI(`e2e ${appName}-e2e -- --project=chromium`, { - verbose: true, - }); - expect(e2eResults).toContain('Successfully ran target e2e for project'); - expect(await killPorts()).toBeTruthy(); - } - }, 250_000); + if (runE2ETests()) { + // TODO(Colum): investigate why webkit is failing + const e2eResults = runCLI( + `e2e ${appName}-e2e -- --project=chromium`, + { + verbose: true, + } + ); + expect(e2eResults).toContain( + 'Successfully ran target e2e for project' + ); + expect(await killPorts()).toBeTruthy(); + } + }, 250_000); - it('should be able to generate a react app + lib (with CSR and SSR)', async () => { - const appName = uniq('app'); - const libName = uniq('lib'); - const libWithNoComponents = uniq('lib'); - const logoSvg = readFileSync(join(__dirname, 'logo.svg')).toString(); - const blueSvg = ``; - const redSvg = ``; + it('should be able to generate a react app + lib (with CSR and SSR)', async () => { + const appName = uniq('app'); + const libName = uniq('lib'); + const libWithNoComponents = uniq('lib'); + const logoSvg = readFileSync(join(__dirname, 'logo.svg')).toString(); + const blueSvg = ``; + const redSvg = ``; - runCLI( - `generate @nx/react:app apps/${appName} --style=css --bundler=webpack --unit-test-runner=jest --no-interactive --skipFormat --linter=eslint` - ); - runCLI( - `generate @nx/react:lib libs/${libName} --style=css --no-interactive --unit-test-runner=jest --skipFormat --linter=eslint` - ); - runCLI( - `generate @nx/react:lib libs/${libWithNoComponents} --no-interactive --no-component --unit-test-runner=jest --skipFormat --linter=eslint` - ); + runCLI( + `generate @nx/react:app apps/${appName} --style=css --bundler=webpack --unit-test-runner=jest --no-interactive --skipFormat --linter=eslint` + ); + runCLI( + `generate @nx/react:lib libs/${libName} --style=css --no-interactive --unit-test-runner=jest --skipFormat --linter=eslint` + ); + runCLI( + `generate @nx/react:lib libs/${libWithNoComponents} --no-interactive --no-component --unit-test-runner=jest --skipFormat --linter=eslint` + ); - // Libs should not include package.json by default - checkFilesDoNotExist(`libs/${libName}/package.json`); + // Libs should not include package.json by default + checkFilesDoNotExist(`libs/${libName}/package.json`); - const mainPath = `apps/${appName}/src/main.tsx`; - updateFile( - mainPath, - ` + const mainPath = `apps/${appName}/src/main.tsx`; + updateFile( + mainPath, + ` import '@${proj}/${libWithNoComponents}'; import '@${proj}/${libName}'; ${readFile(mainPath)} ` - ); + ); - updateFile(`apps/${appName}/src/app/blue/img.svg`, blueSvg); // ensure that same filenames do not conflict - updateFile(`apps/${appName}/src/app/red/img.svg`, redSvg); // ensure that same filenames do not conflict - updateFile(`apps/${appName}/src/app/logo.svg`, logoSvg); - updateFile( - `apps/${appName}/src/app/app.tsx`, - ` + updateFile(`apps/${appName}/src/app/blue/img.svg`, blueSvg); // ensure that same filenames do not conflict + updateFile(`apps/${appName}/src/app/red/img.svg`, redSvg); // ensure that same filenames do not conflict + updateFile(`apps/${appName}/src/app/logo.svg`, logoSvg); + updateFile( + `apps/${appName}/src/app/app.tsx`, + ` import { ReactComponent as Logo } from './logo.svg'; import blue from './blue/img.svg'; import red from './red/img.svg'; @@ -178,149 +270,149 @@ describe('React Applications', () => { export default App; ` - ); + ); - // Make sure global stylesheets are properly processed. - const stylesPath = `apps/${appName}/src/styles.css`; - updateFile( - stylesPath, - ` + // Make sure global stylesheets are properly processed. + const stylesPath = `apps/${appName}/src/styles.css`; + updateFile( + stylesPath, + ` .foobar { background-image: url('/bg.png'); } ` - ); - - const libTestResults = await runCLIAsync(`test ${libName}`); - expect(libTestResults.combinedOutput).toContain( - 'Test Suites: 1 passed, 1 total' - ); - - await testGeneratedApp(appName, { - checkSourceMap: true, - checkStyles: true, - checkLinter: true, - // TODO(jack): check why Playwright tests are timing out in CI - checkE2E: false, - }); - - // Set up SSR and check app - runCLI(`generate @nx/react:setup-ssr ${appName} --skipFormat`); - checkFilesExist(`apps/${appName}/src/main.server.tsx`); - checkFilesExist(`apps/${appName}/server.ts`); + ); - await testGeneratedApp(appName, { - checkSourceMap: false, - checkStyles: false, - checkLinter: false, - // TODO(jack): check why Playwright tests are timing out in CI - checkE2E: false, - }); - }, 500_000); + const libTestResults = await runCLIAsync(`test ${libName}`); + expect(libTestResults.combinedOutput).toContain( + 'Test Suites: 1 passed, 1 total' + ); - it('should generate app with routing', async () => { - const appName = uniq('app'); + await testGeneratedApp(appName, { + checkSourceMap: true, + checkStyles: true, + checkLinter: true, + // TODO(jack): check why Playwright tests are timing out in CI + checkE2E: false, + }); - runCLI( - `generate @nx/react:app apps/${appName} --routing --bundler=webpack --no-interactive --skipFormat --linter=eslint --unitTestRunner=jest` - ); + // Set up SSR and check app + runCLI(`generate @nx/react:setup-ssr ${appName} --skipFormat`); + checkFilesExist(`apps/${appName}/src/main.server.tsx`); + checkFilesExist(`apps/${appName}/server.ts`); + + await testGeneratedApp(appName, { + checkSourceMap: false, + checkStyles: false, + checkLinter: false, + // TODO(jack): check why Playwright tests are timing out in CI + checkE2E: false, + }); + }, 500_000); - runCLI(`build ${appName}`); + it('should generate app with routing', async () => { + const appName = uniq('app'); - checkFilesExistWithHash(`dist/apps/${appName}`, [ - `index.html`, - `runtime.*.js`, - `main.*.js`, - ]); - }, 250_000); + runCLI( + `generate @nx/react:app apps/${appName} --routing --bundler=webpack --no-interactive --skipFormat --linter=eslint --unitTestRunner=jest` + ); - it('should be able to add a redux slice', async () => { - const appName = uniq('app'); - const libName = uniq('lib'); + runCLI(`build ${appName}`); - runCLI( - `g @nx/react:app apps/${appName} --bundler=webpack --no-interactive --skipFormat --unitTestRunner=jest --linter=eslint` - ); - runCLI( - `g @nx/react:redux apps/${appName}/src/app/lemon/lemon --skipFormat` - ); - runCLI( - `g @nx/react:lib libs/${libName} --unit-test-runner=jest --no-interactive --skipFormat` - ); - runCLI( - `g @nx/react:redux libs/${libName}/src/lib/orange/orange --skipFormat` - ); + checkFilesExistWithHash(`dist/apps/${appName}`, [ + `index.html`, + `runtime.*.js`, + `main.*.js`, + ]); + }, 250_000); - let lintResults = runCLI(`lint ${appName}`); - expect(lintResults).toContain( - `Successfully ran target lint for project ${appName}` - ); - const appTestResults = await runCLIAsync(`test ${appName}`); - expect(appTestResults.combinedOutput).toContain( - `Successfully ran target test for project ${appName}` - ); + it('should be able to add a redux slice', async () => { + const appName = uniq('app'); + const libName = uniq('lib'); - lintResults = runCLI(`lint ${libName}`); - expect(lintResults).toContain( - `Successfully ran target lint for project ${libName}` - ); - const libTestResults = await runCLIAsync(`test ${libName}`); - expect(libTestResults.combinedOutput).toContain( - `Successfully ran target test for project ${libName}` - ); - }, 250_000); + runCLI( + `g @nx/react:app apps/${appName} --bundler=webpack --no-interactive --skipFormat --unitTestRunner=jest --linter=eslint` + ); + runCLI( + `g @nx/react:redux apps/${appName}/src/app/lemon/lemon --skipFormat` + ); + runCLI( + `g @nx/react:lib libs/${libName} --unit-test-runner=jest --no-interactive --skipFormat` + ); + runCLI( + `g @nx/react:redux libs/${libName}/src/lib/orange/orange --skipFormat` + ); - it('should support generating projects with the new name and root format', () => { - const appName = uniq('app1'); - const libName = uniq('@my-org/lib1'); + let lintResults = runCLI(`lint ${appName}`); + expect(lintResults).toContain( + `Successfully ran target lint for project ${appName}` + ); + const appTestResults = await runCLIAsync(`test ${appName}`); + expect(appTestResults.combinedOutput).toContain( + `Successfully ran target test for project ${appName}` + ); - runCLI( - `generate @nx/react:app ${appName} --bundler=webpack --no-interactive --skipFormat --linter=eslint --unitTestRunner=jest` - ); + lintResults = runCLI(`lint ${libName}`); + expect(lintResults).toContain( + `Successfully ran target lint for project ${libName}` + ); + const libTestResults = await runCLIAsync(`test ${libName}`); + expect(libTestResults.combinedOutput).toContain( + `Successfully ran target test for project ${libName}` + ); + }, 250_000); - // check files are generated without the layout directory ("apps/") and - // using the project name as the directory when no directory is provided - checkFilesExist(`${appName}/src/main.tsx`); - // check build works - expect(runCLI(`build ${appName}`)).toContain( - `Successfully ran target build for project ${appName}` - ); - // check tests pass - const appTestResult = runCLI(`test ${appName}`); - expect(appTestResult).toContain( - `Successfully ran target test for project ${appName}` - ); + it('should support generating projects with the new name and root format', () => { + const appName = uniq('app1'); + const libName = uniq('@my-org/lib1'); - runCLI( - `generate @nx/react:lib ${libName} --unit-test-runner=jest --buildable --no-interactive --skipFormat --linter=eslint` - ); + runCLI( + `generate @nx/react:app ${appName} --bundler=webpack --no-interactive --skipFormat --linter=eslint --unitTestRunner=jest` + ); - // check files are generated without the layout directory ("libs/") and - // using the project name as the directory when no directory is provided - checkFilesExist(`${libName}/src/index.ts`); - // check build works - expect(runCLI(`build ${libName}`)).toContain( - `Successfully ran target build for project ${libName}` - ); - // check tests pass - const libTestResult = runCLI(`test ${libName}`); - expect(libTestResult).toContain( - `Successfully ran target test for project ${libName}` - ); - }, 500_000); + // check files are generated without the layout directory ("apps/") and + // using the project name as the directory when no directory is provided + checkFilesExist(`${appName}/src/main.tsx`); + // check build works + expect(runCLI(`build ${appName}`)).toContain( + `Successfully ran target build for project ${appName}` + ); + // check tests pass + const appTestResult = runCLI(`test ${appName}`); + expect(appTestResult).toContain( + `Successfully ran target test for project ${appName}` + ); - describe('React Applications: --style option', () => { - // TODO(crystal, @jaysoo): Investigate why this is failng - xit('should support styled-jsx', async () => { - const appName = uniq('app'); runCLI( - `generate @nx/react:app ${appName} --style=styled-jsx --bundler=vite --no-interactive --skipFormat --linter=eslint --unitTestRunner=vitest` + `generate @nx/react:lib ${libName} --unit-test-runner=jest --buildable --no-interactive --skipFormat --linter=eslint` ); - // update app to use styled-jsx - updateFile( - `apps/${appName}/src/app/app.tsx`, - ` + // check files are generated without the layout directory ("libs/") and + // using the project name as the directory when no directory is provided + checkFilesExist(`${libName}/src/index.ts`); + // check build works + expect(runCLI(`build ${libName}`)).toContain( + `Successfully ran target build for project ${libName}` + ); + // check tests pass + const libTestResult = runCLI(`test ${libName}`); + expect(libTestResult).toContain( + `Successfully ran target test for project ${libName}` + ); + }, 500_000); + + describe('React Applications: --style option', () => { + // TODO(crystal, @jaysoo): Investigate why this is failng + xit('should support styled-jsx', async () => { + const appName = uniq('app'); + runCLI( + `generate @nx/react:app ${appName} --style=styled-jsx --bundler=vite --no-interactive --skipFormat --linter=eslint --unitTestRunner=vitest` + ); + + // update app to use styled-jsx + updateFile( + `apps/${appName}/src/app/app.tsx`, + ` import NxWelcome from './nx-welcome'; export function App() { @@ -336,13 +428,13 @@ describe('React Applications', () => { export default App; ` - ); + ); - // update e2e test to check for styled-jsx change + // update e2e test to check for styled-jsx change - updateFile( - `apps/${appName}-e2e/src/e2e/app.cy.ts`, - ` + updateFile( + `apps/${appName}-e2e/src/e2e/app.cy.ts`, + ` describe('react-test', () => { beforeEach(() => cy.visit('/')); @@ -353,23 +445,23 @@ describe('React Applications', () => { }); ` - ); - if (runE2ETests()) { - const e2eResults = runCLI(`e2e ${appName}-e2e --verbose`); - expect(e2eResults).toContain('All specs passed!'); - } - }, 250_000); - - it('should support tailwind', async () => { - const appName = uniq('app'); - runCLI( - `generate @nx/react:app apps/${appName} --style=tailwind --bundler=vite --no-interactive --skipFormat --linter=eslint --unitTestRunner=vitest` - ); + ); + if (runE2ETests()) { + const e2eResults = runCLI(`e2e ${appName}-e2e --verbose`); + expect(e2eResults).toContain('All specs passed!'); + } + }, 250_000); + + it('should support tailwind', async () => { + const appName = uniq('app'); + runCLI( + `generate @nx/react:app apps/${appName} --style=tailwind --bundler=vite --no-interactive --skipFormat --linter=eslint --unitTestRunner=vitest` + ); - // update app to use styled-jsx - updateFile( - `apps/${appName}/src/app/app.tsx`, - ` + // update app to use styled-jsx + updateFile( + `apps/${appName}/src/app/app.tsx`, + ` import NxWelcome from './nx-welcome'; export function App() { @@ -383,37 +475,38 @@ describe('React Applications', () => { export default App; ` - ); + ); - runCLI(`build ${appName}`); - const outputAssetFiles = listFiles(`dist/apps/${appName}/assets`); - const styleFile = outputAssetFiles.find((filename) => - filename.endsWith('.css') - ); - if (!styleFile) { - throw new Error('Could not find bundled css file'); - } - const styleFileContents = readFile( - `dist/apps/${appName}/assets/${styleFile}` - ); - const isStyleFileUsingTWClasses = - styleFileContents.includes('w-20') && - styleFileContents.includes('h-20'); - expect(isStyleFileUsingTWClasses).toBeTruthy(); - }, 250_000); - }); + runCLI(`build ${appName}`); + const outputAssetFiles = listFiles(`dist/apps/${appName}/assets`); + const styleFile = outputAssetFiles.find((filename) => + filename.endsWith('.css') + ); + if (!styleFile) { + throw new Error('Could not find bundled css file'); + } + const styleFileContents = readFile( + `dist/apps/${appName}/assets/${styleFile}` + ); + const isStyleFileUsingTWClasses = + styleFileContents.includes('w-20') && + styleFileContents.includes('h-20'); + expect(isStyleFileUsingTWClasses).toBeTruthy(); + }, 250_000); + }); - describe('--format', () => { - it('should be formatted on freshly created apps', async () => { - const appName = uniq('app'); - runCLI( - `generate @nx/react:app ${appName} --bundler=webpack --no-interactive --linter=eslint --unitTestRunner=jest` - ); + describe('--format', () => { + it('should be formatted on freshly created apps', async () => { + const appName = uniq('app'); + runCLI( + `generate @nx/react:app ${appName} --bundler=webpack --no-interactive --linter=eslint --unitTestRunner=jest` + ); - const stdout = runCLI(`format:check --projects=${appName}`, { - silenceError: true, + const stdout = runCLI(`format:check --projects=${appName}`, { + silenceError: true, + }); + expect(stdout).toEqual(''); }); - expect(stdout).toEqual(''); }); }); }); diff --git a/e2e/utils/create-project-utils.ts b/e2e/utils/create-project-utils.ts index 05406751870dfb..4e6361172c098d 100644 --- a/e2e/utils/create-project-utils.ts +++ b/e2e/utils/create-project-utils.ts @@ -76,10 +76,12 @@ export function newProject({ name = uniq('proj'), packageManager = getSelectedPackageManager(), packages, + workspaces = false, }: { name?: string; packageManager?: 'npm' | 'yarn' | 'pnpm' | 'bun'; readonly packages?: Array; + workspaces?: boolean; } = {}): string { const newProjectStart = performance.mark('new-project:start'); try { @@ -95,6 +97,7 @@ export function newProject({ runCreateWorkspace(projScope, { preset: 'apps', packageManager, + workspaces, }); const createNxWorkspaceEnd = performance.mark('create-nx-workspace:end'); createNxWorkspaceMeasure = performance.measure( @@ -228,6 +231,7 @@ export function runCreateWorkspace( ssr, framework, prefix, + workspaces, }: { preset: string; appName?: string; @@ -249,13 +253,17 @@ export function runCreateWorkspace( ssr?: boolean; framework?: string; prefix?: string; + workspaces?: boolean; } ) { projName = name; const pm = getPackageManagerCommand({ packageManager }); + preset = workspaces ? 'ts' : preset; + let command = `${pm.createWorkspace} ${name} --preset=${preset} --nxCloud=skip --no-interactive`; + if (appName) { command += ` --appName=${appName}`; } @@ -327,6 +335,10 @@ export function runCreateWorkspace( command += ` --prefix=${prefix}`; } + if (workspaces) { + command += ` --workspaces`; + } + try { const create = execSync(`${command}${isVerbose() ? ' --verbose' : ''}`, { cwd, diff --git a/packages/expo/src/generators/application/application.ts b/packages/expo/src/generators/application/application.ts index 4c971cbc324b12..8d1593245fba58 100644 --- a/packages/expo/src/generators/application/application.ts +++ b/packages/expo/src/generators/application/application.ts @@ -1,4 +1,5 @@ import { + detectPackageManager, formatFiles, GeneratorCallback, joinPathFragments, @@ -21,6 +22,7 @@ import { Schema } from './schema'; import { ensureDependencies } from '../../utils/ensure-dependencies'; import { initRootBabelConfig } from '../../utils/init-root-babel-config'; import { logShowProjectCommand } from '@nx/devkit/src/utils/log-show-project-command'; +import { addProjectToTsSolutionWorkspace } from '@nx/react/src/utils/add-app-to-pnpm-workspace'; export async function expoApplicationGenerator( host: Tree, @@ -95,6 +97,12 @@ export async function expoApplicationGeneratorInternal( : undefined ); + // If we are using the new TS solution + // We need to update the workspace file (package.json or pnpm-workspaces.yaml) to include the new project + if (options.useTsSolution) { + addProjectToTsSolutionWorkspace(host, options.appProjectRoot); + } + if (!options.skipFormat) { await formatFiles(host); } diff --git a/packages/expo/src/generators/library/library.ts b/packages/expo/src/generators/library/library.ts index 83743bf5a6e444..68107049918dae 100644 --- a/packages/expo/src/generators/library/library.ts +++ b/packages/expo/src/generators/library/library.ts @@ -38,6 +38,7 @@ import { addBuildTargetDefaults } from '@nx/devkit/src/generators/target-default import { logShowProjectCommand } from '@nx/devkit/src/utils/log-show-project-command'; import { updateTsconfigFiles } from '@nx/js/src/utils/typescript/ts-solution-setup'; import { getImportPath } from '@nx/js/src/utils/get-import-path'; +import { addProjectToTsSolutionWorkspace } from '@nx/react/src/utils/add-app-to-pnpm-workspace'; export async function expoLibraryGenerator( host: Tree, @@ -130,6 +131,10 @@ export async function expoLibraryGeneratorInternal( : undefined ); + if (options.isUsingTsSolutionConfig) { + addProjectToTsSolutionWorkspace(host, `${options.projectRoot}/*`); + } + if (!options.skipFormat) { await formatFiles(host); } diff --git a/packages/next/src/generators/application/application.ts b/packages/next/src/generators/application/application.ts index c17d968e74df5e..8436d9e95ddc1a 100644 --- a/packages/next/src/generators/application/application.ts +++ b/packages/next/src/generators/application/application.ts @@ -1,5 +1,6 @@ import { addDependenciesToPackageJson, + detectPackageManager, formatFiles, GeneratorCallback, joinPathFragments, @@ -31,6 +32,7 @@ import { showPossibleWarnings } from './lib/show-possible-warnings'; import { tsLibVersion } from '../../utils/versions'; import { logShowProjectCommand } from '@nx/devkit/src/utils/log-show-project-command'; import { updateTsconfigFiles } from '@nx/js/src/utils/typescript/ts-solution-setup'; +import { addProjectToTsSolutionWorkspace } from '@nx/react/src/utils/add-app-to-pnpm-workspace'; export async function applicationGenerator(host: Tree, schema: Schema) { return await applicationGeneratorInternal(host, { @@ -132,6 +134,12 @@ export async function applicationGeneratorInternal(host: Tree, schema: Schema) { options.src ? 'src' : '.' ); + // If we are using the new TS solution + // We need to update the workspace file (package.json or pnpm-workspaces.yaml) to include the new project + if (options.useTsSolution) { + addProjectToTsSolutionWorkspace(host, options.appProjectRoot); + } + if (!options.skipFormat) { await formatFiles(host); } diff --git a/packages/next/src/generators/library/lib/normalize-options.ts b/packages/next/src/generators/library/lib/normalize-options.ts index a77b07a9623feb..cc2d85876ecc51 100644 --- a/packages/next/src/generators/library/lib/normalize-options.ts +++ b/packages/next/src/generators/library/lib/normalize-options.ts @@ -4,10 +4,12 @@ import { ensureProjectName, } from '@nx/devkit/src/generators/project-name-and-root-utils'; import { Schema } from '../schema'; +import { isUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup'; export interface NormalizedSchema extends Schema { importPath: string; projectRoot: string; + isUsingTsSolutionConfig: boolean; } export async function normalizeOptions( @@ -35,5 +37,6 @@ export async function normalizeOptions( ...options, importPath, projectRoot, + isUsingTsSolutionConfig: isUsingTsSolutionSetup(host), }; } diff --git a/packages/next/src/generators/library/library.ts b/packages/next/src/generators/library/library.ts index 0269a3cf36a2cf..890a047f57d43e 100644 --- a/packages/next/src/generators/library/library.ts +++ b/packages/next/src/generators/library/library.ts @@ -16,6 +16,7 @@ import { Schema } from './schema'; import { normalizeOptions } from './lib/normalize-options'; import { eslintConfigNextVersion, tsLibVersion } from '../../utils/versions'; import { updateTsconfigFiles } from '@nx/js/src/utils/typescript/ts-solution-setup'; +import { addProjectToTsSolutionWorkspace } from '@nx/react/src/utils/add-app-to-pnpm-workspace'; export async function libraryGenerator(host: Tree, rawOptions: Schema) { return await libraryGeneratorInternal(host, { @@ -154,6 +155,10 @@ export async function libraryGeneratorInternal(host: Tree, rawOptions: Schema) { : undefined ); + if (options.isUsingTsSolutionConfig) { + addProjectToTsSolutionWorkspace(host, `${options.projectRoot}/*`); + } + if (!options.skipFormat) { await formatFiles(host); } diff --git a/packages/react-native/src/generators/application/application.ts b/packages/react-native/src/generators/application/application.ts index 5518a43a0347a2..5f7eb3870df94d 100644 --- a/packages/react-native/src/generators/application/application.ts +++ b/packages/react-native/src/generators/application/application.ts @@ -1,4 +1,5 @@ import { + detectPackageManager, formatFiles, GeneratorCallback, joinPathFragments, @@ -26,6 +27,7 @@ import { ensureDependencies } from '../../utils/ensure-dependencies'; import { syncDeps } from '../../executors/sync-deps/sync-deps.impl'; import { PackageJson } from 'nx/src/utils/package-json'; import { updateTsconfigFiles } from '@nx/js/src/utils/typescript/ts-solution-setup'; +import { addProjectToTsSolutionWorkspace } from '@nx/react/src/utils/add-app-to-pnpm-workspace'; export async function reactNativeApplicationGenerator( host: Tree, @@ -143,6 +145,12 @@ export async function reactNativeApplicationGeneratorInternal( : undefined ); + // If we are using the new TS solution + // We need to update the workspace file (package.json or pnpm-workspaces.yaml) to include the new project + if (options.useTsSolution) { + addProjectToTsSolutionWorkspace(host, options.appProjectRoot); + } + if (!options.skipFormat) { await formatFiles(host); } diff --git a/packages/react-native/src/generators/library/library.ts b/packages/react-native/src/generators/library/library.ts index 227cd2d302431d..ec1afed43a386f 100644 --- a/packages/react-native/src/generators/library/library.ts +++ b/packages/react-native/src/generators/library/library.ts @@ -39,6 +39,7 @@ import { updateTsconfigFiles, } from '@nx/js/src/utils/typescript/ts-solution-setup'; import { getImportPath } from '@nx/js/src/utils/get-import-path'; +import { addProjectToTsSolutionWorkspace } from '@nx/react/src/utils/add-app-to-pnpm-workspace'; export async function reactNativeLibraryGenerator( host: Tree, @@ -130,6 +131,10 @@ export async function reactNativeLibraryGeneratorInternal( : undefined ); + if (options.isUsingTsSolutionConfig) { + addProjectToTsSolutionWorkspace(host, `${options.projectRoot}/*`); + } + if (!options.skipFormat) { await formatFiles(host); } diff --git a/packages/react/package.json b/packages/react/package.json index e5d7f0a3348d83..9072e08551402e 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -44,7 +44,8 @@ "@nx/web": "file:../web", "@nx/module-federation": "file:../module-federation", "express": "^4.19.2", - "http-proxy-middleware": "^3.0.3" + "http-proxy-middleware": "^3.0.3", + "@zkochan/js-yaml": "0.0.7" }, "publishConfig": { "access": "public" diff --git a/packages/react/src/generators/application/application.ts b/packages/react/src/generators/application/application.ts index c9a421896e46a0..7eda8623ba5110 100644 --- a/packages/react/src/generators/application/application.ts +++ b/packages/react/src/generators/application/application.ts @@ -10,6 +10,7 @@ import { setDefaults } from './lib/set-defaults'; import { addStyledModuleDependencies } from '../../rules/add-styled-dependencies'; import { addDependenciesToPackageJson, + detectPackageManager, ensurePackage, formatFiles, GeneratorCallback, @@ -41,7 +42,11 @@ import { initGenerator as jsInitGenerator } from '@nx/js'; import { logShowProjectCommand } from '@nx/devkit/src/utils/log-show-project-command'; import { setupTailwindGenerator } from '../setup-tailwind/setup-tailwind'; import { useFlatConfig } from '@nx/eslint/src/utils/flat-config'; -import { updateTsconfigFiles } from '@nx/js/src/utils/typescript/ts-solution-setup'; +import { + isUsingTsSolutionSetup, + updateTsconfigFiles, +} from '@nx/js/src/utils/typescript/ts-solution-setup'; +import { addProjectToTsSolutionWorkspace } from '../../utils/add-app-to-pnpm-workspace'; async function addLinting(host: Tree, options: NormalizedSchema) { const tasks: GeneratorCallback[] = []; @@ -115,7 +120,7 @@ export async function applicationGeneratorInternal( ...schema, tsConfigName: schema.rootProject ? 'tsconfig.json' : 'tsconfig.base.json', skipFormat: true, - addTsPlugin: schema.useTsSolution, + addTsPlugin: isUsingTsSolutionSetup(host), formatter: schema.formatter, }); tasks.push(jsInitTask); @@ -367,6 +372,12 @@ export async function applicationGeneratorInternal( moduleResolution: 'bundler', }); + // If we are using the new TS solution + // We need to update the workspace file (package.json or pnpm-workspaces.yaml) to include the new project + if (options.useTsSolution) { + addProjectToTsSolutionWorkspace(host, options.appProjectRoot); + } + if (!options.skipFormat) { await formatFiles(host); } diff --git a/packages/react/src/generators/library/lib/normalize-options.ts b/packages/react/src/generators/library/lib/normalize-options.ts index 76233f3dd833ba..9110d3f1a4d7fd 100644 --- a/packages/react/src/generators/library/lib/normalize-options.ts +++ b/packages/react/src/generators/library/lib/normalize-options.ts @@ -13,7 +13,6 @@ import { import { assertValidStyle } from '../../../utils/assertion'; import { NormalizedSchema, Schema } from '../schema'; import { isUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup'; -import { promptWhenInteractive } from '@nx/devkit/src/generators/prompt'; export async function normalizeOptions( host: Tree, diff --git a/packages/react/src/generators/library/library.ts b/packages/react/src/generators/library/library.ts index cfa3e4832c3a55..9bbf8c7ff69f46 100644 --- a/packages/react/src/generators/library/library.ts +++ b/packages/react/src/generators/library/library.ts @@ -33,6 +33,7 @@ import { installCommonDependencies } from './lib/install-common-dependencies'; import { setDefaults } from './lib/set-defaults'; import { updateTsconfigFiles } from '@nx/js/src/utils/typescript/ts-solution-setup'; import { ensureProjectIsExcludedFromPluginRegistrations } from '@nx/js/src/utils/typescript/plugin'; +import { addProjectToTsSolutionWorkspace } from '../../utils/add-app-to-pnpm-workspace'; export async function libraryGenerator(host: Tree, schema: Schema) { return await libraryGeneratorInternal(host, { @@ -278,6 +279,9 @@ export async function libraryGeneratorInternal(host: Tree, schema: Schema) { : undefined ); + if (options.isUsingTsSolutionConfig) { + addProjectToTsSolutionWorkspace(host, `${options.projectRoot}/*`); + } if (!options.skipFormat) { await formatFiles(host); } diff --git a/packages/react/src/utils/add-app-to-pnpm-workspace.ts b/packages/react/src/utils/add-app-to-pnpm-workspace.ts new file mode 100644 index 00000000000000..20ac8ddb047b14 --- /dev/null +++ b/packages/react/src/utils/add-app-to-pnpm-workspace.ts @@ -0,0 +1,33 @@ +import { detectPackageManager, readJson, Tree } from '@nx/devkit'; + +export function addProjectToTsSolutionWorkspace( + tree: Tree, + projectDir: string +) { + if (detectPackageManager() === 'pnpm') { + const { load, dump } = require('@zkochan/js-yaml'); + if (tree.exists('pnpm-workspace.yaml')) { + const workspaceFile = tree.read('pnpm-workspace.yaml', 'utf-8'); + const yamlData = load(workspaceFile); + + if (!yamlData.packages) { + yamlData.packages = []; + } + + if (!yamlData.packages.includes(projectDir)) { + yamlData.packages.push(projectDir); + tree.write('pnpm-workspace.yaml', dump(yamlData, { indent: 2 })); + } + } + } else { + // Update package.json + const packageJson = readJson(tree, 'package.json'); + if (!packageJson.workspaces) { + packageJson.workspaces = []; + } + if (!packageJson.workspaces.includes(projectDir)) { + packageJson.workspaces.push(projectDir); + tree.write('package.json', JSON.stringify(packageJson, null, 2)); + } + } +} diff --git a/packages/remix/src/generators/application/application.impl.ts b/packages/remix/src/generators/application/application.impl.ts index e66b13fd2885db..5c9f00cef63d98 100644 --- a/packages/remix/src/generators/application/application.impl.ts +++ b/packages/remix/src/generators/application/application.impl.ts @@ -1,5 +1,6 @@ import { addProjectConfiguration, + detectPackageManager, ensurePackage, formatFiles, generateFiles, @@ -48,6 +49,7 @@ import { isUsingTsSolutionSetup, updateTsconfigFiles, } from '@nx/js/src/utils/typescript/ts-solution-setup'; +import { addProjectToTsSolutionWorkspace } from '@nx/react/src/utils/add-app-to-pnpm-workspace'; export function remixApplicationGenerator( tree: Tree, @@ -374,6 +376,12 @@ export default {...nxPreset}; '.' ); + // If we are using the new TS solution + // We need to update the workspace file (package.json or pnpm-workspaces.yaml) to include the new project + if (options.useTsSolution) { + addProjectToTsSolutionWorkspace(tree, options.projectRoot); + } + tasks.push(() => { logShowProjectCommand(options.projectName); }); diff --git a/packages/workspace/src/generators/new/generate-workspace-files.spec.ts b/packages/workspace/src/generators/new/generate-workspace-files.spec.ts index a53182eebf46f7..bbb0ce981a8b52 100644 --- a/packages/workspace/src/generators/new/generate-workspace-files.spec.ts +++ b/packages/workspace/src/generators/new/generate-workspace-files.spec.ts @@ -332,8 +332,8 @@ describe('@nx/workspace:generateWorkspaceFiles', () => { const packageJson = tree.read('/proj/pnpm-workspace.yaml', 'utf-8'); expect(packageJson).toMatchInlineSnapshot(` "packages: - - apps/** - - packages/** + - "apps/**" + - "packages/**" " `); }); diff --git a/packages/workspace/src/generators/new/generate-workspace-files.ts b/packages/workspace/src/generators/new/generate-workspace-files.ts index da8bb757206806..951aa263dc4756 100644 --- a/packages/workspace/src/generators/new/generate-workspace-files.ts +++ b/packages/workspace/src/generators/new/generate-workspace-files.ts @@ -429,7 +429,7 @@ function setUpWorkspacesInPackageJson(tree: Tree, options: NormalizedSchema) { tree.write( join(options.directory, 'pnpm-workspace.yaml'), `packages: - - ${workspaces.join('\n - ')} + ${workspaces.map((workspace) => `- "${workspace}"`).join('\n ')} ` ); } else {