From b1713be2c31d743e50686ad3324c9d494a0666c3 Mon Sep 17 00:00:00 2001 From: Colum Ferry Date: Fri, 21 Jun 2024 16:01:53 +0100 Subject: [PATCH] feat(remix): add convert-to-inferred generator (#26601) - feat(remix): add convert-to-inferred migration - feat(remix): add serve executor logic ## Current Behavior ## Expected Behavior ## Related Issue(s) Fixes # --- docs/generated/manifests/menus.json | 8 + docs/generated/manifests/nx-api.json | 9 + docs/generated/packages-metadata.json | 9 + .../remix/generators/convert-to-inferred.json | 30 ++ docs/shared/reference/sitemap.md | 1 + packages/remix/generators.json | 5 + .../convert-to-inferred.spec.ts | 440 ++++++++++++++++++ .../convert-to-inferred.ts | 71 +++ .../lib/build-post-target-transformer.spec.ts | 137 ++++++ .../lib/build-post-target-transformer.ts | 111 +++++ .../lib/serve-post-target-transformer.ts | 75 +++ .../convert-to-inferred/lib/utils.ts | 19 + .../convert-to-inferred/schema.json | 19 + 13 files changed, 934 insertions(+) create mode 100644 docs/generated/packages/remix/generators/convert-to-inferred.json create mode 100644 packages/remix/src/generators/convert-to-inferred/convert-to-inferred.spec.ts create mode 100644 packages/remix/src/generators/convert-to-inferred/convert-to-inferred.ts create mode 100644 packages/remix/src/generators/convert-to-inferred/lib/build-post-target-transformer.spec.ts create mode 100644 packages/remix/src/generators/convert-to-inferred/lib/build-post-target-transformer.ts create mode 100644 packages/remix/src/generators/convert-to-inferred/lib/serve-post-target-transformer.ts create mode 100644 packages/remix/src/generators/convert-to-inferred/lib/utils.ts create mode 100644 packages/remix/src/generators/convert-to-inferred/schema.json diff --git a/docs/generated/manifests/menus.json b/docs/generated/manifests/menus.json index d6052a791d878..f930fed36a0f7 100644 --- a/docs/generated/manifests/menus.json +++ b/docs/generated/manifests/menus.json @@ -9325,6 +9325,14 @@ "children": [], "isExternal": false, "disableCollapsible": false + }, + { + "id": "convert-to-inferred", + "path": "/nx-api/remix/generators/convert-to-inferred", + "name": "convert-to-inferred", + "children": [], + "isExternal": false, + "disableCollapsible": false } ], "isExternal": false, diff --git a/docs/generated/manifests/nx-api.json b/docs/generated/manifests/nx-api.json index 3b5ece33a10f5..c17e269ef2f6b 100644 --- a/docs/generated/manifests/nx-api.json +++ b/docs/generated/manifests/nx-api.json @@ -2723,6 +2723,15 @@ "originalFilePath": "/packages/remix/src/generators/error-boundary/schema.json", "path": "/nx-api/remix/generators/error-boundary", "type": "generator" + }, + "/nx-api/remix/generators/convert-to-inferred": { + "description": "Convert existing Remix project(s) using `@nx/remix:*` executors to use `@nx/remix/plugin`. Defaults to migrating all projects. Pass '--project' to migrate only one target.", + "file": "generated/packages/remix/generators/convert-to-inferred.json", + "hidden": false, + "name": "convert-to-inferred", + "originalFilePath": "/packages/remix/src/generators/convert-to-inferred/schema.json", + "path": "/nx-api/remix/generators/convert-to-inferred", + "type": "generator" } }, "path": "/nx-api/remix" diff --git a/docs/generated/packages-metadata.json b/docs/generated/packages-metadata.json index 469d989cf42c7..349f07543e3e1 100644 --- a/docs/generated/packages-metadata.json +++ b/docs/generated/packages-metadata.json @@ -2695,6 +2695,15 @@ "originalFilePath": "/packages/remix/src/generators/error-boundary/schema.json", "path": "remix/generators/error-boundary", "type": "generator" + }, + { + "description": "Convert existing Remix project(s) using `@nx/remix:*` executors to use `@nx/remix/plugin`. Defaults to migrating all projects. Pass '--project' to migrate only one target.", + "file": "generated/packages/remix/generators/convert-to-inferred.json", + "hidden": false, + "name": "convert-to-inferred", + "originalFilePath": "/packages/remix/src/generators/convert-to-inferred/schema.json", + "path": "remix/generators/convert-to-inferred", + "type": "generator" } ], "githubRoot": "https://github.com/nrwl/nx/blob/master", diff --git a/docs/generated/packages/remix/generators/convert-to-inferred.json b/docs/generated/packages/remix/generators/convert-to-inferred.json new file mode 100644 index 0000000000000..f270258e6b7e4 --- /dev/null +++ b/docs/generated/packages/remix/generators/convert-to-inferred.json @@ -0,0 +1,30 @@ +{ + "name": "convert-to-inferred", + "factory": "./src/generators/convert-to-inferred/convert-to-inferred", + "schema": { + "$schema": "https://json-schema.org/schema", + "$id": "NxRemixConvertToInferred", + "description": "Convert existing Remix project(s) using `@nx/remix:*` executors to use `@nx/remix/plugin`. Defaults to migrating all projects. Pass '--project' to migrate only one target.", + "title": "Convert Remix project from executor to plugin", + "type": "object", + "properties": { + "project": { + "type": "string", + "description": "The project to convert from using the `@nx/remix:*` executors to use `@nx/remix/plugin`.", + "x-priority": "important" + }, + "skipFormat": { + "type": "boolean", + "description": "Whether to format files at the end of the migration.", + "default": false + } + }, + "presets": [] + }, + "description": "Convert existing Remix project(s) using `@nx/remix:*` executors to use `@nx/remix/plugin`. Defaults to migrating all projects. Pass '--project' to migrate only one target.", + "implementation": "/packages/remix/src/generators/convert-to-inferred/convert-to-inferred.ts", + "aliases": [], + "hidden": false, + "path": "/packages/remix/src/generators/convert-to-inferred/schema.json", + "type": "generator" +} diff --git a/docs/shared/reference/sitemap.md b/docs/shared/reference/sitemap.md index 64f087724ddbf..fa66bb18bdc8b 100644 --- a/docs/shared/reference/sitemap.md +++ b/docs/shared/reference/sitemap.md @@ -644,6 +644,7 @@ - [storybook-configuration](/nx-api/remix/generators/storybook-configuration) - [meta](/nx-api/remix/generators/meta) - [error-boundary](/nx-api/remix/generators/error-boundary) + - [convert-to-inferred](/nx-api/remix/generators/convert-to-inferred) - [rollup](/nx-api/rollup) - [executors](/nx-api/rollup/executors) - [rollup](/nx-api/rollup/executors/rollup) diff --git a/packages/remix/generators.json b/packages/remix/generators.json index e4d5ce16f1c1c..04107b65f9ef4 100644 --- a/packages/remix/generators.json +++ b/packages/remix/generators.json @@ -85,6 +85,11 @@ "implementation": "./src/generators/error-boundary/error-boundary.impl", "schema": "./src/generators/error-boundary/schema.json", "description": "Add an ErrorBoundary to an existing route" + }, + "convert-to-inferred": { + "factory": "./src/generators/convert-to-inferred/convert-to-inferred", + "schema": "./src/generators/convert-to-inferred/schema.json", + "description": "Convert existing Remix project(s) using `@nx/remix:*` executors to use `@nx/remix/plugin`. Defaults to migrating all projects. Pass '--project' to migrate only one target." } } } diff --git a/packages/remix/src/generators/convert-to-inferred/convert-to-inferred.spec.ts b/packages/remix/src/generators/convert-to-inferred/convert-to-inferred.spec.ts new file mode 100644 index 0000000000000..aa3ff36130c85 --- /dev/null +++ b/packages/remix/src/generators/convert-to-inferred/convert-to-inferred.spec.ts @@ -0,0 +1,440 @@ +import { + type ProjectGraph, + type Tree, + type ProjectConfiguration, + joinPathFragments, + writeJson, + addProjectConfiguration, + readProjectConfiguration, + readNxJson, + type ExpandedPluginConfiguration, + updateNxJson, +} from '@nx/devkit'; +import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; +import { TempFs } from '@nx/devkit/internal-testing-utils'; +import { getRelativeProjectJsonSchemaPath } from 'nx/src/generators/utils/project-configuration'; +import { join } from 'path'; +import { convertToInferred } from './convert-to-inferred'; + +let fs: TempFs; +let projectGraph: ProjectGraph; + +jest.mock('@nx/devkit', () => ({ + ...jest.requireActual('@nx/devkit'), + createProjectGraphAsync: jest + .fn() + .mockImplementation(() => Promise.resolve(projectGraph)), + updateProjectConfiguration: jest + .fn() + .mockImplementation((tree, projectName, projectConfiguration) => { + function handleEmptyTargets( + projectName: string, + projectConfiguration: ProjectConfiguration + ): void { + if ( + projectConfiguration.targets && + !Object.keys(projectConfiguration.targets).length + ) { + // Re-order `targets` to appear after the `// target` comment. + delete projectConfiguration.targets; + projectConfiguration[ + '// targets' + ] = `to see all targets run: nx show project ${projectName} --web`; + projectConfiguration.targets = {}; + } else { + delete projectConfiguration['// targets']; + } + } + + const projectConfigFile = joinPathFragments( + projectConfiguration.root, + 'project.json' + ); + + if (!tree.exists(projectConfigFile)) { + throw new Error( + `Cannot update Project ${projectName} at ${projectConfiguration.root}. It either doesn't exist yet, or may not use project.json for configuration. Use \`addProjectConfiguration()\` instead if you want to create a new project.` + ); + } + handleEmptyTargets(projectName, projectConfiguration); + writeJson(tree, projectConfigFile, { + name: projectConfiguration.name ?? projectName, + $schema: getRelativeProjectJsonSchemaPath(tree, projectConfiguration), + ...projectConfiguration, + root: undefined, + }); + projectGraph.nodes[projectName].data = projectConfiguration; + }), +})); + +function addProject(tree: Tree, name: string, project: ProjectConfiguration) { + addProjectConfiguration(tree, name, project); + projectGraph.nodes[name] = { + name, + type: project.projectType === 'application' ? 'app' : 'lib', + data: { + projectType: project.projectType, + root: project.root, + targets: project.targets, + }, + }; +} + +interface TestProjectOptions { + appName: string; + appRoot: string; + outputPath: string; + buildTargetName: string; + serveTargetName: string; +} + +const defaultTestProjectOptions: TestProjectOptions = { + appName: 'app1', + appRoot: 'apps/app1', + outputPath: 'dist/apps/app1', + buildTargetName: 'build', + serveTargetName: 'serve', +}; + +function writeRemixConfig(tree: Tree, projectRoot: string) { + const remixConfig = { + ignoredRouteFiles: ['**/.*'], + }; + const remixConfigContents = `const config = ${JSON.stringify(remixConfig)}; +export default config;`; + + tree.write(`${projectRoot}/remix.config.js`, remixConfigContents); + fs.createFileSync(`${projectRoot}/remix.config.js`, remixConfigContents); + tree.write(`${projectRoot}/package.json`, `{"type":"module"}`); + fs.createFileSync(`${projectRoot}/package.json`, `{"type":"module"}`); + jest.doMock( + join(fs.tempDir, projectRoot, 'remix.config.js'), + () => remixConfig, + { virtual: true } + ); +} + +function createTestProject( + tree: Tree, + opts: Partial = defaultTestProjectOptions, + extraTargetOptions: any = {}, + extraConfigurations: any = {} +) { + let projectOpts = { ...defaultTestProjectOptions, ...opts }; + const project: ProjectConfiguration = { + name: projectOpts.appName, + root: projectOpts.appRoot, + projectType: 'application', + targets: { + [projectOpts.buildTargetName]: { + executor: '@nx/remix:build', + options: { + outputPath: projectOpts.outputPath, + ...extraTargetOptions, + }, + configurations: { + ...extraConfigurations, + }, + }, + [projectOpts.serveTargetName]: { + executor: '@nx/remix:serve', + options: { + port: 4200, + ...extraTargetOptions, + }, + configurations: { + ...extraConfigurations, + }, + }, + }, + }; + + writeRemixConfig(tree, project.root); + + addProject(tree, project.name, project); + fs.createFileSync( + `${projectOpts.appRoot}/project.json`, + JSON.stringify(project) + ); + return project; +} + +describe('Remix - Convert To Inferred', () => { + let tree: Tree; + beforeEach(() => { + fs = new TempFs('remix'); + tree = createTreeWithEmptyWorkspace(); + tree.root = fs.tempDir; + + projectGraph = { + nodes: {}, + dependencies: {}, + externalNodes: {}, + }; + }); + + afterEach(() => { + fs.cleanup(); + jest.resetModules(); + }); + + describe('--project', () => { + it('should correctly migrate a single project', async () => { + // ARRANGE + const project = createTestProject(tree); + const project2 = createTestProject(tree, { + appRoot: 'apps/project2', + appName: 'project2', + }); + + const project2Targets = project2.targets; + + // ACT + await convertToInferred(tree, { + project: project.name, + skipFormat: true, + }); + + // ASSERT + const updatedProject = readProjectConfiguration(tree, project.name); + expect(updatedProject.targets).toMatchInlineSnapshot(` + { + "serve": { + "options": { + "env": { + "PORT": "4200", + }, + }, + }, + } + `); + + const updatedProject2 = readProjectConfiguration(tree, project2.name); + expect(updatedProject2.targets).toStrictEqual(project2Targets); + + const nxJsonPlugins = readNxJson(tree).plugins; + const remixPlugin = nxJsonPlugins.find( + (plugin): plugin is ExpandedPluginConfiguration => + typeof plugin !== 'string' && + plugin.plugin === '@nx/remix/plugin' && + plugin.include?.length === 1 + ); + expect(remixPlugin).toBeTruthy(); + expect(remixPlugin.include).toEqual([`${project.root}/**/*`]); + }); + + it('should add a new plugin registration when the target name differs', async () => { + // ARRANGE + const project = createTestProject(tree); + const project2 = createTestProject(tree, { + appRoot: 'apps/project2', + appName: 'project2', + }); + + const project2Targets = project2.targets; + + const nxJson = readNxJson(tree); + nxJson.plugins ??= []; + nxJson.plugins.push({ + plugin: '@nx/remix/plugin', + options: { + buildTargetName: 'build', + devTargetName: defaultTestProjectOptions.serveTargetName, + startTargetName: 'start', + typecheckTargetName: 'typecheck', + staticServeTargetName: 'static-serve', + }, + }); + updateNxJson(tree, nxJson); + + // ACT + await convertToInferred(tree, { + project: project.name, + skipFormat: true, + }); + + // ASSERT + const updatedProject = readProjectConfiguration(tree, project.name); + expect(updatedProject.targets).toMatchInlineSnapshot(` + { + "serve": { + "options": { + "env": { + "PORT": "4200", + }, + }, + }, + } + `); + + const updatedProject2 = readProjectConfiguration(tree, project2.name); + expect(updatedProject2.targets).toStrictEqual(project2Targets); + + const nxJsonPlugins = readNxJson(tree).plugins; + const remixPluginRegistrations = nxJsonPlugins.filter( + (plugin): plugin is ExpandedPluginConfiguration => + typeof plugin !== 'string' && plugin.plugin === '@nx/remix/plugin' + ); + expect(remixPluginRegistrations.length).toBe(2); + expect(remixPluginRegistrations[1].include).toMatchInlineSnapshot(` + [ + "apps/app1/**/*", + ] + `); + }); + + it('should merge target defaults', async () => { + // ARRANGE + const project = createTestProject(tree); + const project2 = createTestProject(tree, { + appRoot: 'apps/project2', + appName: 'project2', + }); + + const project2Targets = project2.targets; + + const nxJson = readNxJson(tree); + nxJson.targetDefaults ??= {}; + nxJson.targetDefaults['@nx/remix:build'] = { + options: { + sourcemap: true, + }, + }; + updateNxJson(tree, nxJson); + + // ACT + await convertToInferred(tree, { + project: project.name, + skipFormat: true, + }); + + // ASSERT + const updatedProject = readProjectConfiguration(tree, project.name); + expect(updatedProject.targets).toMatchInlineSnapshot(` + { + "build": { + "options": { + "sourcemap": true, + }, + }, + "serve": { + "options": { + "env": { + "PORT": "4200", + }, + }, + }, + } + `); + + const updatedProject2 = readProjectConfiguration(tree, project2.name); + expect(updatedProject2.targets).toStrictEqual(project2Targets); + }); + + it('should manage configurations correctly', async () => { + // ARRANGE + const project = createTestProject(tree, undefined, undefined, { + dev: { + outputPath: 'apps/dev/app1', + }, + }); + const project2 = createTestProject(tree, { + appRoot: 'apps/project2', + appName: 'project2', + }); + + const project2Targets = project2.targets; + // ACT + await convertToInferred(tree, { + project: project.name, + skipFormat: true, + }); + + // ASSERT + const updatedProject = readProjectConfiguration(tree, project.name); + expect(updatedProject.targets).toMatchInlineSnapshot(` + { + "build": { + "configurations": { + "dev": {}, + }, + }, + "serve": { + "configurations": { + "dev": { + "outputPath": "apps/dev/app1", + }, + }, + "options": { + "env": { + "PORT": "4200", + }, + }, + }, + } + `); + + const updatedProject2 = readProjectConfiguration(tree, project2.name); + expect(updatedProject2.targets).toStrictEqual(project2Targets); + + const remixConfigContents = tree.read( + `${project.root}/remix.config.js`, + 'utf-8' + ); + expect(remixConfigContents).toMatchInlineSnapshot(` + "const config = {"ignoredRouteFiles":["**/.*"]}; + export default config;" + `); + }); + }); + + describe('all projects', () => { + it('should correctly migrate all projects', async () => { + // ARRANGE + const project = createTestProject(tree); + const project2 = createTestProject(tree, { + appRoot: 'apps/project2', + appName: 'project2', + }); + // ACT + await convertToInferred(tree, { + skipFormat: true, + }); + + // ASSERT + const updatedProject = readProjectConfiguration(tree, project.name); + expect(updatedProject.targets).toMatchInlineSnapshot(` + { + "serve": { + "options": { + "env": { + "PORT": "4200", + }, + }, + }, + } + `); + + const updatedProject2 = readProjectConfiguration(tree, project2.name); + expect(updatedProject2.targets).toMatchInlineSnapshot(` + { + "serve": { + "options": { + "env": { + "PORT": "4200", + }, + }, + }, + } + `); + + const nxJsonPlugins = readNxJson(tree).plugins; + const remixPlugin = nxJsonPlugins.find( + (plugin): plugin is ExpandedPluginConfiguration => + typeof plugin !== 'string' && plugin.plugin === '@nx/remix/plugin' + ); + expect(remixPlugin).toBeTruthy(); + expect(remixPlugin.include).toBeUndefined(); + }); + }); +}); diff --git a/packages/remix/src/generators/convert-to-inferred/convert-to-inferred.ts b/packages/remix/src/generators/convert-to-inferred/convert-to-inferred.ts new file mode 100644 index 0000000000000..03633b86ec258 --- /dev/null +++ b/packages/remix/src/generators/convert-to-inferred/convert-to-inferred.ts @@ -0,0 +1,71 @@ +import { + addDependenciesToPackageJson, + createProjectGraphAsync, + formatFiles, + runTasksInSerial, + type Tree, +} from '@nx/devkit'; +import { AggregatedLog } from '@nx/devkit/src/generators/plugin-migrations/aggregate-log-util'; +import { migrateExecutorToPluginV1 } from '@nx/devkit/src/generators/plugin-migrations/executor-to-plugin-migrator'; +import { buildPostTargetTransformer } from './lib/build-post-target-transformer'; +import { servePostTargetTransformer } from './lib/serve-post-target-transformer'; +import { createNodes } from '../../plugins/plugin'; + +interface Schema { + project?: string; + skipFormat?: boolean; +} + +export async function convertToInferred(tree: Tree, options: Schema) { + const projectGraph = await createProjectGraphAsync(); + const migrationLogs = new AggregatedLog(); + const migratedBuildProjects = await migrateExecutorToPluginV1( + tree, + projectGraph, + '@nx/remix:build', + '@nx/remix/plugin', + (targetName) => ({ + buildTargetName: targetName, + devTargetName: 'dev', + startTargetName: 'start', + typecheckTargetName: 'typecheck', + staticServeTargetName: 'static-serve', + }), + buildPostTargetTransformer(migrationLogs), + createNodes, + options.project + ); + + const migratedServeProjects = await migrateExecutorToPluginV1( + tree, + projectGraph, + '@nx/remix:serve', + '@nx/remix/plugin', + (targetName) => ({ + buildTargetName: 'build', + devTargetName: targetName, + startTargetName: 'start', + typecheckTargetName: 'typecheck', + staticServeTargetName: 'static-serve', + }), + servePostTargetTransformer(migrationLogs), + createNodes, + options.project + ); + + const migratedProjects = + migratedBuildProjects.size + migratedServeProjects.size; + if (migratedProjects === 0) { + throw new Error('Could not find any targets to migrate.'); + } + + if (!options.skipFormat) { + await formatFiles(tree); + } + + return () => { + migrationLogs.flushLogs(); + }; +} + +export default convertToInferred; diff --git a/packages/remix/src/generators/convert-to-inferred/lib/build-post-target-transformer.spec.ts b/packages/remix/src/generators/convert-to-inferred/lib/build-post-target-transformer.spec.ts new file mode 100644 index 0000000000000..f94bf3b78c4d3 --- /dev/null +++ b/packages/remix/src/generators/convert-to-inferred/lib/build-post-target-transformer.spec.ts @@ -0,0 +1,137 @@ +import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; +import { AggregatedLog } from '@nx/devkit/src/generators/plugin-migrations/aggregate-log-util'; +import { buildPostTargetTransformer } from './build-post-target-transformer'; + +describe('buildPostTargetTransformer', () => { + it('should migrate outputPath correctly', () => { + // ARRANGE + const tree = createTreeWithEmptyWorkspace(); + + const targetConfiguration = { + options: { + outputPath: 'dist/apps/myapp', + }, + }; + + const inferredTargetConfiguration = {}; + + const migrationLogs = new AggregatedLog(); + + tree.write('apps/myapp/remix.config.js', remixConfig); + tree.write('apps/myapp/package.json', `{"type": "module"}`); + + // ACT + const target = buildPostTargetTransformer(migrationLogs)( + targetConfiguration, + tree, + { projectName: 'myapp', root: 'apps/myapp' }, + inferredTargetConfiguration + ); + + // ASSERT + const configFile = tree.read('apps/myapp/remix.config.js', 'utf-8'); + expect(configFile).toMatchInlineSnapshot(` + "import { createWatchPaths } from '@nx/remix'; + import { dirname } from 'path'; + import { fileURLToPath } from 'url'; + + const __dirname = dirname(fileURLToPath(import.meta.url)); + + /** + * @type {import('@remix-run/dev').AppConfig} + */ + export default { + ignoredRouteFiles: ['**/.*'], + // appDirectory: "app", + // assetsBuildDirectory: "public/build", + // serverBuildPath: "build/index.js", + // publicPath: "/build/", + watchPaths: () => createWatchPaths(__dirname), + };" + `); + expect(target).toMatchInlineSnapshot(` + { + "options": {}, + } + `); + }); + + it('should handle configurations correctly', () => { + // ARRANGE + const tree = createTreeWithEmptyWorkspace(); + + const targetConfiguration = { + options: { + outputPath: 'dist/apps/myapp', + }, + configurations: { + dev: { + outputPath: 'dist/dev/apps/myapp', + }, + }, + }; + + const inferredTargetConfiguration = {}; + + const migrationLogs = new AggregatedLog(); + + tree.write('apps/myapp/remix.config.js', remixConfig); + tree.write('apps/myapp/package.json', `{"type": "module"}`); + + // ACT + const target = buildPostTargetTransformer(migrationLogs)( + targetConfiguration, + tree, + { projectName: 'myapp', root: 'apps/myapp' }, + inferredTargetConfiguration + ); + + // ASSERT + const configFile = tree.read('apps/myapp/remix.config.js', 'utf-8'); + expect(configFile).toMatchInlineSnapshot(` + "import { createWatchPaths } from '@nx/remix'; + import { dirname } from 'path'; + import { fileURLToPath } from 'url'; + + const __dirname = dirname(fileURLToPath(import.meta.url)); + + /** + * @type {import('@remix-run/dev').AppConfig} + */ + export default { + ignoredRouteFiles: ['**/.*'], + // appDirectory: "app", + // assetsBuildDirectory: "public/build", + // serverBuildPath: "build/index.js", + // publicPath: "/build/", + watchPaths: () => createWatchPaths(__dirname), + };" + `); + expect(target).toMatchInlineSnapshot(` + { + "configurations": { + "dev": {}, + }, + "options": {}, + } + `); + }); +}); + +const remixConfig = `import { createWatchPaths } from '@nx/remix'; +import { dirname } from 'path'; +import { fileURLToPath } from 'url'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +/** + * @type {import('@remix-run/dev').AppConfig} + */ +export default { + ignoredRouteFiles: ['**/.*'], + // appDirectory: "app", + // assetsBuildDirectory: "public/build", + // serverBuildPath: "build/index.js", + // publicPath: "/build/", + watchPaths: () => createWatchPaths(__dirname), +};`; diff --git a/packages/remix/src/generators/convert-to-inferred/lib/build-post-target-transformer.ts b/packages/remix/src/generators/convert-to-inferred/lib/build-post-target-transformer.ts new file mode 100644 index 0000000000000..e44c781f1273d --- /dev/null +++ b/packages/remix/src/generators/convert-to-inferred/lib/build-post-target-transformer.ts @@ -0,0 +1,111 @@ +import { type Tree, type TargetConfiguration } from '@nx/devkit'; +import { AggregatedLog } from '@nx/devkit/src/generators/plugin-migrations/aggregate-log-util'; +import { getConfigFilePath } from './utils'; +import { processTargetOutputs } from '@nx/devkit/src/generators/plugin-migrations/plugin-migration-utils'; + +export function buildPostTargetTransformer(migrationLogs: AggregatedLog) { + return ( + target: TargetConfiguration, + tree: Tree, + projectDetails: { projectName: string; root: string }, + inferredTargetConfiguration: TargetConfiguration + ) => { + const remixConfigPath = getConfigFilePath(tree, projectDetails.root); + + if (target.options) { + handlePropertiesFromTargetOptions( + tree, + target.options, + projectDetails.projectName, + projectDetails.root, + migrationLogs + ); + } + + if (target.configurations) { + for (const configurationName in target.configurations) { + const configuration = target.configurations[configurationName]; + handlePropertiesFromTargetOptions( + tree, + configuration, + projectDetails.projectName, + projectDetails.root, + migrationLogs + ); + } + + if (Object.keys(target.configurations).length === 0) { + if ('defaultConfiguration' in target) { + delete target.defaultConfiguration; + } + delete target.configurations; + } + + if ( + 'defaultConfiguration' in target && + !target.configurations[target.defaultConfiguration] + ) { + delete target.defaultConfiguration; + } + } + + if (target.outputs) { + target.outputs = target.outputs.filter( + (out) => !out.includes('options.outputPath') + ); + processTargetOutputs(target, [], inferredTargetConfiguration, { + projectName: projectDetails.projectName, + projectRoot: projectDetails.root, + }); + } + + return target; + }; +} + +function handlePropertiesFromTargetOptions( + tree: Tree, + options: any, + projectName: string, + projectRoot: string, + migrationLogs: AggregatedLog +) { + if ('outputPath' in options) { + migrationLogs.addLog({ + project: projectName, + executorName: '@nx/remix:build', + log: "Unable to migrate 'outputPath'. The Remix Config will contain the locations the build artifact will be output to.", + }); + delete options.outputPath; + } + + if ('includeDevDependenciesInPackageJson' in options) { + migrationLogs.addLog({ + project: projectName, + executorName: '@nx/remix:build', + log: "Unable to migrate `includeDevDependenciesInPackageJson` to Remix Config. Use the `@nx/dependency-checks` ESLint rule to update your project's package.json.", + }); + + delete options.includeDevDependenciesInPackageJson; + } + + if ('generatePackageJson' in options) { + migrationLogs.addLog({ + project: projectName, + executorName: '@nx/remix:build', + log: "Unable to migrate `generatePackageJson` to Remix Config. Use the `@nx/dependency-checks` ESLint rule to update your project's package.json.", + }); + + delete options.generatePackageJson; + } + + if ('generateLockfile' in options) { + migrationLogs.addLog({ + project: projectName, + executorName: '@nx/remix:build', + log: 'Unable to migrate `generateLockfile` to Remix Config. This option is not supported.', + }); + + delete options.generateLockfile; + } +} diff --git a/packages/remix/src/generators/convert-to-inferred/lib/serve-post-target-transformer.ts b/packages/remix/src/generators/convert-to-inferred/lib/serve-post-target-transformer.ts new file mode 100644 index 0000000000000..7594b0ca18083 --- /dev/null +++ b/packages/remix/src/generators/convert-to-inferred/lib/serve-post-target-transformer.ts @@ -0,0 +1,75 @@ +import { type Tree, type TargetConfiguration } from '@nx/devkit'; +import { AggregatedLog } from '@nx/devkit/src/generators/plugin-migrations/aggregate-log-util'; +import { REMIX_PROPERTY_MAPPINGS } from './utils'; + +export function servePostTargetTransformer(migrationLogs: AggregatedLog) { + return ( + target: TargetConfiguration, + tree: Tree, + projectDetails: { projectName: string; root: string }, + inferredTargetConfiguration: TargetConfiguration + ) => { + if (target.options) { + handlePropertiesFromTargetOptions( + tree, + target.options, + projectDetails.projectName, + projectDetails.root + ); + } + + if (target.configurations) { + for (const configurationName in target.configurations) { + const configuration = target.configurations[configurationName]; + + handlePropertiesFromTargetOptions( + tree, + configuration, + projectDetails.projectName, + projectDetails.root + ); + } + + if (Object.keys(target.configurations).length === 0) { + if ('defaultConfiguration' in target) { + delete target.defaultConfiguration; + } + delete target.configurations; + } + + if ( + 'defaultConfiguration' in target && + !target.configurations[target.defaultConfiguration] + ) { + delete target.defaultConfiguration; + } + } + + return target; + }; +} + +function handlePropertiesFromTargetOptions( + tree: Tree, + options: any, + projectName: string, + projectRoot: string +) { + if ('debug' in options) { + delete options.debug; + } + + if ('port' in options) { + options.env ??= {}; + options.env.PORT = `${options.port}`; + delete options.port; + } + + for (const [prevKey, newKey] of Object.entries(REMIX_PROPERTY_MAPPINGS)) { + if (prevKey in options) { + let prevValue = options[prevKey]; + delete options[prevKey]; + options[newKey] = prevValue; + } + } +} diff --git a/packages/remix/src/generators/convert-to-inferred/lib/utils.ts b/packages/remix/src/generators/convert-to-inferred/lib/utils.ts new file mode 100644 index 0000000000000..3dedcf5aa8e2e --- /dev/null +++ b/packages/remix/src/generators/convert-to-inferred/lib/utils.ts @@ -0,0 +1,19 @@ +import { type Tree, joinPathFragments } from '@nx/devkit'; +import { tsquery } from '@phenomnomnominal/tsquery'; + +export const REMIX_PROPERTY_MAPPINGS = { + sourcemap: 'sourcemap', + devServerPort: 'port', + command: 'command', + manual: 'manual', + tlsKey: 'tls-key', + tlsCert: 'tls-cert', +}; + +export function getConfigFilePath(tree: Tree, root: string) { + return [ + joinPathFragments(root, `remix.config.js`), + joinPathFragments(root, `remix.config.cjs`), + joinPathFragments(root, `remix.config.mjs`), + ].find((f) => tree.exists(f)); +} diff --git a/packages/remix/src/generators/convert-to-inferred/schema.json b/packages/remix/src/generators/convert-to-inferred/schema.json new file mode 100644 index 0000000000000..e527112844e54 --- /dev/null +++ b/packages/remix/src/generators/convert-to-inferred/schema.json @@ -0,0 +1,19 @@ +{ + "$schema": "https://json-schema.org/schema", + "$id": "NxRemixConvertToInferred", + "description": "Convert existing Remix project(s) using `@nx/remix:*` executors to use `@nx/remix/plugin`. Defaults to migrating all projects. Pass '--project' to migrate only one target.", + "title": "Convert Remix project from executor to plugin", + "type": "object", + "properties": { + "project": { + "type": "string", + "description": "The project to convert from using the `@nx/remix:*` executors to use `@nx/remix/plugin`.", + "x-priority": "important" + }, + "skipFormat": { + "type": "boolean", + "description": "Whether to format files at the end of the migration.", + "default": false + } + } +}