From f9bef406cfbf675640b4134189d3133249fc952f Mon Sep 17 00:00:00 2001 From: Kamil Nowak Date: Fri, 15 May 2026 15:13:40 +0200 Subject: [PATCH 01/18] feat: add React plugin with preset generator and workspace creation support - Add `@onecx/nx-plugin-react` package with React preset generator - Implement plugin resolution system in create-workspace with flavor-based mapping - Add validation for supported flavors (angular, react, ngrx, standalone-ngrx) - Include AI agent rules for React, PrimeReact, PrimeFlex, React Router, and Vitest - Add coding standards rules for DDD, ESLint, Prettier, and expert support level - Improve error handling in create-workspace with proper exit codes - Configure build, --- create-workspace/bin/index.ts | 32 +- nx-plugin-react/.eslintrc.json | 37 + nx-plugin-react/README.md | 11 + nx-plugin-react/generators.json | 14 + nx-plugin-react/jest.config.ts | 9 + nx-plugin-react/package.json | 13 + nx-plugin-react/project.json | 51 + .../src/generators/preset/generator.ts | 22 + .../src/generators/preset/schema.d.ts | 4 + .../src/generators/preset/schema.json | 27 + .../coding_practices-architecture-ddd.mdc | 15 + ...oding_practices-static_analysis-eslint.mdc | 15 + ...ing_practices-static_analysis-prettier.mdc | 15 + ...practices-support_level-support_expert.mdc | 16 + .../rules/frontend-react-primereact.mdc | 13 + .../frontend-react-react_coding_standards.mdc | 19 + .../rules/frontend-react-react_router.mdc | 19 + .../rules/frontend-styling-primeflex.mdc | 14 + .../.agents/rules/testing-unit-vitest.mdc | 20 + .../.agents/skills/frontend-design/SKILL.md | 42 + .../.agents/skills/react-doctor/SKILL.md | 32 + .../src/generators/react/files/.dockerignore | 38 + .../src/generators/react/files/.editorconfig | 16 + .../src/generators/react/files/.gitignore.org | 45 + .../generators/react/files/.prettierignore | 22 + .../src/generators/react/files/.prettierrc | 9 + .../src/generators/react/files/Dockerfile | 16 + .../src/generators/react/files/LICENSE | 176 ++++ .../src/generators/react/files/apigen.yaml | 2 + .../generators/react/files/eslint.config.js | 61 ++ .../react/files/helm/Chart.yaml.template | 21 + .../react/files/helm/values.yaml.template | 41 + .../react/files/index.html.template | 14 + .../react/files/nginx/locations.conf | 14 + .../react/files/src/App.test.tsx.template | 21 + .../react/files/src/App.tsx.template | 8 + .../files/src/assets/api/openapi-bff.yaml | 5 + .../react/files/src/assets/env.json | 4 + .../files/src/assets/styles.css.template | 4 + .../files/src/bootstrap.test.ts.template | 19 + .../react/files/src/bootstrap.ts.template | 6 + .../files/src/i18n/config.test.ts.template | 20 + .../react/files/src/i18n/config.ts.template | 23 + .../react/files/src/i18n/sources/de.json | 6 + .../react/files/src/i18n/sources/en.json | 6 + .../react/files/src/index.css.template | 7 + .../files/src/pages/Welcome.test.tsx.template | 25 + .../files/src/pages/Welcome.tsx.template | 16 + .../react/files/src/router.test.tsx.template | 57 + .../react/files/src/router.tsx.template | 25 + .../react/files/src/setupTests.ts.template | 1 + .../react/files/vite.config.ts.template | 128 +++ .../src/generators/react/generator.ts | 280 +++++ .../src/generators/react/schema.d.ts | 4 + .../src/generators/react/schema.json | 18 + .../react/steps/general-openapi.step.ts | 33 + .../src/generators/shared/generator.utils.ts | 104 ++ .../openapi/models/openapi-default.model.ts | 8 + .../shared/openapi/openapi.utils.ts | 209 ++++ .../src/generators/shared/parameters.utils.ts | 182 ++++ .../src/generators/shared/safeReplace.ts | 120 +++ nx-plugin-react/src/index.ts | 0 nx-plugin-react/tsconfig.json | 16 + nx-plugin-react/tsconfig.lib.json | 10 + nx-plugin-react/tsconfig.spec.json | 14 + nx.json | 5 + package-lock.json | 984 +++++++++++++++--- package.json | 1 + tsconfig.base.json | 19 +- 69 files changed, 3148 insertions(+), 155 deletions(-) create mode 100644 nx-plugin-react/.eslintrc.json create mode 100644 nx-plugin-react/README.md create mode 100644 nx-plugin-react/generators.json create mode 100644 nx-plugin-react/jest.config.ts create mode 100644 nx-plugin-react/package.json create mode 100644 nx-plugin-react/project.json create mode 100644 nx-plugin-react/src/generators/preset/generator.ts create mode 100644 nx-plugin-react/src/generators/preset/schema.d.ts create mode 100644 nx-plugin-react/src/generators/preset/schema.json create mode 100644 nx-plugin-react/src/generators/react/files/.agents/rules/coding_practices-architecture-ddd.mdc create mode 100644 nx-plugin-react/src/generators/react/files/.agents/rules/coding_practices-static_analysis-eslint.mdc create mode 100644 nx-plugin-react/src/generators/react/files/.agents/rules/coding_practices-static_analysis-prettier.mdc create mode 100644 nx-plugin-react/src/generators/react/files/.agents/rules/coding_practices-support_level-support_expert.mdc create mode 100644 nx-plugin-react/src/generators/react/files/.agents/rules/frontend-react-primereact.mdc create mode 100644 nx-plugin-react/src/generators/react/files/.agents/rules/frontend-react-react_coding_standards.mdc create mode 100644 nx-plugin-react/src/generators/react/files/.agents/rules/frontend-react-react_router.mdc create mode 100644 nx-plugin-react/src/generators/react/files/.agents/rules/frontend-styling-primeflex.mdc create mode 100644 nx-plugin-react/src/generators/react/files/.agents/rules/testing-unit-vitest.mdc create mode 100644 nx-plugin-react/src/generators/react/files/.agents/skills/frontend-design/SKILL.md create mode 100644 nx-plugin-react/src/generators/react/files/.agents/skills/react-doctor/SKILL.md create mode 100644 nx-plugin-react/src/generators/react/files/.dockerignore create mode 100644 nx-plugin-react/src/generators/react/files/.editorconfig create mode 100644 nx-plugin-react/src/generators/react/files/.gitignore.org create mode 100644 nx-plugin-react/src/generators/react/files/.prettierignore create mode 100644 nx-plugin-react/src/generators/react/files/.prettierrc create mode 100644 nx-plugin-react/src/generators/react/files/Dockerfile create mode 100644 nx-plugin-react/src/generators/react/files/LICENSE create mode 100644 nx-plugin-react/src/generators/react/files/apigen.yaml create mode 100644 nx-plugin-react/src/generators/react/files/eslint.config.js create mode 100644 nx-plugin-react/src/generators/react/files/helm/Chart.yaml.template create mode 100644 nx-plugin-react/src/generators/react/files/helm/values.yaml.template create mode 100644 nx-plugin-react/src/generators/react/files/index.html.template create mode 100644 nx-plugin-react/src/generators/react/files/nginx/locations.conf create mode 100644 nx-plugin-react/src/generators/react/files/src/App.test.tsx.template create mode 100644 nx-plugin-react/src/generators/react/files/src/App.tsx.template create mode 100644 nx-plugin-react/src/generators/react/files/src/assets/api/openapi-bff.yaml create mode 100644 nx-plugin-react/src/generators/react/files/src/assets/env.json create mode 100644 nx-plugin-react/src/generators/react/files/src/assets/styles.css.template create mode 100644 nx-plugin-react/src/generators/react/files/src/bootstrap.test.ts.template create mode 100644 nx-plugin-react/src/generators/react/files/src/bootstrap.ts.template create mode 100644 nx-plugin-react/src/generators/react/files/src/i18n/config.test.ts.template create mode 100644 nx-plugin-react/src/generators/react/files/src/i18n/config.ts.template create mode 100644 nx-plugin-react/src/generators/react/files/src/i18n/sources/de.json create mode 100644 nx-plugin-react/src/generators/react/files/src/i18n/sources/en.json create mode 100644 nx-plugin-react/src/generators/react/files/src/index.css.template create mode 100644 nx-plugin-react/src/generators/react/files/src/pages/Welcome.test.tsx.template create mode 100644 nx-plugin-react/src/generators/react/files/src/pages/Welcome.tsx.template create mode 100644 nx-plugin-react/src/generators/react/files/src/router.test.tsx.template create mode 100644 nx-plugin-react/src/generators/react/files/src/router.tsx.template create mode 100644 nx-plugin-react/src/generators/react/files/src/setupTests.ts.template create mode 100644 nx-plugin-react/src/generators/react/files/vite.config.ts.template create mode 100644 nx-plugin-react/src/generators/react/generator.ts create mode 100644 nx-plugin-react/src/generators/react/schema.d.ts create mode 100644 nx-plugin-react/src/generators/react/schema.json create mode 100644 nx-plugin-react/src/generators/react/steps/general-openapi.step.ts create mode 100644 nx-plugin-react/src/generators/shared/generator.utils.ts create mode 100644 nx-plugin-react/src/generators/shared/openapi/models/openapi-default.model.ts create mode 100644 nx-plugin-react/src/generators/shared/openapi/openapi.utils.ts create mode 100644 nx-plugin-react/src/generators/shared/parameters.utils.ts create mode 100644 nx-plugin-react/src/generators/shared/safeReplace.ts create mode 100644 nx-plugin-react/src/index.ts create mode 100644 nx-plugin-react/tsconfig.json create mode 100644 nx-plugin-react/tsconfig.lib.json create mode 100644 nx-plugin-react/tsconfig.spec.json diff --git a/create-workspace/bin/index.ts b/create-workspace/bin/index.ts index d457fa98..5c01b9cc 100644 --- a/create-workspace/bin/index.ts +++ b/create-workspace/bin/index.ts @@ -2,23 +2,42 @@ import { createWorkspace } from 'create-nx-workspace'; +const SUPPORTED_FLAVORS = ['angular', 'react', 'ngrx', 'standalone-ngrx']; + +const PLUGIN_MAP: Record = { + react: '@onecx/nx-plugin-react', +}; + +function resolvePlugin(flavor: string): string { + return PLUGIN_MAP[flavor] ?? '@onecx/nx-plugin'; +} + async function main() { const flavor = process.argv[2]; // TODO: use libraries like yargs or enquirer to set your workspace name if (!flavor) { throw new Error('Please provide a flavor for the workspace.'); } + if (!SUPPORTED_FLAVORS.includes(flavor)) { + throw new Error( + `Unknown flavor "${flavor}". Supported flavors: ${SUPPORTED_FLAVORS.join( + ', ' + )}` + ); + } const name = process.argv[3]; // TODO: use libraries like yargs or enquirer to set your workspace name if (!name) { throw new Error('Please provide a name for the workspace.'); } + const plugin = resolvePlugin(flavor); + console.log(`Creating the workspace ${name} with the ${flavor} preset`); - // This assumes "@onecx/nx-plugin" and "create-workspace" are at the same version + // This assumes the preset package and "create-workspace" are at the same version // eslint-disable-next-line @typescript-eslint/no-var-requires const presetVersion = require('../package.json').version; - await createWorkspace(`@onecx/nx-plugin@${presetVersion}`, { + await createWorkspace(`${plugin}@${presetVersion}`, { flavor, name, nxCloud: 'skip', @@ -27,7 +46,12 @@ async function main() { verbose: true, }); - console.log(`Successfully created the workspace ${name} with the ${flavor} preset`); + console.log( + `Successfully created the workspace ${name} with the ${flavor} preset` + ); } -main(); +main().catch((e) => { + console.error(e.message); + process.exit(1); +}); diff --git a/nx-plugin-react/.eslintrc.json b/nx-plugin-react/.eslintrc.json new file mode 100644 index 00000000..8642d535 --- /dev/null +++ b/nx-plugin-react/.eslintrc.json @@ -0,0 +1,37 @@ +{ + "extends": ["../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.json"], + "parser": "jsonc-eslint-parser", + "rules": { + "@nx/dependency-checks": [ + "error", + { + "ignoredFiles": ["{projectRoot}/eslint.config.{js,cjs,mjs}"] + } + ] + } + }, + { + "files": ["./package.json", "./generators.json"], + "parser": "jsonc-eslint-parser", + "rules": { + "@nx/nx-plugin-checks": "error" + } + } + ] +} diff --git a/nx-plugin-react/README.md b/nx-plugin-react/README.md new file mode 100644 index 00000000..bf1a4913 --- /dev/null +++ b/nx-plugin-react/README.md @@ -0,0 +1,11 @@ +# nx-plugin-react + +This library was generated with [Nx](https://nx.dev). + +## Building + +Run `nx build nx-plugin-react` to build the library. + +## Running unit tests + +Run `nx test nx-plugin-react` to execute the unit tests via [Jest](https://jestjs.io). diff --git a/nx-plugin-react/generators.json b/nx-plugin-react/generators.json new file mode 100644 index 00000000..7db3cadd --- /dev/null +++ b/nx-plugin-react/generators.json @@ -0,0 +1,14 @@ +{ + "generators": { + "react": { + "factory": "./src/generators/react/generator", + "schema": "./src/generators/react/schema.json", + "description": "react generator" + }, + "preset": { + "factory": "./src/generators/preset/generator", + "schema": "./src/generators/preset/schema.json", + "description": "preset generator" + } + } +} diff --git a/nx-plugin-react/jest.config.ts b/nx-plugin-react/jest.config.ts new file mode 100644 index 00000000..48d0e724 --- /dev/null +++ b/nx-plugin-react/jest.config.ts @@ -0,0 +1,9 @@ +export default { + displayName: 'nx-plugin-react', + preset: '../jest.preset.js', + transform: { + '^.+\\.[tj]s$': ['ts-jest', { tsconfig: '/tsconfig.spec.json' }], + }, + moduleFileExtensions: ['ts', 'js', 'html'], + coverageDirectory: '../coverage/nx-plugin-react', +}; diff --git a/nx-plugin-react/package.json b/nx-plugin-react/package.json new file mode 100644 index 00000000..b02efcc4 --- /dev/null +++ b/nx-plugin-react/package.json @@ -0,0 +1,13 @@ +{ + "name": "nx-plugin-react", + "version": "0.0.1", + "dependencies": { + "@nx/devkit": "19.8.14", + "tslib": "^2.3.0" + }, + "type": "commonjs", + "main": "./src/index.js", + "typings": "./src/index.d.ts", + "private": true, + "generators": "./generators.json" +} diff --git a/nx-plugin-react/project.json b/nx-plugin-react/project.json new file mode 100644 index 00000000..323a419d --- /dev/null +++ b/nx-plugin-react/project.json @@ -0,0 +1,51 @@ +{ + "name": "nx-plugin-react", + "$schema": "../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "nx-plugin-react/src", + "projectType": "library", + "tags": [], + "targets": { + "build": { + "executor": "@nx/js:tsc", + "outputs": ["{options.outputPath}"], + "options": { + "outputPath": "dist/nx-plugin-react", + "main": "nx-plugin-react/src/index.ts", + "tsConfig": "nx-plugin-react/tsconfig.lib.json", + "assets": [ + "nx-plugin-react/*.md", + { + "input": "./nx-plugin-react/src", + "glob": "**/!(*.ts)", + "output": "./src" + }, + { + "input": "./nx-plugin-react/src", + "glob": "**/*.d.ts", + "output": "./src" + }, + { + "input": "./nx-plugin-react", + "glob": "generators.json", + "output": "." + }, + { + "input": "./nx-plugin-react", + "glob": "executors.json", + "output": "." + } + ] + } + }, + "lint": { + "executor": "@nx/eslint:lint" + }, + "test": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "options": { + "jestConfig": "nx-plugin-react/jest.config.ts" + } + } + } +} diff --git a/nx-plugin-react/src/generators/preset/generator.ts b/nx-plugin-react/src/generators/preset/generator.ts new file mode 100644 index 00000000..0dbfdc3d --- /dev/null +++ b/nx-plugin-react/src/generators/preset/generator.ts @@ -0,0 +1,22 @@ +import { Tree } from '@nx/devkit'; +import { PresetGeneratorSchema } from './schema'; +import reactGenerator from '../react/generator'; + +export async function presetGenerator( + tree: Tree, + options: PresetGeneratorSchema +) { + const generators = { + react: reactGenerator, + }; + + if (!generators[options.flavor]) { + throw 'Unknown flavor: ' + options.flavor; + } + const generatorCallback = await generators[options.flavor](tree, options); + return async () => { + await generatorCallback(); + }; +} + +export default presetGenerator; diff --git a/nx-plugin-react/src/generators/preset/schema.d.ts b/nx-plugin-react/src/generators/preset/schema.d.ts new file mode 100644 index 00000000..1c647fa3 --- /dev/null +++ b/nx-plugin-react/src/generators/preset/schema.d.ts @@ -0,0 +1,4 @@ +export interface PresetGeneratorSchema { + flavor: string; + name: string; +} diff --git a/nx-plugin-react/src/generators/preset/schema.json b/nx-plugin-react/src/generators/preset/schema.json new file mode 100644 index 00000000..23c42aaf --- /dev/null +++ b/nx-plugin-react/src/generators/preset/schema.json @@ -0,0 +1,27 @@ +{ + "$schema": "http://json-schema.org/schema", + "$id": "Preset", + "title": "", + "type": "object", + "properties": { + "flavor": { + "type": "string", + "description": "", + "$default": { + "$source": "argv", + "index": 0 + }, + "x-prompt": "What flavor would you like to use?" + }, + "name": { + "type": "string", + "description": "", + "$default": { + "$source": "argv", + "index": 1 + }, + "x-prompt": "What name would you like to use?" + } + }, + "required": ["name"] +} diff --git a/nx-plugin-react/src/generators/react/files/.agents/rules/coding_practices-architecture-ddd.mdc b/nx-plugin-react/src/generators/react/files/.agents/rules/coding_practices-architecture-ddd.mdc new file mode 100644 index 00000000..3573ab50 --- /dev/null +++ b/nx-plugin-react/src/generators/react/files/.agents/rules/coding_practices-architecture-ddd.mdc @@ -0,0 +1,15 @@ +--- +trigger: always_on +--- +## CODING_PRACTICES + +### Guidelines for ARCHITECTURE + +#### DDD + +- Define bounded contexts to separate different parts of the domain with clear boundaries +- Implement ubiquitous language within each context to align code with business terminology +- Create rich domain models with behavior, not just data structures, for {{core_domain_entities}} +- Use value objects for concepts with no identity but defined by their attributes +- Implement domain events to communicate between bounded contexts +- Use aggregates to enforce consistency boundaries and transactional integrity diff --git a/nx-plugin-react/src/generators/react/files/.agents/rules/coding_practices-static_analysis-eslint.mdc b/nx-plugin-react/src/generators/react/files/.agents/rules/coding_practices-static_analysis-eslint.mdc new file mode 100644 index 00000000..bcff2d17 --- /dev/null +++ b/nx-plugin-react/src/generators/react/files/.agents/rules/coding_practices-static_analysis-eslint.mdc @@ -0,0 +1,15 @@ +--- +trigger: always_on +--- +## CODING_PRACTICES + +### Guidelines for STATIC_ANALYSIS + +#### ESLINT + +- Configure project-specific rules in eslint.config.js to enforce consistent coding standards +- Use shareable configs like eslint-config-airbnb or eslint-config-standard as a foundation +- Implement custom rules for {{project_specific_patterns}} to maintain codebase consistency +- Configure integration with Prettier to avoid rule conflicts for code formatting +- Use the --fix flag in CI/CD pipelines to automatically correct fixable issues +- Implement staged linting with husky and lint-staged to prevent committing non-compliant code diff --git a/nx-plugin-react/src/generators/react/files/.agents/rules/coding_practices-static_analysis-prettier.mdc b/nx-plugin-react/src/generators/react/files/.agents/rules/coding_practices-static_analysis-prettier.mdc new file mode 100644 index 00000000..3fe3d820 --- /dev/null +++ b/nx-plugin-react/src/generators/react/files/.agents/rules/coding_practices-static_analysis-prettier.mdc @@ -0,0 +1,15 @@ +--- +trigger: always_on +--- +## CODING_PRACTICES + +### Guidelines for STATIC_ANALYSIS + +#### PRETTIER + +- Define a consistent .prettierrc configuration across all {{project_repositories}} +- Configure editor integration to format on save for immediate feedback +- Use .prettierignore to exclude generated files, build artifacts, and {{specific_excluded_patterns}} +- Set printWidth based on team preferences (80-120 characters) to improve code readability +- Configure consistent quote style and semicolon usage to match team conventions +- Implement CI checks to ensure all committed code adheres to the defined style diff --git a/nx-plugin-react/src/generators/react/files/.agents/rules/coding_practices-support_level-support_expert.mdc b/nx-plugin-react/src/generators/react/files/.agents/rules/coding_practices-support_level-support_expert.mdc new file mode 100644 index 00000000..105bdba8 --- /dev/null +++ b/nx-plugin-react/src/generators/react/files/.agents/rules/coding_practices-support_level-support_expert.mdc @@ -0,0 +1,16 @@ +--- +trigger: always_on +--- +## CODING_PRACTICES + +### Guidelines for SUPPORT_LEVEL + +#### SUPPORT_EXPERT + +- Favor elegant, maintainable solutions over verbose code. Assume understanding of language idioms and design patterns. +- Highlight potential performance implications and optimization opportunities in suggested code. +- Frame solutions within broader architectural contexts and suggest design alternatives when appropriate. +- Focus comments on 'why' not 'what' - assume code readability through well-named functions and variables. +- Proactively address edge cases, race conditions, and security considerations without being prompted. +- When debugging, provide targeted diagnostic approaches rather than shotgun solutions. +- Suggest comprehensive testing strategies rather than just example tests, including considerations for mocking, test organization, and coverage. diff --git a/nx-plugin-react/src/generators/react/files/.agents/rules/frontend-react-primereact.mdc b/nx-plugin-react/src/generators/react/files/.agents/rules/frontend-react-primereact.mdc new file mode 100644 index 00000000..cef519b7 --- /dev/null +++ b/nx-plugin-react/src/generators/react/files/.agents/rules/frontend-react-primereact.mdc @@ -0,0 +1,13 @@ +--- +trigger: always_on +--- +## FRONTEND + +### Guidelines for REACT + +#### PRIMEREACT + +- Use PrimeReact components as the default UI building blocks instead of custom HTML when possible +- Prefer PrimeReact layout and form components (e.g., `Card`, `Button`, `InputText`, `DataTable`) for consistent styling +- Keep custom components thin wrappers around PrimeReact to avoid duplicating behavior +- When extending, follow PrimeReact theming and pass-through props rather than overriding styles directly diff --git a/nx-plugin-react/src/generators/react/files/.agents/rules/frontend-react-react_coding_standards.mdc b/nx-plugin-react/src/generators/react/files/.agents/rules/frontend-react-react_coding_standards.mdc new file mode 100644 index 00000000..0314ba7e --- /dev/null +++ b/nx-plugin-react/src/generators/react/files/.agents/rules/frontend-react-react_coding_standards.mdc @@ -0,0 +1,19 @@ +--- +trigger: always_on +--- +## FRONTEND + +### Guidelines for REACT + +#### REACT_CODING_STANDARDS + +- Use functional components with hooks instead of class components +- Implement React.memo() for expensive components that render often with the same props +- Utilize React.lazy() and Suspense for code-splitting and performance optimization +- Use the useCallback hook for event handlers passed to child components to prevent unnecessary re-renders +- Prefer useMemo for expensive calculations to avoid recomputation on every render +- Implement useId() for generating unique IDs for accessibility attributes +- Use the new use hook for data fetching in React 19+ projects +- Leverage Server Components for {{data_fetching_heavy_components}} when using React with Next.js or similar frameworks +- Consider using the new useOptimistic hook for optimistic UI updates in forms +- Use useTransition for non-urgent state updates to keep the UI responsive diff --git a/nx-plugin-react/src/generators/react/files/.agents/rules/frontend-react-react_router.mdc b/nx-plugin-react/src/generators/react/files/.agents/rules/frontend-react-react_router.mdc new file mode 100644 index 00000000..32dd66e0 --- /dev/null +++ b/nx-plugin-react/src/generators/react/files/.agents/rules/frontend-react-react_router.mdc @@ -0,0 +1,19 @@ +--- +trigger: always_on +--- +## FRONTEND + +### Guidelines for REACT + +#### REACT_ROUTER + +- Use createBrowserRouter instead of BrowserRouter for better data loading and error handling +- Implement lazy loading with React.lazy() for route components to improve initial load time +- Use the useNavigate hook instead of the navigate component prop for programmatic navigation +- Leverage loader and action functions to handle data fetching and mutations at the route level +- Implement error boundaries with errorElement to gracefully handle routing and data errors +- Use relative paths with dot notation (e.g., "../parent") to maintain route hierarchy flexibility +- Utilize the useRouteLoaderData hook to access data from parent routes +- Implement fetchers for non-navigation data mutations +- Use route.lazy() for route-level code splitting with automatic loading states +- Implement shouldRevalidate functions to control when data revalidation happens after navigation diff --git a/nx-plugin-react/src/generators/react/files/.agents/rules/frontend-styling-primeflex.mdc b/nx-plugin-react/src/generators/react/files/.agents/rules/frontend-styling-primeflex.mdc new file mode 100644 index 00000000..6d1c7b16 --- /dev/null +++ b/nx-plugin-react/src/generators/react/files/.agents/rules/frontend-styling-primeflex.mdc @@ -0,0 +1,14 @@ +--- +trigger: always_on +--- +## FRONTEND + +### Guidelines for STYLING + +#### PRIMEFLEX + +- Use PrimeFlex utility classes for layout, spacing, and responsive behavior instead of bespoke CSS when possible +- Prefer PrimeFlex grid/flex utilities (e.g., `grid`, `col-12`, `md:col-6`, `flex`, `gap-2`) for structure and alignment +- Keep custom CSS focused on component-specific visuals that PrimeFlex cannot express +- Use PrimeFlex spacing scale consistently (`p-`, `m-`, `gap-`) to avoid arbitrary pixel values +- Apply responsive variants (`sm:`, `md:`, `lg:`, `xl:`) for adaptive layouts diff --git a/nx-plugin-react/src/generators/react/files/.agents/rules/testing-unit-vitest.mdc b/nx-plugin-react/src/generators/react/files/.agents/rules/testing-unit-vitest.mdc new file mode 100644 index 00000000..37571ef1 --- /dev/null +++ b/nx-plugin-react/src/generators/react/files/.agents/rules/testing-unit-vitest.mdc @@ -0,0 +1,20 @@ +--- +trigger: always_on +--- +## TESTING + +### Guidelines for UNIT + +#### VITEST + +- Leverage the `vi` object for test doubles - Use `vi.fn()` for function mocks, `vi.spyOn()` to monitor existing functions, and `vi.stubGlobal()` for global mocks. Prefer spies over mocks when you only need to verify interactions without changing behavior. +- Master `vi.mock()` factory patterns - Place mock factory functions at the top level of your test file, return typed mock implementations, and use `mockImplementation()` or `mockReturnValue()` for dynamic control during tests. Remember the factory runs before imports are processed. +- Create setup files for reusable configuration - Define global mocks, custom matchers, and environment setup in dedicated files referenced in your `vitest.config.ts`. This keeps your test files clean while ensuring consistent test environments. +- Use inline snapshots for readable assertions - Replace complex equality checks with `expect(value).toMatchInlineSnapshot()` to capture expected output directly in your test file, making changes more visible in code reviews. +- Monitor coverage with purpose and only when asked - Configure coverage thresholds in `vitest.config.ts` to ensure critical code paths are tested, but focus on meaningful tests rather than arbitrary coverage percentages. +- Make watch mode part of your workflow - Run `vitest --watch` during development for instant feedback as you modify code, filtering tests with `-t` to focus on specific areas under development. +- Explore UI mode for complex test suites - Use `vitest --ui` to visually navigate large test suites, inspect test results, and debug failures more efficiently during development. +- Handle optional dependencies with smart mocking - Use conditional mocking to test code with optional dependencies by implementing `vi.mock()` with the factory pattern for modules that might not be available in all environments. +- Configure jsdom for DOM testing - Set `environment: 'jsdom'` in your configuration for frontend component tests and combine with testing-library utilities for realistic user interaction simulation. +- Structure tests for maintainability - Group related tests with descriptive `describe` blocks, use explicit assertion messages, and follow the Arrange-Act-Assert pattern to make tests self-documenting. +- Leverage TypeScript type checking in tests - Enable strict typing in your tests to catch type errors early, use `expectTypeOf()` for type-level assertions, and ensure mocks preserve the original type signatures. diff --git a/nx-plugin-react/src/generators/react/files/.agents/skills/frontend-design/SKILL.md b/nx-plugin-react/src/generators/react/files/.agents/skills/frontend-design/SKILL.md new file mode 100644 index 00000000..5be498e2 --- /dev/null +++ b/nx-plugin-react/src/generators/react/files/.agents/skills/frontend-design/SKILL.md @@ -0,0 +1,42 @@ +--- +name: frontend-design +description: Create distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, artifacts, posters, or applications (examples include websites, landing pages, dashboards, React components, HTML/CSS layouts, or when styling/beautifying any web UI). Generates creative, polished code and UI design that avoids generic AI aesthetics. +license: Complete terms in LICENSE.txt +--- + +This skill guides creation of distinctive, production-grade frontend interfaces that avoid generic "AI slop" aesthetics. Implement real working code with exceptional attention to aesthetic details and creative choices. + +The user provides frontend requirements: a component, page, application, or interface to build. They may include context about the purpose, audience, or technical constraints. + +## Design Thinking + +Before coding, understand the context and commit to a BOLD aesthetic direction: +- **Purpose**: What problem does this interface solve? Who uses it? +- **Tone**: Pick an extreme: brutally minimal, maximalist chaos, retro-futuristic, organic/natural, luxury/refined, playful/toy-like, editorial/magazine, brutalist/raw, art deco/geometric, soft/pastel, industrial/utilitarian, etc. There are so many flavors to choose from. Use these for inspiration but design one that is true to the aesthetic direction. +- **Constraints**: Technical requirements (framework, performance, accessibility). +- **Differentiation**: What makes this UNFORGETTABLE? What's the one thing someone will remember? + +**CRITICAL**: Choose a clear conceptual direction and execute it with precision. Bold maximalism and refined minimalism both work - the key is intentionality, not intensity. + +Then implement working code (HTML/CSS/JS, React, Vue, etc.) that is: +- Production-grade and functional +- Visually striking and memorable +- Cohesive with a clear aesthetic point-of-view +- Meticulously refined in every detail + +## Frontend Aesthetics Guidelines + +Focus on: +- **Typography**: Choose fonts that are beautiful, unique, and interesting. Avoid generic fonts like Arial and Inter; opt instead for distinctive choices that elevate the frontend's aesthetics; unexpected, characterful font choices. Pair a distinctive display font with a refined body font. +- **Color & Theme**: Commit to a cohesive aesthetic. Use CSS variables for consistency. Dominant colors with sharp accents outperform timid, evenly-distributed palettes. +- **Motion**: Use animations for effects and micro-interactions. Prioritize CSS-only solutions for HTML. Use Motion library for React when available. Focus on high-impact moments: one well-orchestrated page load with staggered reveals (animation-delay) creates more delight than scattered micro-interactions. Use scroll-triggering and hover states that surprise. +- **Spatial Composition**: Unexpected layouts. Asymmetry. Overlap. Diagonal flow. Grid-breaking elements. Generous negative space OR controlled density. +- **Backgrounds & Visual Details**: Create atmosphere and depth rather than defaulting to solid colors. Add contextual effects and textures that match the overall aesthetic. Apply creative forms like gradient meshes, noise textures, geometric patterns, layered transparencies, dramatic shadows, decorative borders, custom cursors, and grain overlays. + +NEVER use generic AI-generated aesthetics like overused font families (Inter, Roboto, Arial, system fonts), cliched color schemes (particularly purple gradients on white backgrounds), predictable layouts and component patterns, and cookie-cutter design that lacks context-specific character. + +Interpret creatively and make unexpected choices that feel genuinely designed for the context. No design should be the same. Vary between light and dark themes, different fonts, different aesthetics. NEVER converge on common choices (Space Grotesk, for example) across generations. + +**IMPORTANT**: Match implementation complexity to the aesthetic vision. Maximalist designs need elaborate code with extensive animations and effects. Minimalist or refined designs need restraint, precision, and careful attention to spacing, typography, and subtle details. Elegance comes from executing the vision well. + +Remember: Claude is capable of extraordinary creative work. Don't hold back, show what can truly be created when thinking outside the box and committing fully to a distinctive vision. diff --git a/nx-plugin-react/src/generators/react/files/.agents/skills/react-doctor/SKILL.md b/nx-plugin-react/src/generators/react/files/.agents/skills/react-doctor/SKILL.md new file mode 100644 index 00000000..9c556745 --- /dev/null +++ b/nx-plugin-react/src/generators/react/files/.agents/skills/react-doctor/SKILL.md @@ -0,0 +1,32 @@ +--- +name: react-doctor +description: Use when finishing a feature, fixing a bug, before committing React code, or when the user wants to improve code quality or clean up a codebase. Checks for score regression. Covers lint, dead code, accessibility, bundle size, architecture diagnostics. +version: "1.0.0" +--- + +# React Doctor + +Scans React codebases for security, performance, correctness, and architecture issues. Outputs a 0–100 health score. + +## After making React code changes: + +Run `npx -y react-doctor@latest . --verbose --diff` and check the score did not regress. + +If the score dropped, fix the regressions before committing. + +## For general cleanup or code improvement: + +Run `npx -y react-doctor@latest . --verbose` (without `--diff`) to scan the full codebase. Fix issues by severity — errors first, then warnings. + +## Command + +```bash +npx -y react-doctor@latest . --verbose --diff +``` + +| Flag | Purpose | +| ----------- | --------------------------------------------- | +| `.` | Scan current directory | +| `--verbose` | Show affected files and line numbers per rule | +| `--diff` | Only scan changed files vs base branch | +| `--score` | Output only the numeric score | diff --git a/nx-plugin-react/src/generators/react/files/.dockerignore b/nx-plugin-react/src/generators/react/files/.dockerignore new file mode 100644 index 00000000..a3a89cd6 --- /dev/null +++ b/nx-plugin-react/src/generators/react/files/.dockerignore @@ -0,0 +1,38 @@ +# See http://help.github.com/ignore-files/ for more about ignoring files. + +# compiled output +tmp +out-tsc +reports + +# dependencies +node_modules + +# profiling files +chrome-profiler-events*.json +speed-measure-plugin*.json + +# IDEs and editors +.idea +.project +.classpath +.history +.settings +.vscode +*.launch + +# misc +.copy-build-to +.nx +.eslintcache +.sass-cache +.husky/_ +.gitlab* +connect.lock +typings +*.log +*.sh + +# System Files +.DS_Store +Thumbs.db diff --git a/nx-plugin-react/src/generators/react/files/.editorconfig b/nx-plugin-react/src/generators/react/files/.editorconfig new file mode 100644 index 00000000..59d9a3a3 --- /dev/null +++ b/nx-plugin-react/src/generators/react/files/.editorconfig @@ -0,0 +1,16 @@ +# Editor configuration, see https://editorconfig.org +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.ts] +quote_type = single + +[*.md] +max_line_length = off +trim_trailing_whitespace = false diff --git a/nx-plugin-react/src/generators/react/files/.gitignore.org b/nx-plugin-react/src/generators/react/files/.gitignore.org new file mode 100644 index 00000000..ff9d372a --- /dev/null +++ b/nx-plugin-react/src/generators/react/files/.gitignore.org @@ -0,0 +1,45 @@ +# See http://help.github.com/ignore-files/ for more about ignoring files. + +# compiled output +dist +tmp +/out-tsc + +# dependencies +node_modules + +# IDEs and editors +/.idea +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# IDE - VSCode +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json + +# misc +/.sass-cache +/connect.lock +/coverage +/libpeerconnection.log +npm-debug.log +yarn-error.log +testem.log +/typings + +# System Files +.DS_Store +Thumbs.db + +# NX +.nx + +# Reports +reports diff --git a/nx-plugin-react/src/generators/react/files/.prettierignore b/nx-plugin-react/src/generators/react/files/.prettierignore new file mode 100644 index 00000000..81f421a1 --- /dev/null +++ b/nx-plugin-react/src/generators/react/files/.prettierignore @@ -0,0 +1,22 @@ +# Add files here to ignore them from prettier formatting +.nx +.husky +.docusaurus +.github +.scannerwork +.dockerignore +.prettierignore +.browserslistrc +.eslintcache +dist +helm +nginx +reports +node_modules +LICENSE +CHANGELOG.md +README.md +Dockerfile +*.log +*.sh +src/app/shared/generated/** diff --git a/nx-plugin-react/src/generators/react/files/.prettierrc b/nx-plugin-react/src/generators/react/files/.prettierrc new file mode 100644 index 00000000..79e02813 --- /dev/null +++ b/nx-plugin-react/src/generators/react/files/.prettierrc @@ -0,0 +1,9 @@ +{ + "trailingComma": "none", + "tabWidth": 2, + "useTabs": false, + "semi": false, + "singleQuote": true, + "bracketSpacing": true, + "printWidth": 120 +} diff --git a/nx-plugin-react/src/generators/react/files/Dockerfile b/nx-plugin-react/src/generators/react/files/Dockerfile new file mode 100644 index 00000000..1a73a10a --- /dev/null +++ b/nx-plugin-react/src/generators/react/files/Dockerfile @@ -0,0 +1,16 @@ +FROM ghcr.io/onecx/docker-spa-base:2.6.0 + +# Copy nginx configuration +COPY nginx/locations.conf $DIR_LOCATION/locations.conf +# Copy application build +COPY dist/<%= fileName %>/ $DIR_HTML + +# Optional extend list of application environments +#ENV CONFIG_ENV_LIST BFF_URL,APP_BASE_HREF + +# Application environments default values +ENV BFF_URL http://<%= fileName %>-bff:8080/ +ENV APP_BASE_HREF / + +RUN chmod 775 -R "$DIR_HTML"/assets +USER 1001 diff --git a/nx-plugin-react/src/generators/react/files/LICENSE b/nx-plugin-react/src/generators/react/files/LICENSE new file mode 100644 index 00000000..ce0c9b71 --- /dev/null +++ b/nx-plugin-react/src/generators/react/files/LICENSE @@ -0,0 +1,176 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to the Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/nx-plugin-react/src/generators/react/files/apigen.yaml b/nx-plugin-react/src/generators/react/files/apigen.yaml new file mode 100644 index 00000000..0ce19630 --- /dev/null +++ b/nx-plugin-react/src/generators/react/files/apigen.yaml @@ -0,0 +1,2 @@ +removeOperationIdPrefix: true +removeOperationIdPrefixCount: 2 diff --git a/nx-plugin-react/src/generators/react/files/eslint.config.js b/nx-plugin-react/src/generators/react/files/eslint.config.js new file mode 100644 index 00000000..315429fd --- /dev/null +++ b/nx-plugin-react/src/generators/react/files/eslint.config.js @@ -0,0 +1,61 @@ +const nx = require('@nx/eslint-plugin') + +module.exports = [ + ...nx.configs['flat/base'], + ...nx.configs['flat/typescript'], + ...nx.configs['flat/javascript'], + { + ignores: [ + 'dist', + 'helm', + 'nginx', + 'reports', + 'node_modules', + '.nx', + '.eslintcache', + '.husky', + '.docusaurus', + '.github', + '.scannerwork', + '.dockerignore', + '.prettierignore', + '.browserslistrc', + '.eslintcache', + 'LICENSE', + 'CHANGELOG.md', + 'README.md', + 'Dockerfile', + '*.log', + '*.sh', + 'src/app/shared/generated/**', + 'src/**/*.ico', + 'src/**/*.svg' + ] + }, + { + files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'], + // Override or add rules here + rules: {} + }, + ...nx.configs['flat/react'], + { + files: ['**/*.ts', '**/*.tsx'], + rules: { + "@typescript-eslint/no-unused-vars": ["error", { "vars": "all", "args": "none" }], + '@typescript-eslint/no-explicit-any': 'warn', + 'react-hooks/rules-of-hooks': 'error', + 'react-hooks/exhaustive-deps': 'warn' + } + }, + { + files: ['**/*.spec.ts', '**/*.spec.tsx'], + rules: { + '@typescript-eslint/no-require-imports': [ + 'off', + { + allowAsImport: true + } + ] + } + } +] diff --git a/nx-plugin-react/src/generators/react/files/helm/Chart.yaml.template b/nx-plugin-react/src/generators/react/files/helm/Chart.yaml.template new file mode 100644 index 00000000..da866c6f --- /dev/null +++ b/nx-plugin-react/src/generators/react/files/helm/Chart.yaml.template @@ -0,0 +1,21 @@ +apiVersion: v2 +name: <%= fileName %> +version: 0.0.0 +description: <%= fileName %> UI +home: https://github.com/onecx-apps/<%= fileName %> +keywords: + - <%= fileName %> +sources: + - https://github.com/onecx-apps/<%= fileName %> +maintainers: + - name: # ACTION: add name of maintainer + email: # ACTION: add email of maintainer +dependencies: + - name: helm-product + version: ^0 + repository: oci://ghcr.io/onecx/charts + alias: product + - name: helm-angular-app + version: ^0 + repository: oci://ghcr.io/onecx/charts + alias: app diff --git a/nx-plugin-react/src/generators/react/files/helm/values.yaml.template b/nx-plugin-react/src/generators/react/files/helm/values.yaml.template new file mode 100644 index 00000000..32a3a0ee --- /dev/null +++ b/nx-plugin-react/src/generators/react/files/helm/values.yaml.template @@ -0,0 +1,41 @@ +productName: &product_name onecx-<%= fileName %> +product: + info: + data: + name: *product_name + description: "OneCX <%= fileName %> UI" + displayName: "OneCX <%= fileName %>" + basePath: "/onecx-<%= fileName %>" +app: + name: onecx-<%= fileName %>-ui + image: + repository: 'onecx-apps/onecx-<%= fileName %>-ui' + routing: + enabled: true + path: /mfe/<%= propertyName %>/ + operator: + # Microfrontend + microfrontend: + enabled: true + specs: + main: + productName: *product_name + exposedModule: './App' + description: 'OneCX <%= remoteModuleName %> UI' + note: 'OneCX <%= remoteModuleName %> UI module auto import via MF operator' + type: MODULE + technology: WEBCOMPONENTMODULE + remoteName: <%= remoteModuleFileName %> + tagName: ocx-<%= remoteModuleFileName %>-component + # Microservice + microservice: + spec: + productName: *product_name + name: OneCX <%= remoteModuleName %> UI + description: OneCX <%= remoteModuleName %> Frontend + # Permission + permission: + enabled: true + spec: + permissions: + # ACTION P: Adjust permissions for the entity diff --git a/nx-plugin-react/src/generators/react/files/index.html.template b/nx-plugin-react/src/generators/react/files/index.html.template new file mode 100644 index 00000000..9a004bfa --- /dev/null +++ b/nx-plugin-react/src/generators/react/files/index.html.template @@ -0,0 +1,14 @@ + + + + + OneCX <%= className %> + + + + + +
+ + + diff --git a/nx-plugin-react/src/generators/react/files/nginx/locations.conf b/nx-plugin-react/src/generators/react/files/nginx/locations.conf new file mode 100644 index 00000000..33db441e --- /dev/null +++ b/nx-plugin-react/src/generators/react/files/nginx/locations.conf @@ -0,0 +1,14 @@ +location @@APP_BASE_HREFbff/ { + if ($request_method = 'OPTIONS') { + add_header "Access-Control-Allow-Origin" $http_origin; + add_header 'Access-Control-Allow-Credentials' 'true'; + add_header "Access-Control-Allow-Methods" "GET, POST, OPTIONS, HEAD"; + add_header "Access-Control-Allow-Headers" "Authorization, Origin, X-Requested-With, Content-Type, Accept"; + add_header 'Content-Length' 0; + add_header 'Content-Type' 'text/plain charset=UTF-8'; + return 204; + } + proxy_pass @@BFF_URL; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $remote_addr; +} diff --git a/nx-plugin-react/src/generators/react/files/src/App.test.tsx.template b/nx-plugin-react/src/generators/react/files/src/App.test.tsx.template new file mode 100644 index 00000000..b0ce0edf --- /dev/null +++ b/nx-plugin-react/src/generators/react/files/src/App.test.tsx.template @@ -0,0 +1,21 @@ +import { describe, it, expect, vi } from 'vitest' + +vi.mock('@onecx/react-utils', () => ({ + withApp: vi.fn((component, config) => () => ({ component, config })), +})) + +describe('App', () => { + it('calls withApp with AppRouter and correct config', async () => { + const { withApp } = await import('@onecx/react-utils') + const { default: App } = await import('./App') + + expect(withApp).toHaveBeenCalledTimes(1) + expect(App()).toEqual({ + component: expect.any(Function), + config: { + PRODUCT_NAME: 'onecx-<%= fileName %>', + REMOTES_NAME: 'onecx-<%= fileName %>-ui', + }, + }) + }) +}) diff --git a/nx-plugin-react/src/generators/react/files/src/App.tsx.template b/nx-plugin-react/src/generators/react/files/src/App.tsx.template new file mode 100644 index 00000000..fafe8ed5 --- /dev/null +++ b/nx-plugin-react/src/generators/react/files/src/App.tsx.template @@ -0,0 +1,8 @@ +import { withApp } from '@onecx/react-utils' +import AppRouter from './router' + +// Wrap routes with portal-aware providers/configuration. +export default withApp(AppRouter, { + PRODUCT_NAME: 'onecx-<%= fileName %>', + REMOTES_NAME: 'onecx-<%= fileName %>-ui' +}) diff --git a/nx-plugin-react/src/generators/react/files/src/assets/api/openapi-bff.yaml b/nx-plugin-react/src/generators/react/files/src/assets/api/openapi-bff.yaml new file mode 100644 index 00000000..dd818a2d --- /dev/null +++ b/nx-plugin-react/src/generators/react/files/src/assets/api/openapi-bff.yaml @@ -0,0 +1,5 @@ +openapi: 3.0.0 +info: + title: OneCX <%= className %> BFF API + version: 1.0.0 +paths: {} diff --git a/nx-plugin-react/src/generators/react/files/src/assets/env.json b/nx-plugin-react/src/generators/react/files/src/assets/env.json new file mode 100644 index 00000000..c4e49c38 --- /dev/null +++ b/nx-plugin-react/src/generators/react/files/src/assets/env.json @@ -0,0 +1,4 @@ +{ + "BFF_URL": "${BFF_URL}", + "APP_BASE_HREF": "${APP_BASE_HREF}" +} diff --git a/nx-plugin-react/src/generators/react/files/src/assets/styles.css.template b/nx-plugin-react/src/generators/react/files/src/assets/styles.css.template new file mode 100644 index 00000000..1f95456a --- /dev/null +++ b/nx-plugin-react/src/generators/react/files/src/assets/styles.css.template @@ -0,0 +1,4 @@ +@import 'primereact/resources/themes/lara-light-cyan/theme.css'; +@import 'primeflex/primeflex.min.css'; +@import 'primeicons/primeicons.css'; +@import '../index.css'; diff --git a/nx-plugin-react/src/generators/react/files/src/bootstrap.test.ts.template b/nx-plugin-react/src/generators/react/files/src/bootstrap.test.ts.template new file mode 100644 index 00000000..56b0ca83 --- /dev/null +++ b/nx-plugin-react/src/generators/react/files/src/bootstrap.test.ts.template @@ -0,0 +1,19 @@ +import { vi } from 'vitest' + + +vi.mock('@onecx/react-webcomponents', () => ({ + createViteAppWebComponent: vi.fn() +})) + +describe('bootstrap.ts', () => { + it('calls init and createViteAppWebComponent with correct params', async () => { + // Reset modules so bootstrap.ts runs fresh + await import('./bootstrap.ts') + const { createViteAppWebComponent } = await import( + '@onecx/react-webcomponents' + ) + const { default: App } = await import('./App.tsx') + + expect(createViteAppWebComponent).toHaveBeenCalledWith(App, 'onecx-<%= fileName %>-ui-entrypoint') + }) +}) diff --git a/nx-plugin-react/src/generators/react/files/src/bootstrap.ts.template b/nx-plugin-react/src/generators/react/files/src/bootstrap.ts.template new file mode 100644 index 00000000..cb52821f --- /dev/null +++ b/nx-plugin-react/src/generators/react/files/src/bootstrap.ts.template @@ -0,0 +1,6 @@ +import { createViteAppWebComponent } from '@onecx/react-webcomponents' + +import App from './App.tsx' + +// Web component entrypoint for embedding into the portal shell. +createViteAppWebComponent(App, 'onecx-<%= fileName %>-ui-entrypoint') diff --git a/nx-plugin-react/src/generators/react/files/src/i18n/config.test.ts.template b/nx-plugin-react/src/generators/react/files/src/i18n/config.test.ts.template new file mode 100644 index 00000000..bb6685d8 --- /dev/null +++ b/nx-plugin-react/src/generators/react/files/src/i18n/config.test.ts.template @@ -0,0 +1,20 @@ +import i18n from "./config"; + +describe("i18n config", () => { + it("should initialize with correct resources and language", () => { + expect(i18n.language).toBe("en"); + expect(i18n.options.resources).toBeDefined(); + expect(i18n.options.resources?.en).toBeDefined(); + expect(i18n.options.resources?.de).toBeDefined(); + }); + + it("should have interpolation.escapeValue set to false", () => { + expect(i18n.options.interpolation?.escapeValue).toBe(false); + }); + + it("should return translations for en and de namespaces", () => { + // These will return the key if not found, but config is loaded + expect(i18n.t("translation:someKey", { lng: "en" })).toBeDefined(); + expect(i18n.t("translation:someKey", { lng: "de" })).toBeDefined(); + }); +}); diff --git a/nx-plugin-react/src/generators/react/files/src/i18n/config.ts.template b/nx-plugin-react/src/generators/react/files/src/i18n/config.ts.template new file mode 100644 index 00000000..785538af --- /dev/null +++ b/nx-plugin-react/src/generators/react/files/src/i18n/config.ts.template @@ -0,0 +1,23 @@ +import i18n from "i18next"; +import { initReactI18next } from "react-i18next"; +import enTranslation from "./sources/en.json"; +import deTranslation from "./sources/de.json"; + +const resources = { + en: { + translation: enTranslation, + }, + de: { + translation: deTranslation, + }, +}; + +i18n.use(initReactI18next).init({ + resources, + fallbackLng: "en", + interpolation: { + escapeValue: false, // react already safes from xss + }, +}); + +export default i18n; diff --git a/nx-plugin-react/src/generators/react/files/src/i18n/sources/de.json b/nx-plugin-react/src/generators/react/files/src/i18n/sources/de.json new file mode 100644 index 00000000..f8b06732 --- /dev/null +++ b/nx-plugin-react/src/generators/react/files/src/i18n/sources/de.json @@ -0,0 +1,6 @@ +{ + "welcome": { + "title": "Willkommen bei OneCX <%= className %>", + "subtitle": "Ihre Anwendung ist bereit." + } +} diff --git a/nx-plugin-react/src/generators/react/files/src/i18n/sources/en.json b/nx-plugin-react/src/generators/react/files/src/i18n/sources/en.json new file mode 100644 index 00000000..f06dd04a --- /dev/null +++ b/nx-plugin-react/src/generators/react/files/src/i18n/sources/en.json @@ -0,0 +1,6 @@ +{ + "welcome": { + "title": "Welcome to OneCX <%= className %>", + "subtitle": "Your application is ready." + } +} diff --git a/nx-plugin-react/src/generators/react/files/src/index.css.template b/nx-plugin-react/src/generators/react/files/src/index.css.template new file mode 100644 index 00000000..5c4cb285 --- /dev/null +++ b/nx-plugin-react/src/generators/react/files/src/index.css.template @@ -0,0 +1,7 @@ +body { + margin: 0; + padding: 0; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} diff --git a/nx-plugin-react/src/generators/react/files/src/pages/Welcome.test.tsx.template b/nx-plugin-react/src/generators/react/files/src/pages/Welcome.test.tsx.template new file mode 100644 index 00000000..d658b624 --- /dev/null +++ b/nx-plugin-react/src/generators/react/files/src/pages/Welcome.test.tsx.template @@ -0,0 +1,25 @@ +import { describe, it, expect, vi } from 'vitest' +import { render, screen } from '@testing-library/react' +import Welcome from './Welcome' + +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key: string, fallback: string) => fallback, + }), +})) + +describe('Welcome', () => { + it('renders welcome title', () => { + render() + expect( + screen.getByText('Welcome to OneCX <%= className %>') + ).toBeInTheDocument() + }) + + it('renders subtitle', () => { + render() + expect( + screen.getByText('Your application is ready.') + ).toBeInTheDocument() + }) +}) diff --git a/nx-plugin-react/src/generators/react/files/src/pages/Welcome.tsx.template b/nx-plugin-react/src/generators/react/files/src/pages/Welcome.tsx.template new file mode 100644 index 00000000..d656022e --- /dev/null +++ b/nx-plugin-react/src/generators/react/files/src/pages/Welcome.tsx.template @@ -0,0 +1,16 @@ +import { useTranslation } from 'react-i18next' + +export default function Welcome() { + const { t } = useTranslation() + + return ( +
+

+ {t('welcome.title', 'Welcome to OneCX <%= className %>')} +

+

+ {t('welcome.subtitle', 'Your application is ready.')} +

+
+ ) +} diff --git a/nx-plugin-react/src/generators/react/files/src/router.test.tsx.template b/nx-plugin-react/src/generators/react/files/src/router.test.tsx.template new file mode 100644 index 00000000..a652e39b --- /dev/null +++ b/nx-plugin-react/src/generators/react/files/src/router.test.tsx.template @@ -0,0 +1,57 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { render } from '@testing-library/react' + +const mockUseRoutes = vi.fn() +const mockUseAppHref = vi.fn() + +vi.mock('react-router', () => ({ + useRoutes: mockUseRoutes, +})) + +vi.mock('@onecx/react-webcomponents', () => ({ + useAppHref: mockUseAppHref, +})) + +vi.mock('./pages/Welcome', () => ({ + default: () => null, +})) + +vi.mock('./i18n/config', () => ({})) + +describe('AppRoutes', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('returns null when href is not available', async () => { + mockUseAppHref.mockReturnValue({ href: '' }) + mockUseRoutes.mockReturnValue(null) + + const { default: AppRoutes } = await import('./router') + const { container } = render() + expect(container.firstChild).toBeNull() + }) + + it('renders routes when href is available', async () => { + mockUseAppHref.mockReturnValue({ href: '/app' }) + mockUseRoutes.mockReturnValue(
routes
) + + const { default: AppRoutes } = await import('./router') + const { container } = render() + expect(container.firstChild).not.toBeNull() + }) + + it('passes correct routes to useRoutes', async () => { + mockUseAppHref.mockReturnValue({ href: '/app' }) + mockUseRoutes.mockReturnValue(
routes
) + + await import('./router') + + expect(mockUseRoutes).toHaveBeenCalledWith([ + { + path: '/app/', + element: expect.any(Object), + }, + ]) + }) +}) diff --git a/nx-plugin-react/src/generators/react/files/src/router.tsx.template b/nx-plugin-react/src/generators/react/files/src/router.tsx.template new file mode 100644 index 00000000..decf723f --- /dev/null +++ b/nx-plugin-react/src/generators/react/files/src/router.tsx.template @@ -0,0 +1,25 @@ +import { useRoutes } from 'react-router' +import Welcome from './pages/Welcome' +import { useAppHref } from '@onecx/react-webcomponents' +import './i18n/config' + +function AppRoutes() { + const { href } = useAppHref() + + const routes = [ + { + path: `${href}/`, + element: + } + ] + + const routing = useRoutes(routes) + + if (!href) { + return null + } + + return routing +} + +export default AppRoutes diff --git a/nx-plugin-react/src/generators/react/files/src/setupTests.ts.template b/nx-plugin-react/src/generators/react/files/src/setupTests.ts.template new file mode 100644 index 00000000..a9d0dd31 --- /dev/null +++ b/nx-plugin-react/src/generators/react/files/src/setupTests.ts.template @@ -0,0 +1 @@ +import '@testing-library/jest-dom/vitest' diff --git a/nx-plugin-react/src/generators/react/files/vite.config.ts.template b/nx-plugin-react/src/generators/react/files/vite.config.ts.template new file mode 100644 index 00000000..bb707a9d --- /dev/null +++ b/nx-plugin-react/src/generators/react/files/vite.config.ts.template @@ -0,0 +1,128 @@ +/// +import { defineConfig, loadEnv } from "vite"; +import react from "@vitejs/plugin-react"; +import { viteStaticCopy } from "vite-plugin-static-copy"; +import { writeFileSync } from "fs"; +import path from "path"; + +import { federation } from "@module-federation/vite"; +import type { ModuleFederationOptions } from "@module-federation/vite/lib/utils/normalizeModuleFederationOptions"; + +/// +const mfConfig: ModuleFederationOptions = { + name: "onecx-<%= fileName %>-ui", + filename: "remoteEntry.js", + exposes: { + "./OneCX<%= className %>RemoteModule": "./src/bootstrap.ts", + }, + shared: { + react: { + requiredVersion: dependencies.react, + singleton: true, + }, + "react-dom": { + requiredVersion: dependencies["react-dom"], + singleton: true, + }, + "react-router": { + requiredVersion: dependencies["react-router"], + singleton: true, + }, + i18next: { + requiredVersion: dependencies.i18next, + singleton: true, + }, + "react-i18next": { + requiredVersion: dependencies["react-i18next"], + singleton: true, + }, + primereact: { + requiredVersion: dependencies.primereact, + singleton: true, + }, + primeicons: { + requiredVersion: dependencies.primeicons, + singleton: true, + }, + "@onecx/accelerator": { + requiredVersion: dependencies["@onecx/accelerator"], + singleton: true, + }, + "@onecx/integration-interface": { + requiredVersion: dependencies["@onecx/integration-interface"], + singleton: true, + }, + "@onecx/react-utils": { + requiredVersion: dependencies["@onecx/react-utils"], + singleton: true, + }, + "@onecx/react-remote-components": { + requiredVersion: dependencies["@onecx/react-remote-components"], + singleton: true, + }, + "@onecx/react-integration-interface": { + requiredVersion: dependencies["@onecx/react-integration-interface"], + singleton: true, + }, + "@onecx/react-webcomponents": { + requiredVersion: dependencies["@onecx/react-webcomponents"], + singleton: true, + }, + "@onecx/react-auth": { + requiredVersion: dependencies["@onecx/react-auth"], + singleton: true, + }, + }, +}; +export default defineConfig(({ mode }) => { + const selfEnv = loadEnv(mode, process.cwd()); + return { + root: __dirname, + base: mode === "production" ? "/mfe/onecx-<%= fileName %>/" : "/", + plugins: [ + viteStaticCopy({ + targets: [ + { + src: path.resolve(__dirname, "./src/assets/styles.css"), + dest: "", // this will place it directly under /mfe/onecx-<%= fileName %>-ui/ + }, + { + src: path.resolve(__dirname, "./src/assets/env.json"), + dest: "assets", + }, + ], + }), + react(), + federation(mfConfig), + ], + server: { + proxy: { + "/api": { + target: "http://localhost:8080", + changeOrigin: true, + rewrite: (path) => path.replace(/^\/api/, ""), + }, + }, + }, + test: { + globals: true, + testTimeout: 10000, + environment: "jsdom", + setupFiles: "./src/setupTests.tsx", + coverage: { + reporter: ["text", "html", "cobertura"], + all: true, + include: ["src/**/*.{ts,tsx}"], + exclude: [ + "**/*.test.{ts,tsx}", + "src/main.tsx", + "src/vite-env.d.ts", + "src/**/custom-types.d.ts", + "src/**/global.d.ts", + "src/api/*", + "src/assets/*", + ], + }, + }, + }; +}); diff --git a/nx-plugin-react/src/generators/react/generator.ts b/nx-plugin-react/src/generators/react/generator.ts new file mode 100644 index 00000000..a1a331e4 --- /dev/null +++ b/nx-plugin-react/src/generators/react/generator.ts @@ -0,0 +1,280 @@ +import { + addDependenciesToPackageJson, + formatFiles, + generateFiles, + GeneratorCallback, + joinPathFragments, + names, + readProjectConfiguration, + Tree, + updateJson, + updateProjectConfiguration, +} from '@nx/devkit'; +import { applicationGenerator } from '@nx/react'; +import { execSync } from 'child_process'; +import * as ora from 'ora'; + +import processParams, { GeneratorParameter } from '../shared/parameters.utils'; +import { GeneratorProcessor } from '../shared/generator.utils'; +import { ReactGeneratorSchema } from './schema'; +import { GeneralOpenAPIStep } from './steps/general-openapi.step'; + +const PARAMETERS: GeneratorParameter[] = [ + { + key: 'chatty', + type: 'boolean', + required: 'never', + default: false, + }, +]; + +export async function reactGenerator( + tree: Tree, + options: ReactGeneratorSchema +): Promise { + function log(command: unknown) { + if (options.chatty) { + console.log(''); + console.log('generate react ==> ' + command); + } + } + const parameters = await processParams( + PARAMETERS, + options + ); + Object.assign(options, parameters); + + const spinner = ora('Adding React').start(); + const directory = options.name; + + const applicationGeneratorCallback = await applicationGenerator(tree, { + name: options.name, + directory: directory, + style: 'css', + tags: ``, + projectNameAndRootFormat: 'as-provided', + e2eTestRunner: 'none', + bundler: 'vite', + unitTestRunner: 'vitest', + routing: false, + linter: 'eslint', + }); + + tree.delete(`${directory}/src/app/app.tsx`); + tree.delete(`${directory}/src/app/app.spec.tsx`); + tree.delete(`${directory}/src/app/app.module.css`); + + tree.delete(`${directory}/src/app/nx-welcome.tsx`); + + generateFiles( + tree, + joinPathFragments(__dirname, './files'), + `${directory}/`, + { + ...options, + className: names(options.name).className, + remoteModuleName: names(options.name).className, + remoteModuleFileName: names(options.name).fileName, + fileName: names(options.name).fileName, + constantName: names(options.name).constantName, + propertyName: names(options.name).propertyName, + } + ); + + const generatorProcessor = new GeneratorProcessor(); + generatorProcessor.addStep(new GeneralOpenAPIStep()); + + generatorProcessor.run(tree, options, spinner); + + addBaseToPackageJson(tree, options); + addScriptsToPackageJson(tree, options); + addExtensionsToPackageJson(tree); + + const oneCXLibVersion = '^8.2.0'; + const reactVersion = '^19.0.0'; + const nxVersion = '22.0.2'; + + addDependenciesToPackageJson( + tree, + { + '@onecx/accelerator': oneCXLibVersion, + '@onecx/react-utils': oneCXLibVersion, + '@onecx/react-remote-components': oneCXLibVersion, + '@onecx/react-integration-interface': oneCXLibVersion, + '@onecx/react-webcomponents': oneCXLibVersion, + '@onecx/react-auth': oneCXLibVersion, + '@onecx/integration-interface': oneCXLibVersion, + react: reactVersion, + 'react-dom': reactVersion, + 'react-router': '^7.13.0"', + 'react-i18next': '"^16.5.4', + i18next: '^25.8.0', + primereact: '^10.9.7', + primeicons: '^7.0.0', + primeflex: '^4.0.0', + }, + { + '@nx/react': nxVersion, + '@nx/vite': nxVersion, + '@nx/devkit': nxVersion, + '@nx/js': nxVersion, + '@nx/web': nxVersion, + '@nx/workspace': nxVersion, + '@nx/plugin': nxVersion, + '@nx/eslint': nxVersion, + '@nx/eslint-plugin': nxVersion, + '@openapitools/openapi-generator-cli': '^2.16.3', + '@vitejs/plugin-react': '^4.4.0', + '@eslint/js': '^8.57.1', + eslint: '^9.8.0', + 'eslint-config-prettier': '^10.0.0', + 'eslint-plugin-import': '2.31.0', + 'eslint-plugin-prettier': '^5.2.1', + 'eslint-plugin-react': '^7.37.0', + 'eslint-plugin-react-hooks': '^5.0.0', + nx: nxVersion, + prettier: '^3.5.3', + 'sonar-scanner': '^3.1.0', + typescript: '^5.9.0', + vite: '^6.0.0', + vitest: '^3.0.0', + '@vitest/coverage-v8': '^3.0.0', + jsdom: '^26.0.0', + '@module-federation/vite': '^1.0.0', + 'vite-plugin-static-copy': '^2.0.0', + '@testing-library/dom': '^10.0.0', + '@testing-library/jest-dom': '^6.0.0', + '@testing-library/react': '^16.0.0', + '@types/react': '^19.0.0', + '@types/react-dom': '^19.0.0', + '@types/node': '^22.0.0', + } + ); + + adaptTsConfig(tree, options); + adaptProjectConfiguration(tree, options); + + await formatFiles(tree); + + spinner.succeed(); + + return async () => { + await applicationGeneratorCallback(); + let cmd = 'rm -rf .vscode '; + log(cmd); + execSync(cmd, { cwd: tree.root, stdio: 'inherit' }); + + cmd = 'mv -f .gitignore.org .gitignore'; + log(cmd); + execSync(cmd, { cwd: tree.root, stdio: 'inherit' }); + + cmd = 'npm run apigen '; + log(cmd); + execSync(cmd, { cwd: tree.root, stdio: 'inherit' }); + + const files = tree + .listChanges() + .map((c) => c.path) + .filter((p) => p.endsWith('.ts') || p.endsWith('.tsx')) + .join(' '); + cmd = 'npx prettier --write '; + log(cmd); + execSync(cmd + files, { cwd: tree.root, stdio: 'inherit' }); + }; +} + +function addBaseToPackageJson(tree: Tree, options: ReactGeneratorSchema) { + updateJson(tree, 'package.json', (pkgJson) => { + pkgJson.name = 'onecx-' + names(options.name).fileName + '-ui'; + pkgJson.private = true; + pkgJson.license = 'Apache-2.0'; + return pkgJson; + }); +} + +function addExtensionsToPackageJson(tree: Tree) { + updateJson(tree, 'package.json', (pkgJson) => { + pkgJson.jestSonar = { + reportPath: 'reports', + }; + return pkgJson; + }); +} + +function addScriptsToPackageJson(tree: Tree, _options: ReactGeneratorSchema) { + updateJson(tree, 'package.json', (pkgJson) => { + pkgJson.scripts = pkgJson.scripts ?? {}; + pkgJson.scripts[ + 'apigen' + ] = `openapi-generator-cli generate -i src/assets/api/openapi-bff.yaml -c apigen.yaml -o src/app/shared/generated -g typescript-fetch --type-mappings AnyType=object`; + pkgJson.scripts['start'] = 'nx serve --host 0.0.0.0'; + pkgJson.scripts['build'] = `nx build`; + pkgJson.scripts['clean'] = + 'npm cache clean --force && npx clear-npx-cache && rm -rf *.log dist reports .nx .eslintcache ./node_modules/.cache/prettier/.prettier-cache'; + pkgJson.scripts['format'] = 'nx format:write --uncommitted'; + pkgJson.scripts['lint'] = 'nx lint'; + pkgJson.scripts['lint:fix'] = 'nx lint --fix'; + pkgJson.scripts['sonar'] = 'npx sonar-scanner'; + pkgJson.scripts['test'] = 'nx test'; + pkgJson.scripts['test:ci'] = 'nx test --watch=false --code-coverage'; + return pkgJson; + }); +} + +function adaptTsConfig(tree: Tree, options: ReactGeneratorSchema) { + const directory = options.name; + const filePath = `${directory}/tsconfig.app.json`; + + updateJson(tree, filePath, (json) => { + json.files = ['src/main.tsx', 'src/bootstrap.tsx']; + json.compilerOptions = json.compilerOptions ?? {}; + json.compilerOptions.jsx = 'react-jsx'; + return json; + }); +} + +function adaptProjectConfiguration(tree: Tree, options: ReactGeneratorSchema) { + const config = readProjectConfiguration(tree, options.name); + config.targets['serve'].executor = '@nx/vite:dev-server'; + config.targets['serve'].options = { + ...(config.targets['serve'].options ?? {}), + host: '0.0.0.0', + port: 4200, + headers: { + Allow: 'GET, POST, PUT, DELETE, PATCH, OPTIONS', + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Headers': '*', + 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS', + }, + }; + config.targets['build'].executor = '@nx/vite:build'; + config.targets['build'].options = { + ...(config.targets['build'].options ?? {}), + assets: [ + ...(config.targets['build'].options?.assets ?? []), + { + glob: '**/*', + input: './node_modules/@onecx/react-utils/assets/', + output: '/onecx-react-utils/assets/', + }, + ], + }; + config.targets['build'].configurations = { + ...(config.targets['build'].configurations ?? {}), + production: { + ...(config.targets['build'].configurations?.production ?? {}), + fileReplacements: [ + ...(config.targets['build'].configurations?.production + ?.fileReplacements ?? []), + { + replace: 'src/environments/environment.ts', + with: 'src/environments/environment.prod.ts', + }, + ], + }, + }; + config.targets['test'].executor = '@nx/vite:test'; + updateProjectConfiguration(tree, names(options.name).fileName, config); +} + +export default reactGenerator; diff --git a/nx-plugin-react/src/generators/react/schema.d.ts b/nx-plugin-react/src/generators/react/schema.d.ts new file mode 100644 index 00000000..637e62de --- /dev/null +++ b/nx-plugin-react/src/generators/react/schema.d.ts @@ -0,0 +1,4 @@ +export interface ReactGeneratorSchema { + name: string; + chatty?: boolean; +} diff --git a/nx-plugin-react/src/generators/react/schema.json b/nx-plugin-react/src/generators/react/schema.json new file mode 100644 index 00000000..344860ac --- /dev/null +++ b/nx-plugin-react/src/generators/react/schema.json @@ -0,0 +1,18 @@ +{ + "$schema": "http://json-schema.org/schema", + "$id": "React", + "title": "", + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "", + "$default": { + "$source": "argv", + "index": 0 + }, + "x-prompt": "What name would you like to use?" + } + }, + "required": ["name"] +} diff --git a/nx-plugin-react/src/generators/react/steps/general-openapi.step.ts b/nx-plugin-react/src/generators/react/steps/general-openapi.step.ts new file mode 100644 index 00000000..d32d15ba --- /dev/null +++ b/nx-plugin-react/src/generators/react/steps/general-openapi.step.ts @@ -0,0 +1,33 @@ +import { Tree, joinPathFragments } from '@nx/devkit'; +import { GeneratorStep } from '../../shared/generator.utils'; +import { ReactGeneratorSchema } from '../schema'; +import { OpenAPIUtil } from '../../shared/openapi/openapi.utils'; + +export class GeneralOpenAPIStep + implements GeneratorStep +{ + process(tree: Tree, options: ReactGeneratorSchema): void { + const openApiFolderPath = 'src/assets/api'; + const bffOpenApiPath = 'openapi-bff.yaml'; + const bffOpenApiContent = tree.read( + joinPathFragments(openApiFolderPath, bffOpenApiPath), + 'utf8' + ); + + const resource = options.name; + + const apiUtil = new OpenAPIUtil(bffOpenApiContent); + const res = apiUtil + .servers() + .add(`http://onecx-${resource.toLocaleLowerCase()}-bff:8080/`, { + url: `http://onecx-${resource.toLocaleLowerCase()}-bff:8080/`, + }) + .done() + .finalize(); + + tree.write(joinPathFragments(openApiFolderPath, bffOpenApiPath), res); + } + getTitle(): string { + return 'Adapting OpenAPI'; + } +} diff --git a/nx-plugin-react/src/generators/shared/generator.utils.ts b/nx-plugin-react/src/generators/shared/generator.utils.ts new file mode 100644 index 00000000..240e0c76 --- /dev/null +++ b/nx-plugin-react/src/generators/shared/generator.utils.ts @@ -0,0 +1,104 @@ +import { Tree } from '@nx/devkit'; +import ora = require('ora'); + +interface GeneratorStepErrorParameters { + stopExecution: boolean; +} + +const DEFAULT_ERROR_PARAMETERS: GeneratorStepErrorParameters = { + stopExecution: false, +}; + +export class GeneratorStepError extends Error { + errorParameters: GeneratorStepErrorParameters; + + constructor(message: string, parameters?: GeneratorStepErrorParameters) { + super(message); + this.errorParameters = { + ...DEFAULT_ERROR_PARAMETERS, + ...parameters, + }; + } +} + +export interface GeneratorStep { + process(tree: Tree, options: T): void; + getTitle(): string; +} + +export class GeneratorProcessor { + private steps: GeneratorStep[] = []; + private errors: GeneratorStepError[] = []; + private _printErrors = false; + + addStep(step: GeneratorStep) { + this.steps.push(step); + } + + async run(tree: Tree, options: T, ora?: ora.Ora, printErrors = false) { + this._printErrors = printErrors; + this.errors = []; + for (const step of this.steps) { + if (ora) { + const stepTitle = step.getTitle().trimEnd(); + ora.info(stepTitle); + } + try { + step.process(tree, options); + } catch (error) { + if (error instanceof GeneratorStepError) { + const gsf = error as GeneratorStepError; + this.errors.push(gsf); + if (gsf.errorParameters.stopExecution) { + break; + } + } + } + } + this.printErrors(ora); + } + + getErrors(): GeneratorStepError[] { + return this.errors; + } + + hasStoppedExecution(): boolean { + return this.errors.find((e) => e.errorParameters.stopExecution) + ?.errorParameters.stopExecution; + } + + printErrors(ora?: ora.Ora) { + if (this.errors.length > 0 && this._printErrors) { + if (ora) { + ora.fail('Some errors occurred during generation:'); + } else { + console.error('Some errors occurred during generation:'); + } + this.errors.forEach((e) => { + console.error(e.message); + }); + if (this.hasStoppedExecution()) { + console.error( + 'One of the errors above stopped the generation, check for possible issues.' + ); + } + } + } + + static async runBatch( + tree: Tree, + options: T, + steps: GeneratorStep[], + ora?: ora.Ora, + printErrors = false + ): Promise> { + const genProc = new GeneratorProcessor(); + steps.forEach((s) => genProc.addStep(s)); + await genProc.run(tree, options, ora, printErrors); + return genProc; + } + + static getServiceName(name: string): string { + return name + 'APIService'; + } +} diff --git a/nx-plugin-react/src/generators/shared/openapi/models/openapi-default.model.ts b/nx-plugin-react/src/generators/shared/openapi/models/openapi-default.model.ts new file mode 100644 index 00000000..4ff5f675 --- /dev/null +++ b/nx-plugin-react/src/generators/shared/openapi/models/openapi-default.model.ts @@ -0,0 +1,8 @@ +export interface OpenAPIDefault { + type: 'get' | 'post' | 'put' | 'delete'; + operationId: string; + tags: string[]; + description: string; + requestBody?: object; + responses?: object; +} diff --git a/nx-plugin-react/src/generators/shared/openapi/openapi.utils.ts b/nx-plugin-react/src/generators/shared/openapi/openapi.utils.ts new file mode 100644 index 00000000..2e01203d --- /dev/null +++ b/nx-plugin-react/src/generators/shared/openapi/openapi.utils.ts @@ -0,0 +1,209 @@ +import { parse, stringify } from 'yaml'; +export const COMMENT_KEY = '~comment~'; + +interface OpenAPIRoute { + path: string; + component: string; + pathMatch: string; +} + +/** + * This utility can be used to adapt OpenAPI YAML Files + * It provides a builder-like interface to interact and bases + * on the YAML library to parse / stringify. + * When you want to add a comment to your JSON, you can do so by using an + * object with the COMMENT_KEY as key. Though be aware, once this utility + * parses the OpenAPI again, all previous comments will be lost (as JSON + * does not have comments). + */ +export class OpenAPIUtil { + private yamlContent: object; + + constructor(yamlContent: string) { + this.yamlContent = parse(yamlContent); + } + + /** + * Quick access to the servers section of the YAML + * @returns interface to add items to the section + */ + servers(): OpenAPIArraySectionUtil { + if (!this.yamlContent['servers']) { + this.yamlContent['servers'] = []; + } + return new OpenAPIArraySectionUtil(this, this.yamlContent['servers']); + } + + /** + * Quick access to the tags section of the YAML + * @returns interface to add items to the section + */ + tags(): OpenAPIArraySectionUtil { + if (!this.yamlContent['tags']) { + this.yamlContent['tags'] = []; + } + return new OpenAPIArraySectionUtil(this, this.yamlContent['tags']); + } + + /** + * Quick access to the routes section of the YAML + * @returns interface to add items to the section + */ + routes(): OpenAPIArraySectionUtil { + if (!this.yamlContent['routes']) { + this.yamlContent['routes'] = {}; + } + return new OpenAPIArraySectionUtil(this, this.yamlContent['routes']); + } + + /** + * Quick access to the paths section of the YAML + * @returns interface to set items of the section + */ + paths(): OpenAPIObjectSectionUtil { + if (!this.yamlContent['paths']) { + this.yamlContent['paths'] = {}; + } + return new OpenAPIObjectSectionUtil(this, this.yamlContent['paths']); + } + + /** + * Quick access to the schemas section of the YAML + * @returns interface to set items of the section + */ + schemas(): OpenAPIObjectSectionUtil { + if (!this.yamlContent['components']) { + this.yamlContent['components'] = {}; + } + if (!this.yamlContent['components']['schemas']) { + this.yamlContent['components']['schemas'] = {}; + } + return new OpenAPIObjectSectionUtil( + this, + this.yamlContent['components']['schemas'] + ); + } + + /** + * Access to the full YAML + * @returns interface to set items of the section + */ + full(): OpenAPIObjectSectionUtil { + return new OpenAPIObjectSectionUtil(this, this.yamlContent); + } + + finalize(): string { + let asString = stringify(this.yamlContent, { + lineWidth: 0, + }); + // Replace comments + asString = asString.replaceAll(`~comment~:`, '#'); + return asString; + } +} + +export interface ObjectSetOptions { + // Add a comment after insertion + comment?: string | undefined; + // If a value already exists for a key, what action should be performed + existStrategy: 'skip' | 'replace' | 'extend'; +} + +export class OpenAPIObjectSectionUtil { + private readonly util: OpenAPIUtil; + private sectionContent: object; + + constructor(util: OpenAPIUtil, sectionContent: object) { + this.util = util; + this.sectionContent = sectionContent; + } + + /** + * Sets an entry of this section content + * @param key key of the entry object + * @param value value of the entry object + * @param comment comment for the entry (added last) + * @param options configure existStrategy and comment + * @returns + */ + set(key: string, value: object, options?: ObjectSetOptions) { + const existStrategy = options ? options.existStrategy : 'skip'; + if (this.sectionContent[key] != null) { + if (existStrategy == 'extend') { + this.sectionContent[key] = { + ...this.sectionContent[key], + ...value, + }; + return this; + } + if (existStrategy == 'skip') { + return this; + } + // Replace is same as initial set + } + this.sectionContent[key] = value; + if (options?.comment) { + this.sectionContent[key][COMMENT_KEY] = options.comment; + } + return this; + } + + get(key: string) { + return this.sectionContent[key]; + } + + /** + * Return to util interface + * @returns initial util interface + */ + done() { + return this.util; + } +} + +export class OpenAPIArraySectionUtil { + private readonly util: OpenAPIUtil; + private sectionContent: T[]; + + constructor(util: OpenAPIUtil, sectionContent: T[]) { + this.util = util; + this.sectionContent = sectionContent; + } + + /** + * Add a new item to the section + * @param key key of the entry object + * @param value value of the entry object + * @param options configure existStrategy and comment + * @returns this util + */ + add(key: string, value: T, options?: ObjectSetOptions) { + const existStrategy = options ? options.existStrategy : 'skip'; + const existingItem = this.sectionContent.find((item: any) => item.name === key); + if (existingItem != null) { + if (existStrategy == 'skip') { + return this; + } + } + this.sectionContent.push(value); // add item to array + return this; + } + + /** + * Allows to run a manipulator on this section + * @param manipulator method to invoke with section data, return value is set + * @returns this util + */ + manipulate(manipulator: (sectionContent: T[]) => T[]) { + this.sectionContent = manipulator(this.sectionContent); + return this; + } + + /** + * Return to util interface + * @returns initial util interface + */ + done() { + return this.util; + } +} diff --git a/nx-plugin-react/src/generators/shared/parameters.utils.ts b/nx-plugin-react/src/generators/shared/parameters.utils.ts new file mode 100644 index 00000000..28bef286 --- /dev/null +++ b/nx-plugin-react/src/generators/shared/parameters.utils.ts @@ -0,0 +1,182 @@ +import yargs = require('yargs'); +import { prompt } from 'enquirer'; +import * as pc from 'picocolors'; + +// eslint-disable-next-line @typescript-eslint/no-var-requires + +const NON_INTERACTIVE_KEY = 'non-interactive'; + +interface ShowRule { + showIf: (values: T) => boolean; +} +interface GeneratorParameterBasic { + key: string; + required: 'always' | 'interactive' | 'never'; + default: + | string + | number + | boolean + | ((values: T) => string | number | boolean); + initial?: + | string + | number + | boolean + | ((values: T) => string | number | boolean); + prompt?: string; + showRules?: ShowRule[]; + showInSummary?: boolean; + choices?: string[]; +} + +interface GeneratorParameterInput extends GeneratorParameterBasic { + type: 'boolean' | 'text' | 'number'; +} + +interface GeneratorParameterChoices extends GeneratorParameterBasic { + type: 'select'; + choices: string[]; +} + +export type GeneratorParameter = + | GeneratorParameterInput + | GeneratorParameterChoices; + +/** + * This method validates if parameters have been set through the command line interface. + * If not, it checks whether they are required and if so, prompts the user for input. + * If they are not required, the default values are used. + * @returns dict with all parameters + */ +async function processParams( + parameters: GeneratorParameter[], + options: T +): Promise { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const { hideBin } = require('yargs/helpers'); + const argv = yargs(hideBin(process.argv)).argv; + + const parameterValues = Object.assign({}, options); + const interactiveParameters: GeneratorParameter[] = []; + + for (const parameter of parameters) { + // Prefill with defaults + if (typeof parameter.default == 'function') { + parameterValues[parameter.key] = parameter.default(parameterValues); + } else { + if (parameterValues[parameter.key] == undefined) { + parameterValues[parameter.key] = parameter.default; + } + } + // Check if provided by either CLI or continue with interactive + if (argv[parameter.key] != null) { + parameterValues[parameter.key] = argv[parameter.key]; + } else { + if ( + parameter.required == 'always' || + (parameter.required == 'interactive' && !argv[NON_INTERACTIVE_KEY]) + ) { + interactiveParameters.push(parameter); + } + } + } + + let showSummary = false; + for (const parameter of interactiveParameters) { + // First filter interactive by rules + if (parameter.showRules) { + let show = true; + for (const rule of parameter.showRules) { + if (!rule.showIf(parameterValues)) { + show = false; + break; + } + } + if (!show) continue; + } + let result = {}; + const defaultValue = parameterValues[parameter.key]; + if (parameter.type == 'boolean') { + result = await prompt({ + type: 'confirm', + name: parameter.key, + message: parameter.prompt, + }); + } else if (parameter.type == 'text') { + result = await prompt({ + type: 'text', + name: parameter.key, + initial: defaultValue, + message: parameter.prompt, + }); + } else if (parameter.type == 'number') { + result = await prompt({ + type: 'number', + name: parameter.key, + initial: defaultValue, + message: parameter.prompt, + }); + } else if (parameter.type == 'select') { + result = await prompt({ + type: 'select', + name: parameter.key, + message: parameter.prompt, + choices: parameter.choices, + }); + } + Object.assign(parameterValues, result); + showSummary = showSummary || parameter.showInSummary; + } + + if (showSummary) { + let inputsFinal = false; + while (!inputsFinal) { + console.log(pc.bold(' *** Summary ***')); + for (const parameter of parameters) { + if (!parameter.showInSummary) continue; + console.log( + pc.bold(parameter.key) + ': ' + parameterValues[parameter.key] + ); + } + + const confirm = await prompt({ + type: 'confirm', + name: 'adapt', + message: 'Do you need to adapt your inputs?', + }); + if (!confirm['adapt']) { + inputsFinal = false; + break; + } + + const result = await prompt({ + type: 'form', + name: 'data', + message: 'Edit your input:', + choices: parameters + .filter((p) => p.showInSummary) + .map((p) => ({ + name: p.key, + message: p.prompt, + initial: `${parameterValues[p.key]}`, + })), + }); + + // Map types again + for (const parameter of parameters.filter((p) => p.showInSummary)) { + if (parameter.type === 'boolean' && typeof result['data'][parameter.key] === 'string') { + const val = result['data'][parameter.key].toLowerCase(); + if (val === 'true') result['data'][parameter.key] = true; + else if (val === 'false') result['data'][parameter.key] = false; + } else if (parameter.type === 'number' && typeof result['data'][parameter.key] === 'string') { + result['data'][parameter.key] = Number(result['data'][parameter.key]); + } + } + + Object.assign(parameterValues, result['data']); + } + } + + return parameterValues as T; +} + +export default processParams; diff --git a/nx-plugin-react/src/generators/shared/safeReplace.ts b/nx-plugin-react/src/generators/shared/safeReplace.ts new file mode 100644 index 00000000..a69d1a27 --- /dev/null +++ b/nx-plugin-react/src/generators/shared/safeReplace.ts @@ -0,0 +1,120 @@ +import { Tree } from '@nx/devkit'; +import { GeneratorStepError } from './generator.utils'; + +interface ReplacementResult { + success: boolean; + errors?: string[]; + content?: string; +} + +/** + * Performs replacements in a given string content based on the provided patterns and replacements. + * + * @param content - The original content in which replacements will be performed. + * @param find - The pattern(s) to search for. Can be a string, regex, or an array of strings/regexes. + * @param replaceWith - The replacement string(s) for the pattern(s). Can be a string or an array of strings. + * @returns A `ReplacementResult` object containing the success status, errors (if any), and the modified content. + */ +function performReplacements( + content: string, + find: string | RegExp | (string | RegExp)[], + replaceWith: string | string[] +): ReplacementResult { + let allReplacementsSuccessful = true; + const replacementErrors: string[] = []; + let newContent = content; + + const findArray = Array.isArray(find) ? find : [find]; + const replaceWithArray = Array.isArray(replaceWith) + ? replaceWith + : [replaceWith]; + + for (let i = 0; i < findArray.length; i++) { + const currentFind = findArray[i]; + const currentReplaceWith = replaceWithArray[i]; + + try { + if (typeof currentFind === 'string' || currentFind instanceof RegExp) { + if (newContent.includes(currentReplaceWith)) { + replacementErrors.push( + `Text already exists in the document: ${currentReplaceWith}` + ); + } + + if ( + typeof currentFind === 'string' && + !newContent.includes(currentFind) + ) { + replacementErrors.push( + `Could not find the pattern: ${currentFind}. Attempted to replace with: ${currentReplaceWith}` + ); + } + + if (currentFind instanceof RegExp && !currentFind.test(newContent)) { + replacementErrors.push( + `Could not find the pattern: ${currentFind}. Attempted to replace with: ${currentReplaceWith}` + ); + } + + newContent = newContent.replace(currentFind, currentReplaceWith); + } + } catch (error) { + allReplacementsSuccessful = false; + replacementErrors.push(error.message); + } + } + + return { + success: allReplacementsSuccessful, + errors: replacementErrors.length > 0 ? replacementErrors : undefined, + content: newContent, + }; +} + +/** + * Safely performs replacements in a file within an Nx workspace. + * If replacements fail, it appends detailed error messages to the file and logs the errors to the console. + * + * @param goal - A description of the goal of the replacement (e.g. "Add new feature X"). + * @param file - The path to the file in which replacements should be performed. + * @param find - The pattern(s) to search for. Can be a string, regex, or an array of strings/regexes. + * @param replaceWith - The replacement string(s) for the pattern(s). Can be a string or an array of strings. + * @param tree - The Nx `Tree` object representing the file system. + * @throws {GeneratorStepError} If the file does not exist. + */ +export function safeReplace( + goal: string, + file: string, + find: string | RegExp | (string | RegExp)[], + replaceWith: string | string[], + tree: Tree +): void { + if (!tree.exists(file)) { + throw new GeneratorStepError(`File not found: ${file}`); + } + + const content = tree.read(file, 'utf8'); + + const result = performReplacements(content, find, replaceWith); + + if (result.success) { + tree.write(file, result.content); + } else { + const comment = `// Generator Failure occurred! +// The goal of the generation was to: ${goal} +// +// The following replacements failed: +${result.errors.map((error) => `// ${error}`).join('\n')} +// +// Please perform the replacements manually. +`; + + const newContent = `${comment}\n${result.content}`; + tree.write(file, newContent); + + console.error( + `Error: Some replacements could not be completed. Review the file for more information: ${file}` + ); + console.error(`Errors: ${result.errors.join('\n')}`); + } +} diff --git a/nx-plugin-react/src/index.ts b/nx-plugin-react/src/index.ts new file mode 100644 index 00000000..e69de29b diff --git a/nx-plugin-react/tsconfig.json b/nx-plugin-react/tsconfig.json new file mode 100644 index 00000000..150e7382 --- /dev/null +++ b/nx-plugin-react/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + "module": "commonjs" + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/nx-plugin-react/tsconfig.lib.json b/nx-plugin-react/tsconfig.lib.json new file mode 100644 index 00000000..6f3c503a --- /dev/null +++ b/nx-plugin-react/tsconfig.lib.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../dist/out-tsc", + "declaration": true, + "types": ["node"] + }, + "include": ["src/**/*.ts"], + "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"] +} diff --git a/nx-plugin-react/tsconfig.spec.json b/nx-plugin-react/tsconfig.spec.json new file mode 100644 index 00000000..663c8789 --- /dev/null +++ b/nx-plugin-react/tsconfig.spec.json @@ -0,0 +1,14 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node"] + }, + "include": [ + "jest.config.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.d.ts" + ] +} diff --git a/nx.json b/nx.json index 3d525973..e0da5020 100644 --- a/nx.json +++ b/nx.json @@ -32,6 +32,11 @@ "codeCoverage": true } } + }, + "@nx/js:tsc": { + "cache": true, + "dependsOn": ["^build"], + "inputs": ["default", "^default"] } }, "useInferencePlugins": false diff --git a/package-lock.json b/package-lock.json index 57895475..8ed93114 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "onecx-nx-plugins", - "version": "7.1.0-rc.2", + "version": "7.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "onecx-nx-plugins", - "version": "7.1.0-rc.2", + "version": "7.1.0", "license": "Apache-2.0", "workspaces": [ "packages/*" @@ -15,6 +15,7 @@ "@nx/angular": "^19.8.14", "@nx/devkit": "^19.8.14", "@nx/plugin": "^19.8.14", + "@nx/react": "^19.8.14", "@swc/helpers": "^0.5.12", "create-nx-workspace": "^19.8.14", "ora": "^5.3.0", @@ -2094,6 +2095,110 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-react-constant-elements": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.27.1.tgz", + "integrity": "sha512-edoidOjl/ZxvYo4lSBOQGDSyToYVkTAwyVoa2tkuYTSmjrB1+uAedoL5iROVLXkxH+vRgA7uP4tMg2pUJpZ3Ug==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-display-name": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.28.0.tgz", + "integrity": "sha512-D6Eujc2zMxKjfa4Zxl4GHMsmhKKZ9VpcqIchJLvwTxad9zWIYulwYItBovpDOoNLISpcZSXoDJ5gaGbQUDqViA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.28.6.tgz", + "integrity": "sha512-61bxqhiRfAACulXSLd/GxqmAedUSrRZIu/cbaT18T1CetkTmtDN15it7i80ru4DVqRK1WMxQhXs+Lf9kajm5Ow==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/plugin-syntax-jsx": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-development": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.27.1.tgz", + "integrity": "sha512-ykDdF5yI4f1WrAolLqeF3hmYU12j9ntLQl/AOG1HAS21jxyg1Q0/J/tpREuYLfatGdGmXp/3yS0ZA76kOlVq9Q==", + "license": "MIT", + "dependencies": { + "@babel/plugin-transform-react-jsx": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx/node_modules/@babel/helper-annotate-as-pure": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", + "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/plugin-transform-react-pure-annotations": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.27.1.tgz", + "integrity": "sha512-JfuinvDOsD9FVMTHpzA/pBLisxpv1aSf+OIV8lgH3MuWrks19R27e6a6DipIg4aX1Zm9Wpb04p8wljfKrVSnPA==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-pure-annotations/node_modules/@babel/helper-annotate-as-pure": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", + "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/plugin-transform-regenerator": { "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.29.0.tgz", @@ -2460,6 +2565,26 @@ "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" } }, + "node_modules/@babel/preset-react": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.28.5.tgz", + "integrity": "sha512-Z3J8vhRq7CeLjdC58jLv4lnZ5RKFUJWqH5emvxmv9Hv3BD1T9R/Im713R4MTKwvFaV74ejZ3sM01LyEKk4ugNQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-option": "^7.27.1", + "@babel/plugin-transform-react-display-name": "^7.28.0", + "@babel/plugin-transform-react-jsx": "^7.27.1", + "@babel/plugin-transform-react-jsx-development": "^7.27.1", + "@babel/plugin-transform-react-pure-annotations": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/preset-typescript": { "version": "7.28.5", "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.28.5.tgz", @@ -4844,6 +4969,15 @@ "@nx/plugin": "19.8.14" } }, + "node_modules/@nrwl/react": { + "version": "19.8.14", + "resolved": "https://registry.npmjs.org/@nrwl/react/-/react-19.8.14.tgz", + "integrity": "sha512-iTmqiMhTIUU2mYb9hQU+A/1uoI2Z3UnvNkiuxOgYAnS76BlUbkMOD01dh0ZJrRRJMVknMj+8RDYk7Vb6DlIcUg==", + "license": "MIT", + "dependencies": { + "@nx/react": "19.8.14" + } + }, "node_modules/@nrwl/tao": { "version": "19.8.14", "resolved": "https://registry.npmjs.org/@nrwl/tao/-/tao-19.8.14.tgz", @@ -5320,9 +5454,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -5339,9 +5470,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -5358,9 +5486,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -5377,9 +5502,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -5435,6 +5557,28 @@ "tslib": "^2.3.0" } }, + "node_modules/@nx/react": { + "version": "19.8.14", + "resolved": "https://registry.npmjs.org/@nx/react/-/react-19.8.14.tgz", + "integrity": "sha512-3Cg/8uyNdrD1pep2ia6pAfZwBNT/f0SIu1T8RbmQiZZYIEQipaxyMFtIJtcTwOawxU53+slSdG3X19JUwwHcXQ==", + "license": "MIT", + "dependencies": { + "@module-federation/enhanced": "~0.6.0", + "@nrwl/react": "19.8.14", + "@nx/devkit": "19.8.14", + "@nx/eslint": "19.8.14", + "@nx/js": "19.8.14", + "@nx/web": "19.8.14", + "@phenomnomnominal/tsquery": "~5.0.1", + "@svgr/webpack": "^8.0.1", + "express": "^4.19.2", + "file-loader": "^6.2.0", + "http-proxy-middleware": "^3.0.0", + "minimatch": "9.0.3", + "picocolors": "^1.1.0", + "tslib": "^2.3.0" + } + }, "node_modules/@nx/web": { "version": "19.8.14", "resolved": "https://registry.npmjs.org/@nx/web/-/web-19.8.14.tgz", @@ -6056,6 +6200,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -6069,6 +6214,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -6082,6 +6228,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -6095,6 +6242,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -6108,6 +6256,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -6121,6 +6270,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -6134,6 +6284,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -6147,9 +6298,7 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -6163,9 +6312,7 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -6179,9 +6326,7 @@ "cpu": [ "ppc64" ], - "libc": [ - "glibc" - ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -6195,9 +6340,7 @@ "cpu": [ "riscv64" ], - "libc": [ - "glibc" - ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -6211,9 +6354,7 @@ "cpu": [ "riscv64" ], - "libc": [ - "musl" - ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -6227,9 +6368,7 @@ "cpu": [ "s390x" ], - "libc": [ - "glibc" - ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -6243,9 +6382,7 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -6259,9 +6396,7 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -6275,6 +6410,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -6288,6 +6424,7 @@ "cpu": [ "wasm32" ], + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -6301,6 +6438,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz", "integrity": "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -6317,6 +6455,7 @@ "version": "0.10.1", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -6330,6 +6469,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -6343,6 +6483,7 @@ "cpu": [ "ia32" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -6356,6 +6497,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -6492,9 +6634,6 @@ "cpu": [ "arm" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -6509,9 +6648,6 @@ "cpu": [ "arm" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -6526,9 +6662,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -6543,9 +6676,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -6560,9 +6690,6 @@ "cpu": [ "ppc64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -6577,9 +6704,6 @@ "cpu": [ "riscv64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -6594,9 +6718,6 @@ "cpu": [ "s390x" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -6611,9 +6732,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -6628,9 +6746,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -7199,74 +7314,479 @@ "lodash-es": "^4.17.21", "read-package-up": "^11.0.0" }, - "engines": { - "node": ">=20.8.1" + "engines": { + "node": ">=20.8.1" + }, + "peerDependencies": { + "semantic-release": ">=20.1.0" + } + }, + "node_modules/@simple-libs/stream-utils": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@simple-libs/stream-utils/-/stream-utils-1.2.0.tgz", + "integrity": "sha512-KxXvfapcixpz6rVEB6HPjOUZT22yN6v0vI0urQSk1L8MlEWPDFCZkhw2xmkyoTGYeFw7tWTZd7e3lVzRZRN/EA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://ko-fi.com/dangreen" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.10", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", + "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==", + "license": "MIT" + }, + "node_modules/@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@sindresorhus/merge-streams": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", + "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@svgr/babel-plugin-add-jsx-attribute": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz", + "integrity": "sha512-b9MIk7yhdS1pMCZM8VeNfUlSKVRhsHZNMl5O9SfaX0l0t5wjdgu4IDzGB8bpnGBBOjGST3rRFVsaaEtI4W6f7g==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-remove-jsx-attribute": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-8.0.0.tgz", + "integrity": "sha512-BcCkm/STipKvbCl6b7QFrMh/vx00vIP63k2eM66MfHJzPr6O2U0jYEViXkHJWqXqQYjdeA9cuCl5KWmlwjDvbA==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-remove-jsx-empty-expression": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-8.0.0.tgz", + "integrity": "sha512-5BcGCBfBxB5+XSDSWnhTThfI9jcO5f0Ai2V24gZpG+wXF14BzwxxdDb4g6trdOux0rhibGs385BeFMSmxtS3uA==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-replace-jsx-attribute-value": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-8.0.0.tgz", + "integrity": "sha512-KVQ+PtIjb1BuYT3ht8M5KbzWBhdAjjUPdlMtpuw/VjT8coTrItWX6Qafl9+ji831JaJcu6PJNKCV0bp01lBNzQ==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-svg-dynamic-title": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-8.0.0.tgz", + "integrity": "sha512-omNiKqwjNmOQJ2v6ge4SErBbkooV2aAWwaPFs2vUY7p7GhVkzRkJ00kILXQvRhA6miHnNpXv7MRnnSjdRjK8og==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-svg-em-dimensions": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-8.0.0.tgz", + "integrity": "sha512-mURHYnu6Iw3UBTbhGwE/vsngtCIbHE43xCRK7kCw4t01xyGqb2Pd+WXekRRoFOBIY29ZoOhUCTEweDMdrjfi9g==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-transform-react-native-svg": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-8.1.0.tgz", + "integrity": "sha512-Tx8T58CHo+7nwJ+EhUwx3LfdNSG9R2OKfaIXXs5soiy5HtgoAEkDay9LIimLOcG8dJQH1wPZp/cnAv6S9CrR1Q==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-transform-svg-component": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-8.0.0.tgz", + "integrity": "sha512-DFx8xa3cZXTdb/k3kfPeaixecQLgKh5NVBMwD0AQxOzcZawK4oo1Jh9LbrcACUivsCA7TLG8eeWgrDXjTMhRmw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-preset": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-preset/-/babel-preset-8.1.0.tgz", + "integrity": "sha512-7EYDbHE7MxHpv4sxvnVPngw5fuR6pw79SkcrILHJ/iMpuKySNCl5W1qcwPEpU+LgyRXOaAFgH0KhwD18wwg6ug==", + "license": "MIT", + "dependencies": { + "@svgr/babel-plugin-add-jsx-attribute": "8.0.0", + "@svgr/babel-plugin-remove-jsx-attribute": "8.0.0", + "@svgr/babel-plugin-remove-jsx-empty-expression": "8.0.0", + "@svgr/babel-plugin-replace-jsx-attribute-value": "8.0.0", + "@svgr/babel-plugin-svg-dynamic-title": "8.0.0", + "@svgr/babel-plugin-svg-em-dimensions": "8.0.0", + "@svgr/babel-plugin-transform-react-native-svg": "8.1.0", + "@svgr/babel-plugin-transform-svg-component": "8.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/core": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/core/-/core-8.1.0.tgz", + "integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.21.3", + "@svgr/babel-preset": "8.1.0", + "camelcase": "^6.2.0", + "cosmiconfig": "^8.1.3", + "snake-case": "^3.0.4" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/core/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/@svgr/core/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@svgr/core/node_modules/cosmiconfig": { + "version": "8.3.6", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", + "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", + "license": "MIT", + "dependencies": { + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0", + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@svgr/core/node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@svgr/core/node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" + }, + "node_modules/@svgr/core/node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@svgr/hast-util-to-babel-ast": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-8.0.0.tgz", + "integrity": "sha512-EbDKwO9GpfWP4jN9sGdYwPBU0kdomaPIL2Eu4YwmgP+sJeXT+L7bMwJUBnhzfH8Q2qMBqZ4fJwpCyYsAN3mt2Q==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.21.3", + "entities": "^4.4.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/plugin-jsx": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/plugin-jsx/-/plugin-jsx-8.1.0.tgz", + "integrity": "sha512-0xiIyBsLlr8quN+WyuxooNW9RJ0Dpr8uOnH/xrCVO8GLUcwHISwj1AG0k+LFzteTkAA0GbX0kj9q6Dk70PTiPA==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.21.3", + "@svgr/babel-preset": "8.1.0", + "@svgr/hast-util-to-babel-ast": "8.0.0", + "svg-parser": "^2.0.4" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@svgr/core": "*" + } + }, + "node_modules/@svgr/plugin-svgo": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/plugin-svgo/-/plugin-svgo-8.1.0.tgz", + "integrity": "sha512-Ywtl837OGO9pTLIN/onoWLmDQ4zFUycI1g76vuKGEz6evR/ZTJlJuz3G/fIkb6OVBJ2g0o6CGJzaEjfmEo3AHA==", + "license": "MIT", + "dependencies": { + "cosmiconfig": "^8.1.3", + "deepmerge": "^4.3.1", + "svgo": "^3.0.2" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@svgr/core": "*" + } + }, + "node_modules/@svgr/plugin-svgo/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/@svgr/plugin-svgo/node_modules/cosmiconfig": { + "version": "8.3.6", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", + "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", + "license": "MIT", + "dependencies": { + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0", + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" }, "peerDependencies": { - "semantic-release": ">=20.1.0" + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, - "node_modules/@simple-libs/stream-utils": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@simple-libs/stream-utils/-/stream-utils-1.2.0.tgz", - "integrity": "sha512-KxXvfapcixpz6rVEB6HPjOUZT22yN6v0vI0urQSk1L8MlEWPDFCZkhw2xmkyoTGYeFw7tWTZd7e3lVzRZRN/EA==", - "dev": true, + "node_modules/@svgr/plugin-svgo/node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "license": "MIT", - "engines": { - "node": ">=18" + "dependencies": { + "argparse": "^2.0.1" }, - "funding": { - "url": "https://ko-fi.com/dangreen" + "bin": { + "js-yaml": "bin/js-yaml.js" } }, - "node_modules/@sinclair/typebox": { - "version": "0.27.10", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", - "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==", + "node_modules/@svgr/plugin-svgo/node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "license": "MIT" }, - "node_modules/@sindresorhus/is": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", - "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", - "dev": true, + "node_modules/@svgr/plugin-svgo/node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, "engines": { - "node": ">=10" + "node": ">=8" }, "funding": { - "url": "https://github.com/sindresorhus/is?sponsor=1" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@sindresorhus/merge-streams": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", - "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==", - "dev": true, + "node_modules/@svgr/webpack": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/webpack/-/webpack-8.1.0.tgz", + "integrity": "sha512-LnhVjMWyMQV9ZmeEy26maJk+8HTIbd59cH4F2MJ439k9DqejRisfFNGAPvRYlKETuh9LrImlS8aKsBgKjMA8WA==", "license": "MIT", + "dependencies": { + "@babel/core": "^7.21.3", + "@babel/plugin-transform-react-constant-elements": "^7.21.3", + "@babel/preset-env": "^7.20.2", + "@babel/preset-react": "^7.18.6", + "@babel/preset-typescript": "^7.21.0", + "@svgr/core": "8.1.0", + "@svgr/plugin-jsx": "8.1.0", + "@svgr/plugin-svgo": "8.1.0" + }, "engines": { - "node": ">=18" + "node": ">=14" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@sinonjs/commons": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", - "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", - "license": "BSD-3-Clause", - "dependencies": { - "type-detect": "4.0.8" - } - }, - "node_modules/@sinonjs/fake-timers": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", - "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", - "license": "BSD-3-Clause", - "dependencies": { - "@sinonjs/commons": "^3.0.0" + "type": "github", + "url": "https://github.com/sponsors/gregberge" } }, "node_modules/@swc-node/core": { @@ -7403,6 +7923,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -7419,6 +7940,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -7435,6 +7957,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -7451,9 +7974,7 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], + "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -7470,9 +7991,7 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], + "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -7489,9 +8008,7 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], + "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -7508,9 +8025,7 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], + "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -7527,6 +8042,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -7543,6 +8059,7 @@ "cpu": [ "ia32" ], + "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -7559,6 +8076,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -13685,6 +14203,16 @@ "url": "https://github.com/fb55/domutils?sponsor=1" } }, + "node_modules/dot-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", + "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", + "license": "MIT", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, "node_modules/dot-prop": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", @@ -14274,6 +14802,7 @@ "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, "license": "BSD-3-Clause", "optional": true, "engines": { @@ -15055,6 +15584,89 @@ "node": "^10.12.0 || >=12.0.0" } }, + "node_modules/file-loader": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz", + "integrity": "sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==", + "license": "MIT", + "dependencies": { + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/file-loader/node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/file-loader/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/file-loader/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "license": "MIT" + }, + "node_modules/file-loader/node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "license": "MIT", + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/file-loader/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, "node_modules/file-type": { "version": "17.1.6", "resolved": "https://registry.npmjs.org/file-type/-/file-type-17.1.6.tgz", @@ -17485,6 +18097,41 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/jest-circus/node_modules/babel-plugin-macros": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", + "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@babel/runtime": "^7.12.5", + "cosmiconfig": "^7.0.0", + "resolve": "^1.19.0" + }, + "engines": { + "node": ">=10", + "npm": ">=6" + } + }, + "node_modules/jest-circus/node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/jest-circus/node_modules/dedent": { "version": "1.7.2", "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz", @@ -17499,6 +18146,45 @@ } } }, + "node_modules/jest-circus/node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/jest-circus/node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-circus/node_modules/yaml": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz", + "integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==", + "license": "ISC", + "optional": true, + "peer": true, + "engines": { + "node": ">= 6" + } + }, "node_modules/jest-cli": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", @@ -19258,6 +19944,15 @@ "node": ">=4" } }, + "node_modules/lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.3" + } + }, "node_modules/lowercase-keys": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", @@ -19966,6 +20661,16 @@ "license": "MIT", "optional": true }, + "node_modules/no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "license": "MIT", + "dependencies": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + } + }, "node_modules/node-abort-controller": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", @@ -22426,9 +23131,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -22447,9 +23149,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -22468,9 +23167,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -22489,9 +23185,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -29817,6 +30510,16 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/snake-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz", + "integrity": "sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==", + "license": "MIT", + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, "node_modules/sockjs": { "version": "0.3.24", "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", @@ -30605,6 +31308,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/svg-parser": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz", + "integrity": "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==", + "license": "MIT" + }, "node_modules/svgo": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.3.3.tgz", @@ -31520,6 +32229,7 @@ "version": "3.19.3", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "dev": true, "license": "BSD-2-Clause", "optional": true, "bin": { diff --git a/package.json b/package.json index aab3df66..9d792f89 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ }, "dependencies": { "@nx/angular": "^19.8.14", + "@nx/react": "^19.8.14", "@nx/devkit": "^19.8.14", "@nx/plugin": "^19.8.14", "@swc/helpers": "^0.5.12", diff --git a/tsconfig.base.json b/tsconfig.base.json index cd871663..1b4f56e8 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -10,24 +10,15 @@ "importHelpers": true, "target": "es2015", "module": "esnext", - "lib": [ - "es2020", - "dom" - ], + "lib": ["es2020", "dom"], "skipLibCheck": true, "skipDefaultLibCheck": true, "baseUrl": ".", "paths": { - "@onecx/nx-plugin": [ - "nx-plugin/src/index.ts" - ], - "@onecx/release": [ - "tools/release/src/index.ts" - ] + "@onecx/nx-plugin": ["nx-plugin/src/index.ts"], + "@onecx/release": ["tools/release/src/index.ts"], + "nx-plugin-react": ["nx-plugin-react/src/index.ts"] } }, - "exclude": [ - "node_modules", - "tmp" - ] + "exclude": ["node_modules", "tmp"] } From 50bdd68625430a6ceeab1429123f444e0fe3a665 Mon Sep 17 00:00:00 2001 From: Kamil Nowak Date: Fri, 15 May 2026 17:06:58 +0200 Subject: [PATCH 02/18] chore: prepare nx-plugin-react package for publication - Add Apache-2.0 license file - Update package.json with repository information and publish configuration - Add required dependencies: @nx/react, enquirer, ora, picocolors, yaml, yargs - Configure package as public with proper types and main entry points - Update tsconfig to include ES2021 lib --- nx-plugin-react/LICENSE | 201 ++++++++++++++++++++++++++++++++++ nx-plugin-react/package.json | 23 +++- nx-plugin-react/tsconfig.json | 3 +- 3 files changed, 222 insertions(+), 5 deletions(-) create mode 100644 nx-plugin-react/LICENSE diff --git a/nx-plugin-react/LICENSE b/nx-plugin-react/LICENSE new file mode 100644 index 00000000..261eeb9e --- /dev/null +++ b/nx-plugin-react/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/nx-plugin-react/package.json b/nx-plugin-react/package.json index b02efcc4..6e856770 100644 --- a/nx-plugin-react/package.json +++ b/nx-plugin-react/package.json @@ -1,13 +1,28 @@ { - "name": "nx-plugin-react", + "name": "@onecx/nx-plugin-react", "version": "0.0.1", + "license": "Apache-2.0", + "contributors": [], + "repository": { + "type": "git", + "url": "git+https://github.com/onecx/onecx-nx-plugins.git" + }, "dependencies": { "@nx/devkit": "19.8.14", - "tslib": "^2.3.0" + "@nx/react": "19.8.14", + "enquirer": "^2.3.6", + "ora": "^5.3.0", + "picocolors": "^1.0.0", + "tslib": "^2.3.0", + "yaml": "^2.3.4", + "yargs": "^17.7.2" }, "type": "commonjs", "main": "./src/index.js", "typings": "./src/index.d.ts", - "private": true, - "generators": "./generators.json" + "types": "./src/index.d.ts", + "generators": "./generators.json", + "publishConfig": { + "access": "public" + } } diff --git a/nx-plugin-react/tsconfig.json b/nx-plugin-react/tsconfig.json index 150e7382..faa12d3b 100644 --- a/nx-plugin-react/tsconfig.json +++ b/nx-plugin-react/tsconfig.json @@ -1,7 +1,8 @@ { "extends": "../tsconfig.base.json", "compilerOptions": { - "module": "commonjs" + "module": "commonjs", + "lib": ["ES2021"] }, "files": [], "include": [], From 39a5a16c0273ac45b968d217122f84f518c3aa41 Mon Sep 17 00:00:00 2001 From: Kamil Nowak Date: Tue, 19 May 2026 08:42:00 +0200 Subject: [PATCH 03/18] refactor: reorganize React generator file structure and add conditional steps - Move AI agent configuration files from `files` to `files-ai` directory - Split styles into separate directories: `files-styles-primeflex` and `files-styles-tailwind` - Add new `styles` parameter to select between PrimeFlex and Tailwind CSS frameworks - Add new `ai` parameter to optionally include AI agent configuration files - Implement `StylesStep` and `AIStep` generator steps with conditional execution - Add `isApplicable` method to `Gener --- .../coding_practices-architecture-ddd.mdc | 0 ...oding_practices-static_analysis-eslint.mdc | 0 ...ing_practices-static_analysis-prettier.mdc | 0 ...practices-support_level-support_expert.mdc | 0 .../rules/frontend-react-primereact.mdc | 0 .../frontend-react-react_coding_standards.mdc | 0 .../rules/frontend-react-react_router.mdc | 0 .../rules/frontend-styling-primeflex.mdc | 0 .../.agents/rules/testing-unit-vitest.mdc | 0 .../.agents/skills/frontend-design/SKILL.md | 0 .../.agents/skills/react-doctor/SKILL.md | 0 .../src/assets/styles.css.template | 0 .../src/assets/styles.css.template | 5 ++++ .../src/generators/react/generator.ts | 30 +++++++++++++++++++ .../src/generators/react/schema.d.ts | 2 ++ .../src/generators/react/schema.json | 20 +++++++++++++ .../src/generators/react/steps/ai.step.ts | 22 ++++++++++++++ .../src/generators/react/steps/styles.step.ts | 26 ++++++++++++++++ .../src/generators/shared/generator.utils.ts | 4 +++ 19 files changed, 109 insertions(+) rename nx-plugin-react/src/generators/react/{files => files-ai}/.agents/rules/coding_practices-architecture-ddd.mdc (100%) rename nx-plugin-react/src/generators/react/{files => files-ai}/.agents/rules/coding_practices-static_analysis-eslint.mdc (100%) rename nx-plugin-react/src/generators/react/{files => files-ai}/.agents/rules/coding_practices-static_analysis-prettier.mdc (100%) rename nx-plugin-react/src/generators/react/{files => files-ai}/.agents/rules/coding_practices-support_level-support_expert.mdc (100%) rename nx-plugin-react/src/generators/react/{files => files-ai}/.agents/rules/frontend-react-primereact.mdc (100%) rename nx-plugin-react/src/generators/react/{files => files-ai}/.agents/rules/frontend-react-react_coding_standards.mdc (100%) rename nx-plugin-react/src/generators/react/{files => files-ai}/.agents/rules/frontend-react-react_router.mdc (100%) rename nx-plugin-react/src/generators/react/{files => files-ai}/.agents/rules/frontend-styling-primeflex.mdc (100%) rename nx-plugin-react/src/generators/react/{files => files-ai}/.agents/rules/testing-unit-vitest.mdc (100%) rename nx-plugin-react/src/generators/react/{files => files-ai}/.agents/skills/frontend-design/SKILL.md (100%) rename nx-plugin-react/src/generators/react/{files => files-ai}/.agents/skills/react-doctor/SKILL.md (100%) rename nx-plugin-react/src/generators/react/{files => files-styles-primeflex}/src/assets/styles.css.template (100%) create mode 100644 nx-plugin-react/src/generators/react/files-styles-tailwind/src/assets/styles.css.template create mode 100644 nx-plugin-react/src/generators/react/steps/ai.step.ts create mode 100644 nx-plugin-react/src/generators/react/steps/styles.step.ts diff --git a/nx-plugin-react/src/generators/react/files/.agents/rules/coding_practices-architecture-ddd.mdc b/nx-plugin-react/src/generators/react/files-ai/.agents/rules/coding_practices-architecture-ddd.mdc similarity index 100% rename from nx-plugin-react/src/generators/react/files/.agents/rules/coding_practices-architecture-ddd.mdc rename to nx-plugin-react/src/generators/react/files-ai/.agents/rules/coding_practices-architecture-ddd.mdc diff --git a/nx-plugin-react/src/generators/react/files/.agents/rules/coding_practices-static_analysis-eslint.mdc b/nx-plugin-react/src/generators/react/files-ai/.agents/rules/coding_practices-static_analysis-eslint.mdc similarity index 100% rename from nx-plugin-react/src/generators/react/files/.agents/rules/coding_practices-static_analysis-eslint.mdc rename to nx-plugin-react/src/generators/react/files-ai/.agents/rules/coding_practices-static_analysis-eslint.mdc diff --git a/nx-plugin-react/src/generators/react/files/.agents/rules/coding_practices-static_analysis-prettier.mdc b/nx-plugin-react/src/generators/react/files-ai/.agents/rules/coding_practices-static_analysis-prettier.mdc similarity index 100% rename from nx-plugin-react/src/generators/react/files/.agents/rules/coding_practices-static_analysis-prettier.mdc rename to nx-plugin-react/src/generators/react/files-ai/.agents/rules/coding_practices-static_analysis-prettier.mdc diff --git a/nx-plugin-react/src/generators/react/files/.agents/rules/coding_practices-support_level-support_expert.mdc b/nx-plugin-react/src/generators/react/files-ai/.agents/rules/coding_practices-support_level-support_expert.mdc similarity index 100% rename from nx-plugin-react/src/generators/react/files/.agents/rules/coding_practices-support_level-support_expert.mdc rename to nx-plugin-react/src/generators/react/files-ai/.agents/rules/coding_practices-support_level-support_expert.mdc diff --git a/nx-plugin-react/src/generators/react/files/.agents/rules/frontend-react-primereact.mdc b/nx-plugin-react/src/generators/react/files-ai/.agents/rules/frontend-react-primereact.mdc similarity index 100% rename from nx-plugin-react/src/generators/react/files/.agents/rules/frontend-react-primereact.mdc rename to nx-plugin-react/src/generators/react/files-ai/.agents/rules/frontend-react-primereact.mdc diff --git a/nx-plugin-react/src/generators/react/files/.agents/rules/frontend-react-react_coding_standards.mdc b/nx-plugin-react/src/generators/react/files-ai/.agents/rules/frontend-react-react_coding_standards.mdc similarity index 100% rename from nx-plugin-react/src/generators/react/files/.agents/rules/frontend-react-react_coding_standards.mdc rename to nx-plugin-react/src/generators/react/files-ai/.agents/rules/frontend-react-react_coding_standards.mdc diff --git a/nx-plugin-react/src/generators/react/files/.agents/rules/frontend-react-react_router.mdc b/nx-plugin-react/src/generators/react/files-ai/.agents/rules/frontend-react-react_router.mdc similarity index 100% rename from nx-plugin-react/src/generators/react/files/.agents/rules/frontend-react-react_router.mdc rename to nx-plugin-react/src/generators/react/files-ai/.agents/rules/frontend-react-react_router.mdc diff --git a/nx-plugin-react/src/generators/react/files/.agents/rules/frontend-styling-primeflex.mdc b/nx-plugin-react/src/generators/react/files-ai/.agents/rules/frontend-styling-primeflex.mdc similarity index 100% rename from nx-plugin-react/src/generators/react/files/.agents/rules/frontend-styling-primeflex.mdc rename to nx-plugin-react/src/generators/react/files-ai/.agents/rules/frontend-styling-primeflex.mdc diff --git a/nx-plugin-react/src/generators/react/files/.agents/rules/testing-unit-vitest.mdc b/nx-plugin-react/src/generators/react/files-ai/.agents/rules/testing-unit-vitest.mdc similarity index 100% rename from nx-plugin-react/src/generators/react/files/.agents/rules/testing-unit-vitest.mdc rename to nx-plugin-react/src/generators/react/files-ai/.agents/rules/testing-unit-vitest.mdc diff --git a/nx-plugin-react/src/generators/react/files/.agents/skills/frontend-design/SKILL.md b/nx-plugin-react/src/generators/react/files-ai/.agents/skills/frontend-design/SKILL.md similarity index 100% rename from nx-plugin-react/src/generators/react/files/.agents/skills/frontend-design/SKILL.md rename to nx-plugin-react/src/generators/react/files-ai/.agents/skills/frontend-design/SKILL.md diff --git a/nx-plugin-react/src/generators/react/files/.agents/skills/react-doctor/SKILL.md b/nx-plugin-react/src/generators/react/files-ai/.agents/skills/react-doctor/SKILL.md similarity index 100% rename from nx-plugin-react/src/generators/react/files/.agents/skills/react-doctor/SKILL.md rename to nx-plugin-react/src/generators/react/files-ai/.agents/skills/react-doctor/SKILL.md diff --git a/nx-plugin-react/src/generators/react/files/src/assets/styles.css.template b/nx-plugin-react/src/generators/react/files-styles-primeflex/src/assets/styles.css.template similarity index 100% rename from nx-plugin-react/src/generators/react/files/src/assets/styles.css.template rename to nx-plugin-react/src/generators/react/files-styles-primeflex/src/assets/styles.css.template diff --git a/nx-plugin-react/src/generators/react/files-styles-tailwind/src/assets/styles.css.template b/nx-plugin-react/src/generators/react/files-styles-tailwind/src/assets/styles.css.template new file mode 100644 index 00000000..6023ff01 --- /dev/null +++ b/nx-plugin-react/src/generators/react/files-styles-tailwind/src/assets/styles.css.template @@ -0,0 +1,5 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; +@import 'primeicons/primeicons.css'; +@import '../index.css'; diff --git a/nx-plugin-react/src/generators/react/generator.ts b/nx-plugin-react/src/generators/react/generator.ts index a1a331e4..661a5a8f 100644 --- a/nx-plugin-react/src/generators/react/generator.ts +++ b/nx-plugin-react/src/generators/react/generator.ts @@ -18,6 +18,8 @@ import processParams, { GeneratorParameter } from '../shared/parameters.utils'; import { GeneratorProcessor } from '../shared/generator.utils'; import { ReactGeneratorSchema } from './schema'; import { GeneralOpenAPIStep } from './steps/general-openapi.step'; +import { StylesStep } from './steps/styles.step'; +import { AIStep } from './steps/ai.step'; const PARAMETERS: GeneratorParameter[] = [ { @@ -26,6 +28,21 @@ const PARAMETERS: GeneratorParameter[] = [ required: 'never', default: false, }, + { + key: 'styles', + type: 'select', + required: 'interactive', + prompt: 'Which CSS framework would you like to use?', + default: 'primeflex', + choices: ['primeflex', 'tailwind'], + }, + { + key: 'ai', + type: 'boolean', + required: 'interactive', + prompt: 'Would you like to add AI agent configuration files?', + default: false, + }, ]; export async function reactGenerator( @@ -83,6 +100,8 @@ export async function reactGenerator( const generatorProcessor = new GeneratorProcessor(); generatorProcessor.addStep(new GeneralOpenAPIStep()); + generatorProcessor.addStep(new StylesStep()); + generatorProcessor.addStep(new AIStep()); generatorProcessor.run(tree, options, spinner); @@ -90,6 +109,17 @@ export async function reactGenerator( addScriptsToPackageJson(tree, options); addExtensionsToPackageJson(tree); + if (options.styles === 'tailwind') { + addDependenciesToPackageJson( + tree, + {}, + { + tailwindcss: '^4.0.0', + '@tailwindcss/vite': '^4.0.0', + } + ); + } + const oneCXLibVersion = '^8.2.0'; const reactVersion = '^19.0.0'; const nxVersion = '22.0.2'; diff --git a/nx-plugin-react/src/generators/react/schema.d.ts b/nx-plugin-react/src/generators/react/schema.d.ts index 637e62de..eef40f07 100644 --- a/nx-plugin-react/src/generators/react/schema.d.ts +++ b/nx-plugin-react/src/generators/react/schema.d.ts @@ -1,4 +1,6 @@ export interface ReactGeneratorSchema { name: string; chatty?: boolean; + styles?: 'primeflex' | 'tailwind'; + ai?: boolean; } diff --git a/nx-plugin-react/src/generators/react/schema.json b/nx-plugin-react/src/generators/react/schema.json index 344860ac..79533299 100644 --- a/nx-plugin-react/src/generators/react/schema.json +++ b/nx-plugin-react/src/generators/react/schema.json @@ -12,6 +12,26 @@ "index": 0 }, "x-prompt": "What name would you like to use?" + }, + "styles": { + "type": "string", + "description": "CSS framework to use", + "enum": ["primeflex", "tailwind"], + "default": "primeflex", + "x-prompt": { + "message": "Which CSS framework would you like to use?", + "type": "list", + "items": [ + { "value": "primeflex", "label": "PrimeFlex" }, + { "value": "tailwind", "label": "Tailwind CSS" } + ] + } + }, + "ai": { + "type": "boolean", + "description": "Add AI agent configuration files", + "default": false, + "x-prompt": "Would you like to add AI agent configuration files?" } }, "required": ["name"] diff --git a/nx-plugin-react/src/generators/react/steps/ai.step.ts b/nx-plugin-react/src/generators/react/steps/ai.step.ts new file mode 100644 index 00000000..f9ea2a33 --- /dev/null +++ b/nx-plugin-react/src/generators/react/steps/ai.step.ts @@ -0,0 +1,22 @@ +import { Tree, generateFiles, joinPathFragments } from '@nx/devkit'; +import { GeneratorStep } from '../../shared/generator.utils'; +import { ReactGeneratorSchema } from '../schema'; + +export class AIStep implements GeneratorStep { + process(tree: Tree, options: ReactGeneratorSchema): void { + generateFiles( + tree, + joinPathFragments(__dirname, '../files-ai'), + `${options.name}/`, + { ...options } + ); + } + + getTitle(): string { + return 'Adding AI agent configuration files'; + } + + isApplicable(options: ReactGeneratorSchema): boolean { + return options.ai === true; + } +} diff --git a/nx-plugin-react/src/generators/react/steps/styles.step.ts b/nx-plugin-react/src/generators/react/steps/styles.step.ts new file mode 100644 index 00000000..00a46b37 --- /dev/null +++ b/nx-plugin-react/src/generators/react/steps/styles.step.ts @@ -0,0 +1,26 @@ +import { Tree, generateFiles, joinPathFragments } from '@nx/devkit'; +import { GeneratorStep } from '../../shared/generator.utils'; +import { ReactGeneratorSchema } from '../schema'; + +export class StylesStep implements GeneratorStep { + process(tree: Tree, options: ReactGeneratorSchema): void { + const templateDir = + options.styles === 'tailwind' + ? '../files-styles-tailwind' + : '../files-styles-primeflex'; + generateFiles( + tree, + joinPathFragments(__dirname, templateDir), + `${options.name}/`, + { ...options } + ); + } + + getTitle(): string { + return 'Adding styles'; + } + + isApplicable(_options: ReactGeneratorSchema): boolean { + return true; + } +} diff --git a/nx-plugin-react/src/generators/shared/generator.utils.ts b/nx-plugin-react/src/generators/shared/generator.utils.ts index 240e0c76..717aa4bf 100644 --- a/nx-plugin-react/src/generators/shared/generator.utils.ts +++ b/nx-plugin-react/src/generators/shared/generator.utils.ts @@ -24,6 +24,7 @@ export class GeneratorStepError extends Error { export interface GeneratorStep { process(tree: Tree, options: T): void; getTitle(): string; + isApplicable?(options: T): boolean; } export class GeneratorProcessor { @@ -39,6 +40,9 @@ export class GeneratorProcessor { this._printErrors = printErrors; this.errors = []; for (const step of this.steps) { + if (step.isApplicable && !step.isApplicable(options)) { + continue; + } if (ora) { const stepTitle = step.getTitle().trimEnd(); ora.info(stepTitle); From 3950782288ae411e15248c7442ad85ff63c23840 Mon Sep 17 00:00:00 2001 From: Kamil Nowak Date: Tue, 19 May 2026 08:59:57 +0200 Subject: [PATCH 04/18] chore: clean up React generator code and improve type safety - Add @nx/eslint-plugin dependency to package.json - Remove unused options parameter from addScriptsToPackageJson function - Remove unnecessary isApplicable method from StylesStep class - Fix indentation in OpenAPIUtil class - Improve type safety in OpenAPIArraySectionUtil by using proper type assertion for item access --- nx-plugin-react/package.json | 1 + nx-plugin-react/src/generators/react/generator.ts | 4 ++-- nx-plugin-react/src/generators/react/steps/styles.step.ts | 4 ---- .../src/generators/shared/openapi/openapi.utils.ts | 6 ++++-- 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/nx-plugin-react/package.json b/nx-plugin-react/package.json index 6e856770..06b6c678 100644 --- a/nx-plugin-react/package.json +++ b/nx-plugin-react/package.json @@ -9,6 +9,7 @@ }, "dependencies": { "@nx/devkit": "19.8.14", + "@nx/eslint-plugin": "19.8.14", "@nx/react": "19.8.14", "enquirer": "^2.3.6", "ora": "^5.3.0", diff --git a/nx-plugin-react/src/generators/react/generator.ts b/nx-plugin-react/src/generators/react/generator.ts index 661a5a8f..352ed520 100644 --- a/nx-plugin-react/src/generators/react/generator.ts +++ b/nx-plugin-react/src/generators/react/generator.ts @@ -106,7 +106,7 @@ export async function reactGenerator( generatorProcessor.run(tree, options, spinner); addBaseToPackageJson(tree, options); - addScriptsToPackageJson(tree, options); + addScriptsToPackageJson(tree); addExtensionsToPackageJson(tree); if (options.styles === 'tailwind') { @@ -231,7 +231,7 @@ function addExtensionsToPackageJson(tree: Tree) { }); } -function addScriptsToPackageJson(tree: Tree, _options: ReactGeneratorSchema) { +function addScriptsToPackageJson(tree: Tree) { updateJson(tree, 'package.json', (pkgJson) => { pkgJson.scripts = pkgJson.scripts ?? {}; pkgJson.scripts[ diff --git a/nx-plugin-react/src/generators/react/steps/styles.step.ts b/nx-plugin-react/src/generators/react/steps/styles.step.ts index 00a46b37..7d182c44 100644 --- a/nx-plugin-react/src/generators/react/steps/styles.step.ts +++ b/nx-plugin-react/src/generators/react/steps/styles.step.ts @@ -19,8 +19,4 @@ export class StylesStep implements GeneratorStep { getTitle(): string { return 'Adding styles'; } - - isApplicable(_options: ReactGeneratorSchema): boolean { - return true; - } } diff --git a/nx-plugin-react/src/generators/shared/openapi/openapi.utils.ts b/nx-plugin-react/src/generators/shared/openapi/openapi.utils.ts index 2e01203d..92206a11 100644 --- a/nx-plugin-react/src/generators/shared/openapi/openapi.utils.ts +++ b/nx-plugin-react/src/generators/shared/openapi/openapi.utils.ts @@ -34,7 +34,7 @@ export class OpenAPIUtil { return new OpenAPIArraySectionUtil(this, this.yamlContent['servers']); } - /** + /** * Quick access to the tags section of the YAML * @returns interface to add items to the section */ @@ -179,7 +179,9 @@ export class OpenAPIArraySectionUtil { */ add(key: string, value: T, options?: ObjectSetOptions) { const existStrategy = options ? options.existStrategy : 'skip'; - const existingItem = this.sectionContent.find((item: any) => item.name === key); + const existingItem = this.sectionContent.find( + (item) => (item as Record)['name'] === key + ); if (existingItem != null) { if (existStrategy == 'skip') { return this; From 18e589549f78fa20115aaf16357b0ed3c2f7a0c5 Mon Sep 17 00:00:00 2001 From: Kamil Nowak Date: Tue, 19 May 2026 10:15:28 +0200 Subject: [PATCH 05/18] chore: update Nx dependencies to use caret ranges in nx-plugin-react - Change @nx/devkit, @nx/eslint-plugin, and @nx/react from fixed versions to caret ranges (^19.8.14) --- nx-plugin-react/package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/nx-plugin-react/package.json b/nx-plugin-react/package.json index 06b6c678..626e8297 100644 --- a/nx-plugin-react/package.json +++ b/nx-plugin-react/package.json @@ -8,9 +8,9 @@ "url": "git+https://github.com/onecx/onecx-nx-plugins.git" }, "dependencies": { - "@nx/devkit": "19.8.14", - "@nx/eslint-plugin": "19.8.14", - "@nx/react": "19.8.14", + "@nx/devkit": "^19.8.14", + "@nx/eslint-plugin": "^19.8.14", + "@nx/react": "^19.8.14", "enquirer": "^2.3.6", "ora": "^5.3.0", "picocolors": "^1.0.0", From 195741a216a0e3f108293e1705c7a92506856f4a Mon Sep 17 00:00:00 2001 From: Kamil Nowak Date: Tue, 19 May 2026 13:37:16 +0200 Subject: [PATCH 06/18] fix: correct file paths and improve error handling in React plugin - Change directory paths from `${options.name}/` to `.` in React generator and steps - Update react-i18next dependency from ^16.5.4 to ^17.0.8 - Fix react-router dependency version string formatting - Bump nx-plugin-react version from 0.0.1 to 7.1.0 - Add error log file reading in create-workspace error handler - Add publish-local script to package.json - Remove unused options parameter from adaptTsConfig function --- create-workspace/bin/index.ts | 12 ++++++++++++ nx-plugin-react/package.json | 2 +- nx-plugin-react/src/generators/react/generator.ts | 11 +++++------ .../src/generators/react/steps/ai.step.ts | 9 +++------ .../src/generators/react/steps/styles.step.ts | 9 +++------ package.json | 3 ++- 6 files changed, 26 insertions(+), 20 deletions(-) diff --git a/create-workspace/bin/index.ts b/create-workspace/bin/index.ts index 5c01b9cc..0abbc703 100644 --- a/create-workspace/bin/index.ts +++ b/create-workspace/bin/index.ts @@ -1,6 +1,7 @@ #!/usr/bin/env node import { createWorkspace } from 'create-nx-workspace'; +import { readFileSync, existsSync } from 'fs'; const SUPPORTED_FLAVORS = ['angular', 'react', 'ngrx', 'standalone-ngrx']; @@ -53,5 +54,16 @@ async function main() { main().catch((e) => { console.error(e.message); + + const logFileMatch = (e.message as string).match(/Log file:\s*(\S+)/); + if (logFileMatch) { + const logPath = logFileMatch[1]; + if (existsSync(logPath)) { + console.error('\n── Error details ─────────────────────────────'); + console.error(readFileSync(logPath, 'utf-8')); + console.error('──────────────────────────────────────────────'); + } + } + process.exit(1); }); diff --git a/nx-plugin-react/package.json b/nx-plugin-react/package.json index 626e8297..2e07cad1 100644 --- a/nx-plugin-react/package.json +++ b/nx-plugin-react/package.json @@ -1,6 +1,6 @@ { "name": "@onecx/nx-plugin-react", - "version": "0.0.1", + "version": "7.1.0", "license": "Apache-2.0", "contributors": [], "repository": { diff --git a/nx-plugin-react/src/generators/react/generator.ts b/nx-plugin-react/src/generators/react/generator.ts index 352ed520..461238ac 100644 --- a/nx-plugin-react/src/generators/react/generator.ts +++ b/nx-plugin-react/src/generators/react/generator.ts @@ -62,7 +62,7 @@ export async function reactGenerator( Object.assign(options, parameters); const spinner = ora('Adding React').start(); - const directory = options.name; + const directory = '.'; const applicationGeneratorCallback = await applicationGenerator(tree, { name: options.name, @@ -137,7 +137,7 @@ export async function reactGenerator( react: reactVersion, 'react-dom': reactVersion, 'react-router': '^7.13.0"', - 'react-i18next': '"^16.5.4', + 'react-i18next': '^17.0.8', i18next: '^25.8.0', primereact: '^10.9.7', primeicons: '^7.0.0', @@ -181,7 +181,7 @@ export async function reactGenerator( } ); - adaptTsConfig(tree, options); + adaptTsConfig(tree); adaptProjectConfiguration(tree, options); await formatFiles(tree); @@ -251,9 +251,8 @@ function addScriptsToPackageJson(tree: Tree) { }); } -function adaptTsConfig(tree: Tree, options: ReactGeneratorSchema) { - const directory = options.name; - const filePath = `${directory}/tsconfig.app.json`; +function adaptTsConfig(tree: Tree) { + const filePath = 'tsconfig.app.json'; updateJson(tree, filePath, (json) => { json.files = ['src/main.tsx', 'src/bootstrap.tsx']; diff --git a/nx-plugin-react/src/generators/react/steps/ai.step.ts b/nx-plugin-react/src/generators/react/steps/ai.step.ts index f9ea2a33..49b027c0 100644 --- a/nx-plugin-react/src/generators/react/steps/ai.step.ts +++ b/nx-plugin-react/src/generators/react/steps/ai.step.ts @@ -4,12 +4,9 @@ import { ReactGeneratorSchema } from '../schema'; export class AIStep implements GeneratorStep { process(tree: Tree, options: ReactGeneratorSchema): void { - generateFiles( - tree, - joinPathFragments(__dirname, '../files-ai'), - `${options.name}/`, - { ...options } - ); + generateFiles(tree, joinPathFragments(__dirname, '../files-ai'), '.', { + ...options, + }); } getTitle(): string { diff --git a/nx-plugin-react/src/generators/react/steps/styles.step.ts b/nx-plugin-react/src/generators/react/steps/styles.step.ts index 7d182c44..541197a3 100644 --- a/nx-plugin-react/src/generators/react/steps/styles.step.ts +++ b/nx-plugin-react/src/generators/react/steps/styles.step.ts @@ -8,12 +8,9 @@ export class StylesStep implements GeneratorStep { options.styles === 'tailwind' ? '../files-styles-tailwind' : '../files-styles-primeflex'; - generateFiles( - tree, - joinPathFragments(__dirname, templateDir), - `${options.name}/`, - { ...options } - ); + generateFiles(tree, joinPathFragments(__dirname, templateDir), '.', { + ...options, + }); } getTitle(): string { diff --git a/package.json b/package.json index 9d792f89..7276ef2d 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,8 @@ "build": "nx run nx-plugin:build && nx run create-workspace:build", "build-copy": "npm run build && node copy-build.js", "test": "nx run nx-plugin-e2e:e2e", - "lint": "npx nx affected -t lint" + "lint": "npx nx affected -t lint", + "publish-local": "node tools/scripts/publish-local.mjs" }, "dependencies": { "@nx/angular": "^19.8.14", From e40cc00d263ba277c46a616834470313e35283f0 Mon Sep 17 00:00:00 2001 From: Kamil Nowak Date: Tue, 19 May 2026 15:42:09 +0200 Subject: [PATCH 07/18] fix: remove .tsx extension from import and clean up workspace directories - Remove .tsx extension from App import in bootstrap.ts template - Fix react-router dependency version string (remove extra quote) - Update cleanup command to remove apps and libs directories along with .vscode --- .../src/generators/react/files/src/bootstrap.ts.template | 2 +- nx-plugin-react/src/generators/react/generator.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/nx-plugin-react/src/generators/react/files/src/bootstrap.ts.template b/nx-plugin-react/src/generators/react/files/src/bootstrap.ts.template index cb52821f..5dbf3cea 100644 --- a/nx-plugin-react/src/generators/react/files/src/bootstrap.ts.template +++ b/nx-plugin-react/src/generators/react/files/src/bootstrap.ts.template @@ -1,6 +1,6 @@ import { createViteAppWebComponent } from '@onecx/react-webcomponents' -import App from './App.tsx' +import App from './App' // Web component entrypoint for embedding into the portal shell. createViteAppWebComponent(App, 'onecx-<%= fileName %>-ui-entrypoint') diff --git a/nx-plugin-react/src/generators/react/generator.ts b/nx-plugin-react/src/generators/react/generator.ts index 461238ac..372b4478 100644 --- a/nx-plugin-react/src/generators/react/generator.ts +++ b/nx-plugin-react/src/generators/react/generator.ts @@ -136,7 +136,7 @@ export async function reactGenerator( '@onecx/integration-interface': oneCXLibVersion, react: reactVersion, 'react-dom': reactVersion, - 'react-router': '^7.13.0"', + 'react-router': '^7.13.0', 'react-i18next': '^17.0.8', i18next: '^25.8.0', primereact: '^10.9.7', @@ -190,7 +190,7 @@ export async function reactGenerator( return async () => { await applicationGeneratorCallback(); - let cmd = 'rm -rf .vscode '; + let cmd = 'rm -rf .vscode apps libs'; log(cmd); execSync(cmd, { cwd: tree.root, stdio: 'inherit' }); From 8dd8be3383988b062f1d0c83154a31e26f0a6544 Mon Sep 17 00:00:00 2001 From: Kamil Nowak Date: Wed, 20 May 2026 14:35:44 +0200 Subject: [PATCH 08/18] fix: update generated API output path from shared to api directory - Change generated API files location from `src/app/shared/generated` to `src/api/generated` - Update .prettierignore and eslint.config.js to ignore new API path - Remove unused imports from vite.config.ts (loadEnv, writeFileSync) - Consolidate @module-federation/vite imports into single statement - Fix ESLint rule formatting for @typescript-eslint/no-unused-vars (use single quotes) --- .../src/generators/react/files/.prettierignore | 2 +- .../src/generators/react/files/eslint.config.js | 4 ++-- .../src/generators/react/files/vite.config.ts.template | 9 +++------ nx-plugin-react/src/generators/react/generator.ts | 2 +- 4 files changed, 7 insertions(+), 10 deletions(-) diff --git a/nx-plugin-react/src/generators/react/files/.prettierignore b/nx-plugin-react/src/generators/react/files/.prettierignore index 81f421a1..d7d35e2f 100644 --- a/nx-plugin-react/src/generators/react/files/.prettierignore +++ b/nx-plugin-react/src/generators/react/files/.prettierignore @@ -19,4 +19,4 @@ README.md Dockerfile *.log *.sh -src/app/shared/generated/** +src/api/generated/** diff --git a/nx-plugin-react/src/generators/react/files/eslint.config.js b/nx-plugin-react/src/generators/react/files/eslint.config.js index 315429fd..a1087bf1 100644 --- a/nx-plugin-react/src/generators/react/files/eslint.config.js +++ b/nx-plugin-react/src/generators/react/files/eslint.config.js @@ -27,7 +27,7 @@ module.exports = [ 'Dockerfile', '*.log', '*.sh', - 'src/app/shared/generated/**', + 'src/api/generated/**', 'src/**/*.ico', 'src/**/*.svg' ] @@ -41,7 +41,7 @@ module.exports = [ { files: ['**/*.ts', '**/*.tsx'], rules: { - "@typescript-eslint/no-unused-vars": ["error", { "vars": "all", "args": "none" }], + '@typescript-eslint/no-unused-vars': ['error', { vars: 'all', args: 'none' }], '@typescript-eslint/no-explicit-any': 'warn', 'react-hooks/rules-of-hooks': 'error', 'react-hooks/exhaustive-deps': 'warn' diff --git a/nx-plugin-react/src/generators/react/files/vite.config.ts.template b/nx-plugin-react/src/generators/react/files/vite.config.ts.template index bb707a9d..3af351c8 100644 --- a/nx-plugin-react/src/generators/react/files/vite.config.ts.template +++ b/nx-plugin-react/src/generators/react/files/vite.config.ts.template @@ -1,12 +1,10 @@ /// -import { defineConfig, loadEnv } from "vite"; +import { defineConfig } from "vite"; import react from "@vitejs/plugin-react"; import { viteStaticCopy } from "vite-plugin-static-copy"; -import { writeFileSync } from "fs"; +import { dependencies } from "./package.json"; import path from "path"; - -import { federation } from "@module-federation/vite"; -import type { ModuleFederationOptions } from "@module-federation/vite/lib/utils/normalizeModuleFederationOptions"; +import { federation, type ModuleFederationOptions } from '@module-federation/vite' /// const mfConfig: ModuleFederationOptions = { @@ -75,7 +73,6 @@ const mfConfig: ModuleFederationOptions = { }, }; export default defineConfig(({ mode }) => { - const selfEnv = loadEnv(mode, process.cwd()); return { root: __dirname, base: mode === "production" ? "/mfe/onecx-<%= fileName %>/" : "/", diff --git a/nx-plugin-react/src/generators/react/generator.ts b/nx-plugin-react/src/generators/react/generator.ts index 372b4478..edccee70 100644 --- a/nx-plugin-react/src/generators/react/generator.ts +++ b/nx-plugin-react/src/generators/react/generator.ts @@ -236,7 +236,7 @@ function addScriptsToPackageJson(tree: Tree) { pkgJson.scripts = pkgJson.scripts ?? {}; pkgJson.scripts[ 'apigen' - ] = `openapi-generator-cli generate -i src/assets/api/openapi-bff.yaml -c apigen.yaml -o src/app/shared/generated -g typescript-fetch --type-mappings AnyType=object`; + ] = `openapi-generator-cli generate -i src/assets/api/openapi-bff.yaml -c apigen.yaml -o src/api/generated -g typescript-fetch --type-mappings AnyType=object`; pkgJson.scripts['start'] = 'nx serve --host 0.0.0.0'; pkgJson.scripts['build'] = `nx build`; pkgJson.scripts['clean'] = From 3c613145e22b5240e45ab7f411f76ecc9bf79fd5 Mon Sep 17 00:00:00 2001 From: Kamil Nowak Date: Wed, 20 May 2026 14:46:29 +0200 Subject: [PATCH 09/18] fix: remove .tsx extensions from imports and disable federation plugin in test mode - Remove .tsx extensions from bootstrap.test.ts imports (bootstrap and App) - Update setupFiles path from setupTests.tsx to setupTests.ts in vite.config.ts - Conditionally exclude federation plugin when running in test mode to prevent module federation conflicts during testing --- .../generators/react/files/src/bootstrap.test.ts.template | 6 +++--- .../src/generators/react/files/vite.config.ts.template | 5 +++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/nx-plugin-react/src/generators/react/files/src/bootstrap.test.ts.template b/nx-plugin-react/src/generators/react/files/src/bootstrap.test.ts.template index 56b0ca83..09f6804f 100644 --- a/nx-plugin-react/src/generators/react/files/src/bootstrap.test.ts.template +++ b/nx-plugin-react/src/generators/react/files/src/bootstrap.test.ts.template @@ -7,12 +7,12 @@ vi.mock('@onecx/react-webcomponents', () => ({ describe('bootstrap.ts', () => { it('calls init and createViteAppWebComponent with correct params', async () => { - // Reset modules so bootstrap.ts runs fresh - await import('./bootstrap.ts') + // Reset modules so bootstrap runs fresh + await import('./bootstrap') const { createViteAppWebComponent } = await import( '@onecx/react-webcomponents' ) - const { default: App } = await import('./App.tsx') + const { default: App } = await import('./App') expect(createViteAppWebComponent).toHaveBeenCalledWith(App, 'onecx-<%= fileName %>-ui-entrypoint') }) diff --git a/nx-plugin-react/src/generators/react/files/vite.config.ts.template b/nx-plugin-react/src/generators/react/files/vite.config.ts.template index 3af351c8..9a0ad383 100644 --- a/nx-plugin-react/src/generators/react/files/vite.config.ts.template +++ b/nx-plugin-react/src/generators/react/files/vite.config.ts.template @@ -73,6 +73,7 @@ const mfConfig: ModuleFederationOptions = { }, }; export default defineConfig(({ mode }) => { + const isTest = mode === 'test' return { root: __dirname, base: mode === "production" ? "/mfe/onecx-<%= fileName %>/" : "/", @@ -90,7 +91,7 @@ export default defineConfig(({ mode }) => { ], }), react(), - federation(mfConfig), + ...(isTest ? [] : [federation(mfConfig)]) ], server: { proxy: { @@ -105,7 +106,7 @@ export default defineConfig(({ mode }) => { globals: true, testTimeout: 10000, environment: "jsdom", - setupFiles: "./src/setupTests.tsx", + setupFiles: "./src/setupTests.ts", coverage: { reporter: ["text", "html", "cobertura"], all: true, From b35a094b631ad2855f59079003a9a908c452b1ea Mon Sep 17 00:00:00 2001 From: Kamil Nowak Date: Wed, 20 May 2026 15:04:18 +0200 Subject: [PATCH 10/18] feat: add README, environment configs, and update module federation settings - Add comprehensive README.md with development commands, project structure, and environment variables documentation - Create environment.ts and environment.prod.ts configuration files - Update Helm values to use correct exposed module name (OneCX<%= className %>RemoteModule) and tag name (onecx-<%= fileName %>-ui-entrypoint) --- .../generators/react/files/README.md.template | 49 +++++++++++++++++++ .../react/files/helm/values.yaml.template | 4 +- .../environments/environment.prod.ts.template | 3 ++ .../src/environments/environment.ts.template | 3 ++ 4 files changed, 57 insertions(+), 2 deletions(-) create mode 100644 nx-plugin-react/src/generators/react/files/README.md.template create mode 100644 nx-plugin-react/src/generators/react/files/src/environments/environment.prod.ts.template create mode 100644 nx-plugin-react/src/generators/react/files/src/environments/environment.ts.template diff --git a/nx-plugin-react/src/generators/react/files/README.md.template b/nx-plugin-react/src/generators/react/files/README.md.template new file mode 100644 index 00000000..c756502a --- /dev/null +++ b/nx-plugin-react/src/generators/react/files/README.md.template @@ -0,0 +1,49 @@ +# OneCX <%= className %> UI + +React microfrontend for the OneCX platform. + +## Development + +```bash +npm start # Start dev server at http://localhost:4200 +npm run build # Production build +npm test # Run unit tests +npm run lint # Lint +npm run lint:fix # Lint with auto-fix +npm run format # Format with prettier +npm run apigen # Regenerate API client from openapi-bff.yaml +npm run sonar # Run SonarQube analysis +``` + +## Project structure + +``` +src/ +├── app/ +│ └── shared/ +│ └── generated/ # Auto-generated API client (do not edit) +├── assets/ +│ ├── api/ +│ │ └── openapi-bff.yaml # BFF OpenAPI spec +│ └── env.json # Runtime environment config +├── environments/ +│ ├── environment.ts # Development config +│ └── environment.prod.ts # Production config +├── i18n/ +│ └── sources/ # Translation files (en, de) +├── pages/ # Page components +├── App.tsx # App root +├── bootstrap.ts # Web component entrypoint +└── router.tsx # Application routing +``` + +## Module Federation + +This app exposes `./OneCX<%= className %>RemoteModule` as a remote module under the name `onecx-<%= fileName %>-ui`. + +## Environment variables + +| Variable | Default | Description | +|----------------|--------------------------------|----------------------| +| `BFF_URL` | `http://<%= fileName %>-bff:8080/` | Backend-for-Frontend URL | +| `APP_BASE_HREF` | `/` | App base href | diff --git a/nx-plugin-react/src/generators/react/files/helm/values.yaml.template b/nx-plugin-react/src/generators/react/files/helm/values.yaml.template index 32a3a0ee..a83497fe 100644 --- a/nx-plugin-react/src/generators/react/files/helm/values.yaml.template +++ b/nx-plugin-react/src/generators/react/files/helm/values.yaml.template @@ -20,13 +20,13 @@ app: specs: main: productName: *product_name - exposedModule: './App' + exposedModule: './OneCX<%= className %>RemoteModule' description: 'OneCX <%= remoteModuleName %> UI' note: 'OneCX <%= remoteModuleName %> UI module auto import via MF operator' type: MODULE technology: WEBCOMPONENTMODULE remoteName: <%= remoteModuleFileName %> - tagName: ocx-<%= remoteModuleFileName %>-component + tagName: onecx-<%= fileName %>-ui-entrypoint # Microservice microservice: spec: diff --git a/nx-plugin-react/src/generators/react/files/src/environments/environment.prod.ts.template b/nx-plugin-react/src/generators/react/files/src/environments/environment.prod.ts.template new file mode 100644 index 00000000..c9669790 --- /dev/null +++ b/nx-plugin-react/src/generators/react/files/src/environments/environment.prod.ts.template @@ -0,0 +1,3 @@ +export const environment = { + production: true, +}; diff --git a/nx-plugin-react/src/generators/react/files/src/environments/environment.ts.template b/nx-plugin-react/src/generators/react/files/src/environments/environment.ts.template new file mode 100644 index 00000000..a20cfe55 --- /dev/null +++ b/nx-plugin-react/src/generators/react/files/src/environments/environment.ts.template @@ -0,0 +1,3 @@ +export const environment = { + production: false, +}; From f90d5670a0dbf54da5dc91aecd47f162f6141563 Mon Sep 17 00:00:00 2001 From: Kamil Nowak Date: Wed, 20 May 2026 15:22:09 +0200 Subject: [PATCH 11/18] refactor: modernize Tailwind CSS configuration and add PrimeUI integration - Replace deprecated @tailwind directives with @import 'tailwindcss' in styles.css - Add tailwindcss-primeui import for PrimeReact component styling - Conditionally include @tailwindcss/vite plugin in vite.config.ts based on styles option - Add tailwindcss-primeui dependency (^0.3.0) when Tailwind is selected - Remove unused ModuleFederationOptions reference comment --- .../src/assets/styles.css.template | 5 +-- .../src/pages/Welcome.tsx.template | 39 +++++++++++++++++++ .../files/src/pages/Welcome.tsx.template | 29 ++++++++++++-- .../react/files/vite.config.ts.template | 8 ++-- .../src/generators/react/generator.ts | 1 + 5 files changed, 72 insertions(+), 10 deletions(-) create mode 100644 nx-plugin-react/src/generators/react/files-styles-tailwind/src/pages/Welcome.tsx.template diff --git a/nx-plugin-react/src/generators/react/files-styles-tailwind/src/assets/styles.css.template b/nx-plugin-react/src/generators/react/files-styles-tailwind/src/assets/styles.css.template index 6023ff01..14fcf587 100644 --- a/nx-plugin-react/src/generators/react/files-styles-tailwind/src/assets/styles.css.template +++ b/nx-plugin-react/src/generators/react/files-styles-tailwind/src/assets/styles.css.template @@ -1,5 +1,4 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; +@import 'tailwindcss'; +@import 'tailwindcss-primeui'; @import 'primeicons/primeicons.css'; @import '../index.css'; diff --git a/nx-plugin-react/src/generators/react/files-styles-tailwind/src/pages/Welcome.tsx.template b/nx-plugin-react/src/generators/react/files-styles-tailwind/src/pages/Welcome.tsx.template new file mode 100644 index 00000000..c3665707 --- /dev/null +++ b/nx-plugin-react/src/generators/react/files-styles-tailwind/src/pages/Welcome.tsx.template @@ -0,0 +1,39 @@ +import { useTranslation } from 'react-i18next' + +const links = [ + { label: 'OneCX GitHub', href: 'https://github.com/onecx' }, + { label: 'OneCX NPM packages', href: 'https://www.npmjs.com/search?q=%40onecx' }, + { label: 'PrimeReact', href: 'https://primereact.org' }, + { label: 'Tailwind CSS', href: 'https://tailwindcss.com' }, + { label: 'React', href: 'https://react.dev' }, + { label: 'Nx', href: 'https://nx.dev' }, + { label: 'Vite', href: 'https://vitejs.dev' }, +] + +export default function Welcome() { + const { t } = useTranslation() + + return ( +
+

+ {t('welcome.title', 'Welcome to OneCX <%= className %>')} +

+

+ {t('welcome.subtitle', 'Your application is ready.')} +

+
+ {links.map(({ label, href }) => ( + + {label} + + ))} +
+
+ ) +} diff --git a/nx-plugin-react/src/generators/react/files/src/pages/Welcome.tsx.template b/nx-plugin-react/src/generators/react/files/src/pages/Welcome.tsx.template index d656022e..16eeb85a 100644 --- a/nx-plugin-react/src/generators/react/files/src/pages/Welcome.tsx.template +++ b/nx-plugin-react/src/generators/react/files/src/pages/Welcome.tsx.template @@ -1,16 +1,39 @@ import { useTranslation } from 'react-i18next' +const links = [ + { label: 'OneCX GitHub', href: 'https://github.com/onecx' }, + { label: 'OneCX NPM packages', href: 'https://www.npmjs.com/search?q=%40onecx' }, + { label: 'PrimeReact', href: 'https://primereact.org' }, + { label: 'PrimeFlex', href: 'https://primeflex.org' }, + { label: 'React', href: 'https://react.dev' }, + { label: 'Nx', href: 'https://nx.dev' }, + { label: 'Vite', href: 'https://vitejs.dev' }, +] + export default function Welcome() { const { t } = useTranslation() return ( -
-

+
+

{t('welcome.title', 'Welcome to OneCX <%= className %>')}

-

+

{t('welcome.subtitle', 'Your application is ready.')}

+
+ {links.map(({ label, href }) => ( + + {label} + + ))} +
) } diff --git a/nx-plugin-react/src/generators/react/files/vite.config.ts.template b/nx-plugin-react/src/generators/react/files/vite.config.ts.template index 9a0ad383..bf8e8e48 100644 --- a/nx-plugin-react/src/generators/react/files/vite.config.ts.template +++ b/nx-plugin-react/src/generators/react/files/vite.config.ts.template @@ -1,12 +1,11 @@ /// import { defineConfig } from "vite"; -import react from "@vitejs/plugin-react"; +import react from "@vitejs/plugin-react";<% if (styles === 'tailwind') { %> +import tailwindcss from "@tailwindcss/vite";<% } %> import { viteStaticCopy } from "vite-plugin-static-copy"; import { dependencies } from "./package.json"; import path from "path"; import { federation, type ModuleFederationOptions } from '@module-federation/vite' - -/// const mfConfig: ModuleFederationOptions = { name: "onecx-<%= fileName %>-ui", filename: "remoteEntry.js", @@ -90,7 +89,8 @@ export default defineConfig(({ mode }) => { }, ], }), - react(), + react(),<% if (styles === 'tailwind') { %> + tailwindcss(),<% } %> ...(isTest ? [] : [federation(mfConfig)]) ], server: { diff --git a/nx-plugin-react/src/generators/react/generator.ts b/nx-plugin-react/src/generators/react/generator.ts index edccee70..e16c75c5 100644 --- a/nx-plugin-react/src/generators/react/generator.ts +++ b/nx-plugin-react/src/generators/react/generator.ts @@ -116,6 +116,7 @@ export async function reactGenerator( { tailwindcss: '^4.0.0', '@tailwindcss/vite': '^4.0.0', + 'tailwindcss-primeui': '^0.3.0', } ); } From dcb5a51d08ad12fd91175f7a0b1faa25f03f9d85 Mon Sep 17 00:00:00 2001 From: Kamil Nowak Date: Wed, 20 May 2026 15:54:55 +0200 Subject: [PATCH 12/18] docs: update README structure and add Welcome component tests - Update README.md to reflect API directory structure change (api/generated instead of app/shared/generated) - Add tests for documentation links in Welcome component (OneCX GitHub link validation and link count verification) - Fix tsconfig.app.json to reference bootstrap.ts instead of bootstrap.tsx --- .../src/generators/react/files/README.md.template | 5 ++--- .../files/src/pages/Welcome.test.tsx.template | 15 +++++++++++++++ nx-plugin-react/src/generators/react/generator.ts | 2 +- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/nx-plugin-react/src/generators/react/files/README.md.template b/nx-plugin-react/src/generators/react/files/README.md.template index c756502a..a8e06169 100644 --- a/nx-plugin-react/src/generators/react/files/README.md.template +++ b/nx-plugin-react/src/generators/react/files/README.md.template @@ -19,9 +19,8 @@ npm run sonar # Run SonarQube analysis ``` src/ -├── app/ -│ └── shared/ -│ └── generated/ # Auto-generated API client (do not edit) +├── api/ +│ └── generated/ # Auto-generated API client (do not edit) ├── assets/ │ ├── api/ │ │ └── openapi-bff.yaml # BFF OpenAPI spec diff --git a/nx-plugin-react/src/generators/react/files/src/pages/Welcome.test.tsx.template b/nx-plugin-react/src/generators/react/files/src/pages/Welcome.test.tsx.template index d658b624..1f08380d 100644 --- a/nx-plugin-react/src/generators/react/files/src/pages/Welcome.test.tsx.template +++ b/nx-plugin-react/src/generators/react/files/src/pages/Welcome.test.tsx.template @@ -22,4 +22,19 @@ describe('Welcome', () => { screen.getByText('Your application is ready.') ).toBeInTheDocument() }) + + it('renders documentation links', () => { + render() + const onecxLink = screen.getByRole('link', { name: 'OneCX GitHub' }) + expect(onecxLink).toBeInTheDocument() + expect(onecxLink).toHaveAttribute('href', 'https://github.com/onecx') + expect(onecxLink).toHaveAttribute('target', '_blank') + expect(onecxLink).toHaveAttribute('rel', 'noopener noreferrer') + }) + + it('renders all documentation links', () => { + render() + const links = screen.getAllByRole('link') + expect(links.length).toBeGreaterThanOrEqual(5) + }) }) diff --git a/nx-plugin-react/src/generators/react/generator.ts b/nx-plugin-react/src/generators/react/generator.ts index e16c75c5..7e25b2fc 100644 --- a/nx-plugin-react/src/generators/react/generator.ts +++ b/nx-plugin-react/src/generators/react/generator.ts @@ -256,7 +256,7 @@ function adaptTsConfig(tree: Tree) { const filePath = 'tsconfig.app.json'; updateJson(tree, filePath, (json) => { - json.files = ['src/main.tsx', 'src/bootstrap.tsx']; + json.files = ['src/main.tsx', 'src/bootstrap.ts']; json.compilerOptions = json.compilerOptions ?? {}; json.compilerOptions.jsx = 'react-jsx'; return json; From 21357c6765c0d59cc356a99bb7e413eb456e94a2 Mon Sep 17 00:00:00 2001 From: Kamil Nowak Date: Thu, 21 May 2026 15:53:45 +0200 Subject: [PATCH 13/18] test: improve test mocks and add main.tsx entry point - Add @onecx/react-webcomponents mock with useAppHref to App.test.tsx - Add withApp mock to bootstrap.test.ts - Fix type assertion for App component call in App.test.tsx - Update router.test.tsx to properly render AppRoutes component - Remove trailing commas from mock objects for consistency - Add main.tsx template for standalone development mode - Update vite.config.ts to exclude environments and __mocks__ from coverage - Add resolveJsonModule compiler option to tsconfig.app.json - Bump On --- .../react/files/src/App.test.tsx.template | 7 ++++++- .../react/files/src/bootstrap.test.ts.template | 8 ++++++-- .../generators/react/files/src/main.tsx.template | 11 +++++++++++ .../react/files/src/router.test.tsx.template | 13 +++++++------ .../generators/react/files/vite.config.ts.template | 2 ++ nx-plugin-react/src/generators/react/generator.ts | 3 ++- 6 files changed, 34 insertions(+), 10 deletions(-) create mode 100644 nx-plugin-react/src/generators/react/files/src/main.tsx.template diff --git a/nx-plugin-react/src/generators/react/files/src/App.test.tsx.template b/nx-plugin-react/src/generators/react/files/src/App.test.tsx.template index b0ce0edf..1cd4a289 100644 --- a/nx-plugin-react/src/generators/react/files/src/App.test.tsx.template +++ b/nx-plugin-react/src/generators/react/files/src/App.test.tsx.template @@ -4,13 +4,18 @@ vi.mock('@onecx/react-utils', () => ({ withApp: vi.fn((component, config) => () => ({ component, config })), })) +vi.mock('@onecx/react-webcomponents', () => ({ + useAppHref: vi.fn(() => ({ href: '' })), + createViteAppWebComponent: vi.fn() +})) + describe('App', () => { it('calls withApp with AppRouter and correct config', async () => { const { withApp } = await import('@onecx/react-utils') const { default: App } = await import('./App') expect(withApp).toHaveBeenCalledTimes(1) - expect(App()).toEqual({ + expect((App as unknown as () => unknown)()).toEqual({ component: expect.any(Function), config: { PRODUCT_NAME: 'onecx-<%= fileName %>', diff --git a/nx-plugin-react/src/generators/react/files/src/bootstrap.test.ts.template b/nx-plugin-react/src/generators/react/files/src/bootstrap.test.ts.template index 09f6804f..f88fc333 100644 --- a/nx-plugin-react/src/generators/react/files/src/bootstrap.test.ts.template +++ b/nx-plugin-react/src/generators/react/files/src/bootstrap.test.ts.template @@ -1,8 +1,12 @@ import { vi } from 'vitest' - vi.mock('@onecx/react-webcomponents', () => ({ - createViteAppWebComponent: vi.fn() + createViteAppWebComponent: vi.fn(), + useAppHref: vi.fn(() => ({ href: '' })) +})) + +vi.mock('@onecx/react-utils', () => ({ + withApp: vi.fn((component) => component) })) describe('bootstrap.ts', () => { diff --git a/nx-plugin-react/src/generators/react/files/src/main.tsx.template b/nx-plugin-react/src/generators/react/files/src/main.tsx.template new file mode 100644 index 00000000..ff221069 --- /dev/null +++ b/nx-plugin-react/src/generators/react/files/src/main.tsx.template @@ -0,0 +1,11 @@ +import { StrictMode } from 'react' +import * as ReactDOM from 'react-dom/client' + +import App from './App' + +const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement) +root.render( + + + +) diff --git a/nx-plugin-react/src/generators/react/files/src/router.test.tsx.template b/nx-plugin-react/src/generators/react/files/src/router.test.tsx.template index a652e39b..936839d7 100644 --- a/nx-plugin-react/src/generators/react/files/src/router.test.tsx.template +++ b/nx-plugin-react/src/generators/react/files/src/router.test.tsx.template @@ -5,15 +5,15 @@ const mockUseRoutes = vi.fn() const mockUseAppHref = vi.fn() vi.mock('react-router', () => ({ - useRoutes: mockUseRoutes, + useRoutes: mockUseRoutes })) vi.mock('@onecx/react-webcomponents', () => ({ - useAppHref: mockUseAppHref, + useAppHref: mockUseAppHref })) vi.mock('./pages/Welcome', () => ({ - default: () => null, + default: () => null })) vi.mock('./i18n/config', () => ({})) @@ -45,13 +45,14 @@ describe('AppRoutes', () => { mockUseAppHref.mockReturnValue({ href: '/app' }) mockUseRoutes.mockReturnValue(
routes
) - await import('./router') + const { default: AppRoutes } = await import('./router') + render() expect(mockUseRoutes).toHaveBeenCalledWith([ { path: '/app/', - element: expect.any(Object), - }, + element: expect.any(Object) + } ]) }) }) diff --git a/nx-plugin-react/src/generators/react/files/vite.config.ts.template b/nx-plugin-react/src/generators/react/files/vite.config.ts.template index bf8e8e48..4f713e39 100644 --- a/nx-plugin-react/src/generators/react/files/vite.config.ts.template +++ b/nx-plugin-react/src/generators/react/files/vite.config.ts.template @@ -119,6 +119,8 @@ export default defineConfig(({ mode }) => { "src/**/global.d.ts", "src/api/*", "src/assets/*", + "src/environments/*", + "**/__mocks__/*", ], }, }, diff --git a/nx-plugin-react/src/generators/react/generator.ts b/nx-plugin-react/src/generators/react/generator.ts index 7e25b2fc..e21a5937 100644 --- a/nx-plugin-react/src/generators/react/generator.ts +++ b/nx-plugin-react/src/generators/react/generator.ts @@ -121,7 +121,7 @@ export async function reactGenerator( ); } - const oneCXLibVersion = '^8.2.0'; + const oneCXLibVersion = '^8.2.2'; const reactVersion = '^19.0.0'; const nxVersion = '22.0.2'; @@ -259,6 +259,7 @@ function adaptTsConfig(tree: Tree) { json.files = ['src/main.tsx', 'src/bootstrap.ts']; json.compilerOptions = json.compilerOptions ?? {}; json.compilerOptions.jsx = 'react-jsx'; + json.compilerOptions.resolveJsonModule = true; return json; }); } From 0a3122fbbbde34c423f1b2c5a14d22e63d99c02c Mon Sep 17 00:00:00 2001 From: Kamil Nowak Date: Thu, 28 May 2026 13:00:31 +0200 Subject: [PATCH 14/18] feat: enhance AI tool configuration and update dependencies in React generator --- .../.github/copilot-instructions.md | 135 ++++++++++++++++++ .../react/files/vite.config.ts.template | 16 ++- .../src/generators/react/generator.ts | 48 ++++--- .../src/generators/react/schema.d.ts | 2 +- .../src/generators/react/schema.json | 20 ++- .../src/generators/react/steps/ai.step.ts | 21 ++- 6 files changed, 209 insertions(+), 33 deletions(-) create mode 100644 nx-plugin-react/src/generators/react/files-ai-copilot/.github/copilot-instructions.md diff --git a/nx-plugin-react/src/generators/react/files-ai-copilot/.github/copilot-instructions.md b/nx-plugin-react/src/generators/react/files-ai-copilot/.github/copilot-instructions.md new file mode 100644 index 00000000..54c7b66f --- /dev/null +++ b/nx-plugin-react/src/generators/react/files-ai-copilot/.github/copilot-instructions.md @@ -0,0 +1,135 @@ +# GitHub Copilot Instructions + +## Support Level + +- Favor elegant, maintainable solutions over verbose code. Assume understanding of language idioms and design patterns. +- Highlight potential performance implications and optimization opportunities in suggested code. +- Frame solutions within broader architectural contexts and suggest design alternatives when appropriate. +- Focus comments on 'why' not 'what' — assume code readability through well-named functions and variables. +- Proactively address edge cases, race conditions, and security considerations without being prompted. +- When debugging, provide targeted diagnostic approaches rather than shotgun solutions. +- Suggest comprehensive testing strategies rather than just example tests, including considerations for mocking, test organization, and coverage. + +--- + +## Architecture — DDD + +- Define bounded contexts to separate different parts of the domain with clear boundaries. +- Implement ubiquitous language within each context to align code with business terminology. +- Create rich domain models with behavior, not just data structures. +- Use value objects for concepts with no identity but defined by their attributes. +- Implement domain events to communicate between bounded contexts. +- Use aggregates to enforce consistency boundaries and transactional integrity. + +--- + +## React Coding Standards + +- Use functional components with hooks instead of class components. +- Implement `React.memo()` for expensive components that render often with the same props. +- Utilize `React.lazy()` and `Suspense` for code-splitting and performance optimization. +- Use `useCallback` for event handlers passed to child components to prevent unnecessary re-renders. +- Prefer `useMemo` for expensive calculations to avoid recomputation on every render. +- Implement `useId()` for generating unique IDs for accessibility attributes. +- Use `useTransition` for non-urgent state updates to keep the UI responsive. +- Consider `useOptimistic` for optimistic UI updates in forms. + +--- + +## React Router + +- Use `createBrowserRouter` instead of `BrowserRouter` for better data loading and error handling. +- Implement lazy loading with `React.lazy()` for route components to improve initial load time. +- Use the `useNavigate` hook instead of the navigate component prop for programmatic navigation. +- Leverage `loader` and `action` functions to handle data fetching and mutations at the route level. +- Implement error boundaries with `errorElement` to gracefully handle routing and data errors. +- Use relative paths with dot notation (e.g., `"../parent"`) to maintain route hierarchy flexibility. +- Utilize `useRouteLoaderData` to access data from parent routes. +- Implement fetchers for non-navigation data mutations. +- Use `route.lazy()` for route-level code splitting with automatic loading states. +- Implement `shouldRevalidate` functions to control when data revalidation happens after navigation. + +--- + +## PrimeReact + +- Use PrimeReact components as the default UI building blocks instead of custom HTML when possible. +- Prefer PrimeReact layout and form components (`Card`, `Button`, `InputText`, `DataTable`) for consistent styling. +- Keep custom components as thin wrappers around PrimeReact to avoid duplicating behavior. +- When extending, follow PrimeReact theming and pass-through props rather than overriding styles directly. + +--- + +## PrimeFlex + +- Use PrimeFlex utility classes for layout, spacing, and responsive behavior instead of bespoke CSS when possible. +- Prefer PrimeFlex grid/flex utilities (`grid`, `col-12`, `md:col-6`, `flex`, `gap-2`) for structure and alignment. +- Keep custom CSS focused on component-specific visuals that PrimeFlex cannot express. +- Use PrimeFlex spacing scale consistently (`p-`, `m-`, `gap-`) to avoid arbitrary pixel values. +- Apply responsive variants (`sm:`, `md:`, `lg:`, `xl:`) for adaptive layouts. + +--- + +## Static Analysis — ESLint + +- Configure project-specific rules in `eslint.config.js` to enforce consistent coding standards. +- Use shareable configs as a foundation. +- Configure integration with Prettier to avoid rule conflicts. +- Use the `--fix` flag in CI/CD pipelines to automatically correct fixable issues. +- Implement staged linting with husky and lint-staged to prevent committing non-compliant code. + +--- + +## Static Analysis — Prettier + +- Define a consistent `.prettierrc` configuration across all project repositories. +- Configure editor integration to format on save for immediate feedback. +- Use `.prettierignore` to exclude generated files and build artifacts. +- Set `printWidth` based on team preferences (80–120 characters). +- Implement CI checks to ensure all committed code adheres to the defined style. + +--- + +## Testing — Vitest + +- Use `vi.fn()` for function mocks, `vi.spyOn()` to monitor existing functions, and `vi.stubGlobal()` for global mocks. +- Place `vi.mock()` factory functions at the top level of test files; remember the factory runs before imports are processed. +- Define global mocks, custom matchers, and environment setup in dedicated setup files referenced in `vitest.config.ts`. +- Use inline snapshots (`toMatchInlineSnapshot()`) for readable assertions. +- Configure coverage thresholds in `vitest.config.ts` only when asked — focus on meaningful tests, not arbitrary percentages. +- Run `vitest --watch` during development for instant feedback. +- Set `environment: 'jsdom'` for frontend component tests; combine with testing-library for realistic interaction simulation. +- Follow Arrange-Act-Assert pattern and group related tests in descriptive `describe` blocks. +- Use `expectTypeOf()` for type-level assertions; ensure mocks preserve original type signatures. + +--- + +## Skill — Frontend Design + +When building UI components, pages, or applications: + +- Commit to a bold, intentional aesthetic direction before coding (brutally minimal, maximalist, retro-futuristic, editorial, etc.). +- Choose distinctive, characterful fonts — avoid generic choices like Inter, Roboto, Arial. +- Commit to a cohesive color palette with dominant colors and sharp accents. +- Use animations for high-impact moments (staggered page load reveals, hover states) — prefer CSS-only; use Motion library for React. +- Apply unexpected layouts: asymmetry, overlap, diagonal flow, generous negative space, or controlled density. +- Add atmospheric backgrounds: gradient meshes, noise textures, geometric patterns, layered transparencies. +- Never default to purple gradients on white backgrounds or other clichéd AI-generated aesthetics. +- Match implementation complexity to the aesthetic vision. + +--- + +## Skill — React Doctor + +After making React code changes, run the health check: + +```bash +npx -y react-doctor@latest . --verbose --diff +``` + +- `--diff` — scans only changed files vs base branch (use after changes) +- `--verbose` — shows affected files and line numbers per rule +- Without `--diff` — scans full codebase (use for general cleanup) +- `--score` — outputs only the numeric score (0–100) + +If the score dropped after your changes, fix regressions before committing. Fix errors first, then warnings. diff --git a/nx-plugin-react/src/generators/react/files/vite.config.ts.template b/nx-plugin-react/src/generators/react/files/vite.config.ts.template index 4f713e39..b1818215 100644 --- a/nx-plugin-react/src/generators/react/files/vite.config.ts.template +++ b/nx-plugin-react/src/generators/react/files/vite.config.ts.template @@ -4,8 +4,9 @@ import react from "@vitejs/plugin-react";<% if (styles === 'tailwind') { %> import tailwindcss from "@tailwindcss/vite";<% } %> import { viteStaticCopy } from "vite-plugin-static-copy"; import { dependencies } from "./package.json"; -import path from "path"; -import { federation, type ModuleFederationOptions } from '@module-federation/vite' +import path from "node:path"; +import { federation, type ModuleFederationOptions } from "@module-federation/vite" + const mfConfig: ModuleFederationOptions = { name: "onecx-<%= fileName %>-ui", filename: "remoteEntry.js", @@ -37,10 +38,6 @@ const mfConfig: ModuleFederationOptions = { requiredVersion: dependencies.primereact, singleton: true, }, - primeicons: { - requiredVersion: dependencies.primeicons, - singleton: true, - }, "@onecx/accelerator": { requiredVersion: dependencies["@onecx/accelerator"], singleton: true, @@ -89,10 +86,15 @@ export default defineConfig(({ mode }) => { }, ], }), + ...(isTest ? [] : [federation(mfConfig)]), react(),<% if (styles === 'tailwind') { %> tailwindcss(),<% } %> - ...(isTest ? [] : [federation(mfConfig)]) ], + build: { + rollupOptions: { + external: ['chart.js', 'chart.js/auto', 'quill'] + } + }, server: { proxy: { "/api": { diff --git a/nx-plugin-react/src/generators/react/generator.ts b/nx-plugin-react/src/generators/react/generator.ts index e21a5937..637ab936 100644 --- a/nx-plugin-react/src/generators/react/generator.ts +++ b/nx-plugin-react/src/generators/react/generator.ts @@ -37,11 +37,12 @@ const PARAMETERS: GeneratorParameter[] = [ choices: ['primeflex', 'tailwind'], }, { - key: 'ai', - type: 'boolean', + key: 'aiTool', + type: 'select', required: 'interactive', prompt: 'Would you like to add AI agent configuration files?', - default: false, + default: 'none', + choices: ['none', 'agents', 'copilot', 'both'], }, ]; @@ -121,9 +122,9 @@ export async function reactGenerator( ); } - const oneCXLibVersion = '^8.2.2'; + const oneCXLibVersion = '^8.3.1'; const reactVersion = '^19.0.0'; - const nxVersion = '22.0.2'; + const nxVersion = '22.7.4'; addDependenciesToPackageJson( tree, @@ -135,10 +136,11 @@ export async function reactGenerator( '@onecx/react-webcomponents': oneCXLibVersion, '@onecx/react-auth': oneCXLibVersion, '@onecx/integration-interface': oneCXLibVersion, + '@r2wc/react-to-web-component': '^2.1.0', react: reactVersion, 'react-dom': reactVersion, 'react-router': '^7.13.0', - 'react-i18next': '^17.0.8', + 'react-i18next': '^16.5.4', i18next: '^25.8.0', primereact: '^10.9.7', primeicons: '^7.0.0', @@ -155,7 +157,11 @@ export async function reactGenerator( '@nx/eslint': nxVersion, '@nx/eslint-plugin': nxVersion, '@openapitools/openapi-generator-cli': '^2.16.3', - '@vitejs/plugin-react': '^4.4.0', + '@swc-node/register': '~1.11.1', + '@swc/cli': '~0.3.12', + '@swc/core': '^1.15.8', + '@swc/helpers': '~0.5.11', + '@vitejs/plugin-react': '^5.1.1', '@eslint/js': '^8.57.1', eslint: '^9.8.0', 'eslint-config-prettier': '^10.0.0', @@ -164,14 +170,14 @@ export async function reactGenerator( 'eslint-plugin-react': '^7.37.0', 'eslint-plugin-react-hooks': '^5.0.0', nx: nxVersion, - prettier: '^3.5.3', + prettier: '^3.7.4', 'sonar-scanner': '^3.1.0', - typescript: '^5.9.0', - vite: '^6.0.0', - vitest: '^3.0.0', - '@vitest/coverage-v8': '^3.0.0', - jsdom: '^26.0.0', - '@module-federation/vite': '^1.0.0', + typescript: '^5.9.3', + vite: '^7.1.7', + vitest: '^4.0.16', + '@vitest/coverage-v8': '^4.0.16', + jsdom: '^27.0.1', + '@module-federation/vite': '^1.9.4', 'vite-plugin-static-copy': '^2.0.0', '@testing-library/dom': '^10.0.0', '@testing-library/jest-dom': '^6.0.0', @@ -253,9 +259,19 @@ function addScriptsToPackageJson(tree: Tree) { } function adaptTsConfig(tree: Tree) { - const filePath = 'tsconfig.app.json'; + updateJson(tree, 'tsconfig.json', (json) => { + json.compilerOptions = json.compilerOptions ?? {}; + json.compilerOptions.target = 'ES2022'; + json.compilerOptions.module = 'ESNext'; + json.compilerOptions.lib = ['ES2022', 'dom']; + json.compilerOptions.moduleResolution = 'bundler'; + json.compilerOptions.resolveJsonModule = true; + delete json.compilerOptions.emitDecoratorMetadata; + delete json.compilerOptions.experimentalDecorators; + return json; + }); - updateJson(tree, filePath, (json) => { + updateJson(tree, 'tsconfig.app.json', (json) => { json.files = ['src/main.tsx', 'src/bootstrap.ts']; json.compilerOptions = json.compilerOptions ?? {}; json.compilerOptions.jsx = 'react-jsx'; diff --git a/nx-plugin-react/src/generators/react/schema.d.ts b/nx-plugin-react/src/generators/react/schema.d.ts index eef40f07..39b84366 100644 --- a/nx-plugin-react/src/generators/react/schema.d.ts +++ b/nx-plugin-react/src/generators/react/schema.d.ts @@ -2,5 +2,5 @@ export interface ReactGeneratorSchema { name: string; chatty?: boolean; styles?: 'primeflex' | 'tailwind'; - ai?: boolean; + aiTool?: 'none' | 'agents' | 'copilot' | 'both'; } diff --git a/nx-plugin-react/src/generators/react/schema.json b/nx-plugin-react/src/generators/react/schema.json index 79533299..b6713d85 100644 --- a/nx-plugin-react/src/generators/react/schema.json +++ b/nx-plugin-react/src/generators/react/schema.json @@ -27,11 +27,21 @@ ] } }, - "ai": { - "type": "boolean", - "description": "Add AI agent configuration files", - "default": false, - "x-prompt": "Would you like to add AI agent configuration files?" + "aiTool": { + "type": "string", + "description": "Which AI tool to configure (none to skip)", + "enum": ["none", "agents", "copilot", "both"], + "default": "none", + "x-prompt": { + "message": "Would you like to add AI agent configuration files?", + "type": "list", + "items": [ + { "value": "none", "label": "No" }, + { "value": "agents", "label": ".agents (Cursor / Windsurf)" }, + { "value": "copilot", "label": "GitHub Copilot" }, + { "value": "both", "label": "Both" } + ] + } } }, "required": ["name"] diff --git a/nx-plugin-react/src/generators/react/steps/ai.step.ts b/nx-plugin-react/src/generators/react/steps/ai.step.ts index 49b027c0..c3ed9b24 100644 --- a/nx-plugin-react/src/generators/react/steps/ai.step.ts +++ b/nx-plugin-react/src/generators/react/steps/ai.step.ts @@ -4,9 +4,22 @@ import { ReactGeneratorSchema } from '../schema'; export class AIStep implements GeneratorStep { process(tree: Tree, options: ReactGeneratorSchema): void { - generateFiles(tree, joinPathFragments(__dirname, '../files-ai'), '.', { - ...options, - }); + const tool = options.aiTool ?? 'none'; + if (tool === 'agents' || tool === 'both') { + generateFiles(tree, joinPathFragments(__dirname, '../files-ai'), '.', { + ...options, + }); + } + if (tool === 'copilot' || tool === 'both') { + generateFiles( + tree, + joinPathFragments(__dirname, '../files-ai-copilot'), + '.', + { + ...options, + } + ); + } } getTitle(): string { @@ -14,6 +27,6 @@ export class AIStep implements GeneratorStep { } isApplicable(options: ReactGeneratorSchema): boolean { - return options.ai === true; + return options.aiTool !== 'none' && options.aiTool !== undefined; } } From 691f0556b148e4a738d2addb4e69e971776eaeab Mon Sep 17 00:00:00 2001 From: Kamil Nowak Date: Thu, 28 May 2026 16:04:17 +0200 Subject: [PATCH 15/18] feat: update dependencies and move plugin to devDependencies in React generator --- .../src/generators/react/generator.ts | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/nx-plugin-react/src/generators/react/generator.ts b/nx-plugin-react/src/generators/react/generator.ts index 637ab936..e6229d26 100644 --- a/nx-plugin-react/src/generators/react/generator.ts +++ b/nx-plugin-react/src/generators/react/generator.ts @@ -162,11 +162,13 @@ export async function reactGenerator( '@swc/core': '^1.15.8', '@swc/helpers': '~0.5.11', '@vitejs/plugin-react': '^5.1.1', + '@vitest/ui': '^4.1.7', '@eslint/js': '^8.57.1', eslint: '^9.8.0', 'eslint-config-prettier': '^10.0.0', 'eslint-plugin-import': '2.31.0', 'eslint-plugin-prettier': '^5.2.1', + 'eslint-plugin-jsx-a11y': '^6.10.0', 'eslint-plugin-react': '^7.37.0', 'eslint-plugin-react-hooks': '^5.0.0', nx: nxVersion, @@ -174,11 +176,11 @@ export async function reactGenerator( 'sonar-scanner': '^3.1.0', typescript: '^5.9.3', vite: '^7.1.7', - vitest: '^4.0.16', - '@vitest/coverage-v8': '^4.0.16', + vitest: '^4.1.7', + '@vitest/coverage-v8': '^4.1.7', jsdom: '^27.0.1', '@module-federation/vite': '^1.9.4', - 'vite-plugin-static-copy': '^2.0.0', + 'vite-plugin-static-copy': '^4.1.0', '@testing-library/dom': '^10.0.0', '@testing-library/jest-dom': '^6.0.0', '@testing-library/react': '^16.0.0', @@ -225,6 +227,16 @@ function addBaseToPackageJson(tree: Tree, options: ReactGeneratorSchema) { pkgJson.name = 'onecx-' + names(options.name).fileName + '-ui'; pkgJson.private = true; pkgJson.license = 'Apache-2.0'; + + // Nx adds the preset package to dependencies automatically – move it to devDependencies + const pluginKey = '@onecx/nx-plugin-react'; + const pluginVersion = pkgJson.dependencies?.[pluginKey]; + if (pluginVersion) { + delete pkgJson.dependencies[pluginKey]; + pkgJson.devDependencies = pkgJson.devDependencies ?? {}; + pkgJson.devDependencies[pluginKey] = pluginVersion; + } + return pkgJson; }); } From d9ef16ec1696ab844c9c904446042c23970bbedb Mon Sep 17 00:00:00 2001 From: Kamil Nowak Date: Mon, 1 Jun 2026 17:24:48 +0200 Subject: [PATCH 16/18] feat: add pre-commit validation generator with eslint, conventional commits, and detect secrets checks --- nx-plugin-react/generators.json | 5 + .../config/commitlint/commitlint.config.js | 3 + .../src/config/lint-staged/.lintstagedrc | 3 + .../detect-secrets-plugin.ts | 25 +++ .../src/husky-commits/commit-msg/commit-msg | 3 + .../src/husky-commits/pre-commit/pre-commit | 36 ++++ .../pre-commit-validation/generator.ts | 162 ++++++++++++++++++ .../pre-commit-validation/schema.d.ts | 5 + .../pre-commit-validation/schema.json | 28 +++ .../.github/copilot-instructions.md | 12 ++ .../frontend-styling-tailwind.instructions.md | 16 ++ .../rules/frontend-styling-tailwind.mdc | 16 ++ .../src/generators/react/steps/ai.step.ts | 25 +++ .../react/steps/general-openapi.step.ts | 15 +- 14 files changed, 350 insertions(+), 4 deletions(-) create mode 100644 nx-plugin-react/src/generators/pre-commit-validation/files/src/config/commitlint/commitlint.config.js create mode 100644 nx-plugin-react/src/generators/pre-commit-validation/files/src/config/lint-staged/.lintstagedrc create mode 100644 nx-plugin-react/src/generators/pre-commit-validation/files/src/detect-secrets-plugin/detect-secrets-plugin.ts create mode 100644 nx-plugin-react/src/generators/pre-commit-validation/files/src/husky-commits/commit-msg/commit-msg create mode 100644 nx-plugin-react/src/generators/pre-commit-validation/files/src/husky-commits/pre-commit/pre-commit create mode 100644 nx-plugin-react/src/generators/pre-commit-validation/generator.ts create mode 100644 nx-plugin-react/src/generators/pre-commit-validation/schema.d.ts create mode 100644 nx-plugin-react/src/generators/pre-commit-validation/schema.json create mode 100644 nx-plugin-react/src/generators/react/files-ai-copilot/.github/instructions/frontend-styling-tailwind.instructions.md create mode 100644 nx-plugin-react/src/generators/react/files-ai/.agents/rules/frontend-styling-tailwind.mdc diff --git a/nx-plugin-react/generators.json b/nx-plugin-react/generators.json index 7db3cadd..c8853f0f 100644 --- a/nx-plugin-react/generators.json +++ b/nx-plugin-react/generators.json @@ -9,6 +9,11 @@ "factory": "./src/generators/preset/generator", "schema": "./src/generators/preset/schema.json", "description": "preset generator" + }, + "pre-commit-validation": { + "factory": "./src/generators/pre-commit-validation/generator", + "schema": "./src/generators/pre-commit-validation/schema.json", + "description": "pre-commit-validation generator" } } } diff --git a/nx-plugin-react/src/generators/pre-commit-validation/files/src/config/commitlint/commitlint.config.js b/nx-plugin-react/src/generators/pre-commit-validation/files/src/config/commitlint/commitlint.config.js new file mode 100644 index 00000000..84dcb122 --- /dev/null +++ b/nx-plugin-react/src/generators/pre-commit-validation/files/src/config/commitlint/commitlint.config.js @@ -0,0 +1,3 @@ +module.exports = { + extends: ['@commitlint/config-conventional'], +}; diff --git a/nx-plugin-react/src/generators/pre-commit-validation/files/src/config/lint-staged/.lintstagedrc b/nx-plugin-react/src/generators/pre-commit-validation/files/src/config/lint-staged/.lintstagedrc new file mode 100644 index 00000000..f4ff1dfe --- /dev/null +++ b/nx-plugin-react/src/generators/pre-commit-validation/files/src/config/lint-staged/.lintstagedrc @@ -0,0 +1,3 @@ +{ + "*": ["nx lint --cache=true", "npx prettier -c --cache"] +} diff --git a/nx-plugin-react/src/generators/pre-commit-validation/files/src/detect-secrets-plugin/detect-secrets-plugin.ts b/nx-plugin-react/src/generators/pre-commit-validation/files/src/detect-secrets-plugin/detect-secrets-plugin.ts new file mode 100644 index 00000000..99eba4cf --- /dev/null +++ b/nx-plugin-react/src/generators/pre-commit-validation/files/src/detect-secrets-plugin/detect-secrets-plugin.ts @@ -0,0 +1,25 @@ +import * as fs from 'fs'; +import * as path from 'path'; + +const [, , filePath, blacklistFile] = process.argv; + +const blacklist = fs + .readFileSync(blacklistFile, 'utf-8') + .split('\n') + .map((line) => line.trim()) + .filter((line) => line.length > 0); + +const fileContent = fs.readFileSync(filePath, 'utf-8'); + +const secrets = blacklist.filter((word) => fileContent.includes(word)); + +if (secrets.length > 0) { + console.error( + `Forbidden words or secrets found in ${path.basename( + filePath + )}: ${secrets.join(', ')}` + ); + process.exit(1); +} + +process.exit(0); diff --git a/nx-plugin-react/src/generators/pre-commit-validation/files/src/husky-commits/commit-msg/commit-msg b/nx-plugin-react/src/generators/pre-commit-validation/files/src/husky-commits/commit-msg/commit-msg new file mode 100644 index 00000000..cfb9c8b2 --- /dev/null +++ b/nx-plugin-react/src/generators/pre-commit-validation/files/src/husky-commits/commit-msg/commit-msg @@ -0,0 +1,3 @@ +# commitlint +echo "[Husky] Running commitlint check:" +npx --no-install commitlint --edit $1 diff --git a/nx-plugin-react/src/generators/pre-commit-validation/files/src/husky-commits/pre-commit/pre-commit b/nx-plugin-react/src/generators/pre-commit-validation/files/src/husky-commits/pre-commit/pre-commit new file mode 100644 index 00000000..ded26528 --- /dev/null +++ b/nx-plugin-react/src/generators/pre-commit-validation/files/src/husky-commits/pre-commit/pre-commit @@ -0,0 +1,36 @@ +<% if (lint) { %> +# lint +echo "[Husky] Running lint check on staged files:" +npx lint-staged +<% } %> +<% if (secrets) { %> +# detect-secrets +echo "[Husky] Running detect-secrets on staged files:" + +BLACKLIST_FILE=${BLACKLIST_FILE:-"$HOME/blacklist.txt"} +PLUGIN_PATH="scripts/detect-secrets-plugin.js" + +if [ ! -f "$BLACKLIST_FILE" ]; then + echo "Blacklist file not found. Please create a new blacklist.txt in your home directory $HOME/blacklist.txt" + exit 1 +fi + +STAGED_FILES=$(git diff --name-only --cached) + +if [ -z "$STAGED_FILES" ]; then + echo "No staged files detected. Skipping detect-secrets check." + exit 0 +fi + +for file in $STAGED_FILES; do + if [ -f "$file" ]; then + npx ts-node "$PLUGIN_PATH" "$file" "$BLACKLIST_FILE" + if [ $? -ne 0 ]; then + echo "Secret words detected in $file." + exit 1 + fi + fi +done + +echo "No secrets found." +<% } %> diff --git a/nx-plugin-react/src/generators/pre-commit-validation/generator.ts b/nx-plugin-react/src/generators/pre-commit-validation/generator.ts new file mode 100644 index 00000000..521df8d9 --- /dev/null +++ b/nx-plugin-react/src/generators/pre-commit-validation/generator.ts @@ -0,0 +1,162 @@ +import { + formatFiles, + generateFiles, + installPackagesTask, + Tree, +} from '@nx/devkit'; +import * as path from 'path'; +import { PreCommitValidationGeneratorSchema } from './schema'; +import { execSync } from 'child_process'; +import processParams, { GeneratorParameter } from '../shared/parameters.utils'; + +const PARAMETERS: GeneratorParameter[] = [ + { + key: 'enableEslint', + type: 'boolean', + required: 'interactive', + default: true, + prompt: 'Do you want to enable eslint check before committing?', + }, + { + key: 'enableConventionalCommits', + type: 'boolean', + required: 'interactive', + default: true, + prompt: + 'Do you want to enable conventional commits check before committing?', + }, + { + key: 'enableDetectSecrets', + type: 'boolean', + required: 'interactive', + default: true, + prompt: 'Do you want to enable detect secrets check before committing?', + }, +]; + +function checkHuskyInstallation() { + try { + execSync('npm ls husky', { stdio: 'ignore' }); + } catch { + console.log('Installing Husky...'); + execSync('npm install --save-dev husky', { stdio: 'inherit' }); + console.log('Initializing Husky...'); + execSync('npx husky init', { stdio: 'inherit' }); + } +} + +function checkLintStagedInstallation() { + try { + execSync('npm ls lint-staged', { stdio: 'ignore' }); + } catch { + console.log('Installing lint-staged...'); + execSync('npm install --save-dev lint-staged', { stdio: 'inherit' }); + } +} + +function checkDetectSecretsInstallation() { + try { + execSync('npm ls detect-secrets', { stdio: 'ignore' }); + } catch { + console.log('Installing detect-secrets...'); + execSync('npm install --save-dev detect-secrets', { stdio: 'inherit' }); + } +} + +function checkCommitlintInstallation() { + try { + execSync('npm ls commitlint', { stdio: 'ignore' }); + } catch { + console.log('Installing commitlint...'); + execSync( + 'npm install --save-dev @commitlint/cli @commitlint/config-conventional', + { stdio: 'inherit' } + ); + } +} + +export async function preCommitValidationGenerator( + tree: Tree, + options: PreCommitValidationGeneratorSchema +) { + const parameters = await processParams( + PARAMETERS, + options + ); + Object.assign(options, parameters); + + const templatePathHusky = path.join( + __dirname, + 'files', + 'src', + 'husky-commits' + ); + + if (options.enableEslint) { + checkHuskyInstallation(); + checkLintStagedInstallation(); + console.log('Setting up lint-staged...'); + generateFiles( + tree, + path.join(__dirname, 'files', 'src', 'config', 'lint-staged'), + '.', + {} + ); + console.log('Lint-staged files created.'); + } else { + console.log('Skipped lint-staged initialization.'); + } + + if (options.enableConventionalCommits) { + checkHuskyInstallation(); + checkCommitlintInstallation(); + console.log('Setting up ConventionalCommits check...'); + generateFiles( + tree, + path.join(templatePathHusky, 'commit-msg'), + '.husky', + {} + ); + generateFiles( + tree, + path.join(__dirname, 'files', 'src', 'config', 'commitlint'), + '.', + {} + ); + console.log('ConventionalCommits commit-msg hook created.'); + } else { + console.log('Skipped ConventionalCommits commit-msg hook.'); + } + + if (options.enableDetectSecrets) { + checkHuskyInstallation(); + checkDetectSecretsInstallation(); + console.log('Setting up detect-secrets...'); + generateFiles( + tree, + path.join(__dirname, 'files', 'src', 'detect-secrets-plugin'), + 'scripts', + {} + ); + console.log('Detect secrets files created.'); + } else { + console.log('Skipped detect-secrets initialization.'); + } + + if (options.enableEslint || options.enableDetectSecrets) { + checkHuskyInstallation(); + console.log('Setting up pre-commit hook.'); + generateFiles(tree, path.join(templatePathHusky, 'pre-commit'), '.husky/', { + lint: options.enableEslint, + secrets: options.enableDetectSecrets, + }); + console.log('pre-commit hook created.'); + } else { + console.log('Skipped pre-commit hook.'); + } + + await formatFiles(tree); + return () => installPackagesTask(tree); +} + +export default preCommitValidationGenerator; diff --git a/nx-plugin-react/src/generators/pre-commit-validation/schema.d.ts b/nx-plugin-react/src/generators/pre-commit-validation/schema.d.ts new file mode 100644 index 00000000..fa95f0cc --- /dev/null +++ b/nx-plugin-react/src/generators/pre-commit-validation/schema.d.ts @@ -0,0 +1,5 @@ +export interface PreCommitValidationGeneratorSchema { + enableEslint: boolean; + enableConventionalCommits: boolean; + enableDetectSecrets: boolean; +} diff --git a/nx-plugin-react/src/generators/pre-commit-validation/schema.json b/nx-plugin-react/src/generators/pre-commit-validation/schema.json new file mode 100644 index 00000000..cae8c828 --- /dev/null +++ b/nx-plugin-react/src/generators/pre-commit-validation/schema.json @@ -0,0 +1,28 @@ +{ + "$schema": "https://json-schema.org/schema", + "$id": "PreCommitValidation", + "title": "", + "type": "object", + "properties": { + "enableEslintCheck": { + "type": "boolean", + "description": "", + "default": true + }, + "enableConventionalCommitsCheck": { + "type": "boolean", + "description": "", + "default": true + }, + "enableDetectSecretsCheck": { + "type": "boolean", + "description": "", + "default": true + } + }, + "required": [ + "enableEslintCheck", + "enableConventionalCommitsCheck", + "enableDetectSecretsCheck" + ] +} diff --git a/nx-plugin-react/src/generators/react/files-ai-copilot/.github/copilot-instructions.md b/nx-plugin-react/src/generators/react/files-ai-copilot/.github/copilot-instructions.md index 54c7b66f..611015ff 100644 --- a/nx-plugin-react/src/generators/react/files-ai-copilot/.github/copilot-instructions.md +++ b/nx-plugin-react/src/generators/react/files-ai-copilot/.github/copilot-instructions.md @@ -60,6 +60,17 @@ --- +<% if (styles === 'tailwind') { %> +## Tailwind CSS + +- Use Tailwind utility classes for layout, spacing, and responsive behavior instead of bespoke CSS when possible. +- Leverage Tailwind's flex/grid utilities (e.g., `flex`, `gap-2`, `grid`, `col-span-6`) for structure and alignment. +- Keep custom CSS minimal and focused on component-specific visuals that Tailwind cannot express. +- Use Tailwind's spacing scale consistently (`p-`, `m-`, `gap-`) to avoid arbitrary pixel values. +- Apply responsive prefixes (`sm:`, `md:`, `lg:`, `xl:`, `2xl:`) for adaptive layouts. +- Configure custom design tokens in `tailwind.config.ts` to maintain design consistency across the codebase. +- Prefer `@apply` in component CSS only for repeated utility combinations – avoid overusing it. +<% } else { %> ## PrimeFlex - Use PrimeFlex utility classes for layout, spacing, and responsive behavior instead of bespoke CSS when possible. @@ -67,6 +78,7 @@ - Keep custom CSS focused on component-specific visuals that PrimeFlex cannot express. - Use PrimeFlex spacing scale consistently (`p-`, `m-`, `gap-`) to avoid arbitrary pixel values. - Apply responsive variants (`sm:`, `md:`, `lg:`, `xl:`) for adaptive layouts. +<% } %> --- diff --git a/nx-plugin-react/src/generators/react/files-ai-copilot/.github/instructions/frontend-styling-tailwind.instructions.md b/nx-plugin-react/src/generators/react/files-ai-copilot/.github/instructions/frontend-styling-tailwind.instructions.md new file mode 100644 index 00000000..30677465 --- /dev/null +++ b/nx-plugin-react/src/generators/react/files-ai-copilot/.github/instructions/frontend-styling-tailwind.instructions.md @@ -0,0 +1,16 @@ +--- +applyTo: "**/*.tsx, **/*.css" +--- +## FRONTEND + +### Guidelines for STYLING + +#### TAILWIND + +- Use Tailwind utility classes for layout, spacing, and responsive behavior instead of bespoke CSS when possible +- Leverage Tailwind's flex/grid utilities (e.g., `flex`, `gap-2`, `grid`, `col-span-6`) for structure and alignment +- Keep custom CSS minimal and focused on component-specific visuals that Tailwind cannot express +- Use Tailwind's spacing scale consistently (`p-`, `m-`, `gap-`) to avoid arbitrary pixel values +- Apply responsive prefixes (`sm:`, `md:`, `lg:`, `xl:`, `2xl:`) for adaptive layouts +- Configure custom design tokens in `tailwind.config.ts` to maintain design consistency across the codebase +- Prefer `@apply` in component CSS only for repeated utility combinations – avoid overusing it diff --git a/nx-plugin-react/src/generators/react/files-ai/.agents/rules/frontend-styling-tailwind.mdc b/nx-plugin-react/src/generators/react/files-ai/.agents/rules/frontend-styling-tailwind.mdc new file mode 100644 index 00000000..602d0cf7 --- /dev/null +++ b/nx-plugin-react/src/generators/react/files-ai/.agents/rules/frontend-styling-tailwind.mdc @@ -0,0 +1,16 @@ +--- +trigger: always_on +--- +## FRONTEND + +### Guidelines for STYLING + +#### TAILWIND + +- Use Tailwind utility classes for layout, spacing, and responsive behavior instead of bespoke CSS when possible +- Leverage Tailwind's flex/grid utilities (e.g., `flex`, `gap-2`, `grid`, `col-span-6`) for structure and alignment +- Keep custom CSS minimal and focused on component-specific visuals that Tailwind cannot express +- Use Tailwind's spacing scale consistently (`p-`, `m-`, `gap-`) to avoid arbitrary pixel values +- Apply responsive prefixes (`sm:`, `md:`, `lg:`, `xl:`, `2xl:`) for adaptive layouts +- Configure custom design tokens in `tailwind.config.ts` to maintain design consistency across the codebase +- Prefer `@apply` in component CSS only for repeated utility combinations – avoid overusing it diff --git a/nx-plugin-react/src/generators/react/steps/ai.step.ts b/nx-plugin-react/src/generators/react/steps/ai.step.ts index c3ed9b24..45ec553c 100644 --- a/nx-plugin-react/src/generators/react/steps/ai.step.ts +++ b/nx-plugin-react/src/generators/react/steps/ai.step.ts @@ -9,6 +9,12 @@ export class AIStep implements GeneratorStep { generateFiles(tree, joinPathFragments(__dirname, '../files-ai'), '.', { ...options, }); + this.removeUnusedStyleRule( + tree, + options.styles, + '.agents/rules/frontend-styling-primeflex.mdc', + '.agents/rules/frontend-styling-tailwind.mdc' + ); } if (tool === 'copilot' || tool === 'both') { generateFiles( @@ -19,6 +25,25 @@ export class AIStep implements GeneratorStep { ...options, } ); + this.removeUnusedStyleRule( + tree, + options.styles, + '.github/instructions/frontend-styling-primeflex.instructions.md', + '.github/instructions/frontend-styling-tailwind.instructions.md' + ); + } + } + + private removeUnusedStyleRule( + tree: Tree, + styles: string, + primflexFile: string, + tailwindFile: string + ): void { + if (styles === 'tailwind') { + tree.delete(primflexFile); + } else { + tree.delete(tailwindFile); } } diff --git a/nx-plugin-react/src/generators/react/steps/general-openapi.step.ts b/nx-plugin-react/src/generators/react/steps/general-openapi.step.ts index d32d15ba..0ac30c4b 100644 --- a/nx-plugin-react/src/generators/react/steps/general-openapi.step.ts +++ b/nx-plugin-react/src/generators/react/steps/general-openapi.step.ts @@ -1,11 +1,12 @@ import { Tree, joinPathFragments } from '@nx/devkit'; -import { GeneratorStep } from '../../shared/generator.utils'; +import { + GeneratorStep, + GeneratorStepError, +} from '../../shared/generator.utils'; import { ReactGeneratorSchema } from '../schema'; import { OpenAPIUtil } from '../../shared/openapi/openapi.utils'; -export class GeneralOpenAPIStep - implements GeneratorStep -{ +export class GeneralOpenAPIStep implements GeneratorStep { process(tree: Tree, options: ReactGeneratorSchema): void { const openApiFolderPath = 'src/assets/api'; const bffOpenApiPath = 'openapi-bff.yaml'; @@ -14,6 +15,12 @@ export class GeneralOpenAPIStep 'utf8' ); + if (!bffOpenApiContent) { + throw new GeneratorStepError( + `OpenAPI file not found at ${openApiFolderPath}/${bffOpenApiPath} – skipping OpenAPI step.` + ); + } + const resource = options.name; const apiUtil = new OpenAPIUtil(bffOpenApiContent); From f976ae26dc04f6fe3758e8a589c458914718aca7 Mon Sep 17 00:00:00 2001 From: Kamil Nowak Date: Mon, 1 Jun 2026 17:30:26 +0200 Subject: [PATCH 17/18] feat: postcss --- .../generators/react/files/postcss.config.cjs | 3 +++ .../react/files/vite.config.ts.template | 19 +++++++++++++++++++ .../src/generators/react/generator.ts | 2 ++ 3 files changed, 24 insertions(+) create mode 100644 nx-plugin-react/src/generators/react/files/postcss.config.cjs diff --git a/nx-plugin-react/src/generators/react/files/postcss.config.cjs b/nx-plugin-react/src/generators/react/files/postcss.config.cjs new file mode 100644 index 00000000..164dbadc --- /dev/null +++ b/nx-plugin-react/src/generators/react/files/postcss.config.cjs @@ -0,0 +1,3 @@ +module.exports = { + plugins: [require("postcss-import")()], +}; diff --git a/nx-plugin-react/src/generators/react/files/vite.config.ts.template b/nx-plugin-react/src/generators/react/files/vite.config.ts.template index b1818215..3842b795 100644 --- a/nx-plugin-react/src/generators/react/files/vite.config.ts.template +++ b/nx-plugin-react/src/generators/react/files/vite.config.ts.template @@ -85,6 +85,25 @@ export default defineConfig(({ mode }) => { dest: "assets", }, ], + { + name: "process-app-styles", + apply: "build", + async closeBundle() { + const postcss = (await import("postcss")).default; + const postcssImport = (await import("postcss-import")).default; + + const cssPath = path.resolve(__dirname, "./src/assets/styles.css"); + const sourceCss = fs.readFileSync(cssPath, "utf8"); + + const result = await postcss([postcssImport()]).process(sourceCss, { + from: cssPath, + }); + + const outDir = path.resolve(__dirname, "dist"); + fs.mkdirSync(outDir, { recursive: true }); + fs.writeFileSync(path.resolve(outDir, "styles.css"), result.css); + }, + }, }), ...(isTest ? [] : [federation(mfConfig)]), react(),<% if (styles === 'tailwind') { %> diff --git a/nx-plugin-react/src/generators/react/generator.ts b/nx-plugin-react/src/generators/react/generator.ts index e6229d26..d4dcdc6f 100644 --- a/nx-plugin-react/src/generators/react/generator.ts +++ b/nx-plugin-react/src/generators/react/generator.ts @@ -187,6 +187,8 @@ export async function reactGenerator( '@types/react': '^19.0.0', '@types/react-dom': '^19.0.0', '@types/node': '^22.0.0', + '@types/postcss-import': '^14.0.3', + 'postcss-import': '^16.1.1', } ); From 1bedc83db35ee29890817bfef1f808e74510258f Mon Sep 17 00:00:00 2001 From: Kamil Nowak Date: Tue, 2 Jun 2026 09:08:04 +0200 Subject: [PATCH 18/18] feat: add PostCSS configuration and integrate postcss-import in Vite build process --- .../files/{postcss.config.cjs => postcss.config.cjs.template} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename nx-plugin-react/src/generators/react/files/{postcss.config.cjs => postcss.config.cjs.template} (100%) diff --git a/nx-plugin-react/src/generators/react/files/postcss.config.cjs b/nx-plugin-react/src/generators/react/files/postcss.config.cjs.template similarity index 100% rename from nx-plugin-react/src/generators/react/files/postcss.config.cjs rename to nx-plugin-react/src/generators/react/files/postcss.config.cjs.template