fix(react): app generator should handle crystal workspaces (#21537)

This commit is contained in:
Colum Ferry 2024-02-02 19:32:48 +00:00 committed by GitHub
parent b076d728e7
commit 369ed35894
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 83 additions and 401 deletions

View File

@ -9284,14 +9284,6 @@
"children": [],
"isExternal": false,
"disableCollapsible": false
},
{
"id": "cypress",
"path": "/nx-api/remix/generators/cypress",
"name": "cypress",
"children": [],
"isExternal": false,
"disableCollapsible": false
}
],
"isExternal": false,

View File

@ -2645,15 +2645,6 @@
"originalFilePath": "/packages/remix/src/generators/error-boundary/schema.json",
"path": "/nx-api/remix/generators/error-boundary",
"type": "generator"
},
"/nx-api/remix/generators/cypress": {
"description": "Generate a project for testing Remix apps using Cypress",
"file": "generated/packages/remix/generators/cypress.json",
"hidden": false,
"name": "cypress",
"originalFilePath": "/packages/remix/src/generators/cypress/schema.json",
"path": "/nx-api/remix/generators/cypress",
"type": "generator"
}
},
"path": "/nx-api/remix"

View File

@ -2618,15 +2618,6 @@
"originalFilePath": "/packages/remix/src/generators/error-boundary/schema.json",
"path": "remix/generators/error-boundary",
"type": "generator"
},
{
"description": "Generate a project for testing Remix apps using Cypress",
"file": "generated/packages/remix/generators/cypress.json",
"hidden": false,
"name": "cypress",
"originalFilePath": "/packages/remix/src/generators/cypress/schema.json",
"path": "remix/generators/cypress",
"type": "generator"
}
],
"githubRoot": "https://github.com/nrwl/nx/blob/master",

View File

@ -1,66 +0,0 @@
{
"name": "cypress",
"implementation": "/packages/remix/src/generators/cypress/cypress.impl#cypressGeneratorInternal.ts",
"schema": {
"$schema": "https://json-schema.org/schema",
"$id": "NxRemixCypress",
"title": "",
"type": "object",
"description": "Generate a Cypress e2e project for a given application.",
"properties": {
"project": {
"type": "string",
"description": "The name of the frontend project to test.",
"$default": { "$source": "projectName" }
},
"projectNameAndRootFormat": {
"description": "Whether to generate the project name and root directory as provided (`as-provided`) or generate them composing their values and taking the configured layout into account (`derived`).",
"type": "string",
"enum": ["as-provided", "derived"]
},
"baseUrl": {
"type": "string",
"description": "URL to access the application on",
"default": "http://localhost:3000"
},
"name": {
"type": "string",
"description": "Name of the E2E Project",
"$default": { "$source": "argv", "index": 0 },
"x-prompt": "What name would you like to use for the e2e project?"
},
"directory": {
"type": "string",
"description": "A directory where the project is placed"
},
"linter": {
"description": "The tool to use for running lint checks.",
"type": "string",
"enum": ["eslint", "none"],
"default": "eslint"
},
"js": {
"description": "Generate JavaScript files rather than TypeScript files",
"type": "boolean",
"default": false
},
"skipFormat": {
"description": "Skip formatting files",
"type": "boolean",
"default": false
},
"setParserOptionsProject": {
"type": "boolean",
"description": "Whether or not to configure the ESLint \"parserOptions.project\" option. We do not do this by default for lint performance reasons.",
"default": false
}
},
"required": ["name"],
"presets": []
},
"description": "Generate a project for testing Remix apps using Cypress",
"aliases": [],
"hidden": false,
"path": "/packages/remix/src/generators/cypress/schema.json",
"type": "generator"
}

View File

@ -646,7 +646,6 @@
- [storybook-configuration](/nx-api/remix/generators/storybook-configuration)
- [meta](/nx-api/remix/generators/meta)
- [error-boundary](/nx-api/remix/generators/error-boundary)
- [cypress](/nx-api/remix/generators/cypress)
- [rollup](/nx-api/rollup)
- [executors](/nx-api/rollup/executors)
- [rollup](/nx-api/rollup/executors/rollup)

View File

@ -8,6 +8,7 @@ import {
uniq,
updateFile,
runCommandAsync,
listFiles,
} from '@nx/e2e/utils';
describe('remix e2e', () => {
@ -62,13 +63,12 @@ describe('remix e2e', () => {
runCLI(
`generate @nx/remix:app ${plugin} --directory=sub --projectNameAndRootFormat=derived --rootProject=false --no-interactive`
);
const project = readJson(`sub/${plugin}/project.json`);
expect(project.targets.build.options.outputPath).toEqual(
`dist/sub/${plugin}`
);
const result = runCLI(`build ${appName}`);
expect(result).toContain('Successfully ran target build');
// TODO(colum): uncomment line below when fixed
// checkFilesExist(`dist/apps/sub/${plugin}/build/index.js`);
}, 120000);
it('should create src in the specified directory --projectNameAndRootFormat=as-provided', async () => {
@ -76,11 +76,10 @@ describe('remix e2e', () => {
runCLI(
`generate @nx/remix:app ${plugin} --directory=subdir --projectNameAndRootFormat=as-provided --rootProject=false --no-interactive`
);
const project = readJson(`subdir/project.json`);
expect(project.targets.build.options.outputPath).toEqual(`dist/subdir`);
const result = runCLI(`build ${plugin}`);
expect(result).toContain('Successfully ran target build');
checkFilesExist(`dist/subdir/build/index.js`);
}, 120000);
});

View File

@ -85,11 +85,6 @@
"implementation": "./src/generators/error-boundary/error-boundary.impl",
"schema": "./src/generators/error-boundary/schema.json",
"description": "Add an ErrorBoundary to an existing route"
},
"cypress": {
"implementation": "./src/generators/cypress/cypress.impl#cypressGeneratorInternal",
"schema": "./src/generators/cypress/schema.json",
"description": "Generate a project for testing Remix apps using Cypress"
}
}
}

View File

@ -1,15 +1,14 @@
export * from './src/generators/action/action.impl';
export * from './src/generators/application/application.impl';
export * from './src/generators/cypress-component-configuration/cypress-component-configuration.impl';
export * from './src/generators/cypress/cypress.impl';
export * from './src/generators/error-boundary/error-boundary.impl';
export * from './src/generators/library/library.impl';
export * from './src/generators/loader/loader.impl';
export * from './src/generators/meta/meta.impl';
export * from './src/generators/preset/preset.impl';
export * from './src/generators/resource-route/resource-route.impl';
export * from './src/generators/route/route.impl';
export * from './src/generators/setup-tailwind/setup-tailwind.impl';
export * from './src/generators/storybook-configuration/storybook-configuration.impl';
export * from './src/generators/style/style.impl';
export * from './src/generators/init/init';
export { default as actionGenerator } from './src/generators/action/action.impl';
export { default as applicationGenerator } from './src/generators/application/application.impl';
export { default as cypressComponentConfigurationGenerator } from './src/generators/cypress-component-configuration/cypress-component-configuration.impl';
export { default as errorBoundaryGenerator } from './src/generators/error-boundary/error-boundary.impl';
export { default as libraryGenerator } from './src/generators/library/library.impl';
export { default as loaderGenerator } from './src/generators/loader/loader.impl';
export { default as metaGenerator } from './src/generators/meta/meta.impl';
export { default as presetGenerator } from './src/generators/preset/preset.impl';
export { default as resourceRouteGenerator } from './src/generators/resource-route/resource-route.impl';
export { default as routeGenerator } from './src/generators/route/route.impl';
export { default as setupTailwindGenerator } from './src/generators/setup-tailwind/setup-tailwind.impl';
export { default as storybookConfigurationGenerator } from './src/generators/storybook-configuration/storybook-configuration.impl';
export { default as styleGenerator } from './src/generators/style/style.impl';
export { default as initGenerator } from './src/generators/init/init';

View File

@ -10,6 +10,7 @@ import {
readJson,
readProjectConfiguration,
runTasksInSerial,
stripIndents,
toJS,
Tree,
updateJson,
@ -49,7 +50,6 @@ export function remixApplicationGenerator(
});
}
// TODO(@columferry): update this to use crystal?
export async function remixApplicationGeneratorInternal(
tree: Tree,
_options: NxRemixGeneratorSchema
@ -70,7 +70,8 @@ export async function remixApplicationGeneratorInternal(
sourceRoot: `${options.projectRoot}`,
projectType: 'application',
tags: options.parsedTags,
targets: {
targets: !options.addPlugin
? {
build: {
executor: '@nx/remix:build',
outputs: ['{options.outputPath}'],
@ -101,7 +102,8 @@ export async function remixApplicationGeneratorInternal(
cwd: options.projectRoot,
},
},
},
}
: {},
});
const installTask = updateDependencies(tree);
@ -226,6 +228,12 @@ export async function remixApplicationGeneratorInternal(
addPlugin: options.addPlugin,
});
tasks.push(eslintTask);
tree.write(
joinPathFragments(options.projectRoot, '.eslintignore'),
stripIndents`build
public/build`
);
}
if (options.js) {

View File

@ -1,42 +0,0 @@
import { readProjectConfiguration, Tree } from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import generator from './cypress.impl';
import applicationGenerator from '../application/application.impl';
describe('Cypress generator', () => {
let tree: Tree;
beforeEach(() => {
tree = createTreeWithEmptyWorkspace();
});
it('should generate cypress project', async () => {
await applicationGenerator(tree, {
name: 'demo',
e2eTestRunner: 'none',
addPlugin: true,
});
await generator(tree, {
project: 'demo',
name: 'demo-e2e',
addPlugin: true,
});
const config = readProjectConfiguration(tree, 'demo-e2e');
expect(config.targets).toEqual({
e2e: {
executor: '@nx/cypress:cypress',
options: {
cypressConfig: 'demo-e2e/cypress.config.ts',
testingType: 'e2e',
devServerTarget: 'demo:serve:development',
},
configurations: {
ci: {
devServerTarget: 'demo:serve-static',
},
},
},
});
});
});

View File

@ -1,124 +0,0 @@
import {
addDependenciesToPackageJson,
addProjectConfiguration,
GeneratorCallback,
joinPathFragments,
readProjectConfiguration,
runTasksInSerial,
Tree,
updateProjectConfiguration,
} from '@nx/devkit';
import { configurationGenerator } from '@nx/cypress';
import { CypressGeneratorSchema } from './schema';
import { determineProjectNameAndRootOptions } from '@nx/devkit/src/generators/project-name-and-root-utils';
import { nxVersion } from '../../utils/versions';
// TODO(@columferry): Does anything use this?
export function cypressGenerator(tree: Tree, options: CypressGeneratorSchema) {
return cypressGeneratorInternal(tree, { addPlugin: false, ...options });
}
export async function cypressGeneratorInternal(
tree: Tree,
options: CypressGeneratorSchema
): Promise<GeneratorCallback> {
const { projectName: e2eProjectName, projectRoot: e2eProjectRoot } =
await determineProjectNameAndRootOptions(tree, {
name: options.name,
projectType: 'application',
directory: options.directory,
projectNameAndRootFormat: options.projectNameAndRootFormat,
callingGenerator: '@nx/remix:cypress',
});
options.addPlugin ??= process.env.NX_ADD_PLUGINS !== 'false';
const rootProject = e2eProjectRoot === '.';
let projectConfig = readProjectConfiguration(tree, options.project);
options.baseUrl ??= `http://localhost:${projectConfig.targets['serve'].options.port}`;
addFileServerTarget(tree, options, 'serve-static');
addProjectConfiguration(tree, e2eProjectName, {
projectType: 'application',
root: e2eProjectRoot,
sourceRoot: joinPathFragments(e2eProjectRoot, 'src'),
targets: {},
tags: [],
implicitDependencies: [options.name],
});
const installTask = await configurationGenerator(tree, {
project: e2eProjectName,
directory: 'src',
linter: options.linter,
skipPackageJson: false,
skipFormat: true,
devServerTarget: `${options.project}:serve:development`,
baseUrl: options.baseUrl,
rootProject,
addPlugin: options.addPlugin,
});
projectConfig = readProjectConfiguration(tree, e2eProjectName);
tree.delete(
joinPathFragments(projectConfig.sourceRoot, 'support', 'app.po.ts')
);
tree.write(
joinPathFragments(projectConfig.sourceRoot, 'e2e', 'app.cy.ts'),
`describe('webapp', () => {
beforeEach(() => cy.visit('/'));
it('should display welcome message', () => {
cy.get('h1').contains('Welcome to Remix');
});
});`
);
const supportFilePath = joinPathFragments(
projectConfig.sourceRoot,
'support',
'e2e.ts'
);
const supportContent = tree.read(supportFilePath, 'utf-8');
tree.write(
supportFilePath,
`${supportContent}
// from https://github.com/remix-run/indie-stack
Cypress.on("uncaught:exception", (err) => {
// Cypress and React Hydrating the document don't get along
// for some unknown reason. Hopefully we figure out why eventually
// so we can remove this.
if (
/hydrat/i.test(err.message) ||
/Minified React error #418/.test(err.message) ||
/Minified React error #423/.test(err.message)
) {
return false;
}
});`
);
return runTasksInSerial(installTask);
}
function addFileServerTarget(
tree: Tree,
options: CypressGeneratorSchema,
targetName: string
) {
addDependenciesToPackageJson(tree, {}, { '@nx/web': nxVersion });
const projectConfig = readProjectConfiguration(tree, options.project);
projectConfig.targets[targetName] = {
executor: '@nx/web:file-server',
options: {
buildTarget: `${options.project}:build`,
port: projectConfig.targets['serve'].options.port,
},
};
updateProjectConfiguration(tree, options.project, projectConfig);
}
export default cypressGenerator;

View File

@ -1,15 +0,0 @@
import { type ProjectNameAndRootFormat } from '@nx/devkit/src/generators/project-name-and-root-utils';
import { Linter } from '@nx/eslint';
export interface CypressGeneratorSchema {
project: string;
name: string;
baseUrl?: string;
directory?: string;
projectNameAndRootFormat?: ProjectNameAndRootFormat;
linter?: Linter;
js?: boolean;
skipFormat?: boolean;
setParserOptionsProject?: boolean;
addPlugin?: boolean;
}

View File

@ -1,61 +0,0 @@
{
"$schema": "https://json-schema.org/schema",
"$id": "NxRemixCypress",
"title": "",
"type": "object",
"description": "Generate a Cypress e2e project for a given application.",
"properties": {
"project": {
"type": "string",
"description": "The name of the frontend project to test.",
"$default": {
"$source": "projectName"
}
},
"projectNameAndRootFormat": {
"description": "Whether to generate the project name and root directory as provided (`as-provided`) or generate them composing their values and taking the configured layout into account (`derived`).",
"type": "string",
"enum": ["as-provided", "derived"]
},
"baseUrl": {
"type": "string",
"description": "URL to access the application on",
"default": "http://localhost:3000"
},
"name": {
"type": "string",
"description": "Name of the E2E Project",
"$default": {
"$source": "argv",
"index": 0
},
"x-prompt": "What name would you like to use for the e2e project?"
},
"directory": {
"type": "string",
"description": "A directory where the project is placed"
},
"linter": {
"description": "The tool to use for running lint checks.",
"type": "string",
"enum": ["eslint", "none"],
"default": "eslint"
},
"js": {
"description": "Generate JavaScript files rather than TypeScript files",
"type": "boolean",
"default": false
},
"skipFormat": {
"description": "Skip formatting files",
"type": "boolean",
"default": false
},
"setParserOptionsProject": {
"type": "boolean",
"description": "Whether or not to configure the ESLint \"parserOptions.project\" option. We do not do this by default for lint performance reasons.",
"default": false
}
},
"required": ["name"]
}

View File

@ -17,7 +17,7 @@ exports[`@nx/remix/plugin non-root project should create nodes 1`] = `
"^production",
],
"options": {
"outputPath": "{workspaceRoot}/dist",
"outputPath": "{workspaceRoot}/dist/my-app",
},
"outputs": [
"{options.outputPath}",

View File

@ -69,7 +69,13 @@ export const createNodes: CreateNodes<RemixPluginOptions> = [
]);
const targets = targetsCache[hash]
? targetsCache[hash]
: await buildRemixTargets(configFilePath, projectRoot, options, context);
: await buildRemixTargets(
configFilePath,
projectRoot,
options,
context,
siblingFiles
);
calculatedTargets[hash] = targets;
@ -88,7 +94,8 @@ async function buildRemixTargets(
configFilePath: string,
projectRoot: string,
options: RemixPluginOptions,
context: CreateNodesContext
context: CreateNodesContext,
siblingFiles: string[]
) {
const namedInputs = getNamedInputs(projectRoot, context);
const serverBuildPath = await getServerBuildPath(
@ -99,6 +106,7 @@ async function buildRemixTargets(
const targets: Record<string, TargetConfiguration> = {};
targets[options.buildTargetName] = buildTarget(
options.buildTargetName,
projectRoot,
namedInputs
);
targets[options.serveTargetName] = serveTarget(serverBuildPath);
@ -109,7 +117,8 @@ async function buildRemixTargets(
);
targets[options.typecheckTargetName] = typecheckTarget(
projectRoot,
namedInputs
namedInputs,
siblingFiles
);
return targets;
@ -117,8 +126,10 @@ async function buildRemixTargets(
function buildTarget(
buildTargetName: string,
projectRoot: string,
namedInputs: { [inputName: string]: any[] }
): TargetConfiguration {
const pathToOutput = projectRoot === '.' ? '' : `/${projectRoot}`;
return {
cache: true,
dependsOn: [`^${buildTargetName}`],
@ -130,7 +141,7 @@ function buildTarget(
outputs: ['{options.outputPath}'],
executor: '@nx/remix:build',
options: {
outputPath: '{workspaceRoot}/dist',
outputPath: `{workspaceRoot}/dist${pathToOutput}`,
},
};
}
@ -160,16 +171,21 @@ function startTarget(
function typecheckTarget(
projectRoot: string,
namedInputs: { [inputName: string]: any[] }
namedInputs: { [inputName: string]: any[] },
siblingFiles: string[]
): TargetConfiguration {
const hasTsConfigAppJson = siblingFiles.includes('tsconfig.app.json');
const command = `tsc${
hasTsConfigAppJson ? ` --project tsconfig.app.json` : ``
}`;
return {
command,
cache: true,
inputs: [
...('production' in namedInputs
? ['production', '^production']
: ['default', '^default']),
],
command: 'tsc',
options: {
cwd: projectRoot,
},