diff --git a/e2e/react/src/module-federation/ts-solution-mf.test.ts b/e2e/react/src/module-federation/ts-solution-mf.test.ts new file mode 100644 index 00000000000000..5b780fe1d518dc --- /dev/null +++ b/e2e/react/src/module-federation/ts-solution-mf.test.ts @@ -0,0 +1,278 @@ +import { stripIndents } from '@nx/devkit'; +import { + newProject, + cleanupProject, + checkFilesDoNotExist, + checkFilesExist, + getAvailablePort, + killProcessAndPorts, + readFile, + readJson, + runCLI as _runCLI, + runCLIAsync, + runCommandUntil, + runE2ETests, + uniq, + updateFile, + getPackageManagerCommand, + runCommand, +} from '@nx/e2e-utils'; +import { readPort } from './utils'; + +// Using verbose CLI for debugging +function runCLI(cmd: string, opts?: { env?: Record }) { + return _runCLI(cmd, { + verbose: true, + env: { + ...opts?.env, + NX_VERBOSE_LOGGING: 'true', + NX_NATIVE_LOGGING: 'nx::native::db', + }, + }); +} + +describe('React Rspack Module Federation - TS Solution + PM Workspaces', () => { + beforeAll(() => { + newProject({ packages: ['@nx/react'], preset: 'ts' }); + }); + + afterAll(() => cleanupProject()); + + it('should generate host and remote apps without project.json, with package.json exports', async () => { + const shell = uniq('shell'); + const remote1 = uniq('remote1'); + const remote2 = uniq('remote2'); + const shellPort = await getAvailablePort(); + + // Generate host with remotes + runCLI( + `generate @nx/react:host ${shell} --remotes=${remote1},${remote2} --devServerPort=${shellPort} --bundler=rspack --e2eTestRunner=cypress --style=css --no-interactive --skipFormat` + ); + + runCommand(getPackageManagerCommand().install); + + // ======================================== + // Test 1: Verify NO project.json files exist (TS solution uses package.json) + // ======================================== + checkFilesDoNotExist(`${shell}/project.json`); + checkFilesDoNotExist(`${remote1}/project.json`); + checkFilesDoNotExist(`${remote2}/project.json`); + + // ======================================== + // Test 2: Verify package.json files exist with correct structure + // ======================================== + checkFilesExist(`${shell}/package.json`); + checkFilesExist(`${remote1}/package.json`); + checkFilesExist(`${remote2}/package.json`); + + // ======================================== + // Test 3: Verify package.json has simple names (not scoped) in TS solution + // ======================================== + const shellPkgJson = readJson(`${shell}/package.json`); + const remote1PkgJson = readJson(`${remote1}/package.json`); + const remote2PkgJson = readJson(`${remote2}/package.json`); + + // In module federation, packages use simple names to match module-federation.config.ts + expect(shellPkgJson.name).toBe(shell); + expect(remote1PkgJson.name).toBe(remote1); + expect(remote2PkgJson.name).toBe(remote2); + + // ======================================== + // Test 4: Verify host has remotes as devDependencies using simple names + // ======================================== + expect(shellPkgJson.devDependencies).toBeDefined(); + expect(shellPkgJson.devDependencies[remote1]).toBeDefined(); + expect(shellPkgJson.devDependencies[remote2]).toBeDefined(); + + // Verify workspace protocol is used (pnpm/yarn) or * (npm) + const remote1Version = shellPkgJson.devDependencies[remote1]; + expect( + remote1Version === 'workspace:*' || + remote1Version === '*' || + remote1Version.startsWith('workspace:') + ).toBe(true); + + // ======================================== + // Test 5: Verify remote package.json has exports configured + // ======================================== + expect(remote1PkgJson.exports).toBeDefined(); + expect(remote1PkgJson.exports['./Module']).toBeDefined(); + expect(remote1PkgJson.exports['./Module'].types).toBe( + './src/remote-entry.ts' + ); + expect(remote1PkgJson.exports['./Module'].default).toBe( + './src/remote-entry.ts' + ); + + expect(remote2PkgJson.exports).toBeDefined(); + expect(remote2PkgJson.exports['./Module']).toBeDefined(); + + // ======================================== + // Test 6: Verify module federation config files exist + // ======================================== + checkFilesExist(`${shell}/module-federation.config.ts`); + checkFilesExist(`${remote1}/module-federation.config.ts`); + checkFilesExist(`${remote2}/module-federation.config.ts`); + + // ======================================== + // Test 7: Verify NO prod config files exist (not needed in TS solution) + // ======================================== + checkFilesDoNotExist(`${shell}/rspack.config.prod.ts`); + checkFilesDoNotExist(`${remote1}/rspack.config.prod.ts`); + checkFilesDoNotExist(`${remote2}/rspack.config.prod.ts`); + + // ======================================== + // Test 8: Run unit tests + // ======================================== + await expect(runCLIAsync(`test ${shell}`)).resolves.toMatchObject({ + combinedOutput: expect.stringContaining('Test Suites: 1 passed, 1 total'), + }); + + // ======================================== + // Test 9: Build all apps in development and production + // ======================================== + const apps = [shell, remote1, remote2]; + apps.forEach((app) => { + ['development', 'production'].forEach(async (configuration) => { + const cliOutput = runCLI(`run ${app}:build:${configuration}`); + expect(cliOutput).toContain('Successfully ran target'); + }); + }); + + // ======================================== + // Test 10: Serve the host and verify it starts + // ======================================== + const serveResult = await runCommandUntil(`serve ${shell}`, (output) => + output.includes(`http://localhost:${readPort(shell)}`) + ); + + await killProcessAndPorts(serveResult.pid, readPort(shell)); + + // ======================================== + // Test 11: Run E2E tests (if configured) + // ======================================== + if (runE2ETests()) { + updateFile( + `${shell}-e2e/src/integration/app.spec.ts`, + stripIndents` + import { getGreeting } from '../support/app.po'; + + describe('shell app', () => { + it('should display welcome message', () => { + cy.visit('/') + getGreeting().contains('Welcome ${shell}'); + }); + + it('should load remote 1', () => { + cy.visit('/${remote1}') + getGreeting().contains('Welcome ${remote1}'); + }); + + it('should load remote 2', () => { + cy.visit('/${remote2}') + getGreeting().contains('Welcome ${remote2}'); + }); + }); + ` + ); + + const e2eResults = await runCommandUntil( + `e2e ${shell}-e2e --verbose`, + (output) => output.includes('All specs passed!') + ); + + await killProcessAndPorts(e2eResults.pid, readPort(shell)); + } + }, 600_000); // 10 minute timeout for long-running e2e test + + it('should add a new remote to an existing host and update devDependencies', async () => { + const shell = uniq('shell'); + const remote1 = uniq('remote1'); + const remote2 = uniq('remote2'); + const shellPort = await getAvailablePort(); + + // Generate host with one remote + runCLI( + `generate @nx/react:host ${shell} --remotes=${remote1} --devServerPort=${shellPort} --bundler=rspack --e2eTestRunner=none --style=css --no-interactive --skipFormat` + ); + + // Verify initial state + let shellPkgJson = readJson(`${shell}/package.json`); + expect(shellPkgJson.devDependencies[remote1]).toBeDefined(); + + // Generate second remote and attach it to the host + runCLI( + `generate @nx/react:remote ${remote2} --host=${shell} --bundler=rspack --e2eTestRunner=none --style=css --no-interactive --skipFormat` + ); + + // Verify remote was added to host's devDependencies using simple names + shellPkgJson = readJson(`${shell}/package.json`); + const remote2PkgJson = readJson(`${remote2}/package.json`); + expect(shellPkgJson.devDependencies[remote1]).toBeDefined(); + expect(shellPkgJson.devDependencies[remote2]).toBeDefined(); + + // Verify remote has package.json exports + expect(remote2PkgJson.exports['./Module']).toBeDefined(); + + // Verify module federation config was updated + const shellMFConfig = readFile(`${shell}/module-federation.config.ts`); + expect(shellMFConfig).toContain(remote2); + }, 600_000); + + it('should handle workspace libraries correctly with TS solution', async () => { + const shell = uniq('shell'); + const remote = uniq('remote'); + const lib = uniq('lib'); + const shellPort = await getAvailablePort(); + + // Generate a library + runCLI( + `generate @nx/react:library ${lib} --bundler=none --unitTestRunner=jest --no-interactive --skipFormat` + ); + + // Generate host with remote + runCLI( + `generate @nx/react:host ${shell} --remotes=${remote} --devServerPort=${shellPort} --bundler=rspack --e2eTestRunner=none --style=css --no-interactive --skipFormat` + ); + + // Add library as dependency to remote + const remotePkgJsonPath = `${remote}/package.json`; + const remotePkgJson = readJson(remotePkgJsonPath); + const libPkgJson = readJson(`${lib}/package.json`); + + updateFile( + remotePkgJsonPath, + JSON.stringify( + { + ...remotePkgJson, + dependencies: { + ...remotePkgJson.dependencies, + [libPkgJson.name]: 'workspace:*', + }, + }, + null, + 2 + ) + ); + + // Run install to link the workspace dependencies + runCommand(getPackageManagerCommand().install); + + // Import library in remote + const remoteAppPath = `${remote}/src/app/app.tsx`; + const remoteAppContent = readFile(remoteAppPath); + updateFile( + remoteAppPath, + `import { } from '${libPkgJson.name}';\n${remoteAppContent}` + ); + + // Build should succeed + const buildOutput = runCLI(`build ${remote}`); + expect(buildOutput).toContain('Successfully ran target'); + + // Verify library is shared properly + const shellMFConfig = readFile(`${shell}/module-federation.config.ts`); + // The library should be referenced properly (no strict verification since implementation may vary) + expect(shellMFConfig).toBeTruthy(); + }, 600_000); +}); diff --git a/e2e/react/src/module-federation/utils.ts b/e2e/react/src/module-federation/utils.ts index 42eb191d6b3e6c..34ddb34cef2c19 100644 --- a/e2e/react/src/module-federation/utils.ts +++ b/e2e/react/src/module-federation/utils.ts @@ -6,7 +6,13 @@ export function readPort(appName: string): number { try { config = readJson(join('apps', appName, 'project.json')); } catch { - config = readJson(join(appName, 'project.json')); + try { + config = readJson(join(appName, 'project.json')); + } catch { + // TS Solution setup uses package.json + const pkgJson = readJson(join(appName, 'package.json')); + return pkgJson.nx?.targets?.serve?.options?.port; + } } return config.targets.serve.options.port; } diff --git a/packages/module-federation/src/utils/dependencies.ts b/packages/module-federation/src/utils/dependencies.ts index bc4b9246ad42ff..d431fe03d77dcc 100644 --- a/packages/module-federation/src/utils/dependencies.ts +++ b/packages/module-federation/src/utils/dependencies.ts @@ -91,5 +91,7 @@ function getLibraryImportPath( } } - return undefined; + // Return library name if not found in TS path mappings + // This supports TS Solution + PM Workspaces where libs use package.json instead + return library; } diff --git a/packages/module-federation/src/utils/share.spec.ts b/packages/module-federation/src/utils/share.spec.ts index d81d919608fb8e..fa9794d34655d2 100644 --- a/packages/module-federation/src/utils/share.spec.ts +++ b/packages/module-federation/src/utils/share.spec.ts @@ -119,8 +119,14 @@ describe('MF Share Utils', () => { ]); // ASSERT + // With TS solution + PM workspaces support, workspace libs not in TS path mappings are still included expect(sharedLibraries.getAliases()).toEqual({}); - expect(sharedLibraries.getLibraries('libs/shared')).toEqual({}); + expect(sharedLibraries.getLibraries('libs/shared')).toEqual({ + '@myorg/shared': { + requiredVersion: false, + eager: undefined, + }, + }); }); }); diff --git a/packages/module-federation/src/utils/share.ts b/packages/module-federation/src/utils/share.ts index ce88b46da5b7f0..e6737e4da94557 100644 --- a/packages/module-federation/src/utils/share.ts +++ b/packages/module-federation/src/utils/share.ts @@ -40,9 +40,6 @@ export function shareWorkspaceLibraries( } const tsconfigPathAliases = readTsPathMappings(tsConfigPath); - if (!Object.keys(tsconfigPathAliases).length) { - return getEmptySharedLibrariesConfig(); - } // Nested projects must come first, sort them as such const sortedTsConfigPathAliases = {}; @@ -79,6 +76,17 @@ export function shareWorkspaceLibraries( }); } + // Collect workspace libs that are not in TS path mappings + // This supports TS Solution + PM Workspaces where libs use package.json + const workspaceLibrariesAsDeps: string[] = []; + if (Object.keys(sortedTsConfigPathAliases).length !== workspaceLibs.length) { + for (const workspaceLib of workspaceLibs) { + if (!sortedTsConfigPathAliases[workspaceLib.importKey]) { + workspaceLibrariesAsDeps.push(workspaceLib.importKey); + } + } + } + const normalModuleReplacementPluginImpl = bundler === 'rspack' ? RspackNormalModuleReplacementPlugin @@ -110,7 +118,7 @@ export function shareWorkspaceLibraries( joinPathFragments(workspaceRoot, projectRoot, 'package.json') ); } - return pathMappings.reduce((libraries, library) => { + const libraries = pathMappings.reduce((libraries, library) => { // Check to see if the library version is declared in the app's package.json let version = pkgJson?.dependencies?.[library.name]; if (!version && workspaceLibs.length > 0) { @@ -143,6 +151,53 @@ export function shareWorkspaceLibraries( }, }; }, {} as Record); + + // Add workspace libs from package.json dependencies + // This supports TS Solution + PM Workspaces + for (const libraryName of workspaceLibrariesAsDeps) { + let version = + pkgJson?.dependencies?.[libraryName] ?? + pkgJson?.devDependencies?.[libraryName]; + + // Normalize workspace protocol versions (workspace:*, workspace:^, *, etc.) + if (version && (version === '*' || version.startsWith('workspace:'))) { + // Look up the actual version from the library's package.json + const workspaceLib = workspaceLibs.find( + (lib) => lib.importKey === libraryName + ); + if (workspaceLib) { + const libPackageJsonPath = join( + workspaceRoot, + workspaceLib.root, + 'package.json' + ); + if (existsSync(libPackageJsonPath)) { + const libPkgJson = readJsonFile(libPackageJsonPath); + if (libPkgJson?.version) { + version = libPkgJson.version; + } else { + // Library has no version, treat as no version requirement + version = null; + } + } else { + // Can't find library package.json, treat as no version requirement + version = null; + } + } else { + // Can't find workspace library, treat as no version requirement + version = null; + } + } + + libraries[libraryName] = { + ...(version + ? { requiredVersion: version, singleton: true } + : { requiredVersion: false }), + eager, + }; + } + + return libraries as Record; }, getReplacementPlugin: () => new normalModuleReplacementPluginImpl(/./, (req) => { diff --git a/packages/react/src/generators/host/host.ts b/packages/react/src/generators/host/host.ts index 5741697c72bd61..add26b4db39cfb 100644 --- a/packages/react/src/generators/host/host.ts +++ b/packages/react/src/generators/host/host.ts @@ -1,11 +1,14 @@ import { addDependenciesToPackageJson, + detectPackageManager, formatFiles, GeneratorCallback, joinPathFragments, + readJson, readProjectConfiguration, runTasksInSerial, Tree, + updateJson, updateProjectConfiguration, } from '@nx/devkit'; import { updateModuleFederationProject } from '../../rules/update-module-federation-project'; @@ -23,6 +26,7 @@ import { updateModuleFederationE2eProject } from './lib/update-module-federation import { NormalizedSchema, Schema } from './schema'; import { addMfEnvToTargetDefaultInputs } from '../../utils/add-mf-env-to-inputs'; import { isValidVariable } from '@nx/js'; +import { isUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup'; import { moduleFederationEnhancedVersion, nxVersion, @@ -41,7 +45,6 @@ export async function hostGenerator( ...(await normalizeOptions(host, { ...schema, name, - useProjectJson: true, })), js: schema.js ?? false, typescriptConfiguration: schema.js @@ -74,10 +77,23 @@ export async function hostGenerator( // The target use-case is loading remotes as child routes, thus always enable routing. routing: true, skipFormat: true, - useProjectJson: true, }); tasks.push(initTask); + // In TS solution setup, update package.json to use simple name instead of scoped name + if (isUsingTsSolutionSetup(host)) { + const hostPackageJsonPath = joinPathFragments( + options.appProjectRoot, + 'package.json' + ); + if (host.exists(hostPackageJsonPath)) { + updateJson(host, hostPackageJsonPath, (json) => { + json.name = options.projectName; + return json; + }); + } + } + const remotesWithPorts: { name: string; port: number }[] = []; if (schema.remotes) { @@ -113,6 +129,11 @@ export async function hostGenerator( updateModuleFederationE2eProject(host, options); updateModuleFederationTsconfig(host, options); + // Add remotes as devDependencies in TS solution setup + if (isUsingTsSolutionSetup(host) && remotesWithPorts.length > 0) { + addRemotesAsHostDependencies(host, options.projectName, remotesWithPorts); + } + if (options.ssr) { if (options.bundler !== 'rspack') { const setupSsrTask = await setupSsrGenerator(host, { @@ -166,4 +187,37 @@ export async function hostGenerator( return runTasksInSerial(...tasks); } +function addRemotesAsHostDependencies( + tree: Tree, + hostName: string, + remotes: { name: string; port: number }[] +) { + const hostConfig = readProjectConfiguration(tree, hostName); + const hostPackageJsonPath = joinPathFragments( + hostConfig.root, + 'package.json' + ); + + if (!tree.exists(hostPackageJsonPath)) { + throw new Error( + `Host package.json not found at ${hostPackageJsonPath}. ` + + `TypeScript solution setup requires package.json for all projects.` + ); + } + + const packageManager = detectPackageManager(tree.root); + const versionSpec = packageManager === 'npm' ? '*' : 'workspace:*'; + + updateJson(tree, hostPackageJsonPath, (json) => { + json.devDependencies ??= {}; + + for (const remote of remotes) { + // Use simple remote name directly to match module-federation.config.ts + json.devDependencies[remote.name] = versionSpec; + } + + return json; + }); +} + export default hostGenerator; diff --git a/packages/react/src/generators/host/lib/add-module-federation-files.ts b/packages/react/src/generators/host/lib/add-module-federation-files.ts index 9c1adb66db5bba..3e9cc38f78a02d 100644 --- a/packages/react/src/generators/host/lib/add-module-federation-files.ts +++ b/packages/react/src/generators/host/lib/add-module-federation-files.ts @@ -6,7 +6,10 @@ import { offsetFromRoot, readProjectConfiguration, } from '@nx/devkit'; -import { getProjectSourceRoot } from '@nx/js/src/utils/typescript/ts-solution-setup'; +import { + getProjectSourceRoot, + isUsingTsSolutionSetup, +} from '@nx/js/src/utils/typescript/ts-solution-setup'; import { maybeJs } from '../../../utils/maybe-js'; import { createNxRspackPluginOptions, @@ -132,6 +135,15 @@ export function addModuleFederationFiles( processBundlerConfigFile(options, host, 'webpack.config.js'); processBundlerConfigFile(options, host, 'webpack.config.prod.js'); } + + // Delete TypeScript prod config in TS solution setup - not needed in Crystal + if (isUsingTsSolutionSetup(host)) { + const prodConfigFileName = + options.bundler === 'rspack' + ? 'rspack.config.prod.ts' + : 'webpack.config.prod.ts'; + processBundlerConfigFile(options, host, prodConfigFileName); + } } if (options.dynamic) { diff --git a/packages/react/src/generators/remote/lib/setup-package-json-exports-for-remote.ts b/packages/react/src/generators/remote/lib/setup-package-json-exports-for-remote.ts new file mode 100644 index 00000000000000..cefb607e807410 --- /dev/null +++ b/packages/react/src/generators/remote/lib/setup-package-json-exports-for-remote.ts @@ -0,0 +1,44 @@ +import { + joinPathFragments, + readProjectConfiguration, + Tree, + updateJson, +} from '@nx/devkit'; +import { getDefinedCustomConditionName } from '@nx/js/src/utils/typescript/ts-solution-setup'; +import { maybeJs } from '../../../utils/maybe-js'; +import { NormalizedSchema } from '../../application/schema'; + +export function setupPackageJsonExportsForRemote( + tree: Tree, + options: NormalizedSchema +) { + const project = readProjectConfiguration(tree, options.projectName); + const packageJsonPath = joinPathFragments(project.root, 'package.json'); + + if (!tree.exists(packageJsonPath)) { + throw new Error( + `package.json not found at ${packageJsonPath}. ` + + `TypeScript solution setup requires package.json for all projects.` + ); + } + + const exportPath = maybeJs(options, './src/remote-entry.ts'); + const customCondition = getDefinedCustomConditionName(tree); + + updateJson(tree, packageJsonPath, (json) => { + json.exports = { + ...json.exports, + './Module': { + [customCondition]: exportPath, + types: exportPath, + import: exportPath, + default: exportPath, + }, + }; + + // Set types for IDE support (no main needed - this is an app, not a library) + json.types = exportPath; + + return json; + }); +} diff --git a/packages/react/src/generators/remote/lib/update-host-with-remote.ts b/packages/react/src/generators/remote/lib/update-host-with-remote.ts index 62d3098857088f..1597d967062c73 100644 --- a/packages/react/src/generators/remote/lib/update-host-with-remote.ts +++ b/packages/react/src/generators/remote/lib/update-host-with-remote.ts @@ -1,13 +1,18 @@ import { applyChangesToString, + detectPackageManager, joinPathFragments, logger, names, readProjectConfiguration, Tree, + updateJson, } from '@nx/devkit'; import { ensureTypescript } from '@nx/js/src/utils/typescript/ensure-typescript'; -import { getProjectSourceRoot } from '@nx/js/src/utils/typescript/ts-solution-setup'; +import { + getProjectSourceRoot, + isUsingTsSolutionSetup, +} from '@nx/js/src/utils/typescript/ts-solution-setup'; import { addRemoteRoute, addRemoteToConfig, @@ -85,6 +90,11 @@ export function updateHostWithRemote( `Could not find app component at ${appComponentPath}. Did you generate this project with "@nx/react:host" or "@nx/react:consumer"?` ); } + + // Add remote as devDependency in TS solution setup + if (isUsingTsSolutionSetup(host)) { + addRemoteAsHostDependency(host, hostName, remoteName); + } } function findAppComponentPath(host: Tree, sourceRoot: string) { @@ -108,3 +118,33 @@ function findAppComponentPath(host: Tree, sourceRoot: string) { } } } + +function addRemoteAsHostDependency( + tree: Tree, + hostName: string, + remoteName: string +) { + const hostConfig = readProjectConfiguration(tree, hostName); + const hostPackageJsonPath = joinPathFragments( + hostConfig.root, + 'package.json' + ); + + if (!tree.exists(hostPackageJsonPath)) { + throw new Error( + `Host package.json not found at ${hostPackageJsonPath}. ` + + `TypeScript solution setup requires package.json for all projects.` + ); + } + + const packageManager = detectPackageManager(tree.root); + // npm doesn't support workspace: protocol, use * instead + const versionSpec = packageManager === 'npm' ? '*' : 'workspace:*'; + + updateJson(tree, hostPackageJsonPath, (json) => { + json.devDependencies ??= {}; + // Use simple remote name directly to match module-federation.config.ts + json.devDependencies[remoteName] = versionSpec; + return json; + }); +} diff --git a/packages/react/src/generators/remote/remote.ts b/packages/react/src/generators/remote/remote.ts index b8e600957bdce7..e836ac66e25959 100644 --- a/packages/react/src/generators/remote/remote.ts +++ b/packages/react/src/generators/remote/remote.ts @@ -6,16 +6,21 @@ import { joinPathFragments, names, offsetFromRoot, + readJson, readProjectConfiguration, runTasksInSerial, Tree, + updateJson, updateProjectConfiguration, } from '@nx/devkit'; import { join } from 'path'; import { ensureRootProjectName } from '@nx/devkit/src/generators/project-name-and-root-utils'; import { isValidVariable } from '@nx/js'; -import { getProjectSourceRoot } from '@nx/js/src/utils/typescript/ts-solution-setup'; +import { + getProjectSourceRoot, + isUsingTsSolutionSetup, +} from '@nx/js/src/utils/typescript/ts-solution-setup'; import { updateModuleFederationProject } from '../../rules/update-module-federation-project'; import { addMfEnvToTargetDefaultInputs } from '../../utils/add-mf-env-to-inputs'; import { normalizeRemoteName } from '../../utils/normalize-remote'; @@ -33,6 +38,7 @@ import { normalizeOptions } from '../application/lib/normalize-options'; import { NormalizedSchema } from '../application/schema'; import setupSsrGenerator from '../setup-ssr/setup-ssr'; import { addRemoteToDynamicHost } from './lib/add-remote-to-dynamic-host'; +import { setupPackageJsonExportsForRemote } from './lib/setup-package-json-exports-for-remote'; import { setupSsrForRemote } from './lib/setup-ssr-for-remote'; import { setupTspathForRemote } from './lib/setup-tspath-for-remote'; import { updateHostWithRemote } from './lib/update-host-with-remote'; @@ -109,6 +115,19 @@ export function addModuleFederationFiles( if (host.exists(pathToWebpackProdConfig)) { host.delete(pathToWebpackProdConfig); } + + // Delete TypeScript prod config in TS solution setup - not needed in Crystal + if (isUsingTsSolutionSetup(host)) { + const pathToTsProdConfig = joinPathFragments( + options.appProjectRoot, + options.bundler === 'rspack' + ? 'rspack.config.prod.ts' + : 'webpack.config.prod.ts' + ); + if (host.exists(pathToTsProdConfig)) { + host.delete(pathToTsProdConfig); + } + } } } @@ -119,7 +138,6 @@ export async function remoteGenerator(host: Tree, schema: Schema) { ...(await normalizeOptions(host, { ...schema, name, - useProjectJson: true, })), // when js is set to true, we want to use the js configuration js: schema.js ?? false, @@ -154,10 +172,23 @@ export async function remoteGenerator(host: Tree, schema: Schema) { ...options, name: options.projectName, skipFormat: true, - useProjectJson: true, }); tasks.push(initAppTask); + // In TS solution setup, update package.json to use simple name instead of scoped name + if (isUsingTsSolutionSetup(host)) { + const remotePackageJsonPath = joinPathFragments( + options.appProjectRoot, + 'package.json' + ); + if (host.exists(remotePackageJsonPath)) { + updateJson(host, remotePackageJsonPath, (json) => { + json.name = options.projectName; + return json; + }); + } + } + if (options.host) { updateHostWithRemote(host, options.host, options.projectName); } @@ -184,7 +215,13 @@ export async function remoteGenerator(host: Tree, schema: Schema) { addModuleFederationFiles(host, options); updateModuleFederationProject(host, options); - setupTspathForRemote(host, options); + + // Conditionally setup TS path or package.json exports based on TS solution setup + if (isUsingTsSolutionSetup(host)) { + setupPackageJsonExportsForRemote(host, options); + } else { + setupTspathForRemote(host, options); + } if (options.ssr) { if (options.bundler !== 'rspack') { diff --git a/packages/react/src/rules/update-module-federation-project.ts b/packages/react/src/rules/update-module-federation-project.ts index dc413889e0f40e..e6d2b880c4b324 100644 --- a/packages/react/src/rules/update-module-federation-project.ts +++ b/packages/react/src/rules/update-module-federation-project.ts @@ -25,7 +25,7 @@ export function updateModuleFederationProject( isHost = false ) { const projectConfig = readProjectConfiguration(host, options.projectName); - + projectConfig.targets ??= {}; if (options.bundler !== 'rspack') { projectConfig.targets.build.options = { ...(projectConfig.targets.build.options ?? {}),