From 854d0326f86cd317509bfa50b9e48c344af97a52 Mon Sep 17 00:00:00 2001 From: Kilian Panot Date: Tue, 16 Jan 2024 00:33:51 +0900 Subject: [PATCH] feat: clean package install in ngAdd schematics --- package.json | 11 +- .../apis-manager/schematics/ng-add/index.ts | 24 +- .../application/schematics/ng-add/index.ts | 23 +- .../components/schematics/ng-add/index.ts | 24 +- .../configuration/schematics/ng-add/index.ts | 25 +- packages/@o3r/core/package.json | 6 +- .../schematics/ng-add/index.ts.template | 55 ++++- .../schematics/ng-add/schema.json.template | 5 +- .../schematics/ng-add/schema.ts.template | 2 +- packages/@o3r/core/schematics/ng-add/index.ts | 64 +++-- .../schematics/ng-add/project-setup/index.ts | 27 +- .../schematics/rule-factories/store/index.ts | 80 +++--- .../core/schematics/shared/presets/helpers.ts | 37 ++- .../shared/presets/preset.interface.ts | 5 +- .../schematics/ng-add/index.ts | 67 +++-- .../extractors/schematics/ng-add/index.ts | 25 +- .../localization/schematics/ng-add/index.ts | 48 ++-- .../@o3r/mobile/schematics/ng-add/index.ts | 24 +- .../rules-engine/schematics/ng-add/index.ts | 41 +++- .../schematics/schematics/ng-add/index.ts | 59 +++-- .../src/rule-factories/ng-add/dependencies.ts | 178 ++++++++++++++ .../src/rule-factories/ng-add/index.ts | 230 +----------------- .../rule-factories/ng-add/ng-add.helpers.ts | 171 +++++++++++++ .../ng-add/schematics-register.ts | 64 +++++ packages/@o3r/schematics/src/rules/install.ts | 20 +- .../@o3r/schematics/src/tasks/ng-add/index.ts | 1 + .../package-manager/add-dev-dependency.ts | 1 + .../@o3r/schematics/src/utility/loaders.ts | 10 + packages/@o3r/store-sync/package.json | 1 + .../store-sync/schematics/ng-add/index.ts | 61 +++-- .../store-sync/schematics/ng-add/schema.json | 5 - .../@o3r/storybook/schematics/ng-add/index.ts | 21 +- .../@o3r/styling/schematics/ng-add/index.ts | 43 +++- .../test-helpers/schematics/ng-add/index.ts | 24 +- .../@o3r/test-helpers/src/prepare-test-env.ts | 25 +- .../create-test-environment-otter-project.ts | 93 +++++++ .../@o3r/testing/schematics/ng-add/index.ts | 47 +++- packages/@o3r/workspace/package.json | 6 +- .../schematics/ng-add/project-setup.ts | 41 ++-- yarn.lock | 72 ++---- 40 files changed, 1133 insertions(+), 633 deletions(-) create mode 100644 packages/@o3r/schematics/src/rule-factories/ng-add/dependencies.ts create mode 100644 packages/@o3r/schematics/src/rule-factories/ng-add/ng-add.helpers.ts create mode 100644 packages/@o3r/schematics/src/rule-factories/ng-add/schematics-register.ts create mode 100644 packages/@o3r/test-helpers/src/test-environments/create-test-environment-otter-project.ts diff --git a/package.json b/package.json index 7c36453b2e..2ce618bbb0 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 --access=public", "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)", @@ -34,14 +34,14 @@ "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": "podman run -d -it --rm --name verdaccio -p 4873:4873 -v \"$(yarn get:current-dir)/.verdaccio/conf\":/verdaccio/conf verdaccio/verdaccio", + "verdaccio:start-persistent": "podman 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:stop": "podman container stop $(podman ps -a -q --filter=\"name=verdaccio\")", "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": [ @@ -231,7 +231,6 @@ "sass": "~1.70.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/@o3r/apis-manager/schematics/ng-add/index.ts b/packages/@o3r/apis-manager/schematics/ng-add/index.ts index 862fff42c9..af60f1666e 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, type DependencyToAdd } from '@o3r/schematics'; import * as path from 'node:path'; import { NgAddSchematicsSchema } from './schema'; @@ -10,27 +10,33 @@ import { 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 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; + }, {} as Record); + 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..ff9ed65c5d 100644 --- a/packages/@o3r/application/schematics/ng-add/index.ts +++ b/packages/@o3r/application/schematics/ng-add/index.ts @@ -5,6 +5,7 @@ import * as path from 'node:path'; import type { NgAddSchematicsSchema } from './schema'; import { registerDevtools } from './helpers/devtools-registration'; import { generateCmsConfigFile } from './helpers/cms-registration'; +import type { DependencyToAdd } from '@o3r/schematics'; /** * Add Otter application to an Angular Project @@ -15,14 +16,13 @@ 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 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; + }, {} as Record); 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/schematics/ng-add/index.ts b/packages/@o3r/components/schematics/ng-add/index.ts index 5fc190bf91..bea74de7b7 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 } from '@o3r/schematics'; /** * Add Otter components to an Angular Project @@ -17,9 +18,9 @@ function ngAddFn(options: NgAddSchematicsSchema): Rule { const { getDefaultOptionsForSchematic, getO3rPeerDeps, - getProjectNewDependenciesType, + getProjectNewDependenciesTypes, getWorkspaceConfig, - ngAddPackages, + setupDependencies, ngAddPeerDependencyPackages, removePackages, registerPackageCollectionSchematics @@ -36,16 +37,21 @@ 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; + }, {} as Record); const rule = chain([ removePackages(['@otter/components']), - ngAddPackages(depsInfo.o3rPeerDeps, { - skipConfirmation: true, - version: depsInfo.packageVersion, - parentPackageInfo: depsInfo.packageName, + setupDependencies({ projectName: options.projectName, - dependencyType, - workingDirectory + dependencies, + ngAddToRun: depsInfo.o3rPeerDeps }), ngAddPeerDependencyPackages(['chokidar'], packageJsonPath, NodeDependencyType.Dev, {...options, workingDirectory, skipNgAddSchematicRun: true}, '@o3r/components - install builder dependency'), registerPackageCollectionSchematics(packageJson), diff --git a/packages/@o3r/configuration/schematics/ng-add/index.ts b/packages/@o3r/configuration/schematics/ng-add/index.ts index 210c9c5bc3..0b23a5a350 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 type { DependencyToAdd } 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; + }, {} as Record); 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/package.json b/packages/@o3r/core/package.json index 4fa474ff33..d7af27f0e6 100644 --- a/packages/@o3r/core/package.json +++ b/packages/@o3r/core/package.json @@ -142,9 +142,9 @@ "generatorDependencies": { "@angular-eslint/eslint-plugin": "~17.2.0", "@angular/material": "~17.0.1", - "@ngrx/router-store": "~17.0.0", - "@ngrx/effects": "~17.0.0", - "@ngrx/store-devtools": "~17.0.0", + "@ngrx/router-store": "~17.1.0", + "@ngrx/effects": "~17.1.0", + "@ngrx/store-devtools": "~17.1.0", "@o3r/store-sync": "workspace:^", "@types/jest": "~29.5.2", "nx": "~17.2.0", 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..fca5e22c08 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 @@ -2,12 +2,21 @@ 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 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 * @@ -15,18 +24,38 @@ const doCustomAction: Rule = (tree, _context) => { */ 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` }) - ]); + try { + // use dynamic import to properly raise an exception if it is not an Otter project. + const { getProjectNewDependenciesTypes, applyEsLintFix, install, getO3rPeerDeps } = await import('@o3r/schematics'); + // retrieve dependencies following the /^@o3r\/.*/ pattern within the peerDependencies of the current module + const packageJsonPath = path.resolve(__dirname, '..', '..', 'package.json'); + // 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; + }, {} as Record); + return chain([ + // optional custom action dedicated to this module + doCustomAction, + options.skipLinter ? noop() : applyEsLintFix(), + setupDependencies({ + projectName: options.projectName, + dependencies, + ngAddToRun: dependenciesToNgAdd + }) + ]); + } 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 40130c805e..caaf3a7cc8 100644 --- a/packages/@o3r/core/schematics/ng-add/index.ts +++ b/packages/@o3r/core/schematics/ng-add/index.ts @@ -1,13 +1,23 @@ -import { chain, externalSchematic, noop, Rule, SchematicContext, Tree } from '@angular-devkit/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 { AddDevInstall, createSchematicWithMetricsIfInstalled, displayModuleListRule, isPackageInstalled, registerPackageCollectionSchematics, setupSchematicsDefaultParams } from '@o3r/schematics'; +import { createSchematicWithMetricsIfInstalled, displayModuleListRule, registerPackageCollectionSchematics, setupSchematicsDefaultParams } from '@o3r/schematics'; import { prepareProject } from './project-setup/index'; +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 @@ -16,30 +26,37 @@ import { prepareProject } from './project-setup/index'; 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']; - 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); - } + 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] + }; - context.addTask(new AddDevInstall({ - packageName: [ - ...schematicsDependencies.map((dependency) => dependency + o3rCoreVersion) - ].join(' '), - hideOutput: false, - quiet: false, - force: options.forceInstall - } as any)); - await lastValueFrom(context.engine.executePostTasks()); + if (!options.projectName) { + dependenciesSetupConfig.dependencies[workspacePackageName] = { + toWorkspaceOnly: true, + inManifest: [{ + range: `~${o3rCoreVersion}`, + types: [NodeDependencyType.Default] + }] + }; + (dependenciesSetupConfig.ngAddToRun ||= []).push(workspacePackageName); + } - 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; @@ -57,8 +74,9 @@ function ngAddFn(options: NgAddSchematicsSchema): Rule { externalPresetRunner?.rule || noop() ])(t, c); }, + setupDependencies(dependenciesSetupConfig), displayModuleListRule({ packageName: options.projectName }) - ])(tree, context); + ]); }; } 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 db98ffdc01..0e533b8177 100644 --- a/packages/@o3r/core/schematics/ng-add/project-setup/index.ts +++ b/packages/@o3r/core/schematics/ng-add/project-setup/index.ts @@ -13,9 +13,7 @@ import { applyEsLintFix, getO3rPeerDeps, getWorkspaceConfig, - install, mapImportV7toV8, - ngAddPackages, removePackages, renamedPackagesV7toV8, updateImports @@ -25,13 +23,15 @@ import { updateBuildersNames } from '../updates-for-v8/cms-adapters/update-build import { updateOtterGeneratorsNames } from '../updates-for-v8/generators/update-generators-names'; import { packagesToRemove } from '../updates-for-v8/replaced-packages'; import { shouldOtterLinterBeInstalled } from '../utils/index'; +import { getExternalDependenciesVersionRange, type SetupDependenciesOptions } from '@o3r/schematics'; /** * 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) => async (tree: Tree, context: SchematicContext) => { const coreSchematicsFolder = path.resolve(__dirname, '..'); const corePackageJsonPath = path.resolve(coreSchematicsFolder, '..', '..', 'package.json'); const corePackageJsonContent = JSON.parse(fs.readFileSync(corePackageJsonPath, { encoding: 'utf-8' })); @@ -49,9 +49,19 @@ 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 }; + + Object.entries(getExternalDependenciesVersionRange(internalPackagesToInstallWithNgAdd, corePackageJsonPath)) + .forEach(([dep, range]) => { + dependenciesSetupConfig.dependencies[dep] = { + inManifest: [{ + range, + types: [NodeDependencyType.Dev] + }] + }; + }); + (dependenciesSetupConfig.ngAddToRun ||= []).push(...internalPackagesToInstallWithNgAdd); return () => { @@ -65,13 +75,8 @@ export const prepareProject = (options: NgAddSchematicsSchema) => async (tree: T 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 + !options.skipLinter && installOtterLinter ? applyEsLintFix() : noop() ]; return chain(appLibRules)(tree, context); diff --git a/packages/@o3r/core/schematics/rule-factories/store/index.ts b/packages/@o3r/core/schematics/rule-factories/store/index.ts index 81fbb29688..3eac3308a3 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,28 +35,26 @@ 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); }; /** @@ -62,26 +62,22 @@ export function updateStore(options: { projectName?: string | undefined; working * @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/eslint-config-otter/schematics/ng-add/index.ts b/packages/@o3r/eslint-config-otter/schematics/ng-add/index.ts index 8d9292858b..f145661741 100644 --- a/packages/@o3r/eslint-config-otter/schematics/ng-add/index.ts +++ b/packages/@o3r/eslint-config-otter/schematics/ng-add/index.ts @@ -3,6 +3,7 @@ import { createSchematicWithMetricsIfInstalled } from '@o3r/schematics'; import type { NgAddSchematicsSchema } from './schema'; import * as path from 'node:path'; import { updateLinterConfigs } from './linter'; +import type { DependencyToAdd } from '@o3r/schematics'; /** * Add Otter eslint-config to an Angular Project @@ -11,43 +12,59 @@ import { updateLinterConfigs } from './linter'; function ngAddFn(options: NgAddSchematicsSchema): Rule { /* ng add rules */ return async (tree: Tree, context: SchematicContext) => { + const devDependenciesToInstall = [ + 'eslint', + '@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; + }, {} as Record); + 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/extractors/schematics/ng-add/index.ts b/packages/@o3r/extractors/schematics/ng-add/index.ts index 68ed80f178..b3a6ebc78a 100644 --- a/packages/@o3r/extractors/schematics/ng-add/index.ts +++ b/packages/@o3r/extractors/schematics/ng-add/index.ts @@ -2,6 +2,7 @@ import { chain } from '@angular-devkit/schematics'; import type { Rule, SchematicContext, Tree } from '@angular-devkit/schematics'; import { createSchematicWithMetricsIfInstalled } from '@o3r/schematics'; import * as path from 'node:path'; +import type { DependencyToAdd } from '@o3r/schematics'; import { updateCmsAdapter } from '../cms-adapter'; import { NgAddSchematicsSchema } from './schema'; @@ -12,21 +13,25 @@ import { 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 { 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; + }, {} as Record); 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/localization/schematics/ng-add/index.ts b/packages/@o3r/localization/schematics/ng-add/index.ts index 432e5fd086..e194901e37 100644 --- a/packages/@o3r/localization/schematics/ng-add/index.ts +++ b/packages/@o3r/localization/schematics/ng-add/index.ts @@ -4,8 +4,13 @@ import * as path from 'node:path'; import { createSchematicWithMetricsIfInstalled, registerPackageCollectionSchematics, setupSchematicsDefaultParams } from '@o3r/schematics'; import { updateCmsAdapter } from '../cms-adapter'; import type { NgAddSchematicsSchema } from './schema'; +import type { DependencyToAdd } from '@o3r/schematics'; import { registerDevtools } from './helpers/devtools-registration'; +const dependenciesToInstall = [ + 'chokidar' +]; + /** * Add Otter localization to an Angular Project * @param options for the dependencies installations @@ -13,7 +18,14 @@ 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, + 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,25 +35,33 @@ 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; + }, {} as Record); + 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({ diff --git a/packages/@o3r/mobile/schematics/ng-add/index.ts b/packages/@o3r/mobile/schematics/ng-add/index.ts index 823c64e04c..6e8e3d9226 100644 --- a/packages/@o3r/mobile/schematics/ng-add/index.ts +++ b/packages/@o3r/mobile/schematics/ng-add/index.ts @@ -1,5 +1,5 @@ import { chain, Rule, SchematicContext, Tree } from '@angular-devkit/schematics'; -import { createSchematicWithMetricsIfInstalled, getProjectNewDependenciesType } from '@o3r/schematics'; +import { createSchematicWithMetricsIfInstalled, type DependencyToAdd } from '@o3r/schematics'; import * as path from 'node:path'; import type { NgAddSchematicsSchema } from './schema'; @@ -11,20 +11,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 { 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; + }, {} as Record); 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/rules-engine/schematics/ng-add/index.ts b/packages/@o3r/rules-engine/schematics/ng-add/index.ts index 181b2e8cdb..45926b0248 100644 --- a/packages/@o3r/rules-engine/schematics/ng-add/index.ts +++ b/packages/@o3r/rules-engine/schematics/ng-add/index.ts @@ -5,6 +5,11 @@ import type { NgAddSchematicsSchema } from './schema'; import * as path from 'node:path'; import * as fs from 'node:fs'; import { registerDevtools } from './helpers/devtools-registration'; +import type { DependencyToAdd } from '@o3r/schematics'; + +const devDependenciesToInstall = [ + 'jsonpath-plus' +]; /** * Add Otter rules-engine to an Angular Project @@ -15,12 +20,12 @@ function ngAddFn(options: NgAddSchematicsSchema): Rule { return async (tree: Tree, context: SchematicContext) => { try { const { - ngAddPackages, + setupDependencies, 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; + }, {} as Record); + 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 6183eac85c..9cc19c48ce 100644 --- a/packages/@o3r/schematics/schematics/ng-add/index.ts +++ b/packages/@o3r/schematics/schematics/ng-add/index.ts @@ -1,38 +1,43 @@ -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 } 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 type { NgAddSchematicsSchema } from './schema'; import { createSchematicWithMetricsIfInstalled } from '@o3r/schematics'; /** * Add Otter schematics to an Angular Project + * @param options */ -function ngAddFn(): Rule { +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 => { - // eslint-disable-next-line @typescript-eslint/naming-convention - const { AddDevInstall } = await import('@o3r/schematics'); - context.logger.info('Running ng add for schematics'); + return async (): Promise => { + const { setupDependencies, getExternalDependenciesVersionRange } = await import('@o3r/schematics'); 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]; - 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, - 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..eae2a699e9 --- /dev/null +++ b/packages/@o3r/schematics/src/rule-factories/ng-add/dependencies.ts @@ -0,0 +1,178 @@ +import { chain, Rule, Schematic } 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, getWorkspaceConfig, SupportedPackageManagers } from '../../utility'; +import { NodePackageInstallTask, RunSchematicTask } from '@angular-devkit/schematics/tasks'; + +/** + * 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; +} + +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; +} + +/** + * 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 => { + + const ngAddToRun = new Set(Object.keys(options.dependencies) + .filter((dep) => options.ngAddToRun?.some((pattern) => typeof pattern === 'string' ? pattern === dep : pattern.test(dep)))); + const isInstallNeeded = () => typeof options.skipInstall !== 'undefined' ? !options.skipInstall : ngAddToRun.size > 0; + + const editPackageJson = (packageJsonPath: string, packageToInstall: string, dependency: DependencyToAdd, updateNgAddList: boolean): Rule => { + return (tree, context) => { + 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)) { + packageJsonContent[depType]![packageToInstall] = range; + } else { + if (updateNgAddList) { + ngAddToRun.delete(packageToInstall); + } + context.logger.warn(`The dependency ${packageToInstall} (${depType}) will not added ` + + `because its range is inferior to the current one (${range} < ${packageJsonContent[depType]![packageToInstall]!}) in targeted ${packageJsonPath}`); + } + } else { + if (updateNgAddList) { + ngAddToRun.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; + } + 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); + }; + }; + + const addDependencies: Rule = (tree) => { + const workspaceConfig = getWorkspaceConfig(tree); + const workspaceProject = options.projectName && workspaceConfig?.projects?.[options.projectName] || undefined; + const projectDirectory = workspaceProject?.root; + 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 })) : undefined; + + const getOptions = (packageName: string, schema: Schematic) => { + const schemaOptions = schema.description.schemaJson?.properties || {}; + return Object.entries({ projectName: options.projectName, ...options.ngAddOptions, ...options.dependencies[packageName].ngAddOptions }) + .reduce>((accOptions, [key, value]: [string, any]) => { + if (schemaOptions[key]) { + accOptions[key] = value; + } + return accOptions; + }, {}); + }; + + [...ngAddToRun] + .map((packageName) => { + try { + const collection = context.engine.createCollection(packageName); + return { packageName, schematic: collection.createSchematic('ng-add') }; + } catch { + context.logger.warn(`No ng-add found for ${packageName}`); + return undefined; + } + }) + .filter((collection): collection is { packageName: string; schematic: Schematic } => !!collection) + .reduce((id, { packageName, schematic }) => + context.addTask(new RunSchematicTask(packageName, 'ng-add', getOptions(packageName, schematic)), id && [id]), + installId); + }; + + 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 c924347425..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,227 +1,3 @@ -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 { SchematicOptionObject } from '../../interfaces'; -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..1ec5a47878 --- /dev/null +++ b/packages/@o3r/schematics/src/rule-factories/ng-add/ng-add.helpers.ts @@ -0,0 +1,171 @@ +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. + * @deprecated use {@link setupDependencies} instead, will be removed in V11 + * @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 + * @deprecated use {@link setupDependencies}, will be removed in V11 + * @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..5569975094 --- /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 { SchematicOptionObject } from '../../interfaces'; +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 df19410191..3ec3ab6fb9 100644 --- a/packages/@o3r/schematics/src/rules/install.ts +++ b/packages/@o3r/schematics/src/rules/install.ts @@ -1,17 +1,21 @@ -import { Rule, SchematicContext, Tree } from '@angular-devkit/schematics'; +import { 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, SupportedPackageManagers } from '../utility/package-manager-runner'; /** * Install the Otter packages + * @deprecated use {@link setupDependencies} instead, will be removed in V11 * @param tree * @param context + * @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 fc9c62c1f9..aa1fc3054a 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/loaders.ts b/packages/@o3r/schematics/src/utility/loaders.ts index 264a6bb234..223fc41449 100644 --- a/packages/@o3r/schematics/src/utility/loaders.ts +++ b/packages/@o3r/schematics/src/utility/loaders.ts @@ -76,6 +76,7 @@ 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 */ @@ -83,6 +84,15 @@ 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 + * @param tree + */ +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/store-sync/package.json b/packages/@o3r/store-sync/package.json index f4c51b3550..89ca17bfc2 100644 --- a/packages/@o3r/store-sync/package.json +++ b/packages/@o3r/store-sync/package.json @@ -67,6 +67,7 @@ "@o3r/eslint-plugin": "workspace:^", "@o3r/logger": "workspace:^", "@o3r/schematics": "workspace:^", + "@schematics/angular": "~17.0.3", "@types/jest": "~29.5.2", "@types/node": "^20.0.0", "@typescript-eslint/eslint-plugin": "^6.19.0", diff --git a/packages/@o3r/store-sync/schematics/ng-add/index.ts b/packages/@o3r/store-sync/schematics/ng-add/index.ts index fbdc8c64fe..f66051638c 100644 --- a/packages/@o3r/store-sync/schematics/ng-add/index.ts +++ b/packages/@o3r/store-sync/schematics/ng-add/index.ts @@ -1,8 +1,13 @@ import { chain, noop, Rule } from '@angular-devkit/schematics'; import { createSchematicWithMetricsIfInstalled } from '@o3r/schematics'; import type { NgAddSchematicsSchema } from './schema'; +import type { DependencyToAdd } from '@o3r/schematics'; 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,36 +17,40 @@ 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 { 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; + }, {} as Record); + 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({ - 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 283ef8b46e..5a2759ce2e 100644 --- a/packages/@o3r/storybook/schematics/ng-add/index.ts +++ b/packages/@o3r/storybook/schematics/ng-add/index.ts @@ -2,6 +2,7 @@ import { chain, noop, Rule, SchematicContext, Tree } from '@angular-devkit/schem import { createSchematicWithMetricsIfInstalled } from '@o3r/schematics'; import * as path from 'node:path'; import { NgAddSchematicsSchema } from './schema'; +import type { DependencyToAdd } from '@o3r/schematics'; /** * Add Otter storybook to an Angular Project @@ -10,18 +11,28 @@ import { 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, setupDependencies, getO3rPeerDeps, getProjectNewDependenciesTypes, getWorkspaceConfig, removePackages } = await import('@o3r/schematics'); const { updateStorybook } = await import('../storybook-base'); 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; + }, {} as Record); 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/styling/schematics/ng-add/index.ts b/packages/@o3r/styling/schematics/ng-add/index.ts index 49034d9106..1be7605035 100644 --- a/packages/@o3r/styling/schematics/ng-add/index.ts +++ b/packages/@o3r/styling/schematics/ng-add/index.ts @@ -4,6 +4,11 @@ import * as fs from 'node:fs'; import * as path from 'node:path'; import { updateCmsAdapter } from '../cms-adapter'; import type { NgAddSchematicsSchema } from './schema'; +import type { DependencyToAdd } from '@o3r/schematics'; + +const devDependenciesToInstall = [ + 'chokidar' +]; /** * Add Otter styling to an Angular Project @@ -17,14 +22,14 @@ function ngAddFn(options: NgAddSchematicsSchema): Rule { const { getDefaultOptionsForSchematic, 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 +40,33 @@ 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; + }, {} as Record); + Object.entries(getExternalDependenciesVersionRange(devDependenciesToInstall, packageJsonPath)) + .forEach(([dep, range]) => { + dependencies[dep] = { + inManifest: [{ + range, + types: [NodeDependencyType.Dev] + }] + }; + }); 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 +79,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/schematics/ng-add/index.ts b/packages/@o3r/test-helpers/schematics/ng-add/index.ts index 78d62dfac6..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 @@ -13,24 +14,31 @@ const doCustomAction: Rule = (tree, _context) => { * @param options */ function ngAddFn(options: NgAddSchematicsSchema): Rule { - return async (_tree, context) => { + 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 } = 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 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, { - 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 4055fc1d18..6a0277d5ba 100644 --- a/packages/@o3r/test-helpers/src/prepare-test-env.ts +++ b/packages/@o3r/test-helpers/src/prepare-test-env.ts @@ -4,9 +4,9 @@ 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} from './utilities'; +import { createTestEnvironmentOtterProjectWithApp } from './test-environments/create-test-environment-otter-project'; /** * 'blank' only create yarn/npm config @@ -96,29 +96,26 @@ export async function prepareTestEnv(folderName: string, type: PrepareTestEnvTyp } 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 - }); if (type === 'angular-with-o3r-core' || type === 'angular-monorepo-with-o3r-core') { // Create new base app if needed - await createTestEnvironmentAngularWithO3rCore({ + await createTestEnvironmentOtterProjectWithApp({ appName: 'test-app', appDirectory: `base-app-angular${generateMonorepo ? '-monorepo' : ''}-with-o3r-core`, cwd: itTestsFolderPath, globalFolderPath, yarnVersion, + replaceExisting: !process.env.CI + }); + } else { + await createTestEnvironmentAngular({ + appName: 'test-app', + appDirectory: baseAppAngular, + cwd: itTestsFolderPath, + globalFolderPath, + yarnVersion, angularVersion, materialVersion, - baseAngularAppPath: path.join(itTestsFolderPath, baseAppAngular), replaceExisting: !process.env.CI, generateMonorepo }); 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..d029ae63d6 --- /dev/null +++ b/packages/@o3r/test-helpers/src/test-environments/create-test-environment-otter-project.ts @@ -0,0 +1,93 @@ +import { execFileSync, ExecSyncOptions } from 'node:child_process'; +import { existsSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; +import * as path from 'node:path'; +import { + createWithLock, + CreateWithLockOptions, + getPackageManager, + PackageManagerConfig, + packageManagerExec, + packageManagerInstall, + setPackagerManagerConfig +} from '../utilities'; + +export interface CreateTestEnvironmentOtterProjectWithAppOptions extends CreateWithLockOptions, PackageManagerConfig { + /** + * Name of the app to generate + */ + appName: string; + + /** + * Working directory + */ + cwd: string; + + /** + * Otter dependency version + * @default '999.0.0' + */ + o3rVersion: string; +} + +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 = { + appName: '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}); + } + + // Create Project + setPackagerManagerConfig(options, { ...execAppOptions, cwd: options.cwd}); + const createOptions = `--package-manager=${getPackageManager()}`; + execFileSync('npm', `create @o3r@${o3rVersion} ${options.appDirectory} -- ${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')); + } + 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); + + + packageManagerExec('ng config cli.cache.environment all', execAppOptions); + if (options.globalFolderPath) { + packageManagerExec(`ng 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/testing/schematics/ng-add/index.ts b/packages/@o3r/testing/schematics/ng-add/index.ts index 86f596ebb0..fa0c308aa1 100644 --- a/packages/@o3r/testing/schematics/ng-add/index.ts +++ b/packages/@o3r/testing/schematics/ng-add/index.ts @@ -2,15 +2,17 @@ import { apply, chain, MergeStrategy, mergeWith, move, noop, renameTemplateFiles import { addVsCodeRecommendations, createSchematicWithMetricsIfInstalled, + type DependencyToAdd, + getExternalDependenciesVersionRange, getO3rPeerDeps, - getProjectNewDependenciesType, + getProjectNewDependenciesTypes, getTestFramework, getWorkspaceConfig, - ngAddPackages, - ngAddPeerDependencyPackages, O3rCliError, registerPackageCollectionSchematics, - removePackages, setupSchematicsDefaultParams } from '@o3r/schematics'; + removePackages, setupDependencies, + setupSchematicsDefaultParams +} from '@o3r/schematics'; import { askConfirmation } from '@angular/cli/src/utilities/prompt'; import { NodeDependencyType } from '@schematics/angular/utility/dependencies'; import * as fs from 'node:fs'; @@ -20,6 +22,13 @@ import { NgAddSchematicsSchema } from '../../schematics/ng-add/schema'; import { updateFixtureConfig } from './fixture'; import { updatePlaywright } from './playwright'; +const devDependenciesToInstall = [ + 'pixelmatch', + 'pngjs', + 'jest', + 'jest-preset-angular' +]; + function canResolvePlaywright(): boolean { try { require.resolve('playwright/package.json'); @@ -41,8 +50,25 @@ function ngAddFn(options: NgAddSchematicsSchema): Rule { const depsInfo = getO3rPeerDeps(testPackageJsonPath); const workspaceProject = options.projectName ? getWorkspaceConfig(tree)?.projects[options.projectName] : undefined; const workingDirectory = workspaceProject?.root || '.'; - const dependencyType = getProjectNewDependenciesType(workspaceProject); const projectType = workspaceProject?.projectType || 'application'; + const dependencies = depsInfo.o3rPeerDeps.reduce((acc, dep) => { + acc[dep] = { + inManifest: [{ + range: `~${depsInfo.packageVersion}`, + types: getProjectNewDependenciesTypes(workspaceProject) + }] + }; + return acc; + }, {} as Record); + Object.entries(getExternalDependenciesVersionRange(devDependenciesToInstall, testPackageJsonPath)) + .forEach(([dep, range]) => { + dependencies[dep] = { + inManifest: [{ + range, + types: [NodeDependencyType.Dev] + }] + }; + }); let installJest; const testFramework = options.testingFramework || getTestFramework(getWorkspaceConfig(tree), context); @@ -79,16 +105,12 @@ function ngAddFn(options: NgAddSchematicsSchema): Rule { updateFixtureConfig(options, installJest), removePackages(['@otter/testing']), addVsCodeRecommendations(['Orta.vscode-jest']), - ngAddPackages(depsInfo.o3rPeerDeps, { - skipConfirmation: true, - version: depsInfo.packageVersion, - parentPackageInfo: depsInfo.packageName, + setupDependencies({ projectName: options.projectName, - dependencyType: dependencyType, - workingDirectory + dependencies, + ngAddToRun: depsInfo.o3rPeerDeps }), installPlaywright ? updatePlaywright(options) : noop, - ngAddPeerDependencyPackages(['pixelmatch', 'pngjs'], testPackageJsonPath, dependencyType, {...options, workingDirectory, skipNgAddSchematicRun: true}), registerPackageCollectionSchematics(packageJson), setupSchematicsDefaultParams({ // eslint-disable-next-line @typescript-eslint/naming-convention @@ -132,7 +154,6 @@ function ngAddFn(options: NgAddSchematicsSchema): Rule { renameTemplateFiles() ]), MergeStrategy.Default); rules.push( - ngAddPeerDependencyPackages(['jest', 'jest-preset-angular'], testPackageJsonPath, NodeDependencyType.Dev, {...options, workingDirectory, skipNgAddSchematicRun: true}), jestConfigFilesForProject, jestConfigFilesForWorkspace ); diff --git a/packages/@o3r/workspace/package.json b/packages/@o3r/workspace/package.json index 93a9093919..3874d33f70 100644 --- a/packages/@o3r/workspace/package.json +++ b/packages/@o3r/workspace/package.json @@ -101,9 +101,9 @@ }, "generatorDependencies": { "@angular/material": "~17.0.1", - "@ngrx/router-store": "~17.0.0", - "@ngrx/effects": "~17.0.0", - "@ngrx/store-devtools": "~17.0.0" + "@ngrx/router-store": "~17.1.0", + "@ngrx/effects": "~17.1.0", + "@ngrx/store-devtools": "~17.1.0" }, "engines": { "node": ">=18.0.0" diff --git a/packages/@o3r/workspace/schematics/ng-add/project-setup.ts b/packages/@o3r/workspace/schematics/ng-add/project-setup.ts index 432741a40d..5beec4c5d6 100644 --- a/packages/@o3r/workspace/schematics/ng-add/project-setup.ts +++ b/packages/@o3r/workspace/schematics/ng-add/project-setup.ts @@ -1,5 +1,12 @@ import { chain, noop, Rule } from '@angular-devkit/schematics'; -import { addVsCodeRecommendations, applyEsLintFix, getO3rPeerDeps, getWorkspaceConfig, install, ngAddPackages } from '@o3r/schematics'; +import { + addVsCodeRecommendations, + applyEsLintFix, + getO3rPeerDeps, + getWorkspaceConfig, + setupDependencies +} from '@o3r/schematics'; +import type { DependencyToAdd } from '@o3r/schematics'; import { NodeDependencyType } from '@schematics/angular/utility/dependencies'; import * as fs from 'node:fs'; import * as path from 'node:path'; @@ -20,6 +27,9 @@ export const prepareProject = (options: NgAddSchematicsSchema): Rule => { 'EditorConfig.EditorConfig', 'angular.ng-template' ]; + const dependenciesToInstall = [ + '@ama-sdk/schematics' + ]; const ownSchematicsFolder = path.resolve(__dirname, '..'); const ownPackageJsonPath = path.resolve(ownSchematicsFolder, '..', 'package.json'); const depsInfo = getO3rPeerDeps(ownPackageJsonPath); @@ -29,13 +39,22 @@ export const prepareProject = (options: NgAddSchematicsSchema): Rule => { if (!ownPackageJsonContent) { context.logger.error('Could not find @o3r/workspace package. Are you sure it is installed?'); } - const o3rCoreVersion = ownPackageJsonContent.version; const installOtterLinter = await shouldOtterLinterBeInstalled(context); const internalPackagesToInstallWithNgAdd = Array.from(new Set([ ...(installOtterLinter ? ['@o3r/eslint-config-otter'] : []), ...depsInfo.o3rPeerDeps ])); + const dependencies = [...internalPackagesToInstallWithNgAdd, ...dependenciesToInstall].reduce((acc, dep) => { + acc[dep] = { + inManifest: [{ + range: `~${depsInfo.packageVersion}`, + types: [NodeDependencyType.Default] + }] + }; + return acc; + }, {} as Record); + if (installOtterLinter) { vsCodeExtensions.push('dbaeumer.vscode-eslint'); } @@ -47,21 +66,13 @@ export const prepareProject = (options: NgAddSchematicsSchema): Rule => { addVsCodeRecommendations(vsCodeExtensions), updateGitIgnore(workspaceConfig), filterPackageJsonScripts, - ngAddPackages(internalPackagesToInstallWithNgAdd, { - skipConfirmation: true, - version: o3rCoreVersion, - parentPackageInfo: '@o3r/workspace - setup', - dependencyType: NodeDependencyType.Peer - }), - ngAddPackages(['@ama-sdk/schematics'], { - skipConfirmation: true, - version: o3rCoreVersion, - parentPackageInfo: '@o3r/workspace - setup', - dependencyType: NodeDependencyType.Dev + setupDependencies({ + dependencies, + skipInstall: options.skipInstall, + ngAddToRun: internalPackagesToInstallWithNgAdd }), !options.skipLinter && installOtterLinter ? applyEsLintFix() : noop(), - addWorkspacesToProject(), - options.skipInstall ? noop() : install + addWorkspacesToProject() ])(tree, context); }; }; diff --git a/yarn.lock b/yarn.lock index 63ed2e1f86..92f4c7f884 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7516,7 +7516,6 @@ __metadata: sass: "npm:~1.70.0" sass-loader: "npm:^14.0.0" semver: "npm:^7.5.2" - shx: "npm:^0.3.4" standard-version: "npm:^9.0.0" stylelint: "npm:^16.0.2" stylelint-scss: "npm:^6.0.0" @@ -8262,6 +8261,7 @@ __metadata: "@o3r/eslint-plugin": "workspace:^" "@o3r/logger": "workspace:^" "@o3r/schematics": "workspace:^" + "@schematics/angular": "npm:~17.0.3" "@types/jest": "npm:~29.5.2" "@types/node": "npm:^20.0.0" "@typescript-eslint/eslint-plugin": "npm:^6.19.0" @@ -13633,7 +13633,7 @@ __metadata: languageName: node linkType: hard -"axios@npm:1.6.5, axios@npm:^1.5.1": +"axios@npm:1.6.5": version: 1.6.5 resolution: "axios@npm:1.6.5" dependencies: @@ -13644,6 +13644,17 @@ __metadata: languageName: node linkType: hard +"axios@npm:^1.5.1": + version: 1.6.3 + resolution: "axios@npm:1.6.3" + dependencies: + follow-redirects: "npm:^1.15.0" + form-data: "npm:^4.0.0" + proxy-from-env: "npm:^1.1.0" + checksum: 78e72ec40ee49b85f076758e65d04dd966361cbf82f3789e53c5a1b6814ef57825098f8a997ec1846cf004fb7407655c7b96c496d8133d88ca0f73a98a9c452b + languageName: node + linkType: hard + "axobject-query@npm:4.0.0": version: 4.0.0 resolution: "axobject-query@npm:4.0.0" @@ -19029,6 +19040,16 @@ __metadata: languageName: node linkType: hard +"follow-redirects@npm:^1.15.0": + version: 1.15.4 + resolution: "follow-redirects@npm:1.15.4" + peerDependenciesMeta: + debug: + optional: true + checksum: 2e8f5f259a6b02dfa8dc199e08431848a7c3beed32eb4c19945966164a52c89f07b86c3afcc32ebe4279cf0a960520e45a63013d6350309c5ec90133c5d9351a + languageName: node + linkType: hard + "for-each@npm:^0.3.3": version: 0.3.3 resolution: "for-each@npm:0.3.3" @@ -19609,7 +19630,7 @@ __metadata: languageName: node linkType: hard -"glob@npm:7.2.3, glob@npm:^7.0.0, glob@npm:^7.0.3, glob@npm:^7.0.5, glob@npm:^7.0.6, glob@npm:^7.1.3, glob@npm:^7.1.4, glob@npm:^7.1.6, glob@npm:^7.1.7": +"glob@npm:7.2.3, glob@npm:^7.0.3, glob@npm:^7.0.5, glob@npm:^7.0.6, glob@npm:^7.1.3, glob@npm:^7.1.4, glob@npm:^7.1.6, glob@npm:^7.1.7": version: 7.2.3 resolution: "glob@npm:7.2.3" dependencies: @@ -20664,13 +20685,6 @@ __metadata: languageName: node linkType: hard -"interpret@npm:^1.0.0": - version: 1.4.0 - resolution: "interpret@npm:1.4.0" - checksum: 5beec568d3f60543d0f61f2c5969d44dffcb1a372fe5abcdb8013968114d4e4aaac06bc971a4c9f5bd52d150881d8ebad72a8c60686b1361f5f0522f39c0e1a3 - languageName: node - linkType: hard - "interpret@npm:^3.1.1": version: 3.1.1 resolution: "interpret@npm:3.1.1" @@ -27080,15 +27094,6 @@ __metadata: languageName: node linkType: hard -"rechoir@npm:^0.6.2": - version: 0.6.2 - resolution: "rechoir@npm:0.6.2" - dependencies: - resolve: "npm:^1.1.6" - checksum: fe76bf9c21875ac16e235defedd7cbd34f333c02a92546142b7911a0f7c7059d2e16f441fe6fb9ae203f459c05a31b2bcf26202896d89e390eda7514d5d2702b - languageName: node - linkType: hard - "rechoir@npm:^0.8.0": version: 0.8.0 resolution: "rechoir@npm:0.8.0" @@ -27434,7 +27439,7 @@ __metadata: languageName: node linkType: hard -"resolve@npm:1.22.8, resolve@npm:^1.1.5, resolve@npm:^1.1.6, resolve@npm:^1.1.7, resolve@npm:^1.10.0, resolve@npm:^1.12.0, resolve@npm:^1.14.2, resolve@npm:^1.19.0, resolve@npm:^1.20.0, resolve@npm:^1.22.1, resolve@npm:^1.22.4": +"resolve@npm:1.22.8, resolve@npm:^1.1.5, resolve@npm:^1.1.7, resolve@npm:^1.10.0, resolve@npm:^1.12.0, resolve@npm:^1.14.2, resolve@npm:^1.19.0, resolve@npm:^1.20.0, resolve@npm:^1.22.1, resolve@npm:^1.22.4": version: 1.22.8 resolution: "resolve@npm:1.22.8" dependencies: @@ -27454,7 +27459,7 @@ __metadata: languageName: node linkType: hard -"resolve@patch:resolve@npm%3A1.22.8#optional!builtin, resolve@patch:resolve@npm%3A^1.1.5#optional!builtin, resolve@patch:resolve@npm%3A^1.1.6#optional!builtin, resolve@patch:resolve@npm%3A^1.1.7#optional!builtin, resolve@patch:resolve@npm%3A^1.10.0#optional!builtin, resolve@patch:resolve@npm%3A^1.12.0#optional!builtin, resolve@patch:resolve@npm%3A^1.14.2#optional!builtin, resolve@patch:resolve@npm%3A^1.19.0#optional!builtin, resolve@patch:resolve@npm%3A^1.20.0#optional!builtin, resolve@patch:resolve@npm%3A^1.22.1#optional!builtin, resolve@patch:resolve@npm%3A^1.22.4#optional!builtin": +"resolve@patch:resolve@npm%3A1.22.8#optional!builtin, resolve@patch:resolve@npm%3A^1.1.5#optional!builtin, resolve@patch:resolve@npm%3A^1.1.7#optional!builtin, resolve@patch:resolve@npm%3A^1.10.0#optional!builtin, resolve@patch:resolve@npm%3A^1.12.0#optional!builtin, resolve@patch:resolve@npm%3A^1.14.2#optional!builtin, resolve@patch:resolve@npm%3A^1.19.0#optional!builtin, resolve@patch:resolve@npm%3A^1.20.0#optional!builtin, resolve@patch:resolve@npm%3A^1.22.1#optional!builtin, resolve@patch:resolve@npm%3A^1.22.4#optional!builtin": version: 1.22.8 resolution: "resolve@patch:resolve@npm%3A1.22.8#optional!builtin::version=1.22.8&hash=c3c19d" dependencies: @@ -28230,19 +28235,6 @@ __metadata: languageName: node linkType: hard -"shelljs@npm:^0.8.5": - version: 0.8.5 - resolution: "shelljs@npm:0.8.5" - dependencies: - glob: "npm:^7.0.0" - interpret: "npm:^1.0.0" - rechoir: "npm:^0.6.2" - bin: - shjs: bin/shjs - checksum: f2178274b97b44332bbe9ddb78161137054f55ecf701c7a99db9552cb5478fe279ad5f5131d8a7c2f0730e01ccf0c629d01094143f0541962ce1a3d0243d23f7 - languageName: node - linkType: hard - "shiki@npm:^0.14.7": version: 0.14.7 resolution: "shiki@npm:0.14.7" @@ -28255,18 +28247,6 @@ __metadata: languageName: node linkType: hard -"shx@npm:^0.3.4": - version: 0.3.4 - resolution: "shx@npm:0.3.4" - dependencies: - minimist: "npm:^1.2.3" - shelljs: "npm:^0.8.5" - bin: - shx: lib/cli.js - checksum: 5271b60f7e322540799102ad6935121f10c857a995a1321357a7bffd1628674a4758a602153dc5cc126d8dc94d3489586587d3dee54dc3cac0222ca08a78e33a - languageName: node - linkType: hard - "side-channel@npm:^1.0.4": version: 1.0.4 resolution: "side-channel@npm:1.0.4"