diff --git a/create-workspace/bin/index.ts b/create-workspace/bin/index.ts index d457fa98..0abbc703 100644 --- a/create-workspace/bin/index.ts +++ b/create-workspace/bin/index.ts @@ -1,24 +1,44 @@ #!/usr/bin/env node import { createWorkspace } from 'create-nx-workspace'; +import { readFileSync, existsSync } from 'fs'; + +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 +47,23 @@ 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); + + 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/.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/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/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..c8853f0f --- /dev/null +++ b/nx-plugin-react/generators.json @@ -0,0 +1,19 @@ +{ + "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" + }, + "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/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..2e07cad1 --- /dev/null +++ b/nx-plugin-react/package.json @@ -0,0 +1,29 @@ +{ + "name": "@onecx/nx-plugin-react", + "version": "7.1.0", + "license": "Apache-2.0", + "contributors": [], + "repository": { + "type": "git", + "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", + "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", + "types": "./src/index.d.ts", + "generators": "./generators.json", + "publishConfig": { + "access": "public" + } +} 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/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/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-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..611015ff --- /dev/null +++ b/nx-plugin-react/src/generators/react/files-ai-copilot/.github/copilot-instructions.md @@ -0,0 +1,147 @@ +# 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. + +--- + +<% 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. +- 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-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/coding_practices-architecture-ddd.mdc b/nx-plugin-react/src/generators/react/files-ai/.agents/rules/coding_practices-architecture-ddd.mdc new file mode 100644 index 00000000..3573ab50 --- /dev/null +++ b/nx-plugin-react/src/generators/react/files-ai/.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-ai/.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 new file mode 100644 index 00000000..bcff2d17 --- /dev/null +++ b/nx-plugin-react/src/generators/react/files-ai/.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-ai/.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 new file mode 100644 index 00000000..3fe3d820 --- /dev/null +++ b/nx-plugin-react/src/generators/react/files-ai/.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-ai/.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 new file mode 100644 index 00000000..105bdba8 --- /dev/null +++ b/nx-plugin-react/src/generators/react/files-ai/.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-ai/.agents/rules/frontend-react-primereact.mdc b/nx-plugin-react/src/generators/react/files-ai/.agents/rules/frontend-react-primereact.mdc new file mode 100644 index 00000000..cef519b7 --- /dev/null +++ b/nx-plugin-react/src/generators/react/files-ai/.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-ai/.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 new file mode 100644 index 00000000..0314ba7e --- /dev/null +++ b/nx-plugin-react/src/generators/react/files-ai/.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-ai/.agents/rules/frontend-react-react_router.mdc b/nx-plugin-react/src/generators/react/files-ai/.agents/rules/frontend-react-react_router.mdc new file mode 100644 index 00000000..32dd66e0 --- /dev/null +++ b/nx-plugin-react/src/generators/react/files-ai/.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-ai/.agents/rules/frontend-styling-primeflex.mdc b/nx-plugin-react/src/generators/react/files-ai/.agents/rules/frontend-styling-primeflex.mdc new file mode 100644 index 00000000..6d1c7b16 --- /dev/null +++ b/nx-plugin-react/src/generators/react/files-ai/.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-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/files-ai/.agents/rules/testing-unit-vitest.mdc b/nx-plugin-react/src/generators/react/files-ai/.agents/rules/testing-unit-vitest.mdc new file mode 100644 index 00000000..37571ef1 --- /dev/null +++ b/nx-plugin-react/src/generators/react/files-ai/.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-ai/.agents/skills/frontend-design/SKILL.md b/nx-plugin-react/src/generators/react/files-ai/.agents/skills/frontend-design/SKILL.md new file mode 100644 index 00000000..5be498e2 --- /dev/null +++ b/nx-plugin-react/src/generators/react/files-ai/.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-ai/.agents/skills/react-doctor/SKILL.md b/nx-plugin-react/src/generators/react/files-ai/.agents/skills/react-doctor/SKILL.md new file mode 100644 index 00000000..9c556745 --- /dev/null +++ b/nx-plugin-react/src/generators/react/files-ai/.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-styles-primeflex/src/assets/styles.css.template b/nx-plugin-react/src/generators/react/files-styles-primeflex/src/assets/styles.css.template new file mode 100644 index 00000000..1f95456a --- /dev/null +++ b/nx-plugin-react/src/generators/react/files-styles-primeflex/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-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..14fcf587 --- /dev/null +++ b/nx-plugin-react/src/generators/react/files-styles-tailwind/src/assets/styles.css.template @@ -0,0 +1,4 @@ +@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/.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..d7d35e2f --- /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/api/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/README.md.template b/nx-plugin-react/src/generators/react/files/README.md.template new file mode 100644 index 00000000..a8e06169 --- /dev/null +++ b/nx-plugin-react/src/generators/react/files/README.md.template @@ -0,0 +1,48 @@ +# 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/ +├── api/ +│ └── 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/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..a1087bf1 --- /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/api/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..a83497fe --- /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: './OneCX<%= className %>RemoteModule' + description: 'OneCX <%= remoteModuleName %> UI' + note: 'OneCX <%= remoteModuleName %> UI module auto import via MF operator' + type: MODULE + technology: WEBCOMPONENTMODULE + remoteName: <%= remoteModuleFileName %> + tagName: onecx-<%= fileName %>-ui-entrypoint + # 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/postcss.config.cjs.template b/nx-plugin-react/src/generators/react/files/postcss.config.cjs.template new file mode 100644 index 00000000..164dbadc --- /dev/null +++ b/nx-plugin-react/src/generators/react/files/postcss.config.cjs.template @@ -0,0 +1,3 @@ +module.exports = { + plugins: [require("postcss-import")()], +}; 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..1cd4a289 --- /dev/null +++ b/nx-plugin-react/src/generators/react/files/src/App.test.tsx.template @@ -0,0 +1,26 @@ +import { describe, it, expect, vi } from 'vitest' + +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 as unknown as () => unknown)()).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/bootstrap.test.ts.template b/nx-plugin-react/src/generators/react/files/src/bootstrap.test.ts.template new file mode 100644 index 00000000..f88fc333 --- /dev/null +++ b/nx-plugin-react/src/generators/react/files/src/bootstrap.test.ts.template @@ -0,0 +1,23 @@ +import { vi } from 'vitest' + +vi.mock('@onecx/react-webcomponents', () => ({ + createViteAppWebComponent: vi.fn(), + useAppHref: vi.fn(() => ({ href: '' })) +})) + +vi.mock('@onecx/react-utils', () => ({ + withApp: vi.fn((component) => component) +})) + +describe('bootstrap.ts', () => { + it('calls init and createViteAppWebComponent with correct params', async () => { + // Reset modules so bootstrap runs fresh + await import('./bootstrap') + const { createViteAppWebComponent } = await import( + '@onecx/react-webcomponents' + ) + const { default: App } = await import('./App') + + 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..5dbf3cea --- /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' + +// 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/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, +}; 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/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/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..1f08380d --- /dev/null +++ b/nx-plugin-react/src/generators/react/files/src/pages/Welcome.test.tsx.template @@ -0,0 +1,40 @@ +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() + }) + + 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/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..16eeb85a --- /dev/null +++ b/nx-plugin-react/src/generators/react/files/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: '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/src/router.test.tsx.template b/nx-plugin-react/src/generators/react/files/src/router.test.tsx.template new file mode 100644 index 00000000..936839d7 --- /dev/null +++ b/nx-plugin-react/src/generators/react/files/src/router.test.tsx.template @@ -0,0 +1,58 @@ +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
) + + const { default: AppRoutes } = await import('./router') + render() + + 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..3842b795 --- /dev/null +++ b/nx-plugin-react/src/generators/react/files/vite.config.ts.template @@ -0,0 +1,149 @@ +/// +import { defineConfig } from "vite"; +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 "node:path"; +import { federation, type ModuleFederationOptions } from "@module-federation/vite" + +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, + }, + "@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 isTest = mode === 'test' + 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", + }, + ], + { + 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') { %> + tailwindcss(),<% } %> + ], + build: { + rollupOptions: { + external: ['chart.js', 'chart.js/auto', 'quill'] + } + }, + server: { + proxy: { + "/api": { + target: "http://localhost:8080", + changeOrigin: true, + rewrite: (path) => path.replace(/^\/api/, ""), + }, + }, + }, + test: { + globals: true, + testTimeout: 10000, + environment: "jsdom", + setupFiles: "./src/setupTests.ts", + 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/*", + "src/environments/*", + "**/__mocks__/*", + ], + }, + }, + }; +}); 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..d4dcdc6f --- /dev/null +++ b/nx-plugin-react/src/generators/react/generator.ts @@ -0,0 +1,341 @@ +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'; +import { StylesStep } from './steps/styles.step'; +import { AIStep } from './steps/ai.step'; + +const PARAMETERS: GeneratorParameter[] = [ + { + key: 'chatty', + type: 'boolean', + 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: 'aiTool', + type: 'select', + required: 'interactive', + prompt: 'Would you like to add AI agent configuration files?', + default: 'none', + choices: ['none', 'agents', 'copilot', 'both'], + }, +]; + +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 = '.'; + + 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.addStep(new StylesStep()); + generatorProcessor.addStep(new AIStep()); + + generatorProcessor.run(tree, options, spinner); + + addBaseToPackageJson(tree, options); + addScriptsToPackageJson(tree); + addExtensionsToPackageJson(tree); + + if (options.styles === 'tailwind') { + addDependenciesToPackageJson( + tree, + {}, + { + tailwindcss: '^4.0.0', + '@tailwindcss/vite': '^4.0.0', + 'tailwindcss-primeui': '^0.3.0', + } + ); + } + + const oneCXLibVersion = '^8.3.1'; + const reactVersion = '^19.0.0'; + const nxVersion = '22.7.4'; + + 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, + '@r2wc/react-to-web-component': '^2.1.0', + 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', + '@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', + '@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, + prettier: '^3.7.4', + 'sonar-scanner': '^3.1.0', + typescript: '^5.9.3', + vite: '^7.1.7', + vitest: '^4.1.7', + '@vitest/coverage-v8': '^4.1.7', + jsdom: '^27.0.1', + '@module-federation/vite': '^1.9.4', + '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', + '@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', + } + ); + + adaptTsConfig(tree); + adaptProjectConfiguration(tree, options); + + await formatFiles(tree); + + spinner.succeed(); + + return async () => { + await applicationGeneratorCallback(); + let cmd = 'rm -rf .vscode apps libs'; + 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'; + + // 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; + }); +} + +function addExtensionsToPackageJson(tree: Tree) { + updateJson(tree, 'package.json', (pkgJson) => { + pkgJson.jestSonar = { + reportPath: 'reports', + }; + return pkgJson; + }); +} + +function addScriptsToPackageJson(tree: Tree) { + 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/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'] = + '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) { + 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, 'tsconfig.app.json', (json) => { + json.files = ['src/main.tsx', 'src/bootstrap.ts']; + json.compilerOptions = json.compilerOptions ?? {}; + json.compilerOptions.jsx = 'react-jsx'; + json.compilerOptions.resolveJsonModule = true; + 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..39b84366 --- /dev/null +++ b/nx-plugin-react/src/generators/react/schema.d.ts @@ -0,0 +1,6 @@ +export interface ReactGeneratorSchema { + name: string; + chatty?: boolean; + styles?: 'primeflex' | 'tailwind'; + 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 new file mode 100644 index 00000000..b6713d85 --- /dev/null +++ b/nx-plugin-react/src/generators/react/schema.json @@ -0,0 +1,48 @@ +{ + "$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?" + }, + "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" } + ] + } + }, + "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 new file mode 100644 index 00000000..45ec553c --- /dev/null +++ b/nx-plugin-react/src/generators/react/steps/ai.step.ts @@ -0,0 +1,57 @@ +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 { + const tool = options.aiTool ?? 'none'; + if (tool === 'agents' || tool === 'both') { + 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( + tree, + joinPathFragments(__dirname, '../files-ai-copilot'), + '.', + { + ...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); + } + } + + getTitle(): string { + return 'Adding AI agent configuration files'; + } + + isApplicable(options: ReactGeneratorSchema): boolean { + return options.aiTool !== 'none' && options.aiTool !== undefined; + } +} 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..0ac30c4b --- /dev/null +++ b/nx-plugin-react/src/generators/react/steps/general-openapi.step.ts @@ -0,0 +1,40 @@ +import { Tree, joinPathFragments } from '@nx/devkit'; +import { + GeneratorStep, + GeneratorStepError, +} 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' + ); + + if (!bffOpenApiContent) { + throw new GeneratorStepError( + `OpenAPI file not found at ${openApiFolderPath}/${bffOpenApiPath} – skipping OpenAPI step.` + ); + } + + 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/react/steps/styles.step.ts b/nx-plugin-react/src/generators/react/steps/styles.step.ts new file mode 100644 index 00000000..541197a3 --- /dev/null +++ b/nx-plugin-react/src/generators/react/steps/styles.step.ts @@ -0,0 +1,19 @@ +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, + }); + } + + getTitle(): string { + return 'Adding styles'; + } +} 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..717aa4bf --- /dev/null +++ b/nx-plugin-react/src/generators/shared/generator.utils.ts @@ -0,0 +1,108 @@ +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; + isApplicable?(options: T): boolean; +} + +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 (step.isApplicable && !step.isApplicable(options)) { + continue; + } + 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..92206a11 --- /dev/null +++ b/nx-plugin-react/src/generators/shared/openapi/openapi.utils.ts @@ -0,0 +1,211 @@ +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) => (item as Record)['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..faa12d3b --- /dev/null +++ b/nx-plugin-react/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + "module": "commonjs", + "lib": ["ES2021"] + }, + "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..7276ef2d 100644 --- a/package.json +++ b/package.json @@ -10,10 +10,12 @@ "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", + "@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"] }