diff --git a/.github/workflows/it-tests.yml b/.github/workflows/it-tests.yml index af437d20f9..f8a6beb075 100644 --- a/.github/workflows/it-tests.yml +++ b/.github/workflows/it-tests.yml @@ -52,13 +52,12 @@ jobs: matrix: os: [ubuntu-latest, windows-latest] packageManager: [yarn, npm] - exclude: - - os: windows-latest - packageManager: yarn + testEnvironment: [o3r-project-with-app] runs-on: ${{ matrix.os }} env: NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }} ENFORCED_PACKAGE_MANAGER: ${{ matrix.packageManager }} + PREPARE_TEST_ENV_TYPE: ${{ matrix.testEnvironment }} steps: - uses: actions/checkout@v4 - uses: ./tools/github-actions/download-build-output diff --git a/.vscode/settings.json b/.vscode/settings.json index 8854d1e802..1cb24b5faf 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -24,23 +24,19 @@ "typescript.enablePromptUseWorkspaceTsdk": true, "typescript.preferences.preferTypeOnlyAutoImports": true, "jest.jestCommandLine": "yarn jest", + "jest.debugMode": true, "jest.runMode": "on-save", - "jest.outputConfig": { - "revealOn": "demand", - "revealWithFocus": "test-results", - "clearOnRun": "none" - }, "testing.openTesting": "neverOpen", "explorer.fileNesting.enabled": true, "explorer.fileNesting.patterns": { "*.js": "${capture}.js.map, ${capture}.d.ts, ${capture}.d.ts.map", "tsconfig.json": "tsconfig.*.json", "tsconfig.base.json": "tsconfig.*.json", - "package.json": "ng-package.json, project.json, yarn.lock", + "package.json": "ng-package.json, project.json, yarn.lock, .yarnrc.yml, .npmrc, .npmrc.*, .pnp.*", "nx.json": ".nxignore", - ".yarnrc.yml": ".npmrc, .npmrc.*", ".eslintrc.*": ".eslintignore, .eslintrc-*", - "jest.config.js": "jest.config.*" + "jest.config.js": "jest.config.*", + "Dockerfile*": ".dockerignore" }, "stylelint.validate": [ "css", diff --git a/apps/showcase/project.json b/apps/showcase/project.json index c3a99881ac..b65dec041e 100644 --- a/apps/showcase/project.json +++ b/apps/showcase/project.json @@ -207,6 +207,7 @@ ] }, "generate-dark-theme": { + "cache": true, "executor": "@o3r/design:generate-css", "outputs": [ "{options.defaultStyleFile}", @@ -236,6 +237,7 @@ ] }, "generate-horizon-theme": { + "cache": true, "executor": "@o3r/design:generate-css", "outputs": [ "{options.defaultStyleFile}", diff --git a/nx.json b/nx.json index 922e6588bf..25b87b66e8 100644 --- a/nx.json +++ b/nx.json @@ -73,11 +73,21 @@ ], "specs": [ "default", + "{projectRoot}/**/*.spec.ts", "{projectRoot}/jest.config.*", "{projectRoot}/testing/**/*", "{projectRoot}/mocks/**/*", "{workspaceRoot}/jest.config.js" ], + "integration-test": [ + "default", + "{workspaceRoot}/jest.config.it.js", + "{workspaceRoot}/packages/*/create/src/*.ts", + "{workspaceRoot}/packages/@o3r/create/src/*.ts", + "!{workspaceRoot}/packages/@o3r/create/src/*.spec.ts", + "{workspaceRoot}/packages/@o3r/test-helpers/src/**/*.ts", + "!{workspaceRoot}/packages/@o3r/test-helpers/src/**/*.spec.ts" + ], "source": [ "default", "!{projectRoot}/**/*.spec.ts", @@ -132,15 +142,21 @@ { "env": "ENFORCED_PACKAGE_MANAGER" }, + { + "env": "PREPARE_TEST_ENV_TYPE" + }, "default", "schematics", "^schematics", "^source", "specs", - "{workspaceRoot}/jest.config.it.js" + "integration-test" + ], + "outputs": [ + "{projectRoot}/dist-test/it-report.xml" ], - "outputs": ["{projectRoot}/dist-test/it-report.xml"], "options": { + "quiet": false, "passWithNoTests": false }, "configurations": { diff --git a/package.json b/package.json index d0e173c1fe..4e737be9f3 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "build:tools": "yarn nx run-many --target=build --projects=eslint-plugin,workspace --parallel $(yarn get:cpus-number)", "build:lint": "yarn nx run-many --target=build --projects=eslint-plugin --parallel $(yarn get:cpus-number)", "build:swagger-gen": "yarn nx run-many --target=build-swagger --parallel $(yarn get:cpus-number)", - "prepare:publish": "prepare-publish $(yarn workspaces:list | shx sed \"s/^(.+)\\$/\\$1\\/dist/\")", + "prepare:publish": "prepare-publish $(yarn workspaces:list | yarn node -e 'let data=\"\"; process.openStdin().on(\"data\", (c) => data+=c).on(\"end\", () => process.stdout.write(data.split(/[\\n\\r]+/).slice(0, -1).map((l) => l + \"/dist\").join(require(\"os\").EOL)));')", "publish": "yarn run prepare:publish && yarn nx run-many --target=publish --parallel $(yarn get:cpus-number) --nx-bail", "publish:extensions": "yarn nx run-many --target=publish-extension --parallel $(yarn get:cpus-number)", "publish:extensions:affected": "yarn nx affected --target=publish-extension --parallel $(yarn get:cpus-number)", @@ -23,7 +23,7 @@ "test": "yarn nx run-many --target=test --parallel $(yarn get:cpus-number) --cacheDirectory=$(yarn get:current-dir)/.cache/jest", "test:affected": "yarn nx affected --target=test --parallel $(yarn get:cpus-number) --cacheDirectory=$(yarn get:current-dir)/.cache/jest", "test-e2e": "yarn nx run-many --target=test-e2e --parallel $(yarn get:cpus-number)", - "test-int": "yarn nx run-many --target=test-int --parallel 2", + "test-int": "yarn nx run-many --target=test-int --parallel $(yarn get:cpus-number)", "postinstall": "husky install && yarn build:lint && yarn harmonize:version && yarn update-yarn-sdks", "update-yarn-sdks": "node -e \"'pnp' !== '$(yarn config get nodeLinker)' || process.exit(1)\" || yarn dlx @yarnpkg/sdks", "build:storybook": "yarn doc:generate:json && yarn ng run storybook:extract-style && build-storybook", @@ -36,15 +36,16 @@ "doc:generate:json": "yarn update-doc-summary ./docs && yarn compodoc -e json -d .", "start:modules": "yarn run build:dev:modules && yarn run watch:modules", "storybook": "yarn doc:generate:json && yarn ng run storybook:extract-style && start-storybook -p 6006", - "verdaccio:start": "docker run -d -it --rm --name verdaccio -p 4873:4873 -v \"$(shx pwd)/.verdaccio/conf\":/verdaccio/conf verdaccio/verdaccio", - "verdaccio:start-persistent": "docker run -d -it --rm --name verdaccio -p 4873:4873 -v \"$(shx pwd)/.verdaccio/conf\":/verdaccio/conf -v \"$(shx pwd)/.verdaccio/storage\":/verdaccio/storage:z verdaccio/verdaccio", + "verdaccio:start": "docker run -d -it --rm --name verdaccio -p 4873:4873 -v \"$(yarn get:current-dir)/.verdaccio/conf\":/verdaccio/conf verdaccio/verdaccio", + "verdaccio:start-local": "npx --yes verdaccio --config \"$(yarn get:current-dir)/.verdaccio/conf/config-without-docker.yaml\" --listen http://127.0.0.1:4873", + "verdaccio:start-persistent": "docker run -d -it --rm --name verdaccio -p 4873:4873 -v \"$(yarn get:current-dir)/.verdaccio/conf\":/verdaccio/conf -v \"$(yarn get:current-dir)/.verdaccio/storage\":/verdaccio/storage:z verdaccio/verdaccio", "verdaccio:clean": "rimraf -g \".verdaccio/storage/@{o3r,ama-sdk,ama-terasu}\"", "verdaccio:login": "yarn cpy --cwd=./.verdaccio/conf .npmrc . --rename=.npmrc-logged && npx --yes npm-cli-login -u verdaccio -p verdaccio -e test@test.com -r http://127.0.0.1:4873 --config-path \".verdaccio/conf/.npmrc-logged\"", "verdaccio:publish": "yarn verdaccio:clean && yarn set:version 999.0.0 --include \"!**/!(dist)/package.json\" --include !package.json && yarn verdaccio:login && yarn run publish --userconfig \".verdaccio/conf/.npmrc-logged\" --tag=latest --@o3r:registry=http://127.0.0.1:4873 --@ama-sdk:registry=http://127.0.0.1:4873 --@ama-terasu:registry=http://127.0.0.1:4873", "verdaccio:stop": "docker container stop $(docker ps -a -q --filter=\"name=verdaccio\")", "verdaccio:all": "yarn verdaccio:stop && yarn verdaccio:start && yarn verdaccio:publish", "watch:vscode-extension": "yarn nx run vscode-extension:compile:watch", - "workspaces:list": "yarn workspaces list --no-private --json | shx sed \"s/.*\\\"location\\\":\\\"(.*?)\\\".*/\\$1/\"" + "workspaces:list": "yarn workspaces list --no-private --json | yarn node -e 'let data=\"\"; process.openStdin().on(\"data\", (c) => data+=c).on(\"end\", () => process.stdout.write(data.split(/[\\n\\r]+/).slice(0, -1).map((l) => JSON.parse(l).location).join(require(\"os\").EOL)));'" }, "lint-staged": { "*.ts": [ @@ -240,7 +241,6 @@ "sass": "~1.71.0", "sass-loader": "^14.0.0", "semver": "^7.5.2", - "shx": "^0.3.4", "standard-version": "^9.0.0", "stylelint": "^16.0.2", "stylelint-scss": "^6.0.0", diff --git a/packages/@ama-sdk/create/project.json b/packages/@ama-sdk/create/project.json index c7327edff0..3c6b7d4e34 100644 --- a/packages/@ama-sdk/create/project.json +++ b/packages/@ama-sdk/create/project.json @@ -39,8 +39,7 @@ "test-int": { "executor": "@nx/jest:jest", "options": { - "jestConfig": "packages/@ama-sdk/create/testing/jest.config.it.js", - "silent": false + "jestConfig": "packages/@ama-sdk/create/testing/jest.config.it.js" } }, "publish": { diff --git a/packages/@ama-sdk/create/src/index.it.spec.ts b/packages/@ama-sdk/create/src/index.it.spec.ts index fa9a9caa8d..575d712a5d 100644 --- a/packages/@ama-sdk/create/src/index.it.spec.ts +++ b/packages/@ama-sdk/create/src/index.it.spec.ts @@ -13,7 +13,7 @@ import * as fs from 'node:fs'; import { cpSync, mkdirSync } from 'node:fs'; import * as path from 'node:path'; -const appName = 'test-sdk'; +const projectName = 'test-sdk'; const sdkPackageName = '@my-test/sdk'; let sdkFolderPath: string; let sdkPackagePath: string; @@ -25,7 +25,7 @@ describe('Create new sdk command', () => { beforeEach(async () => { const isYarnTest = packageManager.startsWith('yarn'); const yarnVersion = isYarnTest ? getYarnVersionFromRoot(process.cwd()) || 'latest' : undefined; - sdkFolderPath = await prepareTestEnv(appName, 'blank', yarnVersion); + sdkFolderPath = (await prepareTestEnv(projectName, {type: 'blank', yarnVersion })).workspacePath; sdkPackagePath = path.join(sdkFolderPath, sdkPackageName.replace(/^@/, '')); execAppOptions.cwd = sdkFolderPath; @@ -51,19 +51,20 @@ describe('Create new sdk command', () => { test('should generate a full SDK when the specification is provided', () => { expect(() => - packageManagerCreate(`@ama-sdk typescript ${sdkPackageName} --package-manager ${packageManager} --spec-path ./swagger-spec.yml`, execAppOptions)).not.toThrow(); - expect(() => packageManagerRun('build', { ...execAppOptions, cwd: sdkPackagePath })).not.toThrow(); + packageManagerCreate({script: '@ama-sdk', args: ['typescript', sdkPackageName, '--package-manager', packageManager, '--spec-path', './swagger-spec.yml']}, execAppOptions)).not.toThrow(); + expect(() => packageManagerRun({script: 'build'}, { ...execAppOptions, cwd: sdkPackagePath })).not.toThrow(); }); test('should generate an empty SDK ready to be used', () => { - expect(() => packageManagerCreate(`@ama-sdk typescript ${sdkPackageName}`, execAppOptions)).not.toThrow(); - expect(() => packageManagerRun('build', { ...execAppOptions, cwd: sdkPackagePath })).not.toThrow(); + expect(() => packageManagerCreate({script: '@ama-sdk', args: ['typescript', sdkPackageName]}, execAppOptions)).not.toThrow(); + expect(() => packageManagerRun({script: 'build'}, { ...execAppOptions, cwd: sdkPackagePath })).not.toThrow(); expect(() => - packageManagerExec( - `schematics @ama-sdk/schematics:typescript-core --spec-path ${path.join(path.relative(sdkPackagePath, sdkFolderPath), 'swagger-spec.yml')}`, - { ...execAppOptions, cwd: sdkPackagePath } + packageManagerExec({ + script: 'schematics', + args: ['@ama-sdk/schematics:typescript-core', '--spec-path', path.join(path.relative(sdkPackagePath, sdkFolderPath), 'swagger-spec.yml')] + }, { ...execAppOptions, cwd: sdkPackagePath } )).not.toThrow(); - expect(() => packageManagerRun('build', { ...execAppOptions, cwd: sdkPackagePath })).not.toThrow(); - expect(() => packageManagerRun('doc:generate', { ...execAppOptions, cwd: sdkPackagePath })).not.toThrow(); + expect(() => packageManagerRun({script: 'build'}, { ...execAppOptions, cwd: sdkPackagePath })).not.toThrow(); + expect(() => packageManagerRun({ script: 'doc:generate'}, { ...execAppOptions, cwd: sdkPackagePath })).not.toThrow(); }); }); diff --git a/packages/@ama-sdk/create/src/index.ts b/packages/@ama-sdk/create/src/index.ts index 59c1473b36..f94f28a3c2 100644 --- a/packages/@ama-sdk/create/src/index.ts +++ b/packages/@ama-sdk/create/src/index.ts @@ -79,11 +79,12 @@ const getSchematicStepInfo = (schematic: string) => ({ const run = () => { + const runner = process.platform === 'win32' ? `${packageManager}.cmd` : packageManager; const steps: { args: string[]; cwd?: string; runner?: string }[] = [ getSchematicStepInfo(schematicsToRun[0]), ...( packageManager === 'yarn' - ? [{ runner: 'yarn', args: ['set', 'version', getYarnVersion()], cwd: resolve(process.cwd(), targetDirectory)}] + ? [{ runner, args: ['set', 'version', getYarnVersion()], cwd: resolve(process.cwd(), targetDirectory)}] : [] ), ...schematicsToRun.slice(1).map(getSchematicStepInfo) diff --git a/packages/@ama-sdk/schematics/project.json b/packages/@ama-sdk/schematics/project.json index 198d5c8aa5..02f794b2cf 100644 --- a/packages/@ama-sdk/schematics/project.json +++ b/packages/@ama-sdk/schematics/project.json @@ -60,7 +60,6 @@ "executor": "@nx/jest:jest", "options": { "jestConfig": "packages/@ama-sdk/schematics/testing/jest.config.it.js", - "silent": false, "passWithNoTests": true } }, diff --git a/packages/@o3r/analytics/project.json b/packages/@o3r/analytics/project.json index 3c1ab3d96c..031fe7d23c 100644 --- a/packages/@o3r/analytics/project.json +++ b/packages/@o3r/analytics/project.json @@ -77,8 +77,7 @@ "test-int": { "executor": "@nx/jest:jest", "options": { - "jestConfig": "packages/@o3r/analytics/testing/jest.config.it.js", - "silent": false + "jestConfig": "packages/@o3r/analytics/testing/jest.config.it.js" } }, "prepare-publish": { diff --git a/packages/@o3r/analytics/schematics/index.it.spec.ts b/packages/@o3r/analytics/schematics/index.it.spec.ts index b25079df90..60bc1bad5b 100644 --- a/packages/@o3r/analytics/schematics/index.it.spec.ts +++ b/packages/@o3r/analytics/schematics/index.it.spec.ts @@ -2,70 +2,52 @@ import { addImportToAppModule, getDefaultExecSyncOptions, getGitDiff, - packageManagerAdd, packageManagerExec, packageManagerInstall, - packageManagerRun, + packageManagerRunOnProject, prepareTestEnv, setupLocalRegistry } from '@o3r/test-helpers'; -import { join } from 'node:path'; +import { rm } from 'node:fs/promises'; +import * as path from 'node:path'; -const appName = 'test-app-analytics'; +const appFolder = 'test-app-analytics'; const o3rVersion = '999.0.0'; const execAppOptions = getDefaultExecSyncOptions(); -let appFolderPath: string; - +let projectPath: string; +let isInWorkspace: boolean; +let workspacePath: string; +let projectName: string; +let untouchedProjectPath: undefined | string; describe('new otter application with analytics', () => { setupLocalRegistry(); - describe('standalone', () => { - beforeAll(async () => { - appFolderPath = await prepareTestEnv(appName, 'angular-with-o3r-core'); - execAppOptions.cwd = appFolderPath; - }); - test('should add analytics to existing application', async () => { - packageManagerExec(`ng add --skip-confirmation @o3r/analytics@${o3rVersion}`, execAppOptions); - - packageManagerExec('ng g @o3r/core:component test-component --use-otter-analytics=false', execAppOptions); - packageManagerExec('ng g @o3r/analytics:add-analytics --path="src/components/test-component/test-component.component.ts"', execAppOptions); - await addImportToAppModule(appFolderPath, 'TestComponentModule', 'src/components/test-component'); - - const diff = getGitDiff(appFolderPath); - expect(diff.modified).toContain('package.json'); - expect(diff.added).toContain('src/components/test-component/test-component.analytics.ts'); - - expect(() => packageManagerInstall(execAppOptions)).not.toThrow(); - expect(() => packageManagerRun('build', execAppOptions)).not.toThrow(); - }); + beforeAll(async () => { + ({ projectPath, workspacePath, projectName, isInWorkspace, untouchedProjectPath } = await prepareTestEnv(appFolder)); + execAppOptions.cwd = workspacePath; }); - - describe('monorepo', () => { - beforeAll(async () => { - const workspacePath = await prepareTestEnv(`${appName}-monorepo`, 'angular-monorepo-with-o3r-core'); - appFolderPath = join(workspacePath, 'projects', 'test-app'); - execAppOptions.cwd = workspacePath; - }); - test('should add analytics to existing application', () => { - // FIXME workaround for pnp - packageManagerAdd(`@o3r/analytics@${o3rVersion}`, {...execAppOptions, cwd: appFolderPath}); - - const projectName = '--project-name=test-app'; - packageManagerExec(`ng add --skip-confirmation @o3r/analytics@${o3rVersion} ${projectName}`, execAppOptions); - - packageManagerExec(`ng g @o3r/core:component test-component --use-otter-analytics=false ${projectName}`, execAppOptions); - packageManagerExec( - 'ng g @o3r/analytics:add-analytics --path="projects/test-app/src/components/test-component/test-component.component.ts"', - execAppOptions - ); - addImportToAppModule(appFolderPath, 'TestComponentModule', 'src/components/test-component'); - - const diff = getGitDiff(execAppOptions.cwd as string); - expect(diff.all.some((file) => /projects[\\/]dont-modify-me/.test(file))).toBe(false); - expect(diff.modified).toContain('projects/test-app/package.json'); - expect(diff.added).toContain('projects/test-app/src/components/test-component/test-component.analytics.ts'); - - expect(() => packageManagerInstall(execAppOptions)).not.toThrow(); - expect(() => packageManagerRun('build', execAppOptions)).not.toThrow(); - }); + afterAll(async () => { + try { await rm(workspacePath, { recursive: true }); } catch { /* ignore error */ } + }); + test('should add analytics to existing application', () => { + const relativeProjectPath = path.relative(workspacePath, projectPath); + packageManagerExec({script: 'ng', args: ['add', `@o3r/analytics@${o3rVersion}`, '--project-name', projectName, '--skip-confirmation']}, execAppOptions); + + packageManagerExec({script: 'ng', args: ['g', '@o3r/core:component', 'test-component', '--use-otter-analytics', 'false', '--project-name', projectName]}, execAppOptions); + const componentPath = path.normalize(path.join(relativeProjectPath, 'src/components/test-component/test-component.component.ts')); + packageManagerExec({script: 'ng', args: ['g', '@o3r/analytics:add-analytics', '--path', componentPath]}, execAppOptions); + addImportToAppModule(projectPath, 'TestComponentModule', 'src/components/test-component'); + + const diff = getGitDiff(workspacePath); + expect(diff.all.some((file) => /projects[\\/]dont-modify-me/.test(file))).toBe(false); + expect(diff.modified).toContain(path.join(relativeProjectPath, 'package.json').replace(/[\\/]+/g, '/')); + expect(diff.added).toContain(path.join(relativeProjectPath, 'src/components/test-component/test-component.analytics.ts').replace(/[\\/]+/g, '/')); + + if (untouchedProjectPath) { + const relativeUntouchedProjectPath = path.relative(workspacePath, untouchedProjectPath); + expect(diff.all.filter((file) => new RegExp(relativeUntouchedProjectPath.replace(/[\\/]+/g, '[\\\\/]')).test(file)).length).toBe(0); + } + + expect(() => packageManagerInstall(execAppOptions)).not.toThrow(); + expect(() => packageManagerRunOnProject(projectName, isInWorkspace, {script: 'build'}, execAppOptions)).not.toThrow(); }); }); diff --git a/packages/@o3r/analytics/schematics/ng-add/index.ts b/packages/@o3r/analytics/schematics/ng-add/index.ts index b981fa8932..f7bec7701a 100644 --- a/packages/@o3r/analytics/schematics/ng-add/index.ts +++ b/packages/@o3r/analytics/schematics/ng-add/index.ts @@ -1,12 +1,22 @@ import type { Rule } from '@angular-devkit/schematics'; -import { createSchematicWithMetricsIfInstalled } from '@o3r/schematics'; +import { createSchematicWithMetricsIfInstalled, getPackageInstallConfig, setupDependencies } from '@o3r/schematics'; +import type { NgAddSchematicsSchema } from './schema'; +import * as path from 'node:path'; + +const packageJsonPath = path.resolve(__dirname, '..', '..', 'package.json'); /** * Add Otter analytics to an Angular Project + * @param options */ -function ngAddFn(): Rule { +function ngAddFn(options: NgAddSchematicsSchema): Rule { /* ng add rules */ - return () => {}; + return (tree) => { + return setupDependencies({ + projectName: options.projectName, + dependencies: getPackageInstallConfig(packageJsonPath, tree, options.projectName) + }); + }; } /** diff --git a/packages/@o3r/apis-manager/project.json b/packages/@o3r/apis-manager/project.json index e2b2aec2fd..e799e316fa 100644 --- a/packages/@o3r/apis-manager/project.json +++ b/packages/@o3r/apis-manager/project.json @@ -46,8 +46,7 @@ "test-int": { "executor": "@nx/jest:jest", "options": { - "jestConfig": "packages/@o3r/apis-manager/testing/jest.config.it.js", - "silent": false + "jestConfig": "packages/@o3r/apis-manager/testing/jest.config.it.js" } }, "prepare-publish": { diff --git a/packages/@o3r/apis-manager/schematics/index.it.spec.ts b/packages/@o3r/apis-manager/schematics/index.it.spec.ts index 888d78d171..7b41fdd4fb 100644 --- a/packages/@o3r/apis-manager/schematics/index.it.spec.ts +++ b/packages/@o3r/apis-manager/schematics/index.it.spec.ts @@ -3,29 +3,40 @@ import { getGitDiff, packageManagerExec, packageManagerInstall, - packageManagerRun, + packageManagerRunOnProject, prepareTestEnv, setupLocalRegistry } from '@o3r/test-helpers'; +import * as path from 'node:path'; +import { rm } from 'node:fs/promises'; -const appName = 'test-app-apis-manager'; +const appFolder = 'test-app-apis-manager'; const o3rVersion = '999.0.0'; const execAppOptions = getDefaultExecSyncOptions(); -let appFolderPath: string; - +let workspacePath: string; +let projectName: string; +let isInWorkspace: boolean; +let untouchedProjectPath: undefined | string; describe('new otter application with apis-manager', () => { setupLocalRegistry(); beforeAll(async () => { - appFolderPath = await prepareTestEnv(appName, 'angular-with-o3r-core'); - execAppOptions.cwd = appFolderPath; + ({ workspacePath, projectName, isInWorkspace, untouchedProjectPath } = await prepareTestEnv(appFolder)); + execAppOptions.cwd = workspacePath; + }); + afterAll(async () => { + try { await rm(workspacePath, { recursive: true }); } catch { /* ignore error */ } }); test('should add apis-manager to existing application', () => { - packageManagerExec(`ng add --skip-confirmation @o3r/apis-manager@${o3rVersion}`, execAppOptions); - - const diff = getGitDiff(appFolderPath); - expect(diff.modified).toContain('package.json'); + packageManagerExec({script: 'ng', args: ['add', `@o3r/apis-manager@${o3rVersion}`, '--skip-confirmation', '--project-name', projectName]}, execAppOptions); expect(() => packageManagerInstall(execAppOptions)).not.toThrow(); - expect(() => packageManagerRun('build', execAppOptions)).not.toThrow(); + expect(() => packageManagerRunOnProject(projectName, isInWorkspace, {script: 'build'}, execAppOptions)).not.toThrow(); + + const diff = getGitDiff(workspacePath); + expect(diff.modified).toContain('package.json'); + if (untouchedProjectPath) { + const relativeUntouchedProjectPath = path.relative(workspacePath, untouchedProjectPath); + expect(diff.all.filter((file) => new RegExp(relativeUntouchedProjectPath.replace(/[\\/]+/g, '[\\\\/]')).test(file)).length).toBe(0); + } }); }); diff --git a/packages/@o3r/apis-manager/schematics/ng-add/index.ts b/packages/@o3r/apis-manager/schematics/ng-add/index.ts index fb65919ff5..fa5fb6d3dd 100644 --- a/packages/@o3r/apis-manager/schematics/ng-add/index.ts +++ b/packages/@o3r/apis-manager/schematics/ng-add/index.ts @@ -1,5 +1,5 @@ import { chain, noop, Rule, SchematicContext, Tree } from '@angular-devkit/schematics'; -import { createSchematicWithMetricsIfInstalled } from '@o3r/schematics'; +import { createSchematicWithMetricsIfInstalled, getPackageInstallConfig } from '@o3r/schematics'; import * as path from 'node:path'; import type { NgAddSchematicsSchema } from './schema'; @@ -10,27 +10,34 @@ import type { NgAddSchematicsSchema } from './schema'; function ngAddFn(options: NgAddSchematicsSchema): Rule { return async (tree: Tree, context: SchematicContext) => { try { - const { ngAddPackages, getO3rPeerDeps, applyEsLintFix, getWorkspaceConfig, getProjectNewDependenciesType } = await import('@o3r/schematics'); + const { setupDependencies, getO3rPeerDeps, applyEsLintFix, getWorkspaceConfig, getProjectNewDependenciesTypes } = await import('@o3r/schematics'); const { updateApiDependencies } = await import('../helpers/update-api-deps'); - const depsInfo = getO3rPeerDeps(path.resolve(__dirname, '..', '..', 'package.json')); + const packageJsonPath = path.resolve(__dirname, '..', '..', 'package.json'); + const depsInfo = getO3rPeerDeps(packageJsonPath); const rulesToExecute: Rule[] = []; const workspaceProject = options.projectName ? getWorkspaceConfig(tree)?.projects[options.projectName] : undefined; - const workingDirectory = workspaceProject?.root; const projectType = workspaceProject?.projectType || 'application'; if (projectType === 'application') { rulesToExecute.push(updateApiDependencies(options)); } + const dependencies = depsInfo.o3rPeerDeps.reduce((acc, dep) => { + acc[dep] = { + inManifest: [{ + range: `~${depsInfo.packageVersion}`, + types: getProjectNewDependenciesTypes(workspaceProject) + }] + }; + return acc; + }, getPackageInstallConfig(packageJsonPath, tree, options.projectName)); + return () => chain([ ...rulesToExecute, options.skipLinter ? noop : applyEsLintFix(), - ngAddPackages(depsInfo.o3rPeerDeps, { - skipConfirmation: true, - version: depsInfo.packageVersion, - parentPackageInfo: depsInfo.packageName, + setupDependencies({ projectName: options.projectName, - dependencyType: getProjectNewDependenciesType(workspaceProject), - workingDirectory + dependencies, + ngAddToRun: depsInfo.o3rPeerDeps }) ])(tree, context); diff --git a/packages/@o3r/application/schematics/ng-add/index.ts b/packages/@o3r/application/schematics/ng-add/index.ts index 23f9525574..e444bf07c2 100644 --- a/packages/@o3r/application/schematics/ng-add/index.ts +++ b/packages/@o3r/application/schematics/ng-add/index.ts @@ -1,6 +1,6 @@ import type { Rule, SchematicContext, Tree } from '@angular-devkit/schematics'; import { chain } from '@angular-devkit/schematics'; -import { createSchematicWithMetricsIfInstalled } from '@o3r/schematics'; +import { createSchematicWithMetricsIfInstalled, getPackageInstallConfig } from '@o3r/schematics'; import * as path from 'node:path'; import type { NgAddSchematicsSchema } from './schema'; import { registerDevtools } from './helpers/devtools-registration'; @@ -15,14 +15,14 @@ function ngAddFn(options: NgAddSchematicsSchema): Rule { return async (tree: Tree, context: SchematicContext) => { try { const { - addImportToModuleFile, getAppModuleFilePath, getModuleIndex, getWorkspaceConfig, insertImportToModuleFile, ngAddPackages, getO3rPeerDeps, getProjectNewDependenciesType + addImportToModuleFile, getAppModuleFilePath, getModuleIndex, getWorkspaceConfig, insertImportToModuleFile, setupDependencies, getO3rPeerDeps, getProjectNewDependenciesTypes } = await import('@o3r/schematics'); const { isImported } = await import('@schematics/angular/utility/ast-utils'); const ts = await import('typescript'); - const depsInfo = getO3rPeerDeps(path.resolve(__dirname, '..', '..', 'package.json')); + const packageJsonPath = path.resolve(__dirname, '..', '..', 'package.json'); + const depsInfo = getO3rPeerDeps(packageJsonPath); const workspaceProject = options.projectName ? getWorkspaceConfig(tree)?.projects[options.projectName] : undefined; - const workingDirectory = workspaceProject?.root; const addAngularAnimationPreferences: Rule = () => { const moduleFilePath = getAppModuleFilePath(tree, context, options.projectName); @@ -69,17 +69,22 @@ function ngAddFn(options: NgAddSchematicsSchema): Rule { tree.commitUpdate(recorder); return tree; }; - const dependencyType = getProjectNewDependenciesType(workspaceProject); + const dependencies = depsInfo.o3rPeerDeps.reduce((acc, dep) => { + acc[dep] = { + inManifest: [{ + range: `^${depsInfo.packageVersion}`, + types: getProjectNewDependenciesTypes(workspaceProject) + }] + }; + return acc; + }, getPackageInstallConfig(packageJsonPath, tree, options.projectName)); const registerDevtoolRule = await registerDevtools(options); return () => chain([ - ngAddPackages(depsInfo.o3rPeerDeps, { - skipConfirmation: true, - version: depsInfo.packageVersion, - parentPackageInfo: depsInfo.packageName, + setupDependencies({ projectName: options.projectName, - dependencyType, - workingDirectory + dependencies, + ngAddToRun: depsInfo.o3rPeerDeps }), addAngularAnimationPreferences, registerDevtoolRule, diff --git a/packages/@o3r/components/project.json b/packages/@o3r/components/project.json index e3b44ec98e..2b4b525977 100644 --- a/packages/@o3r/components/project.json +++ b/packages/@o3r/components/project.json @@ -67,8 +67,7 @@ "test-int": { "executor": "@nx/jest:jest", "options": { - "jestConfig": "packages/@o3r/components/testing/jest.config.it.js", - "silent": false + "jestConfig": "packages/@o3r/components/testing/jest.config.it.js" } }, "prepare-publish": { diff --git a/packages/@o3r/components/schematics/index.it.spec.ts b/packages/@o3r/components/schematics/index.it.spec.ts index 241fd29133..5af250f483 100644 --- a/packages/@o3r/components/schematics/index.it.spec.ts +++ b/packages/@o3r/components/schematics/index.it.spec.ts @@ -3,29 +3,41 @@ import { getGitDiff, packageManagerExec, packageManagerInstall, - packageManagerRun, + packageManagerRunOnProject, prepareTestEnv, setupLocalRegistry } from '@o3r/test-helpers'; +import * as path from 'node:path'; +import { rm } from 'node:fs/promises'; -const appName = 'test-app-components'; +const appFolder = 'test-app-components'; const o3rVersion = '999.0.0'; const execAppOptions = getDefaultExecSyncOptions(); -let appFolderPath: string; - +let workspacePath: string; +let projectName: string; +let isInWorkspace: boolean; +let untouchedProjectPath: undefined | string; describe('new otter application with components', () => { setupLocalRegistry(); beforeAll(async () => { - appFolderPath = await prepareTestEnv(appName, 'angular-with-o3r-core'); - execAppOptions.cwd = appFolderPath; + ({ workspacePath, projectName, isInWorkspace, untouchedProjectPath } = await prepareTestEnv(appFolder)); + execAppOptions.cwd = workspacePath; + }); + afterAll(async () => { + try { await rm(workspacePath, { recursive: true }); } catch { /* ignore error */ } }); test('should add components to existing application', () => { - packageManagerExec(`ng add --skip-confirmation @o3r/components@${o3rVersion} --enable-metadata-extract`, execAppOptions); + packageManagerExec({script: 'ng', args: ['add', `@o3r/components@${o3rVersion}`, '--skip-confirmation', '--enable-metadata-extract', '--project-name', projectName]}, execAppOptions); - const diff = getGitDiff(appFolderPath); + expect(() => packageManagerInstall(execAppOptions)).not.toThrow(); + expect(() => packageManagerRunOnProject(projectName, isInWorkspace, {script: 'build'}, execAppOptions)).not.toThrow(); + + const diff = getGitDiff(workspacePath); expect(diff.modified).toContain('package.json'); - expect(() => packageManagerInstall(execAppOptions)).not.toThrow(); - expect(() => packageManagerRun('build', execAppOptions)).not.toThrow(); + if (untouchedProjectPath) { + const relativeUntouchedProjectPath = path.relative(workspacePath, untouchedProjectPath); + expect(diff.all.filter((file) => new RegExp(relativeUntouchedProjectPath.replace(/[\\/]+/g, '[\\\\/]')).test(file)).length).toBe(0); + } }); }); diff --git a/packages/@o3r/components/schematics/ng-add/index.ts b/packages/@o3r/components/schematics/ng-add/index.ts index 5fc190bf91..006386e14e 100644 --- a/packages/@o3r/components/schematics/ng-add/index.ts +++ b/packages/@o3r/components/schematics/ng-add/index.ts @@ -5,6 +5,7 @@ import * as fs from 'node:fs'; import { updateCmsAdapter } from '../cms-adapter'; import type { NgAddSchematicsSchema } from './schema'; import { registerDevtools } from './helpers/devtools-registration'; +import { type DependencyToAdd, getPackageInstallConfig } from '@o3r/schematics'; /** * Add Otter components to an Angular Project @@ -17,10 +18,9 @@ function ngAddFn(options: NgAddSchematicsSchema): Rule { const { getDefaultOptionsForSchematic, getO3rPeerDeps, - getProjectNewDependenciesType, + getProjectNewDependenciesTypes, getWorkspaceConfig, - ngAddPackages, - ngAddPeerDependencyPackages, + setupDependencies, removePackages, registerPackageCollectionSchematics } = await import('@o3r/schematics'); @@ -35,19 +35,33 @@ function ngAddFn(options: NgAddSchematicsSchema): Rule { } const workspaceProject = options.projectName ? getWorkspaceConfig(tree)?.projects[options.projectName] : undefined; - const workingDirectory = workspaceProject?.root || '.'; - const dependencyType = getProjectNewDependenciesType(workspaceProject); + const dependencies = depsInfo.o3rPeerDeps.reduce((acc, dep) => { + acc[dep] = { + inManifest: [{ + range: `~${depsInfo.packageVersion}`, + types: getProjectNewDependenciesTypes(workspaceProject) + }] + }; + return acc; + }, getPackageInstallConfig(packageJsonPath, tree, options.projectName)); + const devDependencies: Record = { + chokidar: { + inManifest: [{ + range: packageJson.peerDependencies.chokidar, + types: [NodeDependencyType.Dev] + }] + } + }; const rule = chain([ removePackages(['@otter/components']), - ngAddPackages(depsInfo.o3rPeerDeps, { - skipConfirmation: true, - version: depsInfo.packageVersion, - parentPackageInfo: depsInfo.packageName, + setupDependencies({ projectName: options.projectName, - dependencyType, - workingDirectory + dependencies: { + ...dependencies, + ...devDependencies + }, + ngAddToRun: depsInfo.o3rPeerDeps }), - ngAddPeerDependencyPackages(['chokidar'], packageJsonPath, NodeDependencyType.Dev, {...options, workingDirectory, skipNgAddSchematicRun: true}, '@o3r/components - install builder dependency'), registerPackageCollectionSchematics(packageJson), ...(options.enableMetadataExtract ? [updateCmsAdapter(options)] : []), await registerDevtools(options) diff --git a/packages/@o3r/configuration/project.json b/packages/@o3r/configuration/project.json index 1e6293f710..e9a47af299 100644 --- a/packages/@o3r/configuration/project.json +++ b/packages/@o3r/configuration/project.json @@ -75,8 +75,7 @@ "test-int": { "executor": "@nx/jest:jest", "options": { - "jestConfig": "packages/@o3r/configuration/testing/jest.config.it.js", - "silent": false + "jestConfig": "packages/@o3r/configuration/testing/jest.config.it.js" } }, "prepare-publish": { diff --git a/packages/@o3r/configuration/schematics/index.it.spec.ts b/packages/@o3r/configuration/schematics/index.it.spec.ts index b646403b2f..2b3df9dde5 100644 --- a/packages/@o3r/configuration/schematics/index.it.spec.ts +++ b/packages/@o3r/configuration/schematics/index.it.spec.ts @@ -4,34 +4,49 @@ import { getGitDiff, packageManagerExec, packageManagerInstall, - packageManagerRun, + packageManagerRunOnProject, prepareTestEnv, setupLocalRegistry } from '@o3r/test-helpers'; +import { rm } from 'node:fs/promises'; +import * as path from 'node:path'; -const appName = 'test-app-configuration'; +const appFolder = 'test-app-configuration'; const o3rVersion = '999.0.0'; const execAppOptions = getDefaultExecSyncOptions(); -let appFolderPath: string; - +let projectPath: string; +let workspacePath: string; +let projectName: string; +let isInWorkspace: boolean; +let untouchedProjectPath: undefined | string; describe('new otter application with configuration', () => { setupLocalRegistry(); beforeAll(async () => { - appFolderPath = await prepareTestEnv(appName, 'angular-with-o3r-core'); - execAppOptions.cwd = appFolderPath; + ({ projectPath, workspacePath, projectName, isInWorkspace, untouchedProjectPath } = await prepareTestEnv(appFolder)); + execAppOptions.cwd = workspacePath; + }); + afterAll(async () => { + try { await rm(workspacePath, { recursive: true }); } catch { /* ignore error */ } }); test('should add configuration to existing application', async () => { - packageManagerExec(`ng add --skip-confirmation @o3r/configuration@${o3rVersion}`, execAppOptions); + const relativeProjectPath = path.relative(workspacePath, projectPath); + packageManagerExec({script: 'ng', args: ['add', `@o3r/configuration@${o3rVersion}`, '--skip-confirmation', '--project-name', projectName]}, execAppOptions); - packageManagerExec('ng g @o3r/core:component test-component --use-otter-config=false', execAppOptions); - packageManagerExec('ng g @o3r/configuration:add-config --path="src/components/test-component/test-component.component.ts"', execAppOptions); - await addImportToAppModule(appFolderPath, 'TestComponentModule', 'src/components/test-component'); + const componentPath = path.normalize(path.join(relativeProjectPath, 'src/components/test-component/test-component.component.ts')); + packageManagerExec({script: 'ng', args: ['g', '@o3r/core:component', 'test-component', '--project-name', projectName, '--use-otter-config', 'false']}, execAppOptions); + packageManagerExec({script: 'ng', args: ['g', '@o3r/configuration:add-config', '--path', componentPath]}, execAppOptions); + await addImportToAppModule(projectPath, 'TestComponentModule', 'src/components/test-component'); - const diff = getGitDiff(appFolderPath); + const diff = getGitDiff(workspacePath); expect(diff.modified).toContain('package.json'); - expect(diff.added).toContain('src/components/test-component/test-component.config.ts'); + expect(diff.added).toContain(path.join(relativeProjectPath, 'src/components/test-component/test-component.config.ts').replace(/[\\/]+/g, '/')); + + if (untouchedProjectPath) { + const relativeUntouchedProjectPath = path.relative(workspacePath, untouchedProjectPath); + expect(diff.all.filter((file) => new RegExp(relativeUntouchedProjectPath.replace(/[\\/]+/g, '[\\\\/]')).test(file)).length).toBe(0); + } expect(() => packageManagerInstall(execAppOptions)).not.toThrow(); - expect(() => packageManagerRun('build', execAppOptions)).not.toThrow(); + expect(() => packageManagerRunOnProject(projectName, isInWorkspace, {script: 'build'}, execAppOptions)).not.toThrow(); }); }); diff --git a/packages/@o3r/configuration/schematics/ng-add/index.ts b/packages/@o3r/configuration/schematics/ng-add/index.ts index 210c9c5bc3..d2dbad175a 100644 --- a/packages/@o3r/configuration/schematics/ng-add/index.ts +++ b/packages/@o3r/configuration/schematics/ng-add/index.ts @@ -4,6 +4,7 @@ import * as fs from 'node:fs'; import * as path from 'node:path'; import type { NgAddSchematicsSchema } from './schema'; import { registerDevtools } from './helpers/devtools-registration'; +import { getPackageInstallConfig } from '@o3r/schematics'; /** * Add Otter configuration to an Angular Project @@ -14,8 +15,8 @@ function ngAddFn(options: NgAddSchematicsSchema): Rule { return async (tree: Tree, context: SchematicContext): Promise => { try { const { - ngAddPackages, - getProjectNewDependenciesType, + setupDependencies, + getProjectNewDependenciesTypes, getWorkspaceConfig, getO3rPeerDeps, registerPackageCollectionSchematics, @@ -25,8 +26,15 @@ function ngAddFn(options: NgAddSchematicsSchema): Rule { const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, { encoding: 'utf-8' })); const depsInfo = getO3rPeerDeps(packageJsonPath); const workspaceProject = options.projectName ? getWorkspaceConfig(tree)?.projects[options.projectName] : undefined; - const workingDirectory = workspaceProject?.root || '.'; - const dependencyType = getProjectNewDependenciesType(workspaceProject); + const dependencies = depsInfo.o3rPeerDeps.reduce((acc, dep) => { + acc[dep] = { + inManifest: [{ + range: `~${depsInfo.packageVersion}`, + types: getProjectNewDependenciesTypes(workspaceProject) + }] + }; + return acc; + }, getPackageInstallConfig(packageJsonPath, tree, options.projectName)); context.logger.info(`The package ${depsInfo.packageName as string} comes with a debug mechanism`); context.logger.info('Get more information on the following page: https://github.com/AmadeusITGroup/otter/tree/main/docs/configuration/OVERVIEW.md#Runtime-debugging'); return () => chain([ @@ -45,13 +53,10 @@ function ngAddFn(options: NgAddSchematicsSchema): Rule { useOtterConfig: undefined } }), - ngAddPackages(depsInfo.o3rPeerDeps, { - skipConfirmation: true, - version: depsInfo.packageVersion, - parentPackageInfo: depsInfo.packageName, + setupDependencies({ projectName: options.projectName, - dependencyType, - workingDirectory + dependencies, + ngAddToRun: depsInfo.o3rPeerDeps }), () => registerDevtools(options) ])(tree, context); diff --git a/packages/@o3r/core/ng-package.json b/packages/@o3r/core/ng-package.json index 3873f7b5de..6bad05d700 100644 --- a/packages/@o3r/core/ng-package.json +++ b/packages/@o3r/core/ng-package.json @@ -6,6 +6,6 @@ "entryFile": "src/public_api.ts" }, "allowedNonPeerDependencies": [ - "chokidar", "globby", "semver", "tslib", "uuid", "@o3r/schematics" + "tslib", "uuid", "@o3r/schematics" ] } diff --git a/packages/@o3r/core/project.json b/packages/@o3r/core/project.json index 2ad6b69fdf..5d2c2c2644 100644 --- a/packages/@o3r/core/project.json +++ b/packages/@o3r/core/project.json @@ -53,8 +53,7 @@ "test-int": { "executor": "@nx/jest:jest", "options": { - "jestConfig": "packages/@o3r/core/testing/jest.config.it.js", - "silent": false + "jestConfig": "packages/@o3r/core/testing/jest.config.it.js" } }, "prepare-publish": { diff --git a/packages/@o3r/core/schematics/index.it.spec.ts b/packages/@o3r/core/schematics/index.it.spec.ts index ff4856dd11..fed8f82f98 100644 --- a/packages/@o3r/core/schematics/index.it.spec.ts +++ b/packages/@o3r/core/schematics/index.it.spec.ts @@ -3,263 +3,153 @@ import { getDefaultExecSyncOptions, getGitDiff, packageManagerExec, + packageManagerExecOnProject, packageManagerInstall, - packageManagerRun, - packageManagerWorkspaceExec, - packageManagerWorkspaceRun, + packageManagerRunOnProject, prepareTestEnv, setupLocalRegistry } from '@o3r/test-helpers'; -import { join } from 'node:path'; +import * as path from 'node:path'; import { execSync, spawn } from 'node:child_process'; import getPidFromPort from 'pid-from-port'; +import { rm } from 'node:fs/promises'; const devServerPort = 4200; -const appName = 'test-app-core'; +const appFolder = 'test-app-core'; const o3rVersion = '999.0.0'; const execAppOptions = getDefaultExecSyncOptions(); -let appFolderPath: string; - +let projectPath: string; +let workspacePath: string; +let projectName: string; +let isInWorkspace: boolean; +let untouchedProjectPath: undefined | string; describe('new otter application', () => { setupLocalRegistry(); - describe('standalone', () => { - beforeAll(async () => { - appFolderPath = await prepareTestEnv(appName, 'angular'); - execAppOptions.cwd = appFolderPath; - }); - test('should build empty app', async () => { - packageManagerExec(`ng add --skip-confirmation @o3r/core@${o3rVersion} --preset=all`, execAppOptions); - packageManagerExec(`ng add --skip-confirmation @o3r/analytics@${o3rVersion}`, execAppOptions); - expect(() => packageManagerInstall(execAppOptions)).not.toThrow(); - - packageManagerExec('ng g @o3r/core:store-entity-async --store-name="test-entity-async" --model-name="Bound" --model-id-prop-name="id"', execAppOptions); - await addImportToAppModule(appFolderPath, 'TestEntityAsyncStoreModule', 'src/store/test-entity-async'); - - packageManagerExec('ng g @o3r/core:store-entity-sync --store-name="test-entity-sync" --model-name="Bound" --model-id-prop-name="id"', execAppOptions); - await addImportToAppModule(appFolderPath, 'TestEntitySyncStoreModule', 'src/store/test-entity-sync'); - - packageManagerExec('ng g @o3r/core:store-simple-async --store-name="test-simple-async" --model-name="Bound"', execAppOptions); - await addImportToAppModule(appFolderPath, 'TestSimpleAsyncStoreModule', 'src/store/test-simple-async'); - - packageManagerExec('ng g @o3r/core:store-simple-sync --store-name="test-simple-sync"', execAppOptions); - await addImportToAppModule(appFolderPath, 'TestSimpleSyncStoreModule', 'src/store/test-simple-sync'); - - packageManagerExec('ng g @o3r/core:service test-service --feature-name="base"', execAppOptions); - await addImportToAppModule(appFolderPath, 'TestServiceBaseModule', 'src/services/test-service'); - - packageManagerExec('ng g @o3r/core:page test-page --app-routing-module-path="src/app/app-routing.module.ts"', execAppOptions); - - const defaultOptions = [ - '--activate-dummy', - '--use-otter-config=false', - '--use-otter-theming=false', - '--use-otter-analytics=false', - '--use-localization=false', - '--use-context=false', - '--use-rules-engine=false' - ].join(' '); - packageManagerExec(`ng g @o3r/core:component test-component ${defaultOptions}`, execAppOptions); - await addImportToAppModule(appFolderPath, 'TestComponentModule', 'src/components/test-component'); - - const advancedOptions = [ - '--activate-dummy', - '--use-otter-config=true', - '--use-otter-theming=true', - '--use-otter-analytics=true', - '--use-localization=true', - '--use-context=true', - '--use-rules-engine=true' - ].join(' '); - packageManagerExec(`ng g @o3r/core:component test-component-advanced ${advancedOptions}`, execAppOptions); - await addImportToAppModule(appFolderPath, 'TestComponentAdvancedModule', 'src/components/test-component-advanced'); - - packageManagerExec(`ng g @o3r/core:component test-add-context-component ${defaultOptions}`, execAppOptions); - packageManagerExec('ng g @o3r/core:add-context --path="src/components/test-add-context-component/test-add-context-component.component.ts"', - execAppOptions); - await addImportToAppModule(appFolderPath, 'TestAddContextComponentModule', 'src/components/test-add-context-component'); - - packageManagerExec('ng g @schematics/angular:component test-ng-component', execAppOptions); - packageManagerExec('ng g @o3r/core:convert-component --path="src/app/test-ng-component/test-ng-component.component.ts"', execAppOptions); - - packageManagerExec('ng g @o3r/testing:playwright-scenario --name=test-scenario', execAppOptions); - packageManagerExec('ng g @o3r/testing:playwright-sanity --name=test-sanity', execAppOptions); - - const diff = getGitDiff(execAppOptions.cwd as string); - - // Expect created files inside `test-app` project - expect(diff.added.filter((file) => /e2e-playwright/.test(file)).length).toBeGreaterThan(0); - expect(diff.added.filter((file) => /src[\\/]app/.test(file)).length).toBeGreaterThan(0); - expect(diff.added.filter((file) => /src[\\/]components/.test(file)).length).toBeGreaterThan(0); - expect(diff.added.filter((file) => /src[\\/]environments/.test(file)).length).toBeGreaterThan(0); - expect(diff.added.filter((file) => /src[\\/]services/.test(file)).length).toBeGreaterThan(0); - expect(diff.added.filter((file) => /src[\\/]store/.test(file)).length).toBeGreaterThan(0); - expect(diff.added.filter((file) => /src[\\/]styling/.test(file)).length).toBeGreaterThan(0); - - expect(() => packageManagerRun('build', execAppOptions)).not.toThrow(); - - // should pass the e2e tests - spawn(`npx http-server -p ${devServerPort} ./dist/browser`, [], { - ...execAppOptions, - shell: true, - stdio: ['ignore', 'ignore', 'inherit'] - }); - execSync(`npx --yes wait-on http://127.0.0.1:${devServerPort} -t 20000`, execAppOptions); - - packageManagerExec('playwright install --with-deps', execAppOptions); - expect(() => packageManagerRun('test:playwright', execAppOptions)).not.toThrow(); - expect(() => packageManagerRun('test:playwright:sanity', execAppOptions)).not.toThrow(); - }); - - afterAll(async () => { - try { - const pid = await getPidFromPort(devServerPort); - execSync(process.platform === 'win32' ? `taskkill /f /t /pid ${pid}` : `kill -15 ${pid}`, {stdio: 'inherit'}); - } catch (e) { - // http-server already off - } - }); + beforeAll(async () => { + ({ projectPath, workspacePath, projectName, isInWorkspace, untouchedProjectPath } = await prepareTestEnv(appFolder)); + execAppOptions.cwd = workspacePath; }); - - describe('monorepo', () => { - beforeAll(async () => { - // TODO Should not start the test with @o3r/core already installed - const workspacePath = await prepareTestEnv(`${appName}-monorepo`, 'angular-monorepo-with-o3r-core'); - appFolderPath = join(workspacePath, 'projects', 'test-app'); - execAppOptions.cwd = workspacePath; + test('should build empty app', async () => { + const relativeProjectPath = path.relative(workspacePath, projectPath); + const projectNameOptions = ['--project-name', projectName]; + packageManagerExec({script: 'ng', args: ['add', `@o3r/core@${o3rVersion}`, '--preset', 'all', ...projectNameOptions, '--skip-confirmation']}, execAppOptions); + expect(() => packageManagerInstall(execAppOptions)).not.toThrow(); + + packageManagerExec({script: 'ng', args: ['g', '@o3r/core:store-entity-async', '--store-name', 'test-entity-async', '--model-name', 'Bound', '--model-id-prop-name', 'id', ...projectNameOptions]}, + execAppOptions + ); + await addImportToAppModule(projectPath, 'TestEntityAsyncStoreModule', 'src/store/test-entity-async'); + + packageManagerExec({script: 'ng', args: ['g', '@o3r/core:store-entity-sync', '--store-name', 'test-entity-sync', '--model-name', 'Bound', '--model-id-prop-name', 'id', ...projectNameOptions]}, + execAppOptions + ); + await addImportToAppModule(projectPath, 'TestEntitySyncStoreModule', 'src/store/test-entity-sync'); + + packageManagerExec({script: 'ng', args: ['g', '@o3r/core:store-simple-async', '--store-name', 'test-simple-async', '--model-name', 'Bound', ...projectNameOptions]}, + execAppOptions + ); + await addImportToAppModule(projectPath, 'TestSimpleAsyncStoreModule', 'src/store/test-simple-async'); + + packageManagerExec({script: 'ng', args: ['g', '@o3r/core:store-simple-sync', '--store-name', 'test-simple-sync', ...projectNameOptions]}, + execAppOptions + ); + await addImportToAppModule(projectPath, 'TestSimpleSyncStoreModule', 'src/store/test-simple-sync'); + + packageManagerExec({script: 'ng', args: ['g', '@o3r/core:service', 'test-service', '--feature-name', 'base', ...projectNameOptions]}, + execAppOptions + ); + await addImportToAppModule(projectPath, 'TestServiceBaseModule', 'src/services/test-service'); + + packageManagerExec({script: 'ng', args: ['g', '@o3r/core:page', 'test-page', '--app-routing-module-path', 'apps/test-app/src/app/app-routing.module.ts', ...projectNameOptions]}, + execAppOptions + ); + + const defaultOptions = [ + '--activate-dummy', + '--use-otter-config', 'false', + '--use-otter-theming', 'false', + '--use-otter-analytics', 'false', + '--use-localization', 'false', + '--use-context', 'false', + '--use-rules-engine', 'false' + ]; + packageManagerExec({script: 'ng', args: ['g', '@o3r/core:component', 'test-component', ...defaultOptions, ...projectNameOptions]}, + execAppOptions + ); + await addImportToAppModule(projectPath, 'TestComponentModule', 'src/components/test-component'); + + const advancedOptions = [ + '--activate-dummy', + '--use-otter-config', 'true', + '--use-otter-theming', 'true', + '--use-otter-analytics', 'true', + '--use-localization', 'true', + '--use-context', 'true', + '--use-rules-engine', 'true' + ]; + packageManagerExec({script: 'ng', args: ['g', '@o3r/core:component', 'test-component-advanced', ...advancedOptions, ...projectNameOptions]}, + execAppOptions + ); + await addImportToAppModule(projectPath, 'TestComponentAdvancedModule', 'src/components/test-component-advanced'); + + packageManagerExec({script: 'ng', args: ['g', '@o3r/core:component', 'test-add-context-component', ...defaultOptions, ...projectNameOptions]}, + execAppOptions + ); + packageManagerExec({script: 'ng', args: ['g', '@o3r/core:add-context', '--path', 'apps/test-app/src/components/test-add-context-component/test-add-context-component.component.ts']}, + execAppOptions + ); + await addImportToAppModule(projectPath, 'TestAddContextComponentModule', 'src/components/test-add-context-component'); + + packageManagerExec({script: 'ng', args: ['g', '@schematics/angular:component', 'test-ng-component', '--project', projectName]}, + execAppOptions + ); + packageManagerExec({script: 'ng', args: ['g', '@o3r/core:convert-component', '--path', 'apps/test-app/src/app/test-ng-component/test-ng-component.component.ts']}, + execAppOptions + ); + + packageManagerExec({script: 'ng', args: ['g', '@o3r/testing:playwright-scenario', '--name', 'test-scenario', ...projectNameOptions]}, execAppOptions); + packageManagerExec({script: 'ng', args: ['g', '@o3r/testing:playwright-sanity', '--name', 'test-sanity', ...projectNameOptions]}, execAppOptions); + + const diff = getGitDiff(execAppOptions.cwd as string); + + if (untouchedProjectPath) { + const relativeUntouchedProjectPath = path.relative(workspacePath, untouchedProjectPath); + expect(diff.all.filter((file) => new RegExp(relativeUntouchedProjectPath.replace(/[\\/]+/g, '[\\\\/]')).test(file)).length).toBe(0); + } + + // Expect created files inside `test-app` project + expect(diff.added.filter((file) => new RegExp(path.join(relativeProjectPath, 'e2e-playwright').replace(/[\\/]+/g, '[\\\\/]')).test(file)).length).toBeGreaterThan(0); + expect(diff.added.filter((file) => new RegExp(path.join(relativeProjectPath, 'src/app').replace(/[\\/]+/g, '[\\\\/]')).test(file)).length).toBeGreaterThan(0); + expect(diff.added.filter((file) => new RegExp(path.join(relativeProjectPath, 'src/components').replace(/[\\/]+/g, '[\\\\/]')).test(file)).length).toBeGreaterThan(0); + + expect(diff.added.filter((file) => new RegExp(path.join(relativeProjectPath, 'src/services').replace(/[\\/]+/g, '[\\\\/]')).test(file)).length).toBeGreaterThan(0); + expect(diff.added.filter((file) => new RegExp(path.join(relativeProjectPath, 'src/store').replace(/[\\/]+/g, '[\\\\/]')).test(file)).length).toBeGreaterThan(0); + expect(diff.added.filter((file) => new RegExp(path.join(relativeProjectPath, 'src/styling').replace(/[\\/]+/g, '[\\\\/]')).test(file)).length).toBeGreaterThan(0); + + expect(() => packageManagerRunOnProject(projectName, isInWorkspace, {script: 'build'}, execAppOptions)).not.toThrow(); + + // should pass the e2e tests + spawn(`npx http-server -p ${devServerPort} ${path.join(relativeProjectPath, 'dist/browser')}`, [], { + ...execAppOptions, + shell: true, + stdio: ['ignore', 'ignore', 'inherit'] }); - test('should build empty app', async () => { + execSync(`npx --yes wait-on http://127.0.0.1:${devServerPort} -t 20000`, execAppOptions); - const projectName = '--project-name=test-app'; - packageManagerExec(`ng add --skip-confirmation @o3r/core@${o3rVersion} --preset=all ${projectName}`, execAppOptions); - expect(() => packageManagerInstall(execAppOptions)).not.toThrow(); - - packageManagerExec( - `ng g @o3r/core:store-entity-async --store-name="test-entity-async" --model-name="Bound" --model-id-prop-name="id" ${projectName}`, - execAppOptions - ); - await addImportToAppModule(appFolderPath, 'TestEntityAsyncStoreModule', 'src/store/test-entity-async'); - - packageManagerExec( - `ng g @o3r/core:store-entity-sync --store-name="test-entity-sync" --model-name="Bound" --model-id-prop-name="id" ${projectName}`, - execAppOptions - ); - await addImportToAppModule(appFolderPath, 'TestEntitySyncStoreModule', 'src/store/test-entity-sync'); - - packageManagerExec( - `ng g @o3r/core:store-simple-async --store-name="test-simple-async" --model-name="Bound" ${projectName}`, - execAppOptions - ); - await addImportToAppModule(appFolderPath, 'TestSimpleAsyncStoreModule', 'src/store/test-simple-async'); - - packageManagerExec( - `ng g @o3r/core:store-simple-sync --store-name="test-simple-sync" ${projectName}`, - execAppOptions - ); - await addImportToAppModule(appFolderPath, 'TestSimpleSyncStoreModule', 'src/store/test-simple-sync'); - - packageManagerExec( - `ng g @o3r/core:service test-service --feature-name="base" ${projectName}`, - execAppOptions - ); - await addImportToAppModule(appFolderPath, 'TestServiceBaseModule', 'src/services/test-service'); - - packageManagerExec( - `ng g @o3r/core:page test-page --app-routing-module-path="projects/test-app/src/app/app-routing.module.ts" ${projectName}`, - execAppOptions - ); - - const defaultOptions = [ - '--activate-dummy', - '--use-otter-config=false', - '--use-otter-theming=false', - '--use-otter-analytics=false', - '--use-localization=false', - '--use-context=false', - '--use-rules-engine=false' - ].join(' '); - packageManagerExec( - `ng g @o3r/core:component test-component ${defaultOptions} ${projectName}`, - execAppOptions - ); - await addImportToAppModule(appFolderPath, 'TestComponentModule', 'src/components/test-component'); - - const advancedOptions = [ - '--activate-dummy', - '--use-otter-config=true', - '--use-otter-theming=true', - '--use-otter-analytics=true', - '--use-localization=true', - '--use-context=true', - '--use-rules-engine=true' - ].join(' '); - packageManagerExec( - `ng g @o3r/core:component test-component-advanced ${advancedOptions} ${projectName}`, - execAppOptions - ); - await addImportToAppModule(appFolderPath, 'TestComponentAdvancedModule', 'src/components/test-component-advanced'); - - packageManagerExec( - `ng g @o3r/core:component test-add-context-component ${defaultOptions} ${projectName}`, - execAppOptions - ); - packageManagerExec( - 'ng g @o3r/core:add-context --path="projects/test-app/src/components/test-add-context-component/test-add-context-component.component.ts"', - execAppOptions - ); - await addImportToAppModule(appFolderPath, 'TestAddContextComponentModule', 'src/components/test-add-context-component'); - - packageManagerExec( - 'ng g @schematics/angular:component test-ng-component --project=test-app', - execAppOptions - ); - packageManagerExec( - 'ng g @o3r/core:convert-component --path="projects/test-app/src/app/test-ng-component/test-ng-component.component.ts"', - execAppOptions - ); - - packageManagerExec(`ng g @o3r/testing:playwright-scenario --name=test-scenario ${projectName}`, execAppOptions); - packageManagerExec(`ng g @o3r/testing:playwright-sanity --name=test-sanity ${projectName}`, execAppOptions); - - const diff = getGitDiff(execAppOptions.cwd as string); - - // Expect no file modified inside 'dont-modify-me' project - expect(diff.all.filter((file) => /projects[\\/]dont-modify-me/.test(file)).length).toBe(0); - - // Expect no file created outside 'test-app' project - expect(diff.added.filter((file) => !/projects[\\/]test-app/.test(file)).length).toBe(0); - - // Expect created files inside `test-app` project - expect(diff.added.filter((file) => /projects[\\/]test-app[\\/]e2e-playwright/.test(file)).length).toBeGreaterThan(0); - expect(diff.added.filter((file) => /projects[\\/]test-app[\\/]src[\\/]app/.test(file)).length).toBeGreaterThan(0); - expect(diff.added.filter((file) => /projects[\\/]test-app[\\/]src[\\/]components/.test(file)).length).toBeGreaterThan(0); - // TODO Should not start the test with @o3r/core already installed - // expect(diff.added.filter((file) => /projects[\\/]test-app[\\/]src[\\/]environments/.test(file)).length).toBeGreaterThan(0); - expect(diff.added.filter((file) => /projects[\\/]test-app[\\/]src[\\/]services/.test(file)).length).toBeGreaterThan(0); - expect(diff.added.filter((file) => /projects[\\/]test-app[\\/]src[\\/]store/.test(file)).length).toBeGreaterThan(0); - expect(diff.added.filter((file) => /projects[\\/]test-app[\\/]src[\\/]styling/.test(file)).length).toBeGreaterThan(0); - - expect(() => packageManagerRun('build', execAppOptions)).not.toThrow(); - - // should pass the e2e tests - spawn(`npx http-server -p ${devServerPort} ./projects/test-app/dist/browser`, [], { - ...execAppOptions, - shell: true, - stdio: ['ignore', 'ignore', 'inherit'] - }); - execSync(`npx --yes wait-on http://127.0.0.1:${devServerPort} -t 20000`, execAppOptions); - - packageManagerWorkspaceExec('test-app-project', 'playwright install --with-deps', execAppOptions); - expect(() => packageManagerWorkspaceRun('test-app-project', 'test:playwright', execAppOptions)).not.toThrow(); - expect(() => packageManagerWorkspaceRun('test-app-project', 'test:playwright:sanity', execAppOptions)).not.toThrow(); - }); + packageManagerExecOnProject(projectName, isInWorkspace, {script: 'playwright', args: ['install', '--with-deps']}, execAppOptions); + expect(() => packageManagerRunOnProject(projectName, isInWorkspace, {script: 'test:playwright'}, execAppOptions)).not.toThrow(); + expect(() => packageManagerRunOnProject(projectName, isInWorkspace, {script: 'test:playwright:sanity'}, execAppOptions)).not.toThrow(); + }); - afterAll(async () => { - try { - const pid = await getPidFromPort(devServerPort); - execSync(process.platform === 'win32' ? `taskkill /f /t /pid ${pid}` : `kill -15 ${pid}`, { stdio: 'inherit' }); - } catch (e) { - // http-server already off - } - }); + afterAll(async () => { + try { + const pid = await getPidFromPort(devServerPort); + execSync(process.platform === 'win32' ? `taskkill /f /t /pid ${pid}` : `kill -15 ${pid}`, { stdio: 'inherit' }); + } catch (e) { + // http-server already off + } + }); + afterAll(async () => { + try { await rm(workspacePath, { recursive: true }); } catch { /* ignore error */ } }); }); diff --git a/packages/@o3r/core/schematics/ng-add-create/templates/schematics/ng-add/index.ts.template b/packages/@o3r/core/schematics/ng-add-create/templates/schematics/ng-add/index.ts.template index 2ffc2f105e..e0440487ef 100644 --- a/packages/@o3r/core/schematics/ng-add-create/templates/schematics/ng-add/index.ts.template +++ b/packages/@o3r/core/schematics/ng-add-create/templates/schematics/ng-add/index.ts.template @@ -1,32 +1,61 @@ import { chain, noop, Rule } from '@angular-devkit/schematics'; -import { applyEsLintFix, createSchematicWithMetricsIfInstalled, install, ngAddPackages, getO3rPeerDeps } from '@o3r/schematics'; import type { NgAddSchematicsSchema } from './schema'; import * as path from 'node:path'; +import { type DependencyToAdd } from '@o3r/schematics'; + +const packageJsonPath = path.resolve(__dirname, '..', '..', 'package.json'); const doCustomAction: Rule = (tree, _context) => { // your custom code here return tree; }; +const dependenciesToInstall = [ + // Add the dependencies to install here +]; + +const dependenciesToNgAdd = [ + // Add the dependencies to install with NgAdd here +]; + /** * Add Otter <%= featureName %> to an Otter Project * * @param options */ function ngAddFn(options: NgAddSchematicsSchema): Rule { - return async (_tree, context) => { - // retrieve dependencies following the /^@o3r\/.*/ pattern within the peerDependencies of the current module - const depsInfo = getO3rPeerDeps(path.resolve(__dirname, '..', '..', 'package.json')); - return chain([ - // optional custom action dedicated to this module - doCustomAction, - options.skipLinter ? noop() : applyEsLintFix(), - // install packages needed in the current module - options.skipInstall ? noop : install, - // add the missing Otter modules in the current project - ngAddPackages(depsInfo.o3rPeerDeps, { skipConfirmation: true, version: depsInfo.packageVersion, parentPackageInfo: `${depsInfo.packageName!} - setup` }) - ]); + return async (tree, context) => { + try { + // use dynamic import to properly raise an exception if it is not an Otter project. + const { getProjectNewDependenciesTypes, getPackageInstallConfig, applyEsLintFix, install } = await import('@o3r/schematics'); + // current package version + const version = JSON.stringify(fs.readFileSync(packageJsonPath)).version; + const dependencies = [...dependenciesToInstall, ...dependenciesToNgAdd].reduce((acc, dep) => { + acc[dep] = { + inManifest: [{ + range: `~${version}`, + types: getProjectNewDependenciesTypes(workspaceProject) + }] + }; + return acc; + }, getPackageInstallConfig(packageJsonPath, tree, options.projectName)); + return chain([ + // optional custom action dedicated to this module + doCustomAction, + options.skipLinter ? noop() : applyEsLintFix(), + setupDependencies({ + projectName: options.projectName, + dependencies, + ngAddToRun: dependenciesToNgAdd, + skipInstall: options.skipInstall + }) + ]); + } catch (e) { + // If the installation is initialized in a non-Otter application, mandatory packages will be missing. We need to notify the user + context.logger.error(`[ERROR]: Adding <%= featureName %> has failed. + If the error is related to missing @o3r dependencies you need to install '@o3r/core' to be able to use the <%= featureName %> package. Please run 'ng add @o3r/core' . + Otherwise, use the error message as guidance.`); + throw (e); + } }; } - -export const ngAdd = createSchematicWithMetricsIfInstalled(ngAddFn); diff --git a/packages/@o3r/core/schematics/ng-add-create/templates/schematics/ng-add/schema.json.template b/packages/@o3r/core/schematics/ng-add-create/templates/schematics/ng-add/schema.json.template index 5381b08b5b..aca3948635 100644 --- a/packages/@o3r/core/schematics/ng-add-create/templates/schematics/ng-add/schema.json.template +++ b/packages/@o3r/core/schematics/ng-add-create/templates/schematics/ng-add/schema.json.template @@ -4,7 +4,7 @@ "title": "Add <%= featureName %> project", "description": "'ng add <%= featureName %>' project", "properties": { - "name": { + "projectName": { "type": "string", "description": "Project name", "$default": { @@ -18,8 +18,7 @@ }, "skipInstall": { "type": "boolean", - "description": "Skip the install process", - "default": true + "description": "Skip the install process" } }, "additionalProperties": true, diff --git a/packages/@o3r/core/schematics/ng-add-create/templates/schematics/ng-add/schema.ts.template b/packages/@o3r/core/schematics/ng-add-create/templates/schematics/ng-add/schema.ts.template index 90213f66b1..724b4e1539 100644 --- a/packages/@o3r/core/schematics/ng-add-create/templates/schematics/ng-add/schema.ts.template +++ b/packages/@o3r/core/schematics/ng-add-create/templates/schematics/ng-add/schema.ts.template @@ -2,7 +2,7 @@ import type { SchematicOptionObject } from '@o3r/schematics'; export interface NgAddSchematicsSchema extends SchematicOptionObject { /** Project name */ - name: string | undefined; + projectName: string | undefined; /** Skip the linter process */ skipLinter: boolean; diff --git a/packages/@o3r/core/schematics/ng-add/index.ts b/packages/@o3r/core/schematics/ng-add/index.ts index 03109e8224..972100a0f6 100644 --- a/packages/@o3r/core/schematics/ng-add/index.ts +++ b/packages/@o3r/core/schematics/ng-add/index.ts @@ -1,22 +1,23 @@ -import { chain, externalSchematic, noop } from '@angular-devkit/schematics'; -import type { Rule, SchematicContext, Tree } from '@angular-devkit/schematics'; -import { askConfirmation } from '@angular/cli/src/utilities/prompt'; -import { - AddDevInstall, - createSchematicWithMetricsIfInstalled, - displayModuleListRule, - getWorkspaceConfig, - isPackageInstalled, - registerPackageCollectionSchematics, - setupSchematicsDefaultParams -} from '@o3r/schematics'; +import { chain, noop, Rule } from '@angular-devkit/schematics'; import * as fs from 'node:fs'; import * as path from 'node:path'; -import { lastValueFrom } from 'rxjs'; import type { PackageJson } from 'type-fest'; import { getExternalPreset, presets } from '../shared/presets'; +import { NgAddSchematicsSchema } from './schema'; +import { askConfirmation } from '@angular/cli/src/utilities/prompt'; +import { createSchematicWithMetricsIfInstalled, displayModuleListRule, registerPackageCollectionSchematics, setupSchematicsDefaultParams } from '@o3r/schematics'; import { prepareProject } from './project-setup/index'; -import type { NgAddSchematicsSchema } from './schema'; +import { + type DependencyToAdd, + setupDependencies, + type SetupDependenciesOptions +} from '@o3r/schematics'; +import { NodeDependencyType } from '@schematics/angular/utility/dependencies'; + +const workspacePackageName = '@o3r/workspace'; +const o3rDevDependencies = [ + '@o3r/schematics' +]; /** * Add Otter library to an Angular Project @@ -24,33 +25,38 @@ import type { NgAddSchematicsSchema } from './schema'; */ function ngAddFn(options: NgAddSchematicsSchema): Rule { const corePackageJsonContent = JSON.parse(fs.readFileSync(path.resolve(__dirname, '..', '..', 'package.json'), {encoding: 'utf-8'})) as PackageJson; - const o3rCoreVersion = corePackageJsonContent.version ? `@${corePackageJsonContent.version}` : ''; - const schematicsDependencies = ['@o3r/schematics']; + const o3rCoreVersion = corePackageJsonContent.version!; + + return (): Rule => { + const dependenciesSetupConfig: SetupDependenciesOptions = { + projectName: options.projectName, + dependencies: o3rDevDependencies.reduce((acc, dep) => { + acc[dep] = { + inManifest: [{ + range: `~${o3rCoreVersion}`, + types: [NodeDependencyType.Dev] + }] + }; + return acc; + }, {} as Record), + ngAddToRun: [...o3rDevDependencies] + }; - return async (tree: Tree, context: SchematicContext): Promise => { - // check if the workspace package is installed, if not installed and we are in workspace context, we install - const workspacePackageName = '@o3r/workspace'; - if (!options.projectName && !isPackageInstalled(workspacePackageName)) { - schematicsDependencies.push(workspacePackageName); + if (!options.projectName) { + dependenciesSetupConfig.dependencies[workspacePackageName] = { + toWorkspaceOnly: true, + inManifest: [{ + range: `~${o3rCoreVersion}`, + types: [NodeDependencyType.Default] + }] + }; + (dependenciesSetupConfig.ngAddToRun ||= []).push(workspacePackageName); } - const workspaceProject = options.projectName ? getWorkspaceConfig(tree)?.projects[options.projectName] : undefined; - const workingDirectory = workspaceProject?.root || '.'; - context.addTask(new AddDevInstall({ - packageName: [ - ...schematicsDependencies.map((dependency) => dependency + o3rCoreVersion) - ].join(' '), - hideOutput: false, - quiet: false, - workingDirectory, - force: options.forceInstall - } as any)); - await lastValueFrom(context.engine.executePostTasks()); - return () => chain([ + return chain([ // eslint-disable-next-line @typescript-eslint/naming-convention setupSchematicsDefaultParams({ '*:ng-add': { registerDevtool: options.withDevtool } }), - ...schematicsDependencies.map((dep) => externalSchematic(dep, 'ng-add', options)), - options.projectName ? prepareProject(options) : noop(), + options.projectName ? prepareProject(options, dependenciesSetupConfig) : noop(), registerPackageCollectionSchematics(corePackageJsonContent), async (t, c) => { const { preset, externalPresets, ...forwardOptions } = options; @@ -68,8 +74,9 @@ function ngAddFn(options: NgAddSchematicsSchema): Rule { externalPresetRunner?.rule || noop() ])(t, c); }, - displayModuleListRule({ packageName: options.projectName }) - ])(tree, context); + options.projectName ? displayModuleListRule({ packageName: options.projectName }) : noop(), + setupDependencies(dependenciesSetupConfig) + ]); }; } diff --git a/packages/@o3r/core/schematics/ng-add/project-setup/index.ts b/packages/@o3r/core/schematics/ng-add/project-setup/index.ts index dbc759c466..2ca2436e9d 100644 --- a/packages/@o3r/core/schematics/ng-add/project-setup/index.ts +++ b/packages/@o3r/core/schematics/ng-add/project-setup/index.ts @@ -1,5 +1,4 @@ -import { chain, noop, Rule, SchematicContext, Tree } from '@angular-devkit/schematics'; -import { NodeDependencyType } from '@schematics/angular/utility/dependencies'; +import { chain, noop, type Rule } from '@angular-devkit/schematics'; import * as fs from 'node:fs'; import * as path from 'node:path'; import { @@ -12,10 +11,10 @@ import { import { applyEsLintFix, getO3rPeerDeps, + getProjectNewDependenciesTypes, getWorkspaceConfig, - install, - ngAddPackages, - removePackages + removePackages, + type SetupDependenciesOptions } from '@o3r/schematics'; import type { NgAddSchematicsSchema } from '../schema'; import { updateBuildersNames } from '../updates-for-v8/cms-adapters/update-builders-names'; @@ -27,8 +26,9 @@ import { shouldOtterLinterBeInstalled } from '../utils/index'; * Enable all the otter features requested by the user * Install all the related dependencies and import the features inside the application * @param options installation options to pass to the all the other packages' installation + * @param dependenciesSetupConfig */ -export const prepareProject = (options: NgAddSchematicsSchema) => async (tree: Tree, context: SchematicContext) => { +export const prepareProject = (options: NgAddSchematicsSchema, dependenciesSetupConfig: SetupDependenciesOptions): Rule => async (tree, context) => { const coreSchematicsFolder = path.resolve(__dirname, '..'); const corePackageJsonPath = path.resolve(coreSchematicsFolder, '..', '..', 'package.json'); const corePackageJsonContent = JSON.parse(fs.readFileSync(corePackageJsonPath, { encoding: 'utf-8' })); @@ -46,30 +46,32 @@ export const prepareProject = (options: NgAddSchematicsSchema) => async (tree: T ...(installOtterLinter ? ['@o3r/eslint-config-otter'] : []), ...depsInfo.o3rPeerDeps ])); - const type = projectType === 'library' ? NodeDependencyType.Peer : NodeDependencyType.Default; const projectDirectory = workspaceProject?.root; - const optionsAndWorkingDir = { ...options, workingDirectory: projectDirectory }; + const optionsAndWorkingDir = { ...options, workingDirectory: projectDirectory, dependenciesSetupConfig }; - return () => { + internalPackagesToInstallWithNgAdd + .forEach((dep) => { + dependenciesSetupConfig.dependencies[dep] = { + inManifest: [{ + range: `~${o3rCoreVersion}`, + types: getProjectNewDependenciesTypes(workspaceProject) + }] + }; + }); + (dependenciesSetupConfig.ngAddToRun ||= []).push(...internalPackagesToInstallWithNgAdd); - const appLibRules: Rule[] = [ - updateBuildersNames(), - updateOtterGeneratorsNames(), - updateOtterEnvironmentAdapter(optionsAndWorkingDir, coreSchematicsFolder), - updateStore(optionsAndWorkingDir, projectType), - options.enableCustomization && projectType === 'application' ? updateCustomizationEnvironment(coreSchematicsFolder, o3rCoreVersion, optionsAndWorkingDir, false) : noop, - projectType === 'application' ? updateAdditionalModules(optionsAndWorkingDir, coreSchematicsFolder) : noop, - removePackages(packagesToRemove), - o3rBasicUpdates(options.projectName, o3rCoreVersion, projectType), - ngAddPackages(internalPackagesToInstallWithNgAdd, - { skipConfirmation: true, version: o3rCoreVersion, parentPackageInfo: '@o3r/core - setup', projectName: options.projectName, dependencyType: type, workingDirectory: projectDirectory } - ), - // task that should run after the schematics should be after the ng-add task as they will wait for the package installation before running the other dependencies - !options.skipLinter && installOtterLinter ? applyEsLintFix() : noop(), - // dependencies for store (mainly ngrx, store dev tools, storage sync), playwright, linter are installed by hand if the option is active - options.skipInstall ? noop() : install - ]; + const appLibRules: Rule[] = [ + updateBuildersNames(), + updateOtterGeneratorsNames(), + updateOtterEnvironmentAdapter(optionsAndWorkingDir, coreSchematicsFolder), + updateStore(optionsAndWorkingDir, projectType), + options.enableCustomization && projectType === 'application' ? updateCustomizationEnvironment(coreSchematicsFolder, o3rCoreVersion, optionsAndWorkingDir, false) : noop, + projectType === 'application' ? updateAdditionalModules(optionsAndWorkingDir, dependenciesSetupConfig) : noop, + removePackages(packagesToRemove), + o3rBasicUpdates(options.projectName, o3rCoreVersion, projectType), + // task that should run after the schematics should be after the ng-add task as they will wait for the package installation before running the other dependencies + !options.skipLinter && installOtterLinter ? applyEsLintFix() : noop() + ]; - return chain(appLibRules)(tree, context); - }; + return chain(appLibRules); }; diff --git a/packages/@o3r/core/schematics/rule-factories/additional-modules/index.ts b/packages/@o3r/core/schematics/rule-factories/additional-modules/index.ts index 9c5182ad85..9a8d68ee72 100644 --- a/packages/@o3r/core/schematics/rule-factories/additional-modules/index.ts +++ b/packages/@o3r/core/schematics/rule-factories/additional-modules/index.ts @@ -1,18 +1,21 @@ import { chain, Rule, SchematicContext, Tree } from '@angular-devkit/schematics'; import { getAppModuleFilePath, - getProjectNewDependenciesType, + getProjectNewDependenciesTypes, getWorkspaceConfig, - ngAddPeerDependencyPackages + type SetupDependenciesOptions } from '@o3r/schematics'; import * as ts from 'typescript'; import { addRootImport } from '@schematics/angular/utility'; import { insertImport, isImported } from '@schematics/angular/utility/ast-utils'; import { InsertChange } from '@schematics/angular/utility/change'; -import { NodeDependencyType } from '@schematics/angular/utility/dependencies'; import * as path from 'node:path'; +import * as fs from 'node:fs'; +import type { PackageJson } from 'type-fest'; +import { NodeDependencyType } from '@schematics/angular/utility/dependencies'; const packageJsonPath = path.resolve(__dirname, '..', '..', '..', 'package.json'); +const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, { encoding: 'utf-8' })) as PackageJson & { generatorDependencies: Record }; const ngrxStoreDevtoolsDep = '@ngrx/store-devtools'; /** @@ -20,26 +23,31 @@ const ngrxStoreDevtoolsDep = '@ngrx/store-devtools'; * @param options @see RuleFactory.options * @param options.projectName * @param options.workingDirectory the directory where to execute the rule factory - * @param _rootPath @see RuleFactory.rootPath + * @param dependenciesSetupConfig */ -export function updateAdditionalModules(options: { projectName?: string | undefined; workingDirectory?: string | undefined }, _rootPath: string): Rule { +export function updateAdditionalModules(options: { projectName?: string | undefined; workingDirectory?: string | undefined }, dependenciesSetupConfig: SetupDependenciesOptions): Rule { /** * Update package.json to add additional modules dependencies * @param tree - * @param context */ - const updatePackageJson: Rule = (tree: Tree, context: SchematicContext) => { + const updatePackageJson: Rule = (tree) => { const workspaceProject = options.projectName ? getWorkspaceConfig(tree)?.projects[options.projectName] : undefined; - const type: NodeDependencyType = getProjectNewDependenciesType(workspaceProject); - const generatorDependencies = [ngrxStoreDevtoolsDep]; + const types = getProjectNewDependenciesTypes(workspaceProject); + dependenciesSetupConfig.dependencies.chokidar = { + inManifest: [{ + range: packageJson.peerDependencies!.chokidar, + types: [NodeDependencyType.Dev] + }] + }; + + dependenciesSetupConfig.dependencies[ngrxStoreDevtoolsDep] = { + inManifest: [{ + range: packageJson.generatorDependencies[ngrxStoreDevtoolsDep], + types + }] + }; - try { - return ngAddPeerDependencyPackages(generatorDependencies, packageJsonPath, type, {...options, skipNgAddSchematicRun: true}); - } catch (e) { - context.logger.warn(`Could not find generatorDependencies ${generatorDependencies.join(', ')} in file ${packageJsonPath}`); - } return tree; - }; /** diff --git a/packages/@o3r/core/schematics/rule-factories/otter-environment/index.ts b/packages/@o3r/core/schematics/rule-factories/otter-environment/index.ts index 4e83197c16..7b4accacbc 100644 --- a/packages/@o3r/core/schematics/rule-factories/otter-environment/index.ts +++ b/packages/@o3r/core/schematics/rule-factories/otter-environment/index.ts @@ -83,7 +83,7 @@ export function updateOtterEnvironmentAdapter( OTTER_ITEM_TYPES.forEach((item) => { const path = TYPES_DEFAULT_FOLDER[item].lib ? `modules/${scope}/${TYPES_DEFAULT_FOLDER[item].lib!}` : null; if (path) { - workspace.schematics![item] = { + workspace.schematics![`${item}*`] = { path, ...(workspace.schematics![item] || {}) }; diff --git a/packages/@o3r/core/schematics/rule-factories/playwright/index.ts b/packages/@o3r/core/schematics/rule-factories/playwright/index.ts index f1d58af80a..cdc085532a 100644 --- a/packages/@o3r/core/schematics/rule-factories/playwright/index.ts +++ b/packages/@o3r/core/schematics/rule-factories/playwright/index.ts @@ -1,17 +1,26 @@ import { strings } from '@angular-devkit/core'; import { apply, chain, MergeStrategy, mergeWith, renameTemplateFiles, Rule, SchematicContext, template, Tree, url } from '@angular-devkit/schematics'; -import { getTemplateFolder, NgAddPackageOptions, ngAddPeerDependencyPackages } from '@o3r/schematics'; +import { + type DependencyToAdd, + getTemplateFolder, + NgAddPackageOptions, + setupDependencies +} from '@o3r/schematics'; import { NodeDependencyType } from '@schematics/angular/utility/dependencies'; import * as path from 'node:path'; +import * as fs from 'node:fs'; +import type { PackageJson } from 'type-fest'; /** * Add Playwright to Otter application - * * @param options @see RuleFactory.options * @param options.projectName * @param rootPath @see RuleFactory.rootPath */ export function updatePlaywright(rootPath: string, options: NgAddPackageOptions = {}): Rule { + const ownPackageJsonPath = path.resolve(__dirname, '..', '..', '..', 'package.json'); + const ownPackageJson = JSON.parse(fs.readFileSync(ownPackageJsonPath, { encoding: 'utf-8' })) as PackageJson & { generatorDependencies: Record }; + return (tree: Tree, context: SchematicContext) => { // update gitignore @@ -36,8 +45,19 @@ export function updatePlaywright(rootPath: string, options: NgAddPackageOptions packageJson.scripts['test:playwright:sanity'] ||= 'playwright test --config=e2e-playwright/playwright-config.sanity.ts'; tree.overwrite('/package.json', JSON.stringify(packageJson, null, 2)); } - const corePackageJsonPath = path.resolve(__dirname, '..', '..', '..', 'package.json'); - const ngAddRules = ngAddPeerDependencyPackages(['@playwright/test', 'rimraf'], corePackageJsonPath, NodeDependencyType.Dev, {...options, skipNgAddSchematicRun: true}); + const dependencies = ['@playwright/test', 'rimraf'].reduce((acc, dep) => { + acc[dep] = { + inManifest: [{ + range: ownPackageJson.generatorDependencies[dep], + types: [NodeDependencyType.Dev] + }] + }; + return acc; + }, {} as Record); + const ngAddRules = setupDependencies({ + projectName: options.projectName || undefined, + dependencies + }); // generate files if (!tree.exists('/e2e-playwright/playwright-config.ts')) { diff --git a/packages/@o3r/core/schematics/rule-factories/store/index.ts b/packages/@o3r/core/schematics/rule-factories/store/index.ts index 81fbb29688..227ae0ca35 100644 --- a/packages/@o3r/core/schematics/rule-factories/store/index.ts +++ b/packages/@o3r/core/schematics/rule-factories/store/index.ts @@ -1,30 +1,32 @@ import { chain, Rule, SchematicContext, Tree } from '@angular-devkit/schematics'; import { getAppModuleFilePath, + getExternalDependenciesVersionRange, getModuleIndex, + getProjectNewDependenciesTypes, getWorkspaceConfig, isApplicationThatUsesRouterModule, - ngAddPackages, - ngAddPeerDependencyPackages, insertBeforeModule as o3rInsertBeforeModule, - insertImportToModuleFile as o3rInsertImportToModuleFile + insertImportToModuleFile as o3rInsertImportToModuleFile, + type SetupDependenciesOptions } from '@o3r/schematics'; import { WorkspaceProject } from '@o3r/schematics'; import { addRootImport } from '@schematics/angular/utility'; import { isImported } from '@schematics/angular/utility/ast-utils'; -import { NodeDependencyType } from '@schematics/angular/utility/dependencies'; import * as path from 'node:path'; import * as ts from 'typescript'; import * as fs from 'node:fs'; -const packageJsonPath = path.resolve(__dirname, '..', '..', '..', 'package.json'); +const coreSchematicsFolder = path.resolve(__dirname, '..', '..'); +const corePackageJsonPath = path.resolve(coreSchematicsFolder, '..', 'package.json'); +const corePackageJsonContent = JSON.parse(fs.readFileSync(corePackageJsonPath, { encoding: 'utf-8' })); +const o3rCoreVersion = corePackageJsonContent.version; + const ngrxEffectsDep = '@ngrx/effects'; const ngrxEntityDep = '@ngrx/entity'; const ngrxStoreDep = '@ngrx/store'; const ngrxRouterStore = '@ngrx/router-store'; const ngrxRouterStoreDevToolDep = '@ngrx/store-devtools'; -// TODO Remove this explicit dependency when correctly brought by the ng-add of @o3r/store-sync -const fastDeepEqualDep = 'fast-deep-equal'; /** * Add Redux Store support @@ -33,55 +35,48 @@ const fastDeepEqualDep = 'fast-deep-equal'; * @param options.projectName * @param options.workingDirectory * @param projectType + * @param options.dependenciesSetupConfig + * @param options.workingDirector */ -export function updateStore(options: { projectName?: string | undefined; workingDirectory?: string | undefined}, projectType?: WorkspaceProject['projectType']): Rule { +export function updateStore( + options: { projectName?: string | undefined; workingDirector?: string | undefined; dependenciesSetupConfig: SetupDependenciesOptions }, + projectType?: WorkspaceProject['projectType']): Rule { const addStoreModules: Rule = (tree) => { - const coreSchematicsFolder = path.resolve(__dirname, '..', '..'); - const corePackageJsonPath = path.resolve(coreSchematicsFolder, '..', 'package.json'); - const corePackageJsonContent = JSON.parse(fs.readFileSync(corePackageJsonPath, { encoding: 'utf-8' })); - const o3rCoreVersion = corePackageJsonContent.version; const workspaceConfig = getWorkspaceConfig(tree); const workspaceProject = options.projectName && workspaceConfig?.projects?.[options.projectName] || undefined; - const projectDirectory = workspaceProject?.root; - - return ngAddPackages(['@o3r/store-sync'], - { - skipConfirmation: true, - version: o3rCoreVersion, - parentPackageInfo: '@o3r/core - setup', - projectName: options.projectName, - dependencyType: NodeDependencyType.Default, - workingDirectory: projectDirectory - } - ); + + const storeSyncPackageName = '@o3r/store-sync'; + + options.dependenciesSetupConfig.dependencies[storeSyncPackageName] = { + inManifest: [{ + range: `~${o3rCoreVersion}`, + types: getProjectNewDependenciesTypes(workspaceProject) + }] + }; + (options.dependenciesSetupConfig.ngAddToRun ||= []).push(storeSyncPackageName); }; /** - * Changed package.json start script to run localization generation + * Change package.json with the new dependencies * @param tree - * @param context */ - const updatePackageJson: Rule = (tree: Tree, context: SchematicContext) => { - const type = projectType === 'library' ? NodeDependencyType.Peer : NodeDependencyType.Default; + const updatePackageJson: Rule = (tree: Tree) => { + const workspaceConfig = getWorkspaceConfig(tree); + const workspaceProject = options.projectName && workspaceConfig?.projects?.[options.projectName] || undefined; - const appDeps = [ngrxEffectsDep, ngrxRouterStore, ngrxRouterStoreDevToolDep, fastDeepEqualDep]; + const appDeps = [ngrxEffectsDep, ngrxRouterStore, ngrxRouterStoreDevToolDep]; const corePeerDeps = [ngrxEntityDep, ngrxStoreDep]; - let dependenciesList = [...corePeerDeps]; - - if (projectType === 'application') { - dependenciesList = [...dependenciesList, ...appDeps]; - } - - - return () => { - try { - return ngAddPeerDependencyPackages(dependenciesList, packageJsonPath, type, {...options, skipNgAddSchematicRun: true})(tree, context); - } catch (e: any) { - context.logger.warn(`Could not find generatorDependency ${dependenciesList.join(', ')} in file ${packageJsonPath}`); - return tree; - } - }; + const dependenciesList = projectType === 'application' ? [...corePeerDeps, ...appDeps] : [...corePeerDeps]; + + Object.entries(getExternalDependenciesVersionRange(dependenciesList, corePackageJsonPath)).forEach(([dep, range]) => { + options.dependenciesSetupConfig.dependencies[dep] = { + inManifest: [{ + range, + types: getProjectNewDependenciesTypes(workspaceProject) + }] + }; + }); }; /** diff --git a/packages/@o3r/core/schematics/shared/presets/helpers.ts b/packages/@o3r/core/schematics/shared/presets/helpers.ts index d22d2b2ac9..05f1e2c2c5 100644 --- a/packages/@o3r/core/schematics/shared/presets/helpers.ts +++ b/packages/@o3r/core/schematics/shared/presets/helpers.ts @@ -1,6 +1,5 @@ -import type {Rule} from '@angular-devkit/schematics'; -import { getWorkspaceConfig, ngAddPackages } from '@o3r/schematics'; -import {NodeDependencyType} from '@schematics/angular/utility/dependencies'; +import {noop, type Rule} from '@angular-devkit/schematics'; +import { type DependencyToAdd, getProjectNewDependenciesTypes, getWorkspaceConfig, setupDependencies } from '@o3r/schematics'; import * as fs from 'node:fs'; import * as path from 'node:path'; import type {PackageJson} from 'type-fest'; @@ -14,19 +13,33 @@ import type {PresetOptions} from './preset.interface'; export function defaultPresetRuleFactory(moduleToInstall: string[], options: PresetOptions = {}): Rule { return (tree, _context) => { - const workingDirectory = options?.projectName && getWorkspaceConfig(tree)?.projects[options.projectName]?.root || '.'; + const corePackageJsonContent = JSON.parse(fs.readFileSync(path.resolve(__dirname, '..', '..', '..', 'package.json'), { encoding: 'utf-8' })) as PackageJson; + const workspaceProject = options.projectName ? getWorkspaceConfig(tree)?.projects[options.projectName] : undefined; if (!moduleToInstall.length) { return tree; } - const corePackageJsonContent = JSON.parse(fs.readFileSync(path.resolve(__dirname, '..', '..', '..', 'package.json'), { encoding: 'utf-8' })) as PackageJson; - return ngAddPackages(moduleToInstall, { - ...options, - skipConfirmation: true, - version: corePackageJsonContent.version, - parentPackageInfo: '@o3r/core - preset setup', - dependencyType: NodeDependencyType.Dev, - workingDirectory + const dependencies = moduleToInstall.reduce((acc, dep) => { + acc[dep] = { + inManifest: [{ + range: `~${corePackageJsonContent.version}`, + types: getProjectNewDependenciesTypes(workspaceProject) + }] + }; + return acc; + }, {} as Record); + + if (options.dependenciesSetupConfig) { + options.dependenciesSetupConfig.dependencies = { + ...options.dependenciesSetupConfig.dependencies, + ...dependencies + }; + } + + return options.dependenciesSetupConfig ? noop : setupDependencies({ + projectName: options.projectName, + dependencies, + ngAddToRun: moduleToInstall }); }; } diff --git a/packages/@o3r/core/schematics/shared/presets/preset.interface.ts b/packages/@o3r/core/schematics/shared/presets/preset.interface.ts index 887333af10..d8a1daf6f7 100644 --- a/packages/@o3r/core/schematics/shared/presets/preset.interface.ts +++ b/packages/@o3r/core/schematics/shared/presets/preset.interface.ts @@ -1,6 +1,6 @@ import type { Rule } from '@angular-devkit/schematics'; import type { PresetNames } from '../../ng-add/schema'; -import type { SchematicOptionObject } from '@o3r/schematics'; +import type { SchematicOptionObject, SetupDependenciesOptions } from '@o3r/schematics'; /** Options of the preset runner */ export interface PresetOptions { @@ -9,6 +9,9 @@ export interface PresetOptions { /** Options to forward to the executed schematics */ forwardOptions?: SchematicOptionObject; + + /** Option to provide to the dependency setup helper */ + dependenciesSetupConfig?: SetupDependenciesOptions; } /** Definition of the modules preset */ diff --git a/packages/@o3r/create/README.md b/packages/@o3r/create/README.md index 2147dea3de..06d1eddccb 100644 --- a/packages/@o3r/create/README.md +++ b/packages/@o3r/create/README.md @@ -26,3 +26,10 @@ You can generate an environment with a specific package manager thanks to the `- ```shell npm create @o3r -- --package-manager=yarn [...options] ``` + +## Available options + +The generator accepts all the configurations from Angular `ng new` command, find the list [here](https://angular.io/cli/new#options). +On top of them, the following options can be provided to the initializer: + +- `--yarn-version` : specify the version of yarn to use (default: `latest`) diff --git a/packages/@o3r/create/project.json b/packages/@o3r/create/project.json index 05c690d588..a373c7d773 100644 --- a/packages/@o3r/create/project.json +++ b/packages/@o3r/create/project.json @@ -33,8 +33,7 @@ "test-int": { "executor": "@nx/jest:jest", "options": { - "jestConfig": "packages/@o3r/create/testing/jest.config.it.js", - "silent": false + "jestConfig": "packages/@o3r/create/testing/jest.config.it.js" } }, "prepare-publish": { diff --git a/packages/@o3r/create/src/index.it.spec.ts b/packages/@o3r/create/src/index.it.spec.ts index 908132ee03..1b018b8cdd 100644 --- a/packages/@o3r/create/src/index.it.spec.ts +++ b/packages/@o3r/create/src/index.it.spec.ts @@ -1,35 +1,53 @@ import { getDefaultExecSyncOptions, getPackageManager, - getYarnVersionFromRoot, + type PackageManagerConfig, packageManagerCreate, packageManagerExec, packageManagerInstall, + packageManagerRunOnProject, prepareTestEnv, + setPackagerManagerConfig, setupLocalRegistry } from '@o3r/test-helpers'; +import { existsSync, promises as fs } from 'node:fs'; import * as path from 'node:path'; -const appName = 'test-create-app'; -let baseFolderPath: string; -let appPackagePath: string; -const execAppOptions = getDefaultExecSyncOptions(); -const packageManager = getPackageManager(); +const appFolder = 'test-create-app'; +const workspaceProjectName = 'my-project'; +const o3rVersion = '999.0.0'; +let workspacePath: string; +let packageManagerConfig: PackageManagerConfig; +const execWorkspaceOptions = getDefaultExecSyncOptions(); describe('Create new otter project command', () => { setupLocalRegistry(); beforeEach(async () => { - const isYarnTest = packageManager.startsWith('yarn'); - const yarnVersion = isYarnTest ? getYarnVersionFromRoot(process.cwd()) || 'latest' : undefined; - baseFolderPath = await prepareTestEnv(appName, 'blank', yarnVersion); - appPackagePath = path.join(baseFolderPath, appName); - execAppOptions.cwd = baseFolderPath; + ({ workspacePath, packageManagerConfig, workspacePath } = (await prepareTestEnv(appFolder, {type: 'blank' }))); + execWorkspaceOptions.cwd = workspacePath; }); - test('should generate a project with an application', () => { - expect(() => packageManagerCreate(`@o3r ${appName}`, execAppOptions)).not.toThrow(); - expect(() => packageManagerInstall({ ...execAppOptions, cwd: appPackagePath })).not.toThrow(); - expect(() => packageManagerExec('ng g application my-app', { ...execAppOptions, cwd: appPackagePath })).not.toThrow(); - expect(() => packageManagerExec('ng build my-app', { ...execAppOptions, cwd: appPackagePath })).not.toThrow(); + afterAll(async () => { + try { await fs.rm(workspacePath, { recursive: true }); } catch { /* ignore error */ } + }); + + test('should generate a project with an application', async () => { + const createOptions = ['--package-manager', getPackageManager(), '--skip-confirmation', ...(packageManagerConfig.yarnVersion ? ['--yarn-version', packageManagerConfig.yarnVersion] : [])]; + const inAppPath = path.join(workspacePath, workspaceProjectName); + const execInAppOptions = {...execWorkspaceOptions, cwd: inAppPath }; + + // TODO: remove it when fixing #1356 + await fs.mkdir(inAppPath, { recursive: true }); + setPackagerManagerConfig(packageManagerConfig, execInAppOptions); + + expect(() => packageManagerCreate({ script: `@o3r@${o3rVersion}`, args: [workspaceProjectName, ...createOptions] }, execWorkspaceOptions, 'npm')).not.toThrow(); + expect(existsSync(path.join(inAppPath, 'angular.json'))).toBe(true); + expect(existsSync(path.join(inAppPath, 'package.json'))).toBe(true); + expect(() => packageManagerInstall(execInAppOptions)).not.toThrow(); + + const appName = 'test-application'; + expect(() => packageManagerExec({ script: 'ng', args: ['g', 'application', appName] }, execInAppOptions)).not.toThrow(); + expect(existsSync(path.join(inAppPath, 'project'))).toBe(false); + expect(() => packageManagerRunOnProject(appName, true, { script: 'build' }, execInAppOptions)).not.toThrow(); }); }); diff --git a/packages/@o3r/create/src/index.ts b/packages/@o3r/create/src/index.ts index 7a16f77fed..4231c24d81 100644 --- a/packages/@o3r/create/src/index.ts +++ b/packages/@o3r/create/src/index.ts @@ -6,6 +6,7 @@ import { readFileSync, writeFileSync } from 'node:fs'; import * as minimist from 'minimist'; import type { PackageJson } from 'type-fest'; + const { properties } = JSON.parse( readFileSync(require.resolve('@schematics/angular/ng-new/schema').replace(/\.js$/, '.json'), { encoding: 'utf-8' }) ) as { properties: Record }; @@ -13,6 +14,10 @@ const { version, dependencies, devDependencies } = JSON.parse( readFileSync(resolve(__dirname, 'package.json'), { encoding: 'utf-8' }) ) as PackageJson; +const optionsList = [ + 'yarn-version' +]; + const logo = ` &BPPPB & &BPGB& @@ -119,7 +124,7 @@ const isNgNewOptions = (arg: string) => { }; const schematicsCliOptions: any[][] = Object.entries(argv) - .filter(([key]) => key !== '_') + .filter(([key]) => key !== '_' && !optionsList.includes(key)) .map(([key, value]) => value === true && [key] || value === false && key.length > 1 && [`no-${key}`] || [key, value]) .map(([key, value]) => { const optionKey = key.length > 1 ? `--${key}` : `-${key}`; @@ -141,16 +146,15 @@ const createNgProject = () => { } }; -const addOtterFramework = (relativeDirectory = '.', projectPackageManager = 'npm') => { +const prepareWorkspace = (relativeDirectory = '.', projectPackageManager = 'npm') => { + const cwd = resolve(process.cwd(), relativeDirectory); + const runner = process.platform === 'win32' ? `${projectPackageManager}.cmd` : projectPackageManager; const mandatoryDependencies = [ '@angular-devkit/schematics', '@schematics/angular', '@angular-devkit/core', '@angular-devkit/architect' ]; - const cwd = resolve(process.cwd(), relativeDirectory); - const options = schematicsCliOptions - .flat(); const packageJsonPath = resolve(cwd, 'package.json'); const packageJson: PackageJson = JSON.parse( @@ -164,17 +168,46 @@ const addOtterFramework = (relativeDirectory = '.', projectPackageManager = 'npm }); writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2)); - const { error } = spawnSync(process.platform === 'win32' ? `${projectPackageManager}.cmd` : projectPackageManager, - ['exec', 'ng', 'add', `@o3r/core@~${version}`, ...(projectPackageManager === 'npm' ? ['--'] : []), ...options], { + if (projectPackageManager === 'yarn') { + const setVersionError = spawnSync(runner, ['set', 'version', argv['yarn-version'] || 'stable'], { stdio: 'inherit', cwd + }).error; + + if (setVersionError) { + // eslint-disable-next-line no-console + console.error(setVersionError); + process.exit(2); } - ); + } + + const installError = spawnSync(runner, ['install'], { + stdio: 'inherit', + cwd + }).error; + + if (installError) { + // eslint-disable-next-line no-console + console.error(installError); + process.exit(2); + } +}; + +const addOtterFramework = (relativeDirectory = '.', projectPackageManager = 'npm') => { + const cwd = resolve(process.cwd(), relativeDirectory); + const runner = process.platform === 'win32' ? `${projectPackageManager}.cmd` : projectPackageManager; + const options = schematicsCliOptions + .flat(); + + const { error } = spawnSync(runner, ['exec', 'ng', 'add', `@o3r/core@~${version}`, ...(projectPackageManager === 'npm' ? ['--'] : []), ...options], { + stdio: 'inherit', + cwd + }); if (error) { // eslint-disable-next-line no-console console.error(error); - process.exit(2); + process.exit(3); } }; @@ -182,4 +215,5 @@ const projectFolder = argv._[0]?.replaceAll(' ', '-').toLowerCase() || '.'; console.info(logo); createNgProject(); +prepareWorkspace(projectFolder, packageManager); addOtterFramework(projectFolder, packageManager); diff --git a/packages/@o3r/design/project.json b/packages/@o3r/design/project.json index 29f963fca4..98b499c44c 100644 --- a/packages/@o3r/design/project.json +++ b/packages/@o3r/design/project.json @@ -60,6 +60,12 @@ "jestConfig": "packages/@o3r/design/jest.config.js" } }, + "test-int": { + "executor": "@nx/jest:jest", + "options": { + "jestConfig": "packages/@o3r/design/testing/jest.config.it.js" + } + }, "prepare-publish": { "executor": "nx:run-script", "options": { diff --git a/packages/@o3r/design/schematics/index.it.spec.ts b/packages/@o3r/design/schematics/index.it.spec.ts index 72831205c8..6cdedbc293 100644 --- a/packages/@o3r/design/schematics/index.it.spec.ts +++ b/packages/@o3r/design/schematics/index.it.spec.ts @@ -1,27 +1,42 @@ import { getDefaultExecSyncOptions, + getGitDiff, packageManagerExec, packageManagerInstall, - packageManagerRun, + packageManagerRunOnProject, prepareTestEnv, setupLocalRegistry } from '@o3r/test-helpers'; +import * as path from 'node:path'; +import { rm } from 'node:fs/promises'; -const appName = 'test-app-design'; +const appFolder = 'test-app-design'; const o3rVersion = '999.0.0'; const execAppOptions = getDefaultExecSyncOptions(); -let appFolderPath: string; - +let workspacePath: string; +let projectName: string; +let isInWorkspace: boolean; +let untouchedProjectPath: undefined | string; describe('new otter application with Design', () => { setupLocalRegistry(); beforeAll(async () => { - appFolderPath = await prepareTestEnv(appName, 'angular-with-o3r-core'); - execAppOptions.cwd = appFolderPath; + ({ projectName, isInWorkspace, workspacePath } = await prepareTestEnv(appFolder)); + execAppOptions.cwd = workspacePath; + }); + afterAll(async () => { + try { await rm(workspacePath, { recursive: true }); } catch { /* ignore error */ } }); test('should add design to existing application', () => { - packageManagerExec(`ng add --skip-confirmation @o3r/design@${o3rVersion}`, execAppOptions); + packageManagerExec({script: 'ng', args: ['add', `@o3r/design@${o3rVersion}`, '--skip-confirmation', '--project-name', projectName]}, execAppOptions); expect(() => packageManagerInstall(execAppOptions)).not.toThrow(); - expect(() => packageManagerRun('build', execAppOptions)).not.toThrow(); + expect(() => packageManagerRunOnProject(projectName, isInWorkspace, {script: 'build'}, execAppOptions)).not.toThrow(); + + const diff = getGitDiff(workspacePath); + expect(diff.modified).toContain('package.json'); + if (untouchedProjectPath) { + const relativeUntouchedProjectPath = path.relative(workspacePath, untouchedProjectPath); + expect(diff.all.filter((file) => new RegExp(relativeUntouchedProjectPath.replace(/[\\/]+/g, '[\\\\/]')).test(file)).length).toBe(0); + } }); }); diff --git a/packages/@o3r/design/schematics/ng-add/index.ts b/packages/@o3r/design/schematics/ng-add/index.ts index 1661784b74..a8187b4664 100644 --- a/packages/@o3r/design/schematics/ng-add/index.ts +++ b/packages/@o3r/design/schematics/ng-add/index.ts @@ -1,8 +1,11 @@ import { chain, noop, type Rule } from '@angular-devkit/schematics'; import { registerGenerateCssBuilder } from './register-generate-css'; import { extractToken } from '../extract-token'; -import { setupSchematicsDefaultParams } from '@o3r/schematics'; +import { getPackageInstallConfig, setupDependencies, setupSchematicsDefaultParams } from '@o3r/schematics'; import type { NgAddSchematicsSchema } from './schema'; +import * as path from 'node:path'; + +const packageJsonPath = path.resolve(__dirname, '..', '..', 'package.json'); /** * Add Otter design to an Angular Project @@ -10,7 +13,7 @@ import type { NgAddSchematicsSchema } from './schema'; */ export function ngAdd(options: NgAddSchematicsSchema): Rule { /* ng add rules */ - return chain([ + return (tree) => chain([ registerGenerateCssBuilder(), setupSchematicsDefaultParams({ // eslint-disable-next-line @typescript-eslint/naming-convention @@ -26,6 +29,10 @@ export function ngAdd(options: NgAddSchematicsSchema): Rule { useOtterDesignToken: true } }), + setupDependencies({ + projectName: options.projectName, + dependencies: getPackageInstallConfig(packageJsonPath, tree, options.projectName) + }), options.extractDesignToken ? extractToken({ componentFilePatterns: ['**/*.scss'], includeTags: true }) : noop ]); } diff --git a/packages/@o3r/design/testing/jest.config.it.js b/packages/@o3r/design/testing/jest.config.it.js new file mode 100644 index 0000000000..693a6db125 --- /dev/null +++ b/packages/@o3r/design/testing/jest.config.it.js @@ -0,0 +1,8 @@ +const { dirname } = require('node:path'); +const getJestConfig = require('../../../../jest.config.it').getJestConfig; + +/** @type {import('ts-jest/dist/types').JestConfigWithTsJest} */ +module.exports = { + ...getJestConfig(dirname(__dirname)), + displayName: require('../package.json').name +}; diff --git a/packages/@o3r/dynamic-content/schematics/ng-add/index.ts b/packages/@o3r/dynamic-content/schematics/ng-add/index.ts index 611215c86b..3372bf67cd 100644 --- a/packages/@o3r/dynamic-content/schematics/ng-add/index.ts +++ b/packages/@o3r/dynamic-content/schematics/ng-add/index.ts @@ -1,13 +1,22 @@ -import { noop } from '@angular-devkit/schematics'; import type { Rule } from '@angular-devkit/schematics'; -import { createSchematicWithMetricsIfInstalled } from '@o3r/schematics'; +import { createSchematicWithMetricsIfInstalled, getPackageInstallConfig, setupDependencies } from '@o3r/schematics'; +import * as path from 'node:path'; +import type { NgAddSchematicsSchema } from './schema'; + +const packageJsonPath = path.resolve(__dirname, '..', '..', 'package.json'); /** * Add Otter dynamic-content to an Angular Project + * @param options */ -function ngAddFn(): Rule { +function ngAddFn(options: NgAddSchematicsSchema): Rule { /* ng add rules */ - return noop(); + return (tree) => { + return setupDependencies({ + projectName: options.projectName, + dependencies: getPackageInstallConfig(packageJsonPath, tree, options.projectName) + }); + }; } /** diff --git a/packages/@o3r/eslint-config-otter/rules/typescript/eslint-typescript.cjs b/packages/@o3r/eslint-config-otter/rules/typescript/eslint-typescript.cjs index 9920b61340..cd9b6c3735 100644 --- a/packages/@o3r/eslint-config-otter/rules/typescript/eslint-typescript.cjs +++ b/packages/@o3r/eslint-config-otter/rules/typescript/eslint-typescript.cjs @@ -2,7 +2,8 @@ module.exports = { extends: [ - 'plugin:@typescript-eslint/recommended' + 'plugin:@typescript-eslint/recommended', + 'plugin:@typescript-eslint/recommended-type-checked' ], plugins: [ '@typescript-eslint' diff --git a/packages/@o3r/eslint-config-otter/schematics/ng-add/index.ts b/packages/@o3r/eslint-config-otter/schematics/ng-add/index.ts index 8d9292858b..5d4741a540 100644 --- a/packages/@o3r/eslint-config-otter/schematics/ng-add/index.ts +++ b/packages/@o3r/eslint-config-otter/schematics/ng-add/index.ts @@ -1,5 +1,5 @@ import { chain, Rule, SchematicContext, Tree } from '@angular-devkit/schematics'; -import { createSchematicWithMetricsIfInstalled } from '@o3r/schematics'; +import { createSchematicWithMetricsIfInstalled, getPackageInstallConfig } from '@o3r/schematics'; import type { NgAddSchematicsSchema } from './schema'; import * as path from 'node:path'; import { updateLinterConfigs } from './linter'; @@ -11,43 +11,60 @@ import { updateLinterConfigs } from './linter'; function ngAddFn(options: NgAddSchematicsSchema): Rule { /* ng add rules */ return async (tree: Tree, context: SchematicContext) => { + const devDependenciesToInstall = [ + 'eslint', + '@stylistic/eslint-plugin-ts', + '@angular-eslint/builder', + '@typescript-eslint/parser', + '@typescript-eslint/eslint-plugin', + 'eslint-plugin-jsdoc', + 'eslint-plugin-prefer-arrow', + 'eslint-plugin-unicorn', + 'jsonc-eslint-parser' + ]; + try { const { - addVsCodeRecommendations, ngAddPackages, getWorkspaceConfig, getO3rPeerDeps, getProjectNewDependenciesType, ngAddPeerDependencyPackages, removePackages + getExternalDependenciesVersionRange, + addVsCodeRecommendations, + setupDependencies, + getWorkspaceConfig, + getO3rPeerDeps, + getProjectNewDependenciesTypes, + removePackages } = await import('@o3r/schematics'); const depsInfo = getO3rPeerDeps(path.resolve(__dirname, '..', '..', 'package.json'), true, /^@(?:o3r|ama-sdk|eslint-)/); const workspaceProject = options.projectName ? getWorkspaceConfig(tree)?.projects[options.projectName] : undefined; - const dependencyType = getProjectNewDependenciesType(workspaceProject); const linterSchematicsFolder = path.resolve(__dirname, '..'); // eslint-disable-next-line @typescript-eslint/naming-convention const {NodeDependencyType} = await import('@schematics/angular/utility/dependencies'); - const workingDirectory = options?.projectName && getWorkspaceConfig(tree)?.projects[options.projectName]?.root || '.'; + const packageJsonPath = path.resolve(__dirname, '..', '..', 'package.json'); + const dependencies = depsInfo.o3rPeerDeps.reduce((acc, dep) => { + acc[dep] = { + inManifest: [{ + range: `~${depsInfo.packageVersion}`, + types: getProjectNewDependenciesTypes(workspaceProject) + }] + }; + return acc; + }, getPackageInstallConfig(packageJsonPath, tree, options.projectName, true)); + Object.entries(getExternalDependenciesVersionRange(devDependenciesToInstall, packageJsonPath)) + .forEach(([dep, range]) => { + dependencies[dep] = { + inManifest: [{ + range, + types: [NodeDependencyType.Dev] + }] + }; + }); + return () => chain([ removePackages(['@otter/eslint-config-otter', '@otter/eslint-plugin']), - ngAddPackages(depsInfo.o3rPeerDeps, { - skipConfirmation: true, - version: depsInfo.packageVersion, - parentPackageInfo: depsInfo.packageName, + setupDependencies({ projectName: options.projectName, - dependencyType, - workingDirectory + dependencies, + ngAddToRun: depsInfo.o3rPeerDeps }), - ngAddPeerDependencyPackages( - [ - 'eslint', - '@angular-eslint/builder', - '@typescript-eslint/parser', - '@typescript-eslint/eslint-plugin', - 'eslint-plugin-jsdoc', - 'eslint-plugin-prefer-arrow', - 'eslint-plugin-unicorn', - 'jsonc-eslint-parser' - ], - path.resolve(__dirname, '..', '..', 'package.json'), - NodeDependencyType.Dev, - {...options, workingDirectory, skipNgAddSchematicRun: true}, - '@o3r/eslint-config-otter - peer installs' - ), addVsCodeRecommendations(['dbaeumer.vscode-eslint', 'stylelint.vscode-stylelint']), updateLinterConfigs(options, linterSchematicsFolder) ])(tree, context); diff --git a/packages/@o3r/eslint-plugin/package.json b/packages/@o3r/eslint-plugin/package.json index 0cafbaf6d0..9cde7842ea 100644 --- a/packages/@o3r/eslint-plugin/package.json +++ b/packages/@o3r/eslint-plugin/package.json @@ -20,7 +20,7 @@ "build": "yarn nx build eslint-plugin", "postbuild": "patch-package-json-main", "prepare:publish": "prepare-publish ./dist", - "prepare:build:builders": "yarn cpy './collection.json' dist", + "prepare:build:builders": "yarn cpy 'schematics/**/*.json' 'schematics/**/templates/**' dist/schematics && yarn cpy './collection.json' dist", "build:builders": "tsc -b tsconfig.builders.json --pretty && yarn generate-cjs-manifest" }, "dependencies": { diff --git a/packages/@o3r/extractors/project.json b/packages/@o3r/extractors/project.json index e700fbdcf0..17f42b7640 100644 --- a/packages/@o3r/extractors/project.json +++ b/packages/@o3r/extractors/project.json @@ -53,8 +53,7 @@ "test-int": { "executor": "@nx/jest:jest", "options": { - "jestConfig": "packages/@o3r/extractors/testing/jest.config.it.js", - "silent": false + "jestConfig": "packages/@o3r/extractors/testing/jest.config.it.js" } }, "prepare-publish": { diff --git a/packages/@o3r/extractors/schematics/index.it.spec.ts b/packages/@o3r/extractors/schematics/index.it.spec.ts index 7441654ac8..1dcedc72e0 100644 --- a/packages/@o3r/extractors/schematics/index.it.spec.ts +++ b/packages/@o3r/extractors/schematics/index.it.spec.ts @@ -3,29 +3,41 @@ import { getGitDiff, packageManagerExec, packageManagerInstall, - packageManagerRun, + packageManagerRunOnProject, prepareTestEnv, setupLocalRegistry } from '@o3r/test-helpers'; +import * as path from 'node:path'; +import { rm } from 'node:fs/promises'; -const appName = 'test-app-extractors'; +const appFolder = 'test-app-extractors'; const o3rVersion = '999.0.0'; const execAppOptions = getDefaultExecSyncOptions(); -let appFolderPath: string; - +let workspacePath: string; +let projectName: string; +let isInWorkspace: boolean; +let untouchedProjectPath: undefined | string; describe('new otter application with extractors', () => { setupLocalRegistry(); beforeAll(async () => { - appFolderPath = await prepareTestEnv(appName, 'angular-with-o3r-core'); - execAppOptions.cwd = appFolderPath; + ({ workspacePath, projectName, isInWorkspace, untouchedProjectPath } = await prepareTestEnv(appFolder)); + execAppOptions.cwd = workspacePath; + }); + afterAll(async () => { + try { await rm(workspacePath, { recursive: true }); } catch { /* ignore error */ } }); test('should add extractors to existing application', () => { - packageManagerExec(`ng add --skip-confirmation @o3r/extractors@${o3rVersion}`, execAppOptions); + packageManagerExec({script: 'ng', args: ['add', `@o3r/extractors@${o3rVersion}`, '--skip-confirmation', '--project-name', projectName]}, execAppOptions); - const diff = getGitDiff(appFolderPath); + const diff = getGitDiff(workspacePath); expect(diff.modified).toContain('package.json'); + if (untouchedProjectPath) { + const relativeUntouchedProjectPath = path.relative(workspacePath, untouchedProjectPath); + expect(diff.all.filter((file) => new RegExp(relativeUntouchedProjectPath.replace(/[\\/]+/g, '[\\\\/]')).test(file)).length).toBe(0); + } + expect(() => packageManagerInstall(execAppOptions)).not.toThrow(); - expect(() => packageManagerRun('build', execAppOptions)).not.toThrow(); + expect(() => packageManagerRunOnProject(projectName, isInWorkspace, {script: 'build'}, execAppOptions)).not.toThrow(); }); }); diff --git a/packages/@o3r/extractors/schematics/ng-add/index.ts b/packages/@o3r/extractors/schematics/ng-add/index.ts index 83238b177f..86b9d38e0c 100644 --- a/packages/@o3r/extractors/schematics/ng-add/index.ts +++ b/packages/@o3r/extractors/schematics/ng-add/index.ts @@ -12,21 +12,25 @@ import type { NgAddSchematicsSchema } from './schema'; function ngAddFn(options: NgAddSchematicsSchema): Rule { return async (tree: Tree, context: SchematicContext) => { try { - const { getProjectNewDependenciesType, ngAddPackages, getO3rPeerDeps, getWorkspaceConfig } = await import('@o3r/schematics'); + const { getPackageInstallConfig, getProjectNewDependenciesTypes, setupDependencies, getO3rPeerDeps, getWorkspaceConfig } = await import('@o3r/schematics'); const packageJsonPath = path.resolve(__dirname, '..', '..', 'package.json'); const depsInfo = getO3rPeerDeps(packageJsonPath); const workspaceProject = options.projectName ? getWorkspaceConfig(tree)?.projects[options.projectName] : undefined; - const workingDirectory = workspaceProject?.root || '.'; - const dependencyType = getProjectNewDependenciesType(workspaceProject); + const dependencies = depsInfo.o3rPeerDeps.reduce((acc, dep) => { + acc[dep] = { + inManifest: [{ + range: `~${depsInfo.packageVersion}`, + types: getProjectNewDependenciesTypes(workspaceProject) + }] + }; + return acc; + }, getPackageInstallConfig(packageJsonPath, tree, options.projectName, true)); return chain([ - (t, c) => ngAddPackages(depsInfo.o3rPeerDeps, { - skipConfirmation: true, - version: depsInfo.packageVersion, - parentPackageInfo: `${depsInfo.packageName!} - setup`, + setupDependencies({ projectName: options.projectName, - dependencyType, - workingDirectory - })(t, c), + dependencies, + ngAddToRun: depsInfo.o3rPeerDeps + }), updateCmsAdapter(options, __dirname) ]); } catch (e) { diff --git a/packages/@o3r/forms/schematics/ng-add/index.ts b/packages/@o3r/forms/schematics/ng-add/index.ts index a83282c244..cb3229a651 100644 --- a/packages/@o3r/forms/schematics/ng-add/index.ts +++ b/packages/@o3r/forms/schematics/ng-add/index.ts @@ -1,13 +1,22 @@ -import { noop } from '@angular-devkit/schematics'; import type { Rule } from '@angular-devkit/schematics'; -import { createSchematicWithMetricsIfInstalled } from '@o3r/schematics'; +import { createSchematicWithMetricsIfInstalled, getPackageInstallConfig, setupDependencies } from '@o3r/schematics'; +import type { NgAddSchematicsSchema } from './schema'; +import * as path from 'node:path'; + +const packageJsonPath = path.resolve(__dirname, '..', '..', 'package.json'); /** * Add Otter forms to an Angular Project + * @param options */ -function ngAddFn(): Rule { +function ngAddFn(options: NgAddSchematicsSchema): Rule { /* ng add rules */ - return noop(); + return (tree) => { + return setupDependencies({ + projectName: options.projectName, + dependencies: getPackageInstallConfig(packageJsonPath, tree, options.projectName) + }); + }; } /** diff --git a/packages/@o3r/localization/package.json b/packages/@o3r/localization/package.json index 0038fad9cd..c476011520 100644 --- a/packages/@o3r/localization/package.json +++ b/packages/@o3r/localization/package.json @@ -46,6 +46,7 @@ "@o3r/schematics": "workspace:^", "@schematics/angular": "~17.2.0", "chokidar": "^3.5.2", + "globby": "^11.1.0", "intl-messageformat": "~10.5.0", "rxjs": "^7.8.1", "typescript": "~5.2.2" @@ -72,6 +73,9 @@ "chokidar": { "optional": true }, + "globby": { + "optional": true + }, "typescript": { "optional": true } @@ -141,6 +145,7 @@ "semver": "^7.5.2", "ts-jest": "~29.1.1", "ts-node": "~10.9.1", + "type-fest": "^4.10.2", "typescript": "~5.2.2", "unionfs": "~4.5.1", "zone.js": "~0.14.2" diff --git a/packages/@o3r/localization/project.json b/packages/@o3r/localization/project.json index cdfbd7699c..e944992696 100644 --- a/packages/@o3r/localization/project.json +++ b/packages/@o3r/localization/project.json @@ -63,8 +63,7 @@ "test-int": { "executor": "@nx/jest:jest", "options": { - "jestConfig": "packages/@o3r/localization/testing/jest.config.it.js", - "silent": false + "jestConfig": "packages/@o3r/localization/testing/jest.config.it.js" } }, "prepare-publish": { diff --git a/packages/@o3r/localization/schematics/index.it.spec.ts b/packages/@o3r/localization/schematics/index.it.spec.ts index 90d7ff6e61..642cd635a1 100644 --- a/packages/@o3r/localization/schematics/index.it.spec.ts +++ b/packages/@o3r/localization/schematics/index.it.spec.ts @@ -4,37 +4,50 @@ import { getGitDiff, packageManagerExec, packageManagerInstall, - packageManagerRun, + packageManagerRunOnProject, prepareTestEnv, setupLocalRegistry } from '@o3r/test-helpers'; +import { rm } from 'node:fs/promises'; +import * as path from 'node:path'; -const appName = 'test-app-localization'; +const appFolder = 'test-app-localization'; const o3rVersion = '999.0.0'; const execAppOptions = getDefaultExecSyncOptions(); -let appFolderPath: string; - +let projectPath: string; +let workspacePath: string; +let projectName: string; +let isInWorkspace: boolean; +let untouchedProjectPath: undefined | string; describe('new otter application with localization', () => { setupLocalRegistry(); beforeAll(async () => { - appFolderPath = await prepareTestEnv(appName, 'angular-with-o3r-core'); - execAppOptions.cwd = appFolderPath; + ({ projectPath, workspacePath, projectName, isInWorkspace, untouchedProjectPath } = await prepareTestEnv(appFolder)); + execAppOptions.cwd = workspacePath; + }); + afterAll(async () => { + try { await rm(workspacePath, { recursive: true }); } catch { /* ignore error */ } }); test('should add localization to existing application', async () => { - packageManagerExec(`ng add --skip-confirmation @o3r/localization@${o3rVersion}`, execAppOptions); + const relativeProjectPath = path.relative(workspacePath, projectPath); + packageManagerExec({script: 'ng', args: ['add', `@o3r/localization@${o3rVersion}`, '--skip-confirmation', '--project-name', projectName]}, execAppOptions); - packageManagerExec('ng g @o3r/core:component test-component --use-localization=false', execAppOptions); - packageManagerExec( - 'ng g @o3r/localization:add-localization --activate-dummy --path="src/components/test-component/test-component.component.ts"', - execAppOptions); - await addImportToAppModule(appFolderPath, 'TestComponentModule', 'src/components/test-component'); + const componentPath = path.normalize(path.join(relativeProjectPath, 'src/components/test-component/test-component.component.ts')); + packageManagerExec({script: 'ng', args: ['g', '@o3r/core:component', 'test-component', '--project-name', projectName, '--use-localization', 'false']}, execAppOptions); + packageManagerExec({script: 'ng', args: ['g', '@o3r/localization:add-localization', '--activate-dummy', '--path', componentPath]}, execAppOptions); + await addImportToAppModule(projectPath, 'TestComponentModule', 'src/components/test-component'); - const diff = getGitDiff(appFolderPath); + const diff = getGitDiff(workspacePath); expect(diff.modified).toContain('package.json'); - expect(diff.added).toContain('src/components/test-component/test-component.localization.json'); - expect(diff.added).toContain('src/components/test-component/test-component.translation.ts'); + expect(diff.added).toContain(path.join(relativeProjectPath, 'src/components/test-component/test-component.localization.json').replace(/[\\/]+/g, '/')); + expect(diff.added).toContain(path.join(relativeProjectPath, 'src/components/test-component/test-component.translation.ts').replace(/[\\/]+/g, '/')); + + if (untouchedProjectPath) { + const relativeUntouchedProjectPath = path.relative(workspacePath, untouchedProjectPath); + expect(diff.all.filter((file) => new RegExp(relativeUntouchedProjectPath.replace(/[\\/]+/g, '[\\\\/]')).test(file)).length).toBe(0); + } expect(() => packageManagerInstall(execAppOptions)).not.toThrow(); - expect(() => packageManagerRun('build', execAppOptions)).not.toThrow(); + expect(() => packageManagerRunOnProject(projectName, isInWorkspace, {script: 'build'}, execAppOptions)).not.toThrow(); }); }); diff --git a/packages/@o3r/localization/schematics/localization-base/index.ts b/packages/@o3r/localization/schematics/localization-base/index.ts index c5632a3300..bde8f9464b 100644 --- a/packages/@o3r/localization/schematics/localization-base/index.ts +++ b/packages/@o3r/localization/schematics/localization-base/index.ts @@ -1,18 +1,19 @@ import { apply, chain, MergeStrategy, mergeWith, move, noop, Rule, SchematicContext, template, Tree, url } from '@angular-devkit/schematics'; import { createSchematicWithMetricsIfInstalled, + type DependencyToAdd, findFirstNodeOfKind, getAppModuleFilePath, getModuleIndex, getPackageManagerRunner, - getProjectNewDependenciesType, + getProjectNewDependenciesTypes, getTemplateFolder, getWorkspaceConfig, ignorePatterns, - ngAddPeerDependencyPackages, insertBeforeModule as o3rInsertBeforeModule, insertImportToModuleFile as o3rInsertImportToModuleFile, readPackageJson, + setupDependencies, writeAngularJson } from '@o3r/schematics'; import { @@ -22,13 +23,12 @@ import { import { addRootImport, addRootProvider } from '@schematics/angular/utility'; import { InsertChange } from '@schematics/angular/utility/change'; import * as path from 'node:path'; +import * as fs from 'node:fs'; import * as ts from 'typescript'; +import type { PackageJson } from 'type-fest'; const packageJsonPath = path.resolve(__dirname, '..', '..', 'package.json'); -const ngxTranslateCoreDep = '@ngx-translate/core'; -const intlMessageFormatDep = 'intl-messageformat'; -const formatjsIntlNumberformatDep = '@formatjs/intl-numberformat'; -const angularCdkDep = '@angular/cdk'; +const ownPackageJson = JSON.parse(fs.readFileSync(packageJsonPath, { encoding: 'utf-8' })) as PackageJson; /** * Add Otter localization support @@ -437,20 +437,24 @@ export function updateLocalization(options: { projectName?: string | null | unde /** * Add location required dependencies * @param tree - * @param _context - * @param context */ - const addDependencies: Rule = (tree: Tree, context: SchematicContext) => { + const addDependencies: Rule = (tree: Tree) => { const workspaceProject = options.projectName ? getWorkspaceConfig(tree)?.projects[options.projectName] : undefined; - const workingDirectory = workspaceProject?.root || '.'; - const type = getProjectNewDependenciesType(workspaceProject); - const generatorDependencies = [ngxTranslateCoreDep, intlMessageFormatDep, formatjsIntlNumberformatDep, angularCdkDep]; - try { - return ngAddPeerDependencyPackages(generatorDependencies, packageJsonPath, type, {...options, workingDirectory, skipNgAddSchematicRun: true})(tree, context); - } catch (e: any) { - context.logger.warn(`Could not find generatorDependencies ${generatorDependencies.join(', ')} in file ${packageJsonPath}`); - return tree; - } + const types = getProjectNewDependenciesTypes(workspaceProject); + const generatorDependencies = ['@ngx-translate/core', 'intl-messageformat', '@formatjs/intl-numberformat', '@angular/cdk']; + const dependencies = generatorDependencies.reduce((acc, dep) => { + acc[dep] = { + inManifest: [{ + range: ownPackageJson.peerDependencies![dep], + types + }] + }; + return acc; + }, {} as Record); + return setupDependencies({ + projectName: options.projectName || undefined, + dependencies + }); }; // Ignore generated CMS metadata diff --git a/packages/@o3r/localization/schematics/ng-add/index.ts b/packages/@o3r/localization/schematics/ng-add/index.ts index 432e5fd086..77a672ee75 100644 --- a/packages/@o3r/localization/schematics/ng-add/index.ts +++ b/packages/@o3r/localization/schematics/ng-add/index.ts @@ -6,6 +6,11 @@ import { updateCmsAdapter } from '../cms-adapter'; import type { NgAddSchematicsSchema } from './schema'; import { registerDevtools } from './helpers/devtools-registration'; +const dependenciesToInstall = [ + 'chokidar', + 'globby' +]; + /** * Add Otter localization to an Angular Project * @param options for the dependencies installations @@ -13,7 +18,15 @@ import { registerDevtools } from './helpers/devtools-registration'; function ngAddFn(options: NgAddSchematicsSchema): Rule { return async (tree: Tree, context: SchematicContext) => { try { - const { applyEsLintFix, install, getProjectNewDependenciesType, getWorkspaceConfig, ngAddPackages, ngAddPeerDependencyPackages, getO3rPeerDeps} = await import('@o3r/schematics'); + const { + applyEsLintFix, + getPackageInstallConfig, + getProjectNewDependenciesTypes, + getWorkspaceConfig, + setupDependencies, + getO3rPeerDeps, + getExternalDependenciesVersionRange + } = await import('@o3r/schematics'); const {updateI18n, updateLocalization} = await import('../localization-base'); const packageJsonPath = path.resolve(__dirname, '..', '..', 'package.json'); const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, { encoding: 'utf-8' })); @@ -23,35 +36,39 @@ function ngAddFn(options: NgAddSchematicsSchema): Rule { // eslint-disable-next-line @typescript-eslint/naming-convention const { NodeDependencyType } = await import('@schematics/angular/utility/dependencies'); const workspaceProject = options.projectName ? getWorkspaceConfig(tree)?.projects[options.projectName] : undefined; - const workingDirectory = workspaceProject?.root || '.'; - const dependencyType = getProjectNewDependenciesType(workspaceProject); + const dependencies = depsInfo.o3rPeerDeps.reduce((acc, dep) => { + acc[dep] = { + inManifest: [{ + range: `~${depsInfo.packageVersion}`, + types: getProjectNewDependenciesTypes(workspaceProject) + }] + }; + return acc; + }, getPackageInstallConfig(packageJsonPath, tree, options.projectName)); + Object.entries(getExternalDependenciesVersionRange(dependenciesToInstall, packageJsonPath)).forEach(([dep, range]) => { + dependencies[dep] = { + inManifest: [{ + range, + types: [NodeDependencyType.Dev] + }] + }; + }); const registerDevtoolRule = await registerDevtools(options); return () => chain([ updateLocalization(options, __dirname), updateI18n(options), options.skipLinter ? noop() : applyEsLintFix(), - // install ngx-translate and message format dependencies - options.skipInstall ? noop : install, - (t, c) => ngAddPackages(depsInfo.o3rPeerDeps, { - skipConfirmation: true, - version: depsInfo.packageVersion, - parentPackageInfo: `${depsInfo.packageName!} - setup`, + setupDependencies({ projectName: options.projectName, - dependencyType, - workingDirectory - })(t, c), - ngAddPeerDependencyPackages( - ['chokidar'], packageJsonPath, NodeDependencyType.Dev, {...options, workingDirectory, skipNgAddSchematicRun: true}, '@o3r/localization - install builder dependency'), + dependencies, + ngAddToRun: depsInfo.o3rPeerDeps + }), updateCmsAdapter(options), registerPackageCollectionSchematics(packageJson), setupSchematicsDefaultParams({ // eslint-disable-next-line @typescript-eslint/naming-convention - '@o3r/core:component': { - useLocalization: undefined - }, - // eslint-disable-next-line @typescript-eslint/naming-convention - '@o3r/core:component-presenter': { - useLocalization: undefined + '@o3r/core:component*': { + useLocalization: true } }), registerDevtoolRule diff --git a/packages/@o3r/logger/schematics/ng-add/index.ts b/packages/@o3r/logger/schematics/ng-add/index.ts index a2e9a32335..d02f47dd07 100644 --- a/packages/@o3r/logger/schematics/ng-add/index.ts +++ b/packages/@o3r/logger/schematics/ng-add/index.ts @@ -1,13 +1,22 @@ -import { noop } from '@angular-devkit/schematics'; import type { Rule } from '@angular-devkit/schematics'; -import { createSchematicWithMetricsIfInstalled } from '@o3r/schematics'; +import { createSchematicWithMetricsIfInstalled, getPackageInstallConfig, setupDependencies } from '@o3r/schematics'; +import type { NgAddSchematicsSchema } from './schema'; +import * as path from 'node:path'; + +const packageJsonPath = path.resolve(__dirname, '..', '..', 'package.json'); /** * Add Otter logger to an Angular Project + * @param options */ -function ngAddFn(): Rule { +function ngAddFn(options: NgAddSchematicsSchema): Rule { /* ng add rules */ - return noop(); + return (tree) => { + return setupDependencies({ + projectName: options.projectName, + dependencies: getPackageInstallConfig(packageJsonPath, tree, options.projectName) + }); + }; } /** diff --git a/packages/@o3r/mobile/schematics/ng-add/index.ts b/packages/@o3r/mobile/schematics/ng-add/index.ts index 823c64e04c..f80e0027be 100644 --- a/packages/@o3r/mobile/schematics/ng-add/index.ts +++ b/packages/@o3r/mobile/schematics/ng-add/index.ts @@ -1,8 +1,10 @@ import { chain, Rule, SchematicContext, Tree } from '@angular-devkit/schematics'; -import { createSchematicWithMetricsIfInstalled, getProjectNewDependenciesType } from '@o3r/schematics'; +import { createSchematicWithMetricsIfInstalled } from '@o3r/schematics'; import * as path from 'node:path'; import type { NgAddSchematicsSchema } from './schema'; +const packageJsonPath = path.resolve(__dirname, '..', '..', 'package.json'); + /** * Add Otter mobile to an Angular Project * @param options ng add options @@ -11,20 +13,24 @@ function ngAddFn(options: NgAddSchematicsSchema): Rule { /* ng add rules */ return async (tree: Tree, context: SchematicContext) => { try { - const { ngAddPackages, getO3rPeerDeps, getWorkspaceConfig, removePackages } = await import('@o3r/schematics'); + const { getPackageInstallConfig, getProjectNewDependenciesTypes, setupDependencies, getO3rPeerDeps, getWorkspaceConfig, removePackages } = await import('@o3r/schematics'); const depsInfo = getO3rPeerDeps(path.resolve(__dirname, '..', '..', 'package.json')); const workspaceProject = options.projectName ? getWorkspaceConfig(tree)?.projects[options.projectName] : undefined; - const workingDirectory = workspaceProject?.root || '.'; - const dependencyType = getProjectNewDependenciesType(workspaceProject); + const dependencies = depsInfo.o3rPeerDeps.reduce((acc, dep) => { + acc[dep] = { + inManifest: [{ + range: `~${depsInfo.packageVersion}`, + types: getProjectNewDependenciesTypes(workspaceProject) + }] + }; + return acc; + }, getPackageInstallConfig(packageJsonPath, tree, options.projectName)); return () => chain([ removePackages(['@otter/mobile']), - ngAddPackages(depsInfo.o3rPeerDeps, { - skipConfirmation: true, - version: depsInfo.packageVersion, - parentPackageInfo: depsInfo.packageName, + setupDependencies({ projectName: options.projectName, - dependencyType, - workingDirectory + dependencies, + ngAddToRun: depsInfo.o3rPeerDeps }) ])(tree, context); diff --git a/packages/@o3r/routing/schematics/ng-add/index.ts b/packages/@o3r/routing/schematics/ng-add/index.ts index 533c0a492e..1707cca607 100644 --- a/packages/@o3r/routing/schematics/ng-add/index.ts +++ b/packages/@o3r/routing/schematics/ng-add/index.ts @@ -1,13 +1,22 @@ -import { noop } from '@angular-devkit/schematics'; import type { Rule } from '@angular-devkit/schematics'; -import { createSchematicWithMetricsIfInstalled } from '@o3r/schematics'; +import { createSchematicWithMetricsIfInstalled, getPackageInstallConfig, setupDependencies } from '@o3r/schematics'; +import type { NgAddSchematicsSchema } from './schema'; +import * as path from 'node:path'; + +const packageJsonPath = path.resolve(__dirname, '..', '..', 'package.json'); /** * Add Otter routing to an Angular Project + * @param options */ -function ngAddFn(): Rule { +function ngAddFn(options: NgAddSchematicsSchema): Rule { /* ng add rules */ - return noop(); + return (tree) => { + return setupDependencies({ + projectName: options.projectName, + dependencies: getPackageInstallConfig(packageJsonPath, tree, options.projectName) + }); + }; } /** diff --git a/packages/@o3r/rules-engine/project.json b/packages/@o3r/rules-engine/project.json index 080b71c372..ee32756f40 100644 --- a/packages/@o3r/rules-engine/project.json +++ b/packages/@o3r/rules-engine/project.json @@ -99,8 +99,7 @@ "test-int": { "executor": "@nx/jest:jest", "options": { - "jestConfig": "packages/@o3r/rules-engine/testing/jest.config.it.js", - "silent": false + "jestConfig": "packages/@o3r/rules-engine/testing/jest.config.it.js" } }, "prepare-publish": { diff --git a/packages/@o3r/rules-engine/schematics/index.it.spec.ts b/packages/@o3r/rules-engine/schematics/index.it.spec.ts index 6c48d4ed41..58b26bef12 100644 --- a/packages/@o3r/rules-engine/schematics/index.it.spec.ts +++ b/packages/@o3r/rules-engine/schematics/index.it.spec.ts @@ -4,33 +4,48 @@ import { getGitDiff, packageManagerExec, packageManagerInstall, - packageManagerRun, + packageManagerRunOnProject, prepareTestEnv, setupLocalRegistry } from '@o3r/test-helpers'; +import { rm } from 'node:fs/promises'; +import * as path from 'node:path'; -const appName = 'test-app-rules-engine'; +const appFolder = 'test-app-rules-engine'; const o3rVersion = '999.0.0'; const execAppOptions = getDefaultExecSyncOptions(); -let appFolderPath: string; - +let projectPath: string; +let workspacePath: string; +let projectName: string; +let isInWorkspace: boolean; +let untouchedProjectPath: undefined | string; describe('new otter application with rules-engine', () => { setupLocalRegistry(); beforeAll(async () => { - appFolderPath = await prepareTestEnv(appName, 'angular-with-o3r-core'); - execAppOptions.cwd = appFolderPath; + ({ projectPath, workspacePath, projectName, isInWorkspace, untouchedProjectPath } = await prepareTestEnv(appFolder)); + execAppOptions.cwd = workspacePath; + }); + afterAll(async () => { + try { await rm(workspacePath, { recursive: true }); } catch { /* ignore error */ } }); test('should add rules engine to existing application', async () => { - packageManagerExec(`ng add --skip-confirmation @o3r/rules-engine@${o3rVersion} --enable-metadata-extract`, execAppOptions); + const relativeProjectPath = path.relative(workspacePath, projectPath); + packageManagerExec({script: 'ng', args: ['add', `@o3r/rules-engine@${o3rVersion}`, '--enable-metadata-extract', '--project-name', projectName, '--skip-confirmation']}, execAppOptions); - packageManagerExec('ng g @o3r/core:component test-component --activate-dummy --use-rules-engine=false', execAppOptions); - packageManagerExec('ng g @o3r/rules-engine:rules-engine-to-component --path=src/components/test-component/test-component.component.ts', execAppOptions); - await addImportToAppModule(appFolderPath, 'TestComponentModule', 'src/components/test-component'); + const componentPath = path.normalize(path.join(relativeProjectPath, 'src/components/test-component/test-component.component.ts')); + packageManagerExec({script: 'ng', args: ['g', '@o3r/core:component', 'test-component', '--activate-dummy', '--use-rules-engine', 'false', '--project-name', projectName]}, execAppOptions); + packageManagerExec({script: 'ng', args: ['g', '@o3r/rules-engine:rules-engine-to-component', '--path', componentPath]}, execAppOptions); + await addImportToAppModule(projectPath, 'TestComponentModule', 'src/components/test-component'); - const diff = getGitDiff(appFolderPath); + const diff = getGitDiff(workspacePath); expect(diff.modified).toContain('package.json'); + if (untouchedProjectPath) { + const relativeUntouchedProjectPath = path.relative(workspacePath, untouchedProjectPath); + expect(diff.all.filter((file) => new RegExp(relativeUntouchedProjectPath.replace(/[\\/]+/g, '[\\\\/]')).test(file)).length).toBe(0); + } + expect(() => packageManagerInstall(execAppOptions)).not.toThrow(); - expect(() => packageManagerRun('build', execAppOptions)).not.toThrow(); + expect(() => packageManagerRunOnProject(projectName, isInWorkspace, {script: 'build'}, execAppOptions)).not.toThrow(); }); }); diff --git a/packages/@o3r/rules-engine/schematics/ng-add/index.ts b/packages/@o3r/rules-engine/schematics/ng-add/index.ts index 181b2e8cdb..d01baf56bb 100644 --- a/packages/@o3r/rules-engine/schematics/ng-add/index.ts +++ b/packages/@o3r/rules-engine/schematics/ng-add/index.ts @@ -6,6 +6,10 @@ import * as path from 'node:path'; import * as fs from 'node:fs'; import { registerDevtools } from './helpers/devtools-registration'; +const devDependenciesToInstall = [ + 'jsonpath-plus' +]; + /** * Add Otter rules-engine to an Angular Project * @param options @@ -15,12 +19,13 @@ function ngAddFn(options: NgAddSchematicsSchema): Rule { return async (tree: Tree, context: SchematicContext) => { try { const { - ngAddPackages, + setupDependencies, + getPackageInstallConfig, getDefaultOptionsForSchematic, getO3rPeerDeps, - getProjectNewDependenciesType, + getProjectNewDependenciesTypes, getWorkspaceConfig, - ngAddPeerDependencyPackages, + getExternalDependenciesVersionRange, removePackages, setupSchematicsDefaultParams } = await import('@o3r/schematics'); @@ -32,8 +37,24 @@ function ngAddFn(options: NgAddSchematicsSchema): Rule { depsInfo.o3rPeerDeps = [...depsInfo.o3rPeerDeps , '@o3r/extractors']; } const workspaceProject = options.projectName ? getWorkspaceConfig(tree)?.projects[options.projectName] : undefined; - const workingDirectory = workspaceProject?.root || '.'; - const dependencyType = getProjectNewDependenciesType(workspaceProject); + const dependencies = depsInfo.o3rPeerDeps.reduce((acc, dep) => { + acc[dep] = { + inManifest: [{ + range: `~${depsInfo.packageVersion}`, + types: getProjectNewDependenciesTypes(workspaceProject) + }] + }; + return acc; + }, getPackageInstallConfig(packageJsonPath, tree, options.projectName)); + Object.entries(getExternalDependenciesVersionRange(devDependenciesToInstall, packageJsonPath)) + .forEach(([dep, range]) => { + dependencies[dep] = { + inManifest: [{ + range, + types: getProjectNewDependenciesTypes(workspaceProject) + }] + }; + }); const rule = chain([ registerPackageCollectionSchematics(packageJson), setupSchematicsDefaultParams({ @@ -47,14 +68,10 @@ function ngAddFn(options: NgAddSchematicsSchema): Rule { } }), removePackages(['@otter/rules-engine', '@otter/rules-engine-core']), - ngAddPeerDependencyPackages(['jsonpath-plus'], packageJsonPath, dependencyType, {...options, workingDirectory, skipNgAddSchematicRun: true}, '@o3r/rules-engine - install builder dependency'), - ngAddPackages(depsInfo.o3rPeerDeps, { - skipConfirmation: true, - version: depsInfo.packageVersion, - parentPackageInfo: depsInfo.packageName, + setupDependencies({ projectName: options.projectName, - dependencyType, - workingDirectory + dependencies, + ngAddToRun: depsInfo.o3rPeerDeps }), ...(options.enableMetadataExtract ? [updateCmsAdapter(options)] : []), await registerDevtools(options) diff --git a/packages/@o3r/schematics/schematics/ng-add/index.ts b/packages/@o3r/schematics/schematics/ng-add/index.ts index 75185e543c..c4cac0fe06 100644 --- a/packages/@o3r/schematics/schematics/ng-add/index.ts +++ b/packages/@o3r/schematics/schematics/ng-add/index.ts @@ -1,9 +1,8 @@ -import type { Rule, SchematicContext, Tree } from '@angular-devkit/schematics'; -import * as fs from 'node:fs'; +import type { Rule } from '@angular-devkit/schematics'; +import { type DependencyToAdd, getExternalDependenciesVersionRange, setupDependencies } from '@o3r/schematics'; +import { NodeDependencyType } from '@schematics/angular/utility/dependencies'; import * as path from 'node:path'; -import { lastValueFrom } from 'rxjs'; -import type { PackageJson } from 'type-fest'; -import { AddDevInstall, createSchematicWithMetricsIfInstalled, getWorkspaceConfig } from '../../src/public_api'; +import { createSchematicWithMetricsIfInstalled } from '@o3r/schematics'; import type { NgAddSchematicsSchema } from './schema'; /** @@ -12,30 +11,32 @@ import type { NgAddSchematicsSchema } from './schema'; */ function ngAddFn(options: NgAddSchematicsSchema): Rule { const schematicsDependencies = ['@angular-devkit/architect', '@angular-devkit/schematics', '@angular-devkit/core', '@schematics/angular', 'globby']; - return () => async (tree: Tree, context: SchematicContext): Promise => { - context.logger.info('Running ng add for schematics'); + return () => async (): Promise => { const packageJsonPath = path.resolve(__dirname, '..', '..', 'package.json'); - const treePackageJson = tree.readJson('./package.json') as PackageJson; - const packageJsonContent: PackageJson = JSON.parse(fs.readFileSync(packageJsonPath, {encoding: 'utf-8'})); - const getDependencyVersion = (dependency: string) => packageJsonContent?.dependencies?.[dependency] || packageJsonContent?.peerDependencies?.[dependency]; - const workspaceProject = options.projectName ? getWorkspaceConfig(tree)?.projects[options.projectName] : undefined; - const workingDirectory = workspaceProject?.root || '.'; - let packageName = ''; - for (const dependency of schematicsDependencies) { - const version = getDependencyVersion(dependency); - context.logger.info(`Installing ${dependency}${version || ''}`); - treePackageJson.devDependencies = {...treePackageJson.devDependencies, [dependency]: version}; - packageName = `${packageName} ${dependency}${version ? '@' + version : ''}`; - } - context.addTask(new AddDevInstall({ - hideOutput: false, - packageName, - workingDirectory, - quiet: false - } as any)); - await lastValueFrom(context.engine.executePostTasks()); - tree.overwrite('./package.json', JSON.stringify(treePackageJson)); - return () => tree; + + const dependencies = Object.entries(getExternalDependenciesVersionRange(schematicsDependencies, packageJsonPath)).reduce((acc, [dep, range]) => { + acc[dep] = { + inManifest: [{ + range, + types: [NodeDependencyType.Dev] + }] + }; + return acc; + }, {} as Record); + Object.entries(getExternalDependenciesVersionRange(schematicsDependencies, packageJsonPath)) + .forEach(([dep, range]) => { + dependencies[dep] = { + inManifest: [{ + range, + types: [NodeDependencyType.Dev] + }] + }; + }); + return setupDependencies({ + projectName: options.projectName, + dependencies, + skipInstall: false + }); }; } diff --git a/packages/@o3r/schematics/src/rule-factories/ng-add/dependencies.ts b/packages/@o3r/schematics/src/rule-factories/ng-add/dependencies.ts new file mode 100644 index 0000000000..3c48f8604f --- /dev/null +++ b/packages/@o3r/schematics/src/rule-factories/ng-add/dependencies.ts @@ -0,0 +1,231 @@ +import { chain, Rule, Schematic, type TaskId, Tree } from '@angular-devkit/schematics'; +import { NodeDependencyType } from '@schematics/angular/utility/dependencies'; +import * as path from 'node:path'; +import type { PackageJson } from 'type-fest'; +import * as semver from 'semver'; +import { getPackageManager, getProjectNewDependenciesTypes, getWorkspaceConfig, SupportedPackageManagers } from '../../utility'; +import { NodePackageInstallTask, RunSchematicTask } from '@angular-devkit/schematics/tasks'; +import { readFileSync } from 'node:fs'; + +/** + * Options to be passed to the ng add task + */ +export interface NgAddSchematicOptions { + /** Name of the project */ + projectName?: string | null; + + /** Skip the run of the linter*/ + skipLinter?: boolean; + + /** Skip the install process */ + skipInstall?: boolean; + + [x: string]: any; +} + +export interface DependencyInManifest { + /** + * Range of the dependency + * @default 'latest' + */ + range?: string; + /** + * Types of the dependency + * @default [NodeDependencyType.Default] + */ + types?: NodeDependencyType[]; +} + +export interface DependencyToAdd { + /** Enforce this dependency to be applied to Workspace's manifest only */ + toWorkspaceOnly?: boolean; + /** List of dependency to register in manifest */ + inManifest: DependencyInManifest[]; + /** ng-add schematic option dedicated to the package */ + ngAddOptions?: NgAddSchematicOptions; + /** Determine if the dependency require to be installed */ + requireInstall?: boolean; +} + +export interface SetupDependenciesOptions { + /** Map of dependencies to install */ + dependencies: Record; + /** + * Pattern of list of the dependency for which the ng-add run process is required + */ + ngAddToRun?: (RegExp | string)[]; + /** + * Will skip install in the end of the package.json update. + * if `undefined`, the installation will be process only if a ngAdd run is required. + * If `true` the install will not run in any case + * @default undefined + */ + skipInstall?: boolean; + /** Project Name */ + projectName?: string; + /** default ng-add schematic option */ + ngAddOptions?: NgAddSchematicOptions; + /** Enforce install package manager */ + packageManager?: SupportedPackageManagers; + /** Task will run after the given task ID (if specified) */ + runAfterTasks?: TaskId[]; + /** Callback to run after the task ID is calculated */ + scheduleTaskCallback?: (taskIds?: TaskId[]) => void; +} + +/** + * Retrieve the package install configuration + * This is a workaround to ng-add to add the dependency to the sub-package + * @param packageJsonPath Path to the module package.json file + * @param tree Tree to read the file + * @param projectName Name of the project + * @param devDependencyOnly If true, the dependency will be added as devDependency + */ +export const getPackageInstallConfig = (packageJsonPath: string, tree: Tree, projectName?: string, devDependencyOnly?: boolean): Record => { + if (!projectName) { + return {}; + } + + const packageJson = JSON.parse(readFileSync(packageJsonPath, {encoding: 'utf-8'})) as PackageJson; + const workspaceProject = projectName ? getWorkspaceConfig(tree)?.projects[projectName] : undefined; + return { + [packageJson.name!]: { + inManifest: [{ + range: `~${packageJson.version}`, + types: devDependencyOnly ? [NodeDependencyType.Dev] : getProjectNewDependenciesTypes(workspaceProject) + }], + requireInstall: true + } + }; +}; + +/** + * Setup dependency to a repository. + * Will run manually the ngAdd schematics according to the parameters and install the packages if required + * @param options + */ +export const setupDependencies = (options: SetupDependenciesOptions): Rule => { + + return () => { + const ngAddToRun = new Set(Object.keys(options.dependencies) + .filter((dep) => options.ngAddToRun?.some((pattern) => typeof pattern === 'string' ? pattern === dep : pattern.test(dep)))); + const requiringInstallList = new Set(Object.entries(options.dependencies).filter(([, {requireInstall}]) => requireInstall).map(([dep]) => dep)); + const isInstallNeeded = () => options.skipInstall !== undefined ? !options.skipInstall : (ngAddToRun.size > 0 || requiringInstallList.size > 0); + + const editPackageJson = (packageJsonPath: string, packageToInstall: string, dependency: DependencyToAdd, updateLists: boolean): Rule => { + return (tree, context) => { + if (!tree.exists(packageJsonPath)) { + context.logger.warn(`The file ${packageJsonPath} does not exist, the dependency ${packageToInstall} will not be added`); + return tree; + } + const packageJsonContent = tree.readJson(packageJsonPath) as PackageJson; + + dependency.inManifest.forEach(({ range, types }) => { + (types || [NodeDependencyType.Default]).forEach((depType) => { + if (packageJsonContent[depType]?.[packageToInstall]) { + if (range && semver.validRange(range)) { + const currentMinimalVersion = semver.minVersion(packageJsonContent[depType]?.[packageToInstall] as string); + const myRangeMinimalVersion = semver.minVersion(range); + if (currentMinimalVersion && myRangeMinimalVersion && semver.gt(myRangeMinimalVersion, currentMinimalVersion)) { + context.logger.debug(`The dependency ${packageToInstall} (${depType}@${range}) will be added in ${packageJsonPath}`); + packageJsonContent[depType]![packageToInstall] = range; + } else { + if (updateLists) { + ngAddToRun.delete(packageToInstall); + requiringInstallList.delete(packageToInstall); + } + context.logger.debug(`The dependency ${packageToInstall} (${depType}) is already in ${packageJsonPath}, it will not be added.`); + context.logger.debug(`Because its range is inferior or included to the current one (${range} < ${packageJsonContent[depType]![packageToInstall]!}) in targeted ${packageJsonPath}`); + } + } else { + if (updateLists) { + ngAddToRun.delete(packageToInstall); + requiringInstallList.delete(packageToInstall); + } + context.logger.warn(`The dependency ${packageToInstall} (${depType}) will not added ` + + `because there is already this dependency with a defined range (${packageJsonContent[depType]![packageToInstall]!}) in targeted ${packageJsonPath}`); + } + } else { + packageJsonContent[depType] ||= {}; + packageJsonContent[depType]![packageToInstall] = range; + context.logger.debug(`The dependency ${packageToInstall} (${depType}@${range}) will be added in ${packageJsonPath}`); + } + packageJsonContent[depType] = Object.keys(packageJsonContent[depType]!) + .sort() + .reduce((acc, key) => { + acc[key] = packageJsonContent[depType]![key]; + return acc; + }, {} as PackageJson.Dependency); + }); + }); + + const content = JSON.stringify(packageJsonContent, null, 2); + tree.overwrite(packageJsonPath, content); + return tree; + }; + }; + + const addDependencies: Rule = (tree) => { + const workspaceConfig = getWorkspaceConfig(tree); + const workspaceProject = options.projectName && workspaceConfig?.projects?.[options.projectName] || undefined; + const projectDirectory = workspaceProject?.root; + return chain(Object.entries(options.dependencies) + .map(([packageName, dependencyDetails]) => { + const shouldRunInSubPackage = projectDirectory && !dependencyDetails.toWorkspaceOnly; + const rootPackageRule = editPackageJson('package.json', packageName, dependencyDetails, !shouldRunInSubPackage); + if (shouldRunInSubPackage) { + return chain([ + rootPackageRule, + editPackageJson(path.posix.join(projectDirectory, 'package.json'), packageName, dependencyDetails, true) + ]); + } + return rootPackageRule; + }) + ); + }; + + const runNgAddSchematics: Rule = (_, context) => { + const packageManager = options.packageManager || getPackageManager(); + const installId = isInstallNeeded() ? [context.addTask(new NodePackageInstallTask({ packageManager, quiet: true }), options.runAfterTasks)] : undefined; + + if (installId !== undefined) { + context.logger.debug(`Schedule the installation of the workspace (${ngAddToRun.size > 0 ? 'for: ' + [...ngAddToRun].join(', ') : options.skipInstall ? 'skipped' : 'forced'})`); + } + + const getOptions = (packageName: string, schema?: Schematic) => { + const schemaOptions = schema?.description.schemaJson?.properties; + return Object.fromEntries( + Object.entries({ projectName: options.projectName, ...options.ngAddOptions, ...options.dependencies[packageName].ngAddOptions }) + .filter(([key]) => !schemaOptions || !!schemaOptions[key]) + ); + }; + + const finalTaskId = [...ngAddToRun] + .map((packageName) => { + let schematic: Schematic | undefined; + try { + const collection = context.engine.createCollection(packageName); + schematic = collection.createSchematic('ng-add'); + } catch (e: any) { + context.logger.warn(`The package ${packageName} was not installed, the options check will be skipped`, e); + } + const schematicOptions = getOptions(packageName, schematic); + return { packageName, schematicOptions }; + }) + .reduce((ids, { packageName, schematicOptions }) => { + context.logger.debug(`Schedule the schematic ng-add for ${packageName}`); + return [...ids, context.addTask(new RunSchematicTask(packageName, 'ng-add', schematicOptions), ids)]; + }, [...(installId || []), ...(options.runAfterTasks || [])]); + + if (options.scheduleTaskCallback) { + options.scheduleTaskCallback(finalTaskId); + } + }; + + return chain([ + addDependencies, + runNgAddSchematics + ]); + }; + +}; diff --git a/packages/@o3r/schematics/src/rule-factories/ng-add/index.ts b/packages/@o3r/schematics/src/rule-factories/ng-add/index.ts index ec808009b3..c09168602a 100644 --- a/packages/@o3r/schematics/src/rule-factories/ng-add/index.ts +++ b/packages/@o3r/schematics/src/rule-factories/ng-add/index.ts @@ -1,226 +1,3 @@ -import { chain, externalSchematic, noop, Rule, Schematic, SchematicContext } from '@angular-devkit/schematics'; -import { NodePackageInstallTask } from '@angular-devkit/schematics/tasks'; -import { type NodeDependency, NodeDependencyType } from '@schematics/angular/utility/dependencies'; -import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'; -import * as path from 'node:path'; -import { lastValueFrom } from 'rxjs'; -import type { PackageJson } from 'type-fest'; -import type { SchematicOptionObject } from '../../interfaces/index'; -import type { NgAddPackageOptions } from '../../tasks/index'; -import { getExternalDependenciesVersionRange, getNodeDependencyList, getPackageManager, getWorkspaceConfig, registerCollectionSchematics, writeAngularJson } from '../../utility/index'; - -/** - * Install via `ng add` a list of npm packages. - * @param packages List of packages to be installed via `ng add` - * @param options install options - * @param packageJsonPath path of the package json of the project where they will be installed - */ -export function ngAddPackages(packages: string[], options?: Omit & { version?: string | (string | undefined)[] }, packageJsonPath = '/package.json'): Rule { - if (!packages.length) { - return noop; - } - const cwd = process.cwd().replace(/[\\/]+/g, '/'); - // FileSystem working directory might be different from Tree working directory (when using `yarn workspace` for example) - const fsWorkingDirectory = (options?.workingDirectory && !cwd.endsWith(options.workingDirectory)) ? options.workingDirectory : '.'; - const versions = Object.fromEntries(packages.map<[string, string | undefined]>((packageName, index) => - [packageName, typeof options?.version === 'object' ? options.version[index] : options?.version])); - if (options?.workingDirectory && !packageJsonPath.startsWith(options.workingDirectory)) { - packageJsonPath = path.join(options.workingDirectory, packageJsonPath); - } - - const getInstalledVersion = (packageName: string) => { - let versionFound : string | undefined; - for (const workingDirectory of new Set([fsWorkingDirectory, '.'])) { - try { - const fileSystemPackageJson = JSON.parse(readFileSync(path.join(workingDirectory, 'package.json'), {encoding: 'utf8'})); - let version: string | undefined; - if (options?.dependencyType === NodeDependencyType.Dev) { - version = fileSystemPackageJson.devDependencies[packageName]; - } else if (options?.dependencyType === NodeDependencyType.Peer) { - version = fileSystemPackageJson.peerDependencies[packageName]; - } else { - version = fileSystemPackageJson.dependencies[packageName]; - } - if (versionFound && version !== versionFound) { - // In case of conflict between package.json files, we consider the package as not installed to force its update - return; - } - versionFound = version; - } catch (e) { - return; - } - } - return versionFound; - }; - - const getNgAddSchema = (packageName: string, context: SchematicContext) => { - try { - const collection = context.engine.createCollection(packageName); - return collection.createSchematic('ng-add'); - } catch { - context.logger.warn(`No ng-add found for ${packageName}`); - return undefined; - } - }; - - const getOptions = (schema: Schematic) => { - const schemaOptions = schema.description.schemaJson?.properties || {}; - return Object.entries(options || {}).reduce>((accOptions, [key, value]: [string, any]) => { - if (schemaOptions[key]) { - accOptions[key] = value; - } - return accOptions; - }, {}); - }; - - const installedVersions = packages.map((packageName) => getInstalledVersion(packageName)); - const packageManager = getPackageManager(); - const packagesToInstall = packages.filter((packageName, index) => !installedVersions[index] || installedVersions[index] !== versions[packageName]); - if (packagesToInstall.length < 1) { - return noop; - } - return chain([ - // Update package.json in tree - (tree) => { - const sortDependencies = (packageJson: any, depType: 'dependencies' | 'devDependencies' | 'peerDependencies') => { - packageJson[depType] = packageJson[depType] ? - Object.fromEntries(Object.entries(packageJson[depType]).sort(([key1, _val1], [key2, _val2]) => key1.localeCompare(key2))) : - undefined; - }; - for (const filePath of new Set([packageJsonPath, './package.json'])) { - const packageJson: PackageJson = tree.readJson(filePath) as PackageJson; - packages.forEach((packageName) => { - const version = versions[packageName] || 'latest'; - if (options?.dependencyType === NodeDependencyType.Dev) { - packageJson.devDependencies = {...packageJson.devDependencies, [packageName]: version}; - } else if (options?.dependencyType === NodeDependencyType.Peer) { - packageJson.peerDependencies = {...packageJson.peerDependencies, [packageName]: version}; - } else { - packageJson.dependencies = {...packageJson.dependencies, [packageName]: version}; - } - }); - (['dependencies', 'devDependencies', 'peerDependencies'] as const).forEach((depType) => { - sortDependencies(packageJson, depType); - }); - tree.overwrite(filePath, JSON.stringify(packageJson, null, 2)); - } - }, - // Run ng-adds - async (tree, context) => { - if (options?.skipNgAddSchematicRun) { - context.logger.info(`Package(s) '${packagesToInstall.join(', ')}' was(were) installed. - The run of 'ng-add' schematics for the package(s) is intentionally skipped. You can do the run standalone, later.`); - return noop; - } - - new Set([packageJsonPath, './package.json']).forEach((filePath) => { - mkdirSync(path.join(process.cwd(), path.dirname(filePath)), {recursive: true}); - writeFileSync(path.join(process.cwd(), filePath), tree.readText(filePath)); - }); - context.addTask(new NodePackageInstallTask({ - packageManager: packageManager, - hideOutput: false, - quiet: false - } as any)); - await lastValueFrom(context.engine.executePostTasks()); - - const ngAddsToApply = packagesToInstall - .map((packageName) => ({packageName, ngAddCollection: getNgAddSchema(packageName, context)})) - .filter(({packageName, ngAddCollection}) => { - if (!ngAddCollection) { - context.logger.info( - `No ng-add schematic found for: '${packageName}'. Skipping ng add for: ${packageName}${versions[packageName] ? ' with version: ' + (versions[packageName] as string) : ''}`); - } - return !!ngAddCollection; - }) - .map(({packageName, ngAddCollection}) => externalSchematic(packageName, 'ng-add', getOptions(ngAddCollection!))); - return chain(ngAddsToApply); - } - ]); -} - -/** - * Look for the peer dependencies and run ng add on the package requested version - * @param packages list of the name of the packages needed - * @param packageJsonPath path to package json that needs the peer to be resolved - * @param type how to install the dependency (dev, peer for a library or default for an application) - * @param options - * @param parentPackageInfo for logging purposes - */ -export function ngAddPeerDependencyPackages(packages: string[], packageJsonPath: string, type: NodeDependencyType = NodeDependencyType.Default, - options: NgAddPackageOptions, parentPackageInfo?: string) { - if (!packages.length) { - return noop; - } - const dependencies: NodeDependency[] = getNodeDependencyList( - getExternalDependenciesVersionRange(packages, packageJsonPath), - type - ); - return ngAddPackages(dependencies.map(({name}) => name), { - ...options, - skipConfirmation: true, - version: dependencies.map(({version}) => version), - parentPackageInfo, - dependencyType: type, - workingDirectory: options.workingDirectory - }); -} - -/** - * Register the given package in the Angular CLI schematics - * @param packageJson PackageJson of the project containing the collection to add to the project - * @param angularJsonFile Path to the Angular.json file. Will use the workspace root's angular.json if not specified - */ -export function registerPackageCollectionSchematics(packageJson: PackageJson, angularJsonFile?: string): Rule { - return (tree, context) => { - if (!packageJson.name) { - return tree; - } - const workspace = getWorkspaceConfig(tree, angularJsonFile); - if (!workspace) { - context.logger.error('No workspace found'); - return tree; - } - return writeAngularJson(tree, registerCollectionSchematics(workspace, packageJson.name), angularJsonFile); - }; -} - -/** - * Setup schematics default params in angular.json - * @param schematicsDefaultParams default params to setup by schematic - * @param overrideValue Define if the given value should override the one already defined in the workspace - * @param angularJsonFile Path to the Angular.json file. Will use the workspace root's angular.json if not specified - */ -export function setupSchematicsDefaultParams(schematicsDefaultParams: Record, overrideValue = false, angularJsonFile?: string): Rule { - return (tree, context) => { - const workspace = getWorkspaceConfig(tree, angularJsonFile); - if (!workspace) { - context.logger.error('No workspace found'); - return tree; - } - workspace.schematics ||= {}; - Object.entries(schematicsDefaultParams).forEach(([schematicName, defaultParams]) => { - workspace.schematics![schematicName] = overrideValue ? { - ...workspace.schematics![schematicName], - ...defaultParams - } : { - ...defaultParams, - ...workspace.schematics![schematicName] - }; - }); - Object.values(workspace.projects).forEach((project) => { - Object.entries(schematicsDefaultParams).forEach(([schematicName, defaultParams]) => { - if (project.schematics?.[schematicName]) { - project.schematics[schematicName] = overrideValue ? { - ...project.schematics[schematicName], - ...defaultParams - } : { - ...defaultParams, - ...project.schematics[schematicName] - }; - } - }); - }); - return writeAngularJson(tree, workspace, angularJsonFile); - }; -} +export * from './schematics-register'; +export * from './dependencies'; +export * from './ng-add.helpers'; diff --git a/packages/@o3r/schematics/src/rule-factories/ng-add/ng-add.helpers.ts b/packages/@o3r/schematics/src/rule-factories/ng-add/ng-add.helpers.ts new file mode 100644 index 0000000000..0542753bd4 --- /dev/null +++ b/packages/@o3r/schematics/src/rule-factories/ng-add/ng-add.helpers.ts @@ -0,0 +1,167 @@ +import { chain, externalSchematic, noop, Rule, Schematic, SchematicContext } from '@angular-devkit/schematics'; +import { NodePackageInstallTask } from '@angular-devkit/schematics/tasks'; +import type { NodeDependency } from '@schematics/angular/utility/dependencies'; +import { NodeDependencyType } from '@schematics/angular/utility/dependencies'; +import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'; +import * as path from 'node:path'; +import { lastValueFrom } from 'rxjs'; +import type { PackageJson } from 'type-fest'; +import type { NgAddPackageOptions } from '../../tasks/index'; +import { getExternalDependenciesVersionRange, getNodeDependencyList, getPackageManager } from '../../utility/index'; + +/** + * Install via `ng add` a list of npm packages. + * @param packages List of packages to be installed via `ng add` + * @param options install options + * @param packageJsonPath path of the package json of the project where they will be installed + */ +export function ngAddPackages(packages: string[], options?: Omit & { version?: string | (string | undefined)[] }, packageJsonPath = '/package.json'): Rule { + if (!packages.length) { + return noop; + } + const cwd = process.cwd().replace(/[\\/]+/g, '/'); + // FileSystem working directory might be different from Tree working directory (when using `yarn workspace` for example) + const fsWorkingDirectory = (options?.workingDirectory && !cwd.endsWith(options.workingDirectory)) ? options.workingDirectory : '.'; + const versions = Object.fromEntries(packages.map<[string, string | undefined]>((packageName, index) => + [packageName, typeof options?.version === 'object' ? options.version[index] : options?.version])); + if (options?.workingDirectory && !packageJsonPath.startsWith(options.workingDirectory)) { + packageJsonPath = path.join(options.workingDirectory, packageJsonPath); + } + + const getInstalledVersion = (packageName: string) => { + let versionFound: string | undefined; + for (const workingDirectory of new Set([fsWorkingDirectory, '.'])) { + try { + const fileSystemPackageJson = JSON.parse(readFileSync(path.join(workingDirectory, 'package.json'), { encoding: 'utf8' })); + let version: string | undefined; + if (options?.dependencyType === NodeDependencyType.Dev) { + version = fileSystemPackageJson.devDependencies[packageName]; + } else if (options?.dependencyType === NodeDependencyType.Peer) { + version = fileSystemPackageJson.peerDependencies[packageName]; + } else { + version = fileSystemPackageJson.dependencies[packageName]; + } + if (versionFound && version !== versionFound) { + // In case of conflict between package.json files, we consider the package as not installed to force its update + return; + } + versionFound = version; + } catch (e) { + return; + } + } + return versionFound; + }; + + const getNgAddSchema = (packageName: string, context: SchematicContext) => { + try { + const collection = context.engine.createCollection(packageName); + return collection.createSchematic('ng-add'); + } catch { + context.logger.warn(`No ng-add found for ${packageName}`); + return undefined; + } + }; + + const getOptions = (schema: Schematic) => { + const schemaOptions = schema.description.schemaJson?.properties || {}; + return Object.entries(options || {}).reduce>((accOptions, [key, value]: [string, any]) => { + if (schemaOptions[key]) { + accOptions[key] = value; + } + return accOptions; + }, {}); + }; + + const installedVersions = packages.map((packageName) => getInstalledVersion(packageName)); + const packageManager = getPackageManager(); + const packagesToInstall = packages.filter((packageName, index) => !installedVersions[index] || installedVersions[index] !== versions[packageName]); + if (packagesToInstall.length < 1) { + return noop; + } + return chain([ + // Update package.json in tree + (tree) => { + const sortDependencies = (packageJson: PackageJson, depType: 'dependencies' | 'devDependencies' | 'peerDependencies') => { + packageJson[depType] = packageJson[depType] ? + Object.fromEntries(Object.entries(packageJson[depType] || {}).sort(([key1, _val1], [key2, _val2]) => key1.localeCompare(key2))) : + undefined; + }; + for (const filePath of new Set([packageJsonPath, './package.json'])) { + const packageJson: PackageJson = tree.readJson(filePath) as PackageJson; + packages.forEach((packageName) => { + const version = versions[packageName] || 'latest'; + if (options?.dependencyType === NodeDependencyType.Dev) { + packageJson.devDependencies = { ...packageJson.devDependencies, [packageName]: version }; + } else if (options?.dependencyType === NodeDependencyType.Peer) { + packageJson.peerDependencies = { ...packageJson.peerDependencies, [packageName]: version }; + } else { + packageJson.dependencies = { ...packageJson.dependencies, [packageName]: version }; + } + }); + (['dependencies', 'devDependencies', 'peerDependencies'] as const).forEach((depType) => { + sortDependencies(packageJson, depType); + }); + tree.overwrite(filePath, JSON.stringify(packageJson, null, 2)); + } + }, + // Run ng-adds + async (tree, context) => { + if (options?.skipNgAddSchematicRun) { + context.logger.info(`Package(s) '${packagesToInstall.join(', ')}' was(were) installed. + The run of 'ng-add' schematics for the package(s) is intentionally skipped. You can do the run standalone, later.`); + return noop; + } + + new Set([packageJsonPath, './package.json']).forEach((filePath) => { + mkdirSync(path.join(process.cwd(), path.dirname(filePath)), { recursive: true }); + writeFileSync(path.join(process.cwd(), filePath), tree.readText(filePath)); + }); + context.addTask(new NodePackageInstallTask({ + packageManager: packageManager, + hideOutput: false, + quiet: false + } as any)); + await lastValueFrom(context.engine.executePostTasks()); + + const ngAddsToApply = packagesToInstall + .map((packageName) => ({ packageName, ngAddCollection: getNgAddSchema(packageName, context) })) + .filter(({ packageName, ngAddCollection }) => { + if (!ngAddCollection) { + context.logger.info( + `No ng-add schematic found for: '${packageName}'. Skipping ng add for: ${packageName}${versions[packageName] ? ' with version: ' + (versions[packageName] as string) : ''}`); + } + return !!ngAddCollection; + }) + .map(({ packageName, ngAddCollection }) => externalSchematic(packageName, 'ng-add', getOptions(ngAddCollection!))); + return chain(ngAddsToApply); + } + ]); +} + +/** + * Look for the peer dependencies and run ng add on the package requested version + * @param packages list of the name of the packages needed + * @param packageJsonPath path to package json that needs the peer to be resolved + * @param type how to install the dependency (dev, peer for a library or default for an application) + * @param options + * @param parentPackageInfo for logging purposes + */ +export function ngAddPeerDependencyPackages(packages: string[], packageJsonPath: string, type: NodeDependencyType = NodeDependencyType.Default, + options: NgAddPackageOptions, parentPackageInfo?: string) { + if (!packages.length) { + return noop; + } + const dependencies: NodeDependency[] = getNodeDependencyList( + getExternalDependenciesVersionRange(packages, packageJsonPath), + type + ); + return ngAddPackages(dependencies.map(({ name }) => name), { + ...options, + skipConfirmation: true, + version: dependencies.map(({ version }) => version), + parentPackageInfo, + dependencyType: type, + workingDirectory: options.workingDirectory + }); +} diff --git a/packages/@o3r/schematics/src/rule-factories/ng-add/schematics-register.ts b/packages/@o3r/schematics/src/rule-factories/ng-add/schematics-register.ts new file mode 100644 index 0000000000..2f1a5066c0 --- /dev/null +++ b/packages/@o3r/schematics/src/rule-factories/ng-add/schematics-register.ts @@ -0,0 +1,64 @@ +import type { Rule } from '@angular-devkit/schematics'; +import type { PackageJson } from 'type-fest'; +import type { SchematicOptionObject } from '../../interfaces/index'; +import { getWorkspaceConfig, registerCollectionSchematics, writeAngularJson } from '../../utility'; + + +/** + * Register the given package in the Angular CLI schematics + * @param packageJson PackageJson of the project containing the collection to add to the project + * @param angularJsonFile Path to the Angular.json file. Will use the workspace root's angular.json if not specified + */ +export function registerPackageCollectionSchematics(packageJson: PackageJson, angularJsonFile?: string): Rule { + return (tree, context) => { + if (!packageJson.name) { + return tree; + } + const workspace = getWorkspaceConfig(tree, angularJsonFile); + if (!workspace) { + context.logger.error('No workspace found'); + return tree; + } + return writeAngularJson(tree, registerCollectionSchematics(workspace, packageJson.name), angularJsonFile); + }; +} + +/** + * Setup schematics default params in angular.json + * @param schematicsDefaultParams default params to setup by schematic + * @param overrideValue Define if the given value should override the one already defined in the workspace + * @param angularJsonFile Path to the Angular.json file. Will use the workspace root's angular.json if not specified + */ +export function setupSchematicsDefaultParams(schematicsDefaultParams: Record, overrideValue = false, angularJsonFile?: string): Rule { + return (tree, context) => { + const workspace = getWorkspaceConfig(tree, angularJsonFile); + if (!workspace) { + context.logger.error('No workspace found'); + return tree; + } + workspace.schematics ||= {}; + Object.entries(schematicsDefaultParams).forEach(([schematicName, defaultParams]) => { + workspace.schematics![schematicName] = overrideValue ? { + ...workspace.schematics![schematicName], + ...defaultParams + } : { + ...defaultParams, + ...workspace.schematics![schematicName] + }; + }); + Object.values(workspace.projects).forEach((project) => { + Object.entries(schematicsDefaultParams).forEach(([schematicName, defaultParams]) => { + if (project.schematics?.[schematicName]) { + project.schematics[schematicName] = overrideValue ? { + ...project.schematics[schematicName], + ...defaultParams + } : { + ...defaultParams, + ...project.schematics[schematicName] + }; + } + }); + }); + return writeAngularJson(tree, workspace, angularJsonFile); + }; +} diff --git a/packages/@o3r/schematics/src/rules/install.ts b/packages/@o3r/schematics/src/rules/install.ts index 0b2abc9e65..eb97238228 100644 --- a/packages/@o3r/schematics/src/rules/install.ts +++ b/packages/@o3r/schematics/src/rules/install.ts @@ -1,17 +1,19 @@ -import type { Rule, SchematicContext, Tree } from '@angular-devkit/schematics'; +import type { Rule } from '@angular-devkit/schematics'; import { NodePackageInstallTask } from '@angular-devkit/schematics/tasks'; import { lastValueFrom } from 'rxjs'; -import { getPackageManager } from '../utility/package-manager-runner'; +import { getPackageManager, type SupportedPackageManagers } from '../utility/package-manager-runner'; /** * Install the Otter packages - * @param tree - * @param context + * @deprecated use {@link setupDependencies} instead, will be removed in V11 + * @param options + * @param options.packageManager */ -export async function install(tree: Tree, context: SchematicContext): Promise { - const packageManager = getPackageManager(); - context.logger.info('Running application install'); - context.addTask(new NodePackageInstallTask({packageManager})); - await lastValueFrom(context.engine.executePostTasks()); - return () => tree; +export function install(options?: {packageManager?: SupportedPackageManagers}): Rule { + return async (_, context) => { + const packageManager = options?.packageManager || getPackageManager(); + context.logger.info('Running application install'); + context.addTask(new NodePackageInstallTask({packageManager})); + await lastValueFrom(context.engine.executePostTasks()); + }; } diff --git a/packages/@o3r/schematics/src/tasks/ng-add/index.ts b/packages/@o3r/schematics/src/tasks/ng-add/index.ts index 8a82b829fb..55993c8b49 100644 --- a/packages/@o3r/schematics/src/tasks/ng-add/index.ts +++ b/packages/@o3r/schematics/src/tasks/ng-add/index.ts @@ -31,6 +31,7 @@ export interface NgAddPackageOptions { skipNgAddSchematicRun?: boolean; } +/** @deprecated use {@link setupDependencies} instead, will be removed in V11 */ export class NodePackageNgAddTask implements TaskConfigurationGenerator { public quiet = false; diff --git a/packages/@o3r/schematics/src/tasks/package-manager/add-dev-dependency.ts b/packages/@o3r/schematics/src/tasks/package-manager/add-dev-dependency.ts index 152da6a662..d292a1d553 100644 --- a/packages/@o3r/schematics/src/tasks/package-manager/add-dev-dependency.ts +++ b/packages/@o3r/schematics/src/tasks/package-manager/add-dev-dependency.ts @@ -5,6 +5,7 @@ import { NodePackageInstallTaskOptions } from './interfaces'; /** * Install new dev dependency on your package + * @deprecated use {@link setupDependencies} instead, will be removed in V11 */ export class AddDevInstall extends NodePackageInstallTask { public quiet = false; diff --git a/packages/@o3r/schematics/src/utility/collection.ts b/packages/@o3r/schematics/src/utility/collection.ts index 3ee77ec182..3efdc5cc78 100644 --- a/packages/@o3r/schematics/src/utility/collection.ts +++ b/packages/@o3r/schematics/src/utility/collection.ts @@ -39,8 +39,9 @@ export function getDefaultOptionsForSchematic if (!schematics) { return acc; } + return Object.entries>(schematics) - .filter(([key, _]) => key === `*:${schematicName}` || key === `${collection}:*` || key === `${collection}:${schematicName}`) + .filter(([key, _]) => new RegExp(key.replace(/[*]/g, '.*')).test(`${collection}:${schematicName}`)) .sort(([a], [b]) => (a.match(/\*/g)?.length || 0) - (b.match(/\*/g)?.length || 0)) .map(([_, value]) => value) .reduce((config, value) => ({...config, ...value}), acc); diff --git a/packages/@o3r/schematics/src/utility/dependencies.ts b/packages/@o3r/schematics/src/utility/dependencies.ts index 012e1f9f2c..5e29a18478 100644 --- a/packages/@o3r/schematics/src/utility/dependencies.ts +++ b/packages/@o3r/schematics/src/utility/dependencies.ts @@ -10,7 +10,7 @@ import { NodeDependency, NodeDependencyType } from '@schematics/angular/utility/ export function getExternalDependenciesVersionRange(packageNames: T[], packageJsonPath: string): Record { const packageJsonContent = JSON.parse(fs.readFileSync(packageJsonPath, {encoding: 'utf-8'})); return packageNames.reduce((acc: Partial>, packageName) => { - acc[packageName] = packageJsonContent.peerDependencies?.[packageName] || packageJsonContent.generatorDependencies?.[packageName]; + acc[packageName] = packageJsonContent.peerDependencies?.[packageName] || packageJsonContent.generatorDependencies?.[packageName] || packageJsonContent.dependencies?.[packageName] || 'latest'; return acc; }, {}) as Record; } diff --git a/packages/@o3r/schematics/src/utility/loaders.ts b/packages/@o3r/schematics/src/utility/loaders.ts index 264a6bb234..e2d902ef72 100644 --- a/packages/@o3r/schematics/src/utility/loaders.ts +++ b/packages/@o3r/schematics/src/utility/loaders.ts @@ -76,13 +76,21 @@ export function readPackageJson(tree: Tree, workspaceProject: WorkspaceProject) /** * Return the type of install to run depending on the project type (Peer or default) + * @deprecated use {@link getProjectNewDependenciesTypes instead}, will be removed in V11 * @param project - * @param tree */ export function getProjectNewDependenciesType(project?: WorkspaceProject) { return project?.projectType === 'library' ? NodeDependencyType.Peer : NodeDependencyType.Default; } +/** + * Return the types of install to run depending on the project type + * @param project + */ +export function getProjectNewDependenciesTypes(project?: WorkspaceProject): NodeDependencyType[] { + return project?.projectType === 'library' ? [NodeDependencyType.Peer, NodeDependencyType.Dev] : [NodeDependencyType.Default]; +} + /** * Get the folder of the templates for a specific sub-schematics * @param rootPath Root directory of the schematics ran diff --git a/packages/@o3r/schematics/src/utility/matching-peers.ts b/packages/@o3r/schematics/src/utility/matching-peers.ts index 9cf8672c96..056d5a3487 100644 --- a/packages/@o3r/schematics/src/utility/matching-peers.ts +++ b/packages/@o3r/schematics/src/utility/matching-peers.ts @@ -37,7 +37,7 @@ const basicsPackageName = new Set([ * @param packagePattern Pattern of the package name to look in the packages peer dependencies. * @param versionRangePrefix Prefix to add to the package version to determine Semver Range */ -export function getO3rPeerDeps(packageJsonPath: string, filterBasics = true, packagePattern = /^@(?:o3r|ama-sdk)/, versionRangePrefix = '^') { +export function getO3rPeerDeps(packageJsonPath: string, filterBasics = true, packagePattern = /^@(?:o3r|ama-sdk)/, versionRangePrefix = '') { const depsInfo = getPeerDepWithPattern(packageJsonPath, packagePattern); return { packageName: depsInfo.packageName, diff --git a/packages/@o3r/store-sync/package.json b/packages/@o3r/store-sync/package.json index 6adb147c7e..eb429d6c1c 100644 --- a/packages/@o3r/store-sync/package.json +++ b/packages/@o3r/store-sync/package.json @@ -71,6 +71,7 @@ "@o3r/eslint-plugin": "workspace:^", "@o3r/logger": "workspace:^", "@o3r/schematics": "workspace:^", + "@schematics/angular": "~17.2.0", "@stylistic/eslint-plugin-ts": "^1.5.4", "@types/jest": "~29.5.2", "@types/node": "^20.0.0", diff --git a/packages/@o3r/store-sync/schematics/ng-add/index.ts b/packages/@o3r/store-sync/schematics/ng-add/index.ts index 46afe9ea8c..5a3d6da76b 100644 --- a/packages/@o3r/store-sync/schematics/ng-add/index.ts +++ b/packages/@o3r/store-sync/schematics/ng-add/index.ts @@ -2,7 +2,11 @@ import { chain, noop, Rule } from '@angular-devkit/schematics'; import { createSchematicWithMetricsIfInstalled } from '@o3r/schematics'; import type { NgAddSchematicsSchema } from './schema'; import * as path from 'node:path'; -import { NodePackageInstallTask } from '@angular-devkit/schematics/tasks'; +import { NodeDependencyType } from '@schematics/angular/utility/dependencies'; + +const devDependenciesToInstall = [ + 'fast-deep-equal' +]; /** * Add Otter store-sync to an Otter Project @@ -12,37 +16,48 @@ function ngAddFn(options: NgAddSchematicsSchema): Rule { return async (tree, context) => { try { // use dynamic import to properly raise an exception if it is not an Otter project. - const { applyEsLintFix, install, ngAddPackages, getO3rPeerDeps, getWorkspaceConfig, getPeerDepWithPattern } = await import('@o3r/schematics'); - // retrieve dependencies following the /^@o3r\/.*/ pattern within the peerDependencies of the current module - const depsInfo = getO3rPeerDeps(path.resolve(__dirname, '..', '..', 'package.json')); - const workingDirectory = options?.projectName && getWorkspaceConfig(tree)?.projects[options.projectName]?.root || '.'; + const { + getPackageInstallConfig, + applyEsLintFix, + setupDependencies, + getO3rPeerDeps, + getProjectNewDependenciesTypes, + getWorkspaceConfig, + getExternalDependenciesVersionRange + } = await import('@o3r/schematics'); + + const workspaceProject = options.projectName ? getWorkspaceConfig(tree)?.projects[options.projectName] : undefined; + const packageJsonPath = path.resolve(__dirname, '..', '..', 'package.json'); + const depsInfo = getO3rPeerDeps(packageJsonPath); + + const dependencies = depsInfo.o3rPeerDeps.reduce((acc, dep) => { + acc[dep] = { + inManifest: [{ + range: `~${depsInfo.packageVersion}`, + types: getProjectNewDependenciesTypes(workspaceProject) + }] + }; + return acc; + }, getPackageInstallConfig(packageJsonPath, tree, options.projectName)); + Object.entries(getExternalDependenciesVersionRange(devDependenciesToInstall, packageJsonPath)) + .forEach(([dep, range]) => { + dependencies[dep] = { + inManifest: [{ + range, + types: [NodeDependencyType.Dev] + }] + }; + }); + return chain([ // optional custom action dedicated to this module options.skipLinter ? noop() : applyEsLintFix(), - // install packages needed in the current module - options.skipInstall ? noop : install, // add the missing Otter modules in the current project - ngAddPackages(depsInfo.o3rPeerDeps, { - skipConfirmation: true, - version: depsInfo.packageVersion, - parentPackageInfo: `${depsInfo.packageName!} - setup`, + setupDependencies({ projectName: options.projectName, - workingDirectory - }), - - // Add mandatory peerDependency - (_, ctx) => { - const peerDepToInstall = getPeerDepWithPattern(path.resolve(__dirname, '..', '..', 'package.json'), ['fast-deep-equal']); - ctx.addTask(new NodePackageInstallTask({ - workingDirectory, - packageName: Object.entries(peerDepToInstall.matchingPackagesVersions) - // eslint-disable-next-line @typescript-eslint/no-base-to-string, @typescript-eslint/restrict-template-expressions - .map(([dependency, version]) => `${dependency}@${version || 'latest'}`) - .join(' '), - hideOutput: false, - quiet: false - })); - } + dependencies, + ngAddToRun: depsInfo.o3rPeerDeps + }) ]); } catch (e) { // If the installation is initialized in a non-Otter application, mandatory packages will be missing. We need to notify the user diff --git a/packages/@o3r/store-sync/schematics/ng-add/schema.json b/packages/@o3r/store-sync/schematics/ng-add/schema.json index 818772750c..6e9e6963b0 100644 --- a/packages/@o3r/store-sync/schematics/ng-add/schema.json +++ b/packages/@o3r/store-sync/schematics/ng-add/schema.json @@ -15,11 +15,6 @@ "type": "boolean", "description": "Skip the linter process", "default": true - }, - "skipInstall": { - "type": "boolean", - "description": "Skip the install process", - "default": true } }, "additionalProperties": true, diff --git a/packages/@o3r/storybook/schematics/ng-add/index.ts b/packages/@o3r/storybook/schematics/ng-add/index.ts index 3f056ad9d1..35fa81666e 100644 --- a/packages/@o3r/storybook/schematics/ng-add/index.ts +++ b/packages/@o3r/storybook/schematics/ng-add/index.ts @@ -3,6 +3,8 @@ import { createSchematicWithMetricsIfInstalled } from '@o3r/schematics'; import * as path from 'node:path'; import type { NgAddSchematicsSchema } from './schema'; +const packageJsonPath = path.resolve(__dirname, '..', '..', 'package.json'); + /** * Add Otter storybook to an Angular Project * @param options @@ -10,18 +12,28 @@ import type { NgAddSchematicsSchema } from './schema'; function ngAddFn(options: NgAddSchematicsSchema): Rule { return async (tree: Tree, context: SchematicContext) => { try { - const { applyEsLintFix, install, ngAddPackages, getO3rPeerDeps, getProjectNewDependenciesType, getWorkspaceConfig, removePackages } = await import('@o3r/schematics'); + const { applyEsLintFix, getPackageInstallConfig, setupDependencies, getO3rPeerDeps, getProjectNewDependenciesTypes, getWorkspaceConfig, removePackages } = await import('@o3r/schematics'); const { updateStorybook } = await import('../storybook-base'); - const depsInfo = getO3rPeerDeps(path.resolve(__dirname, '..', '..', 'package.json')); + const depsInfo = getO3rPeerDeps(packageJsonPath); const workspaceProject = options.projectName ? getWorkspaceConfig(tree)?.projects[options.projectName] : undefined; - const workingDirectory = workspaceProject?.root || '.'; - const dependencyType = getProjectNewDependenciesType(workspaceProject); + const dependencies = depsInfo.o3rPeerDeps.reduce((acc, dep) => { + acc[dep] = { + inManifest: [{ + range: `~${depsInfo.packageVersion}`, + types: getProjectNewDependenciesTypes(workspaceProject) + }] + }; + return acc; + }, getPackageInstallConfig(packageJsonPath, tree, options.projectName)); return () => chain([ removePackages(['@otter/storybook']), updateStorybook(options, __dirname), options.skipLinter ? noop() : applyEsLintFix(), - options.skipInstall ? noop() : install, - ngAddPackages(depsInfo.o3rPeerDeps, { skipConfirmation: true, version: depsInfo.packageVersion, parentPackageInfo: depsInfo.packageName, dependencyType, workingDirectory }) + setupDependencies({ + projectName: options.projectName, + dependencies, + ngAddToRun: depsInfo.o3rPeerDeps + }) ])(tree, context); } catch (e) { // storybook needs o3r/core as peer dep. o3r/core will install o3r/schematics diff --git a/packages/@o3r/stylelint-plugin/package.json b/packages/@o3r/stylelint-plugin/package.json index d222ae54d6..b34f820454 100644 --- a/packages/@o3r/stylelint-plugin/package.json +++ b/packages/@o3r/stylelint-plugin/package.json @@ -19,7 +19,7 @@ "build": "yarn nx build stylelint-plugin", "postbuild": "patch-package-json-main", "prepare:publish": "prepare-publish ./dist", - "prepare:build:builders": "yarn cpy './collection.json' dist", + "prepare:build:builders": "yarn cpy 'schematics/**/*.json' 'schematics/**/templates/**' dist/schematics && yarn cpy './collection.json' dist", "build:builders": "tsc -b tsconfig.builders.json --pretty && yarn generate-cjs-manifest" }, "dependencies": { diff --git a/packages/@o3r/stylelint-plugin/project.json b/packages/@o3r/stylelint-plugin/project.json index 7de64cd758..cdc27d62ed 100644 --- a/packages/@o3r/stylelint-plugin/project.json +++ b/packages/@o3r/stylelint-plugin/project.json @@ -30,7 +30,8 @@ "options": { "main": "packages/@o3r/stylelint-plugin/src/public_api.ts", "tsConfig": "packages/@o3r/stylelint-plugin/tsconfig.build.json", - "outputPath": "packages/@o3r/stylelint-plugin/dist" + "outputPath": "packages/@o3r/stylelint-plugin/dist", + "clean": false } }, "lint": { diff --git a/packages/@o3r/stylelint-plugin/schematics/ng-add/index.ts b/packages/@o3r/stylelint-plugin/schematics/ng-add/index.ts index b3509fb39d..acee6b63fd 100644 --- a/packages/@o3r/stylelint-plugin/schematics/ng-add/index.ts +++ b/packages/@o3r/stylelint-plugin/schematics/ng-add/index.ts @@ -1,10 +1,25 @@ -import { noop } from '@angular-devkit/schematics'; import type { Rule } from '@angular-devkit/schematics'; +import { createSchematicWithMetricsIfInstalled, getPackageInstallConfig, setupDependencies } from '@o3r/schematics'; +import type { NgAddSchematicsSchema } from './schema'; +import * as path from 'node:path'; + +const packageJsonPath = path.resolve(__dirname, '..', '..', 'package.json'); /** * Add Otter stylelint-plugin to an Angular Project + * @param options */ -export function ngAdd(): Rule { +function ngAddFn(options: NgAddSchematicsSchema): Rule { /* ng add rules */ - return noop(); + return (tree) => { + return setupDependencies({ + projectName: options.projectName, + dependencies: getPackageInstallConfig(packageJsonPath, tree, options.projectName, true) + }); + }; } + +/** + * Add Otter stylelint-plugin to an Angular Project + */ +export const ngAdd = createSchematicWithMetricsIfInstalled(ngAddFn); diff --git a/packages/@o3r/styling/package.json b/packages/@o3r/styling/package.json index 579f762557..f178f45503 100644 --- a/packages/@o3r/styling/package.json +++ b/packages/@o3r/styling/package.json @@ -154,6 +154,9 @@ "unionfs": "~4.5.1", "zone.js": "~0.14.2" }, + "generatorDependencies": { + "sass-loader": "^14.0.0" + }, "engines": { "node": ">=18.0.0" }, diff --git a/packages/@o3r/styling/project.json b/packages/@o3r/styling/project.json index 7ac2ba472a..de6ab2f865 100644 --- a/packages/@o3r/styling/project.json +++ b/packages/@o3r/styling/project.json @@ -46,8 +46,7 @@ "test-int": { "executor": "@nx/jest:jest", "options": { - "jestConfig": "packages/@o3r/styling/testing/jest.config.it.js", - "silent": false + "jestConfig": "packages/@o3r/styling/testing/jest.config.it.js" } }, "prepare-publish": { diff --git a/packages/@o3r/styling/schematics/index.it.spec.ts b/packages/@o3r/styling/schematics/index.it.spec.ts index f8ed9ff4f6..1f17619e6a 100644 --- a/packages/@o3r/styling/schematics/index.it.spec.ts +++ b/packages/@o3r/styling/schematics/index.it.spec.ts @@ -4,34 +4,51 @@ import { getGitDiff, packageManagerExec, packageManagerInstall, - packageManagerRun, + packageManagerRunOnProject, prepareTestEnv, setupLocalRegistry } from '@o3r/test-helpers'; +import { rm } from 'node:fs/promises'; +import * as path from 'node:path'; -const appName = 'test-app-styling'; +const appFolder = 'test-app-styling'; const o3rVersion = '999.0.0'; const execAppOptions = getDefaultExecSyncOptions(); -let appFolderPath: string; - +let projectPath: string; +let workspacePath: string; +let projectName: string; +let isInWorkspace: boolean; +let untouchedProjectPath: undefined | string; describe('new otter application with styling', () => { setupLocalRegistry(); beforeAll(async () => { - appFolderPath = await prepareTestEnv(appName, 'angular-with-o3r-core'); - execAppOptions.cwd = appFolderPath; + ({ projectPath, workspacePath, projectName, isInWorkspace, untouchedProjectPath } = await prepareTestEnv(appFolder)); + execAppOptions.cwd = workspacePath; + }); + afterAll(async () => { + try { await rm(workspacePath, { recursive: true }); } catch { /* ignore error */ } }); test('should add styling to existing application', async () => { - packageManagerExec(`ng add --skip-confirmation @o3r/styling@${o3rVersion} --enable-metadata-extract`, execAppOptions); + const relativeProjectPath = path.relative(workspacePath, projectPath); + packageManagerExec({script: 'ng', args: ['add', `@o3r/styling@${o3rVersion}`, '--enable-metadata-extract', '--skip-confirmation', '--project-name', projectName]}, execAppOptions); - packageManagerExec('ng g @o3r/core:component --defaults=true test-component --use-otter-theming=false', execAppOptions); - packageManagerExec('ng g @o3r/styling:add-theming --path="src/components/test-component/test-component.style.scss"', execAppOptions); - await addImportToAppModule(appFolderPath, 'TestComponentModule', 'src/components/test-component'); + packageManagerExec({script: 'ng', args: ['g', '@o3r/core:component', '--defaults', 'true', 'test-component', '--use-otter-theming', 'false', '--project-name', projectName]}, execAppOptions); + const filePath = path.normalize(path.join(relativeProjectPath, 'src/components/test-component/test-component.style.scss')); + packageManagerExec({script: 'ng', args: ['g', '@o3r/styling:add-theming', '--path', filePath]}, execAppOptions); + addImportToAppModule(projectPath, 'TestComponentModule', 'src/components/test-component'); + + await addImportToAppModule(projectPath, 'TestComponentModule', 'src/components/test-component'); const diff = getGitDiff(execAppOptions.cwd as string); expect(diff.modified).toContain('package.json'); - expect(diff.added).toContain('src/components/test-component/test-component.style.theme.scss'); + expect(diff.added).toContain(path.join(relativeProjectPath, 'src/components/test-component/test-component.style.theme.scss').replace(/[\\/]+/g, '/')); + + if (untouchedProjectPath) { + const relativeUntouchedProjectPath = path.relative(workspacePath, untouchedProjectPath); + expect(diff.all.filter((file) => new RegExp(relativeUntouchedProjectPath.replace(/[\\/]+/g, '[\\\\/]')).test(file)).length).toBe(0); + } expect(() => packageManagerInstall(execAppOptions)).not.toThrow(); - expect(() => packageManagerRun('build', execAppOptions)).not.toThrow(); + expect(() => packageManagerRunOnProject(projectName, isInWorkspace, {script: 'build'}, execAppOptions)).not.toThrow(); }); }); diff --git a/packages/@o3r/styling/schematics/ng-add/index.ts b/packages/@o3r/styling/schematics/ng-add/index.ts index 49034d9106..38142e6909 100644 --- a/packages/@o3r/styling/schematics/ng-add/index.ts +++ b/packages/@o3r/styling/schematics/ng-add/index.ts @@ -5,6 +5,16 @@ import * as path from 'node:path'; import { updateCmsAdapter } from '../cms-adapter'; import type { NgAddSchematicsSchema } from './schema'; +const devDependenciesToInstall = [ + 'chokidar', + 'sass-loader' +]; + +const dependenciesToInstall = [ + '@angular/material', + '@angular/cdk' +]; + /** * Add Otter styling to an Angular Project * Update the styling if the app/lib used otter v7 @@ -16,15 +26,16 @@ function ngAddFn(options: NgAddSchematicsSchema): Rule { try { const { getDefaultOptionsForSchematic, + getPackageInstallConfig, getO3rPeerDeps, - getProjectNewDependenciesType, + getProjectNewDependenciesTypes, getWorkspaceConfig, - ngAddPackages, - ngAddPeerDependencyPackages, + setupDependencies, removePackages, registerPackageCollectionSchematics, setupSchematicsDefaultParams, - updateSassImports + updateSassImports, + getExternalDependenciesVersionRange } = await import('@o3r/schematics'); options = {...getDefaultOptionsForSchematic(getWorkspaceConfig(tree), '@o3r/styling', 'ng-add', options), ...options}; const {updateThemeFiles, removeV7OtterAssetsInAngularJson} = await import('./theme-files'); @@ -35,20 +46,42 @@ function ngAddFn(options: NgAddSchematicsSchema): Rule { depsInfo.o3rPeerDeps = [...depsInfo.o3rPeerDeps , '@o3r/extractors']; } const workspaceProject = options.projectName ? getWorkspaceConfig(tree)?.projects[options.projectName] : undefined; - const workingDirectory = workspaceProject?.root || '.'; - const dependencyType = getProjectNewDependenciesType(workspaceProject); + const dependencies = depsInfo.o3rPeerDeps.reduce((acc, dep) => { + acc[dep] = { + inManifest: [{ + range: `~${depsInfo.packageVersion}`, + types: getProjectNewDependenciesTypes(workspaceProject) + }] + }; + return acc; + }, getPackageInstallConfig(packageJsonPath, tree, options.projectName)); + Object.entries(getExternalDependenciesVersionRange(devDependenciesToInstall, packageJsonPath)) + .forEach(([dep, range]) => { + dependencies[dep] = { + inManifest: [{ + range, + types: [NodeDependencyType.Dev] + }] + }; + }); + Object.entries(getExternalDependenciesVersionRange(dependenciesToInstall, packageJsonPath)) + .forEach(([dep, range]) => { + dependencies[dep] = { + inManifest: [{ + range, + types: getProjectNewDependenciesTypes(workspaceProject) + }] + }; + }); return () => chain([ removePackages(['@otter/styling']), updateSassImports('o3r'), updateThemeFiles(__dirname, options), removeV7OtterAssetsInAngularJson(options), - ngAddPackages(depsInfo.o3rPeerDeps, { - skipConfirmation: true, - version: depsInfo.packageVersion, - parentPackageInfo: depsInfo.packageName, + setupDependencies({ projectName: options.projectName, - dependencyType, - workingDirectory + dependencies, + ngAddToRun: depsInfo.o3rPeerDeps }), registerPackageCollectionSchematics(JSON.parse(fs.readFileSync(packageJsonPath).toString())), setupSchematicsDefaultParams({ @@ -61,7 +94,6 @@ function ngAddFn(options: NgAddSchematicsSchema): Rule { useOtterTheming: undefined } }), - ngAddPeerDependencyPackages(['chokidar'], packageJsonPath, NodeDependencyType.Dev, {...options, workingDirectory, skipNgAddSchematicRun: true}, depsInfo.packageName), ...(options.enableMetadataExtract ? [updateCmsAdapter(options)] : []) ])(tree, context); } catch (e) { diff --git a/packages/@o3r/test-helpers/project.json b/packages/@o3r/test-helpers/project.json index 435ef28908..4a22866e84 100644 --- a/packages/@o3r/test-helpers/project.json +++ b/packages/@o3r/test-helpers/project.json @@ -27,10 +27,8 @@ "options": { "main": "packages/@o3r/test-helpers/src/public_api.ts", "tsConfig": "packages/@o3r/test-helpers/tsconfig.build.json", - "outputPath": "packages/@o3r/test-helpers/dist", - "clean": false - }, - "dependsOn": ["^build"] + "outputPath": "packages/@o3r/test-helpers/dist" + } }, "prepare-build-builders": { "executor": "nx:run-script", diff --git a/packages/@o3r/test-helpers/schematics/ng-add/index.ts b/packages/@o3r/test-helpers/schematics/ng-add/index.ts index d9b595cc48..3dd63d3bea 100644 --- a/packages/@o3r/test-helpers/schematics/ng-add/index.ts +++ b/packages/@o3r/test-helpers/schematics/ng-add/index.ts @@ -2,6 +2,7 @@ import { chain, noop, Rule } from '@angular-devkit/schematics'; import { createSchematicWithMetricsIfInstalled } from '@o3r/schematics'; import type { NgAddSchematicsSchema } from './schema'; import * as path from 'node:path'; +import type { DependencyToAdd } from '@o3r/schematics'; const doCustomAction: Rule = (tree, _context) => { // your custom code here @@ -16,24 +17,28 @@ function ngAddFn(options: NgAddSchematicsSchema): Rule { return async (tree, context) => { try { // use dynamic import to properly raise an exception if it is not an Otter project. - const { applyEsLintFix, install, ngAddPackages, getO3rPeerDeps, getWorkspaceConfig } = await import('@o3r/schematics'); + const { getWorkspaceConfig, applyEsLintFix, setupDependencies, getO3rPeerDeps, getProjectNewDependenciesTypes } = await import('@o3r/schematics'); // retrieve dependencies following the /^@o3r\/.*/ pattern within the peerDependencies of the current module const depsInfo = getO3rPeerDeps(path.resolve(__dirname, '..', '..', 'package.json')); const workspaceProject = options.projectName ? getWorkspaceConfig(tree)?.projects[options.projectName] : undefined; - const workingDirectory = workspaceProject?.root || '.'; + const dependencies = depsInfo.o3rPeerDeps.reduce((acc, dep) => { + acc[dep] = { + inManifest: [{ + range: `~${depsInfo.packageVersion}`, + types: getProjectNewDependenciesTypes(workspaceProject) + }] + }; + return acc; + }, {} as Record); return chain([ // optional custom action dedicated to this module doCustomAction, options.skipLinter ? noop() : applyEsLintFix(), - // install packages needed in the current module - options.skipInstall ? noop : install, // add the missing Otter modules in the current project - ngAddPackages(depsInfo.o3rPeerDeps, { - workingDirectory, - skipConfirmation: true, - version: depsInfo.packageVersion, + setupDependencies({ projectName: options.projectName, - parentPackageInfo: `${depsInfo.packageName!} - setup` + dependencies, + ngAddToRun: depsInfo.o3rPeerDeps }) ]); } catch (e) { diff --git a/packages/@o3r/test-helpers/src/prepare-test-env.ts b/packages/@o3r/test-helpers/src/prepare-test-env.ts index 5964edba66..e67e72424d 100644 --- a/packages/@o3r/test-helpers/src/prepare-test-env.ts +++ b/packages/@o3r/test-helpers/src/prepare-test-env.ts @@ -2,20 +2,20 @@ import { execSync, ExecSyncOptions } from 'node:child_process'; import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync } from 'node:fs'; import * as path from 'node:path'; import type { PackageJson } from 'type-fest'; -import { minVersion } from 'semver'; -import { createTestEnvironmentAngular } from './test-environments/create-test-environment-angular'; -import { createTestEnvironmentAngularWithO3rCore } from './test-environments/create-test-environment-angular-with-o3r-core'; import { createTestEnvironmentBlank } from './test-environments/create-test-environment-blank'; -import { createWithLock, packageManagerInstall, setPackagerManagerConfig, setupGit } from './utilities'; +import { createWithLock, getPackageManager, type Logger, packageManagerInstall, setPackagerManagerConfig, setupGit } from './utilities/index'; +import { createTestEnvironmentOtterProjectWithApp } from './test-environments/create-test-environment-otter-project'; +import { O3rCliError } from '@o3r/schematics'; /** - * 'blank' only create yarn/npm config - * 'angular' also create a new angular app - * 'angular-with-o3r-core' also preinstall o3r-core with basic preset - * 'angular-monorepo' is the same as 'angular' but with a monorepo structure - * 'angular-monorepo-with-o3r-core' is the same as 'angular-with-o3r-core' but with a monorepo structure + * - 'blank' only create yarn/npm config + * - 'o3r-project' create a new otter project with a new application */ -export type PrepareTestEnvType = 'blank' | 'angular' | 'angular-with-o3r-core' | 'angular-monorepo' | 'angular-monorepo-with-o3r-core'; +// * - 'angular' also create a new angular app +// * - 'angular-with-o3r-core' also preinstall o3r-core with basic preset +// * - 'angular-monorepo' is the same as 'angular' but with a monorepo structure +// * - 'angular-monorepo-with-o3r-core' is the same as 'angular-with-o3r-core' but with a monorepo structure +export type PrepareTestEnvType = 'blank' | 'o3r-project-with-app'; /** * Retrieve the version used by yarn and setup at root level @@ -28,36 +28,41 @@ export function getYarnVersionFromRoot(rootFolderPath: string) { return o3rPackageJson?.packageManager?.split('@')?.[1] || 'latest'; } +export interface PrepareTestEnvOptions { + /** Type of environment to prepare */ + type?: PrepareTestEnvType; + /** Explicitly set the yarn version for yarn environment. Else it will default to the folder package.json */ + yarnVersion?: string; + /** Logger to use for logging */ + logger?: Logger; +} + /** * Prepare a test environment to be used to run tests targeting a local registry - * Test app created for 'angular' and 'angular-with-o3r-core' are reused when called multiple times - * @param folderName name of the folder where the environment will be generated - * @param type type of environment to prepare - * @param yarnVersionParam explicitely set the yarn version for yarn environment. Else it will default to the folder package.json + * @param folderName + * @param options */ -export async function prepareTestEnv(folderName: string, type: PrepareTestEnvType, yarnVersionParam?: string) { +export async function prepareTestEnv(folderName: string, options?: PrepareTestEnvOptions) { + const type = options?.type || process.env.PREPARE_TEST_ENV_TYPE || 'o3r-project'; + const logger = options?.logger || console; + const yarnVersionParam = options?.yarnVersion; const rootFolderPath = process.cwd(); - const itTestsFolderPath = path.join(rootFolderPath, '..', 'it-tests'); - const appFolderPath = path.join(itTestsFolderPath, folderName); - const globalFolderPath = path.join(rootFolderPath, '.cache', 'test-app'); - const cacheFolderPath = path.join(globalFolderPath, 'cache'); + const itTestsFolderPath = path.resolve(rootFolderPath, '..', 'it-tests'); + const workspacePath = path.resolve(itTestsFolderPath, folderName); + const globalFolderPath = path.resolve(rootFolderPath, '.cache', 'test-app'); + const cacheFolderPath = path.resolve(globalFolderPath, 'cache'); - const o3rCorePackageJson: PackageJson & { generatorDependencies?: Record } = - JSON.parse(readFileSync(path.join(rootFolderPath, 'packages', '@o3r', 'core', 'package.json')).toString()); + JSON.parse(readFileSync(path.join(rootFolderPath, 'packages', '@o3r', 'core', 'package.json')).toString()); const yarnVersion: string = yarnVersionParam || getYarnVersionFromRoot(rootFolderPath); - const angularVersion = minVersion(o3rCorePackageJson.devDependencies?.['@angular-devkit/schematics'] || 'latest')?.version; - const materialVersion = minVersion(o3rCorePackageJson.generatorDependencies?.['@angular/material'] || angularVersion || 'latest')?.version; - const generateMonorepo = type === 'angular-monorepo' || type === 'angular-monorepo-with-o3r-core'; - const generateO3rCore = type === 'angular-with-o3r-core' || type === 'angular-monorepo-with-o3r-core'; const execAppOptions: ExecSyncOptions = { - cwd: appFolderPath, + cwd: workspacePath, stdio: 'inherit', // eslint-disable-next-line @typescript-eslint/naming-convention env: {...process.env, NODE_OPTIONS: '', CI: 'true'} }; // Remove all cache entries relative to local workspaces (@o3r, @ama-sdk, @ama-terasu) - if (!process.env.CI && existsSync(cacheFolderPath)) { + if (!!process.env.CI && existsSync(cacheFolderPath)) { const workspacesList = execSync('yarn workspaces:list', {stdio: 'pipe'}).toString().split('\n') .map((workspace) => workspace.replace('packages/', '').replace(/\//, '-')) .filter((workspace) => !!workspace); @@ -72,69 +77,88 @@ export async function prepareTestEnv(folderName: string, type: PrepareTestEnvTyp }); } + const packageManagerConfig = { + yarnVersion, + globalFolderPath, + registry: 'http://127.0.0.1:4873' + }; + // Create it-tests folder if (!existsSync(itTestsFolderPath)) { + logger.debug?.(`Creating it-tests folder`); await createWithLock(() => { mkdirSync(itTestsFolderPath); - setPackagerManagerConfig({yarnVersion, globalFolderPath, registry: 'http://localhost:4873'}, {...execAppOptions, cwd: itTestsFolderPath}); + setPackagerManagerConfig(packageManagerConfig, {...execAppOptions, cwd: itTestsFolderPath}); return Promise.resolve(); }, {lockFilePath: `${itTestsFolderPath}.lock`, cwd: path.join(rootFolderPath, '..'), appDirectory: 'it-tests'}); } // Remove existing app - if (existsSync(appFolderPath)) { - rmSync(appFolderPath, {recursive: true}); + if (existsSync(workspacePath)) { + rmSync(workspacePath, {recursive: true}); } - if (type === 'blank') { - createTestEnvironmentBlank({ - appDirectory: folderName, - cwd: itTestsFolderPath, - globalFolderPath, - yarnVersion - }); - } else { - // Create new base app if needed - const baseAppAngular = `base-app-angular${generateMonorepo ? '-monorepo' : ''}`; - await createTestEnvironmentAngular({ - appName: 'test-app', - appDirectory: baseAppAngular, - cwd: itTestsFolderPath, - globalFolderPath, - yarnVersion, - angularVersion, - materialVersion, - replaceExisting: !process.env.CI, - generateMonorepo - }); + const prepareFinalApp = (baseApp: string) => { + logger.debug?.(`Copying ${baseApp} to ${workspacePath}`); + const baseProjectPath = path.join(itTestsFolderPath, baseApp); + cpSync(baseProjectPath, workspacePath, { recursive: true, dereference: true, filter: (source) => !/node_modules/.test(source) }); + if (existsSync(path.join(workspacePath, 'package.json'))) { + packageManagerInstall(execAppOptions); + } + }; - if (type === 'angular-with-o3r-core' || type === 'angular-monorepo-with-o3r-core') { - // Create new base app if needed - await createTestEnvironmentAngularWithO3rCore({ - appName: 'test-app', - appDirectory: `base-app-angular${generateMonorepo ? '-monorepo' : ''}-with-o3r-core`, + let projectPath = workspacePath; + let projectName = ''; + let isInWorkspace = false; + let untouchedProject: undefined | string; + let untouchedProjectPath: undefined | string; + const appDirectory = `${type}-${getPackageManager()}`; + switch (type) { + case 'blank': { + await createTestEnvironmentBlank({ + appDirectory, cwd: itTestsFolderPath, - globalFolderPath, - yarnVersion, - angularVersion, - materialVersion, - baseAngularAppPath: path.join(itTestsFolderPath, baseAppAngular), - replaceExisting: !process.env.CI, - generateMonorepo + logger, + ...packageManagerConfig }); + projectPath = workspacePath; + break; + } + + case 'o3r-project-with-app': { + projectName = 'test-app'; + untouchedProject = 'dont-modify-me'; + await createTestEnvironmentOtterProjectWithApp({ + projectName, + appDirectory, + cwd: itTestsFolderPath, + logger, + ...packageManagerConfig, + replaceExisting: !process.env.CI + }); + projectPath = path.resolve(workspacePath, 'apps', projectName); + untouchedProjectPath = path.resolve(workspacePath, 'apps', untouchedProject); + isInWorkspace = true; + break; } - } - // Copy base app into test app - if (type !== 'blank') { - const baseApp = `base-app-angular${generateMonorepo ? '-monorepo' : ''}${generateO3rCore ? '-with-o3r-core' : ''}`; - const baseAppPath = path.join(itTestsFolderPath, baseApp); - cpSync(baseAppPath, appFolderPath, {recursive: true, dereference: true, filter: (source) => !/node_modules/.test(source)}); - packageManagerInstall(execAppOptions); + default: { + throw new O3rCliError(`Unknown test environment type: ${type}`); + } } + prepareFinalApp(appDirectory); + // Setup git and initial commit to easily make checks on the diff inside the tests - setupGit(appFolderPath); + setupGit(workspacePath); - return appFolderPath; + return { + workspacePath, + projectPath, + projectName, + isInWorkspace, + untouchedProject, + untouchedProjectPath, + packageManagerConfig + }; } diff --git a/packages/@o3r/test-helpers/src/test-environments/create-test-environment-angular-with-o3r-core.ts b/packages/@o3r/test-helpers/src/test-environments/create-test-environment-angular-with-o3r-core.ts deleted file mode 100644 index ca91ab495d..0000000000 --- a/packages/@o3r/test-helpers/src/test-environments/create-test-environment-angular-with-o3r-core.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { ExecSyncOptions } from 'node:child_process'; -import { cpSync, existsSync } from 'node:fs'; -import path from 'node:path'; -import { createTestEnvironmentAngular, CreateTestEnvironmentAngularOptions } from './create-test-environment-angular'; -import { createWithLock, packageManagerAdd, packageManagerExec, packageManagerInstall } from '../utilities'; - -export interface CreateTestEnvironmentAngularWithO3rCoreOptions extends CreateTestEnvironmentAngularOptions { - /** - * Name of the app to generate - */ - appName: string; - - /** - * Directory used to generate app - */ - appDirectory: string; - - /** - * Working directory - */ - cwd: string; - - /** - * Generate the app inside a monorepo - */ - generateMonorepo?: boolean; - - /** - * Version of Angular to install - */ - angularVersion?: string; - - /** - * Version of Angular Material to install - */ - materialVersion?: string; - - /** - * Angular app to copy to avoid recreating everything - */ - baseAngularAppPath?: string; -} - -/** - * Generate a base angular app with minimal necessary dependencies and @o3r/core installed with basic preset - * Uses a locker mechanism so this function can be called in parallel - * The lock will automatically expire after 10 minutes if the creation of the app failed for whatever reason - * @param inputOptions - */ -export async function createTestEnvironmentAngularWithO3rCore(inputOptions: Partial) { - const options: CreateTestEnvironmentAngularWithO3rCoreOptions = { - appName: 'test-app', - appDirectory: 'test-app', - cwd: process.cwd(), - generateMonorepo: false, - globalFolderPath: process.cwd(), - registry: 'http://127.0.0.1:4873', - useLocker: true, - lockTimeout: 10 * 60 * 1000, - replaceExisting: true, - ...inputOptions - }; - const dependenciesToCheck = [ - {name: '@angular-devkit/schematics', expected: options.angularVersion}, - {name: '@angular/material', expected: options.materialVersion} - ]; - await createWithLock(async () => { - const appFolderPath = path.join(options.cwd, options.appDirectory); - const execAppOptions: ExecSyncOptions = { - cwd: appFolderPath, - stdio: 'inherit', - // eslint-disable-next-line @typescript-eslint/naming-convention - env: {...process.env, NODE_OPTIONS: '', CI: 'true'} - }; - if (options.baseAngularAppPath && existsSync(options.baseAngularAppPath)) { - cpSync(options.baseAngularAppPath, appFolderPath, {recursive: true, dereference: true, filter: (source) => !/node_modules/.test(source)}); - packageManagerInstall(execAppOptions); - } else { - await createTestEnvironmentAngular({...options, useLocker: false}); - } - const o3rVersion = '999.0.0'; - if (options.generateMonorepo) { - packageManagerExec(`ng add --skip-confirmation @o3r/core@${o3rVersion} --skip-git`, execAppOptions); - // FIXME: workaround for yarn pnp (same issue with node_modules but the runner won't complain if package is present in root instead of project) - packageManagerAdd(`@o3r/core@${o3rVersion}`, {...execAppOptions, cwd: path.join(appFolderPath, 'projects', 'test-app')}); - packageManagerExec(`ng add --skip-confirmation @o3r/core@${o3rVersion} --project-name=test-app --skip-git`, execAppOptions); - } else { - packageManagerExec(`ng add --skip-confirmation @o3r/core@${o3rVersion} --skip-git`, execAppOptions); - } - - packageManagerInstall(execAppOptions); - }, {lockFilePath: path.join(options.cwd, `${options.appDirectory}-ongoing.lock`), ...options, dependenciesToCheck}); -} diff --git a/packages/@o3r/test-helpers/src/test-environments/create-test-environment-angular.ts b/packages/@o3r/test-helpers/src/test-environments/create-test-environment-angular.ts deleted file mode 100644 index c43a5a78a7..0000000000 --- a/packages/@o3r/test-helpers/src/test-environments/create-test-environment-angular.ts +++ /dev/null @@ -1,162 +0,0 @@ -import { execFileSync, ExecSyncOptions } from 'node:child_process'; -import { cpSync, existsSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; -import * as path from 'node:path'; -import { - createWithLock, - CreateWithLockOptions, - fixAngularVersion, - getPackageManager, - packageManagerAdd, - PackageManagerConfig, - packageManagerExec, - packageManagerInstall, - setPackagerManagerConfig -} from '../utilities'; - -export interface CreateTestEnvironmentAngularOptions extends CreateWithLockOptions, PackageManagerConfig { - /** - * Name of the app to generate - */ - appName: string; - - /** - * Directory used to generate app - */ - appDirectory: string; - - /** - * Working directory - */ - cwd: string; - - /** - * Generate the app inside a monorepo - */ - generateMonorepo?: boolean; - - /** - * Version of Angular to install - */ - angularVersion?: string; - - /** - * Version of Angular Material to install - */ - materialVersion?: string; -} - -/** - * Generate a base angular app with minimal necessary dependencies - * Uses a locker mechanism so this function can be called in parallel - * The lock will automatically expire after 10 minutes if the creation of the app failed for whatever reason - * @param inputOptions - */ -export async function createTestEnvironmentAngular(inputOptions: Partial) { - const options: CreateTestEnvironmentAngularOptions = { - appName: 'test-app', - appDirectory: 'test-app', - cwd: process.cwd(), - generateMonorepo: false, - globalFolderPath: process.cwd(), - registry: 'http://127.0.0.1:4873', - useLocker: true, - lockTimeout: 10 * 60 * 1000, - replaceExisting: true, - ...inputOptions - }; - const dependenciesToCheck = [ - {name: '@angular-devkit/schematics', expected: options.angularVersion}, - {name: '@angular/material', expected: options.materialVersion} - ]; - await createWithLock(async () => { - const appFolderPath = path.join(options.cwd, options.appDirectory); - const execAppOptions: ExecSyncOptions = { - cwd: appFolderPath, - stdio: 'inherit', - // eslint-disable-next-line @typescript-eslint/naming-convention - env: {...process.env, NODE_OPTIONS: '', CI: 'true'} - }; - - // Prepare folder - if (existsSync(appFolderPath)) { - rmSync(appFolderPath, {recursive: true}); - } - - // Create angular app - if (options.generateMonorepo) { - const createOptions = `--directory=${options.appDirectory} --no-create-application --skip-git --skip-install --package-manager=${getPackageManager()}`; - execFileSync('npm', `create @angular${options.angularVersion ? `@${options.angularVersion}` : ''} ${options.appName} -- ${createOptions}`.split(' '), - // eslint-disable-next-line @typescript-eslint/naming-convention - {...execAppOptions, cwd: options.cwd, shell: process.platform === 'win32'}); - // By default node_modules inside projects are not git-ignored - const gitIgnorePath = path.join(appFolderPath, '.gitignore'); - if (existsSync(gitIgnorePath)) { - const gitIgnore = readFileSync(gitIgnorePath, {encoding: 'utf8'}); - writeFileSync(gitIgnorePath, gitIgnore.replace(/\/(dist|node_modules)/g, '$1')); - } - await fixAngularVersion(appFolderPath); - setPackagerManagerConfig(options, execAppOptions); - packageManagerInstall(execAppOptions); - packageManagerExec('ng g application dont-modify-me --style=scss --routing --skip-install', execAppOptions); - packageManagerExec(`ng g application ${options.appName} --style=scss --routing --skip-install`, execAppOptions); - } else { - const createOptions = `--directory=${options.appDirectory} --style=scss --routing --skip-git --skip-install --package-manager=${getPackageManager()}`; - execFileSync('npm', `create @angular${options.angularVersion ? `@${options.angularVersion}` : ''} ${options.appName} -- ${createOptions}`.split(' '), - // eslint-disable-next-line @typescript-eslint/naming-convention - {...execAppOptions, cwd: options.cwd, shell: process.platform === 'win32'}); - await fixAngularVersion(appFolderPath); - setPackagerManagerConfig(options, execAppOptions); - packageManagerInstall(execAppOptions); - } - packageManagerExec('ng config cli.cache.environment all', execAppOptions); - if (options.globalFolderPath) { - packageManagerExec(`ng config cli.cache.path "${path.join(options.globalFolderPath, '.angular', 'cache')}"`, execAppOptions); - } - - // Add dependencies - const dependenciesToInstall = { - // eslint-disable-next-line @typescript-eslint/naming-convention - '@angular-devkit/schematics': options.angularVersion, - // eslint-disable-next-line @typescript-eslint/naming-convention - '@angular/pwa': options.angularVersion, - // eslint-disable-next-line @typescript-eslint/naming-convention - '@angular/material': options.materialVersion, - // eslint-disable-next-line @typescript-eslint/naming-convention - '@angular-devkit/core': options.angularVersion, - // eslint-disable-next-line @typescript-eslint/naming-convention - '@schematics/angular': options.angularVersion - }; - packageManagerAdd( - `${Object.entries(dependenciesToInstall).map(([depName, version]) => `${depName}${version ? `@~${version}` : ''}`).join(' ')}`, - execAppOptions - ); - - // Run ng-adds - const project = options.generateMonorepo ? '--project=test-app' : ''; - packageManagerExec(`ng add @angular/pwa ${project}`, execAppOptions); - packageManagerExec(`ng add @angular/material ${project}`, execAppOptions); - - if (options.generateMonorepo) { - // TODO remove this when https://github.com/AmadeusITGroup/otter/issues/603 has been resolved - const workspacePackageJsonPath = path.join(appFolderPath, 'package.json'); - const packageJsonString = readFileSync(workspacePackageJsonPath, {encoding: 'utf8'}); - const packageJson = JSON.parse(packageJsonString); - writeFileSync(workspacePackageJsonPath, JSON.stringify({ - ...packageJson, - scripts: {...packageJson.scripts, build: getPackageManager() === 'npm' ? 'npm run build --workspaces' : 'yarn workspaces foreach -A run build'}, - workspaces: ['projects/*'] - }, null, 2)); - writeFileSync(path.join(appFolderPath, 'projects', 'test-app', 'package.json'), packageJsonString.replace(/"test-app"/, '"test-app-project"')); - writeFileSync(path.join(appFolderPath, 'projects', 'dont-modify-me', 'package.json'), packageJsonString.replace(/"test-app"/, '"dont-modify-me"')); - - // TODO remove this if we manage to make 'workspace <> ng add' work with private registry - cpSync(path.join(appFolderPath, '.npmrc'), path.join(appFolderPath, 'projects', 'test-app', '.npmrc')); - - if (getPackageManager() === 'yarn' && options.yarnVersion && Number.parseInt(options.yarnVersion.split('.')[0], 10) < 4) { - execFileSync('yarn', ['plugin', 'import', 'workspace-tools'], {...execAppOptions, shell: process.platform === 'win32'}); - } - } - - packageManagerInstall(execAppOptions); - }, {lockFilePath: path.join(options.cwd, `${options.appDirectory}-ongoing.lock`), ...options, dependenciesToCheck}); -} diff --git a/packages/@o3r/test-helpers/src/test-environments/create-test-environment-blank.ts b/packages/@o3r/test-helpers/src/test-environments/create-test-environment-blank.ts index 4f15da518c..7fed2d7562 100644 --- a/packages/@o3r/test-helpers/src/test-environments/create-test-environment-blank.ts +++ b/packages/@o3r/test-helpers/src/test-environments/create-test-environment-blank.ts @@ -1,9 +1,9 @@ -import { ExecSyncOptions } from 'node:child_process'; -import { existsSync, mkdirSync, rmSync } from 'node:fs'; +import { execFileSync, ExecSyncOptions } from 'node:child_process'; +import { existsSync, promises as fs } from 'node:fs'; import * as path from 'node:path'; -import { PackageManagerConfig, setPackagerManagerConfig } from '../utilities'; +import { createWithLock, type CreateWithLockOptions, type Logger, PackageManagerConfig, setPackagerManagerConfig } from '../utilities'; -export interface CreateTestEnvironmentBlankOptions extends PackageManagerConfig { +export interface CreateTestEnvironmentBlankOptions extends CreateWithLockOptions, PackageManagerConfig { /** * Directory used to generate app */ @@ -13,38 +13,57 @@ export interface CreateTestEnvironmentBlankOptions extends PackageManagerConfig * Working directory */ cwd: string; + + + /** Logger to use for logging */ + logger?: Logger; } /** * Generate a folder with minimal config for package manager to target local packages with Verdaccio * @param inputOptions */ -export function createTestEnvironmentBlank(inputOptions: Partial) { +export async function createTestEnvironmentBlank(inputOptions: Partial) { const options: CreateTestEnvironmentBlankOptions = { appDirectory: 'test-app', cwd: process.cwd(), globalFolderPath: process.cwd(), registry: 'http://127.0.0.1:4873', + useLocker: true, + lockTimeout: 10 * 60 * 1000, + replaceExisting: true, ...inputOptions }; - const appFolderPath = path.join(options.cwd, options.appDirectory); - if (existsSync(appFolderPath)) { - return; - } - - const execAppOptions: ExecSyncOptions = { - cwd: appFolderPath, - stdio: 'inherit', - // eslint-disable-next-line @typescript-eslint/naming-convention - env: {...process.env, NODE_OPTIONS: '', CI: 'true'} - }; + await createWithLock(async () => { + const yarnVersion = options.yarnVersion || 'latest'; + const appFolderPath = path.join(options.cwd, options.appDirectory); + const execAppOptions: ExecSyncOptions = { + cwd: appFolderPath, + stdio: 'inherit', + // eslint-disable-next-line @typescript-eslint/naming-convention + env: {...process.env, NODE_OPTIONS: '', CI: 'true'} + }; + + // Prepare folder + if (existsSync(appFolderPath)) { + await fs.rm(appFolderPath, {recursive: true}); + } + + await fs.mkdir(appFolderPath, {recursive: true}); + + options.logger?.info(`Set yarn version to ${yarnVersion} locally in ${appFolderPath}`); + execFileSync('yarn', ['set', 'version', yarnVersion], { ...execAppOptions, shell: process.platform === 'win32' }); - // Prepare folder - if (existsSync(appFolderPath)) { - rmSync(appFolderPath, {recursive: true}); - } + setPackagerManagerConfig(options, { ...execAppOptions, cwd: options.cwd }); + setPackagerManagerConfig(options, execAppOptions); - mkdirSync(appFolderPath, {recursive: true}); + // some yarn versions generate a package.json that need to be removed to avoid polluting the test workspace + if (existsSync(path.join(appFolderPath, 'package.json'))) { + await fs.rm(path.join(appFolderPath, 'package.json')); + } + if (existsSync(path.join(appFolderPath, 'yarn.lock'))) { + await fs.rm(path.join(appFolderPath, 'yarn.lock')); + } - setPackagerManagerConfig(options, execAppOptions); + }, { lockFilePath: path.join(options.cwd, `${options.appDirectory}-ongoing.lock`), ...options }); } diff --git a/packages/@o3r/test-helpers/src/test-environments/create-test-environment-otter-project.ts b/packages/@o3r/test-helpers/src/test-environments/create-test-environment-otter-project.ts new file mode 100644 index 0000000000..805de8c147 --- /dev/null +++ b/packages/@o3r/test-helpers/src/test-environments/create-test-environment-otter-project.ts @@ -0,0 +1,98 @@ +import { ExecSyncOptions } from 'node:child_process'; +import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; +import * as path from 'node:path'; +import { + createWithLock, + CreateWithLockOptions, + getPackageManager, + type Logger, + PackageManagerConfig, + packageManagerCreate, + packageManagerExec, + packageManagerInstall, + setPackagerManagerConfig +} from '../utilities'; + +export interface CreateTestEnvironmentOtterProjectWithAppOptions extends CreateWithLockOptions, PackageManagerConfig { + /** + * Name of the app to generate + */ + projectName: string; + + /** + * Working directory + */ + cwd: string; + + /** + * Otter dependency version + * @default '999.0.0' + */ + o3rVersion: string; + + /** Logger to use for logging */ + logger?: Logger; +} + +const o3rVersion = '999.0.0'; + +/** + * Generate a base angular app with minimal necessary dependencies + * Uses a locker mechanism so this function can be called in parallel + * The lock will automatically expire after 10 minutes if the creation of the app failed for whatever reason + * @param inputOptions + */ +export async function createTestEnvironmentOtterProjectWithApp(inputOptions: Partial) { + const options: CreateTestEnvironmentOtterProjectWithAppOptions = { + projectName: 'test-app', + appDirectory: 'test-app', + o3rVersion, + cwd: process.cwd(), + globalFolderPath: process.cwd(), + registry: 'http://127.0.0.1:4873', + useLocker: true, + lockTimeout: 10 * 60 * 1000, + replaceExisting: true, + ...inputOptions + }; + + await createWithLock(() => { + const appFolderPath = path.join(options.cwd, options.appDirectory); + const execAppOptions: ExecSyncOptions = { + cwd: appFolderPath, + stdio: 'inherit', + // eslint-disable-next-line @typescript-eslint/naming-convention + env: { ...process.env, NODE_OPTIONS: '', CI: 'true' } + }; + + // Prepare folder + if (existsSync(appFolderPath)) { + rmSync(appFolderPath, { recursive: true }); + } + + // prepare package manager config + setPackagerManagerConfig(options, { ...execAppOptions, cwd: options.cwd }); + try { mkdirSync(appFolderPath, { recursive: true }); } catch { } + setPackagerManagerConfig(options, execAppOptions); + + // Create Project + const createOptions = ['--package-manager', getPackageManager(), '--skip-confirmation', ...(options.yarnVersion ? ['--yarn-version', options.yarnVersion] : [])]; + packageManagerCreate({ script: `@o3r@${o3rVersion}`, args: [options.appDirectory, ...createOptions] }, { ...execAppOptions, cwd: options.cwd}, 'npm'); + const gitIgnorePath = path.join(appFolderPath, '.gitignore'); + if (existsSync(gitIgnorePath)) { + const gitIgnore = readFileSync(gitIgnorePath, { encoding: 'utf8' }); + writeFileSync(gitIgnorePath, gitIgnore.replace(/\/(dist|node_modules)/g, '$1')); + } + packageManagerInstall(execAppOptions); + packageManagerExec({script: 'ng', args: ['g', 'application', 'dont-modify-me']}, execAppOptions); + packageManagerExec({script: 'ng', args: ['g', 'application', options.projectName]}, execAppOptions); + + + packageManagerExec({script: 'ng', args: ['config', 'cli.cache.environment', 'all']}, execAppOptions); + if (options.globalFolderPath) { + packageManagerExec({script: 'ng', args: ['config', 'cli.cache.path', path.join(options.globalFolderPath, '.angular', 'cache')]}, execAppOptions); + } + + return Promise.resolve(); + }, { lockFilePath: path.join(options.cwd, `${options.appDirectory}-ongoing.lock`), ...options }); +} diff --git a/packages/@o3r/test-helpers/src/test-environments/index.ts b/packages/@o3r/test-helpers/src/test-environments/index.ts index 6733438e99..d502149c47 100644 --- a/packages/@o3r/test-helpers/src/test-environments/index.ts +++ b/packages/@o3r/test-helpers/src/test-environments/index.ts @@ -1,3 +1,2 @@ -export * from './create-test-environment-angular-with-o3r-core'; -export * from './create-test-environment-angular'; +export * from './create-test-environment-otter-project'; export * from './create-test-environment-blank'; diff --git a/packages/@o3r/test-helpers/src/utilities/add-package.ts b/packages/@o3r/test-helpers/src/utilities/add-package.ts new file mode 100644 index 0000000000..33a1e4a34a --- /dev/null +++ b/packages/@o3r/test-helpers/src/utilities/add-package.ts @@ -0,0 +1,17 @@ +import { readFileSync, writeFileSync } from 'node:fs'; + +/** + * Add dependencies to package.json files + * @param folders folder containing package.json files + * @param dependencyName Name of the dependency to add + * @param dependencyRange Range of the dependency to add + * @param type Type of the dependency to add + */ +export const addDependenciesToPackageJson = (folders: string[], dependencyName: string, dependencyRange: string, type: 'dependencies' | 'devDependencies' | 'peerDependencies') => { + folders.forEach((folder) => { + const packageJsonPath = `${folder}/package.json`; + const packageJson = JSON.parse(readFileSync(packageJsonPath, {encoding: 'utf8'})); + (packageJson[type] ||= {})[dependencyName] = dependencyRange; + writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2)); + }); +}; diff --git a/packages/@o3r/test-helpers/src/utilities/git.ts b/packages/@o3r/test-helpers/src/utilities/git.ts index f6c136d150..a0de39f391 100644 --- a/packages/@o3r/test-helpers/src/utilities/git.ts +++ b/packages/@o3r/test-helpers/src/utilities/git.ts @@ -7,7 +7,7 @@ import { execFileSync, ExecFileSyncOptionsWithStringEncoding, execSync } from 'n export function setupGit(workingDirectory?: string) { const authorName = 'otter it tests'; const authorEmail = 'fake-email@it-tests.otter'; - execSync('git init -b master && git add -A && git commit -m "initial commit" && git tag -a after-init -m "after-init"', { + execSync('git init -b master && git add -A && git commit --allow-empty -m "initial commit" && git tag -a after-init -m "after-init"', { cwd: workingDirectory, env: { /* eslint-disable @typescript-eslint/naming-convention, camelcase */ diff --git a/packages/@o3r/test-helpers/src/utilities/index.ts b/packages/@o3r/test-helpers/src/utilities/index.ts index ae23b3b2d7..88ce5cc2d6 100644 --- a/packages/@o3r/test-helpers/src/utilities/index.ts +++ b/packages/@o3r/test-helpers/src/utilities/index.ts @@ -6,3 +6,5 @@ export * from './locker'; export * from './package-manager'; export * from './verdaccio'; export * from './virtual-fs'; +export * from './add-package'; +export * from './logger'; diff --git a/packages/@o3r/test-helpers/src/utilities/logger.ts b/packages/@o3r/test-helpers/src/utilities/logger.ts new file mode 100644 index 0000000000..97a0304c1d --- /dev/null +++ b/packages/@o3r/test-helpers/src/utilities/logger.ts @@ -0,0 +1,9 @@ +/** simple Logger interface */ +export interface Logger { + /** Error message to display */ + error: (message: string) => void; + /** Information message to display */ + info: (message: string) => void; + /** Debug message message to display */ + debug?: (message: string) => void; +} diff --git a/packages/@o3r/test-helpers/src/utilities/package-manager.ts b/packages/@o3r/test-helpers/src/utilities/package-manager.ts index 274d05f52b..7890b91a44 100644 --- a/packages/@o3r/test-helpers/src/utilities/package-manager.ts +++ b/packages/@o3r/test-helpers/src/utilities/package-manager.ts @@ -14,25 +14,32 @@ declare global { const PACKAGE_MANAGERS_CMD = { npm: { - add: 'npm install', - create: 'npm create', - exec: 'npm exec', - install: 'npm install', - run: 'npm run', - workspaceExec: 'npm exec -w', - workspaceRun: 'npm run -w' + add: ['npm', 'install'], + create: ['npm', 'create'], + exec: ['npm', 'exec'], + install: ['npm', 'install'], + run: ['npm', 'run'], + workspaceExec: ['npm', 'exec', '--workspace'], + workspaceRun: ['npm', 'run', '--workspace'] }, yarn: { - add: 'yarn add', - create: 'yarn create', - exec: 'yarn', - install: 'yarn install', - run: 'yarn run', - workspaceExec: 'yarn workspace', - workspaceRun: 'yarn workspace' + add: ['yarn', 'add'], + create: ['yarn', 'create'], + exec: ['yarn'], + install: ['yarn', 'install'], + run: ['yarn', 'run'], + workspaceExec: ['yarn', 'workspace'], + workspaceRun: ['yarn', 'workspace'] } }; +type CommandArguments = { + /** Script to run or execute */ + script: string; + /** Arguments to pass to the script */ + args?: string[]; +}; + /** * Get the package manager to be used for the tests by reading environment variable ENFORCED_PACKAGE_MANAGER */ @@ -45,27 +52,35 @@ export function getPackageManager() { /** * Need to add additional dashes when running command like exec on npm * Convert `npm exec test --param` to `npm exec test -- --param` - * @param command + * @param args + * @param packageManager */ -export function addDashesForNpmCommand(command: string) { - return command.replace(/(?