feat(remix): add convert-to-inferred generator (#26601)

- feat(remix): add convert-to-inferred migration
- feat(remix): add serve executor logic

<!-- Please make sure you have read the submission guidelines before
posting an PR -->
<!--
https://github.com/nrwl/nx/blob/master/CONTRIBUTING.md#-submitting-a-pr
-->

<!-- Please make sure that your commit message follows our format -->
<!-- Example: `fix(nx): must begin with lowercase` -->

<!-- If this is a particularly complex change or feature addition, you
can request a dedicated Nx release for this pull request branch. Mention
someone from the Nx team or the `@nrwl/nx-pipelines-reviewers` and they
will confirm if the PR warrants its own release for testing purposes,
and generate it for you if appropriate. -->

## Current Behavior
<!-- This is the behavior we have today -->

## Expected Behavior
<!-- This is the behavior we should expect with the changes in this PR
-->

## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->

Fixes #
This commit is contained in:
Colum Ferry 2024-06-21 16:01:53 +01:00 committed by GitHub
parent efd0994ee6
commit b1713be2c3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 934 additions and 0 deletions

View File

@ -9325,6 +9325,14 @@
"children": [], "children": [],
"isExternal": false, "isExternal": false,
"disableCollapsible": 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, "isExternal": false,

View File

@ -2723,6 +2723,15 @@
"originalFilePath": "/packages/remix/src/generators/error-boundary/schema.json", "originalFilePath": "/packages/remix/src/generators/error-boundary/schema.json",
"path": "/nx-api/remix/generators/error-boundary", "path": "/nx-api/remix/generators/error-boundary",
"type": "generator" "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" "path": "/nx-api/remix"

View File

@ -2695,6 +2695,15 @@
"originalFilePath": "/packages/remix/src/generators/error-boundary/schema.json", "originalFilePath": "/packages/remix/src/generators/error-boundary/schema.json",
"path": "remix/generators/error-boundary", "path": "remix/generators/error-boundary",
"type": "generator" "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", "githubRoot": "https://github.com/nrwl/nx/blob/master",

View File

@ -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"
}

View File

@ -644,6 +644,7 @@
- [storybook-configuration](/nx-api/remix/generators/storybook-configuration) - [storybook-configuration](/nx-api/remix/generators/storybook-configuration)
- [meta](/nx-api/remix/generators/meta) - [meta](/nx-api/remix/generators/meta)
- [error-boundary](/nx-api/remix/generators/error-boundary) - [error-boundary](/nx-api/remix/generators/error-boundary)
- [convert-to-inferred](/nx-api/remix/generators/convert-to-inferred)
- [rollup](/nx-api/rollup) - [rollup](/nx-api/rollup)
- [executors](/nx-api/rollup/executors) - [executors](/nx-api/rollup/executors)
- [rollup](/nx-api/rollup/executors/rollup) - [rollup](/nx-api/rollup/executors/rollup)

View File

@ -85,6 +85,11 @@
"implementation": "./src/generators/error-boundary/error-boundary.impl", "implementation": "./src/generators/error-boundary/error-boundary.impl",
"schema": "./src/generators/error-boundary/schema.json", "schema": "./src/generators/error-boundary/schema.json",
"description": "Add an ErrorBoundary to an existing route" "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."
} }
} }
} }

View File

@ -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<TestProjectOptions> = 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();
});
});
});

View File

@ -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;

View File

@ -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),
};`;

View File

@ -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;
}
}

View File

@ -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;
}
}
}

View File

@ -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));
}

View File

@ -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
}
}
}