diff --git a/packages/@o3r/workspace/package.json b/packages/@o3r/workspace/package.json index 75c316cea5..fbbb25b441 100644 --- a/packages/@o3r/workspace/package.json +++ b/packages/@o3r/workspace/package.json @@ -110,7 +110,8 @@ "@angular/material": "~18.0.0", "@ngrx/router-store": "~18.0.0", "@ngrx/effects": "~18.0.0", - "@ngrx/store-devtools": "~18.0.0" + "@ngrx/store-devtools": "~18.0.0", + "lerna": "^8.1.7" }, "engines": { "node": "^18.19.1 || ^20.11.1 || >=22.0.0" diff --git a/packages/@o3r/workspace/schematics/index.it.spec.ts b/packages/@o3r/workspace/schematics/index.it.spec.ts index 8a3676aa9f..efb66c506f 100644 --- a/packages/@o3r/workspace/schematics/index.it.spec.ts +++ b/packages/@o3r/workspace/schematics/index.it.spec.ts @@ -9,10 +9,12 @@ import { getDefaultExecSyncOptions, getGitDiff, packageManagerExec, packageManagerInstall, + packageManagerRun, packageManagerRunOnProject } from '@o3r/test-helpers'; -import { existsSync } from 'node:fs'; +import { existsSync, promises as fs } from 'node:fs'; import * as path from 'node:path'; +import type { PackageJson } from 'type-fest'; describe('new otter workspace', () => { test('should add sdk to an existing workspace', () => { @@ -78,4 +80,21 @@ describe('new otter workspace', () => { generatedLibFiles.forEach(file => expect(existsSync(path.join(inLibraryPath, file))).toBe(true)); expect(() => packageManagerRunOnProject(libName, true, { script: 'build' }, execAppOptions)).not.toThrow(); }); + + test('should generate a monorepo setup', async () => { + const { workspacePath } = o3rEnvironment.testEnvironment; + const defaultOptions = getDefaultExecSyncOptions(); + // eslint-disable-next-line @typescript-eslint/naming-convention + const execAppOptions = {...defaultOptions, cwd: workspacePath, env: {...defaultOptions.env, NX_CLOUD_ACCESS_TOKEN: ''}}; + expect(() => packageManagerInstall(execAppOptions)).not.toThrow(); + const rootPackageJson = JSON.parse(await fs.readFile(path.join(workspacePath, 'package.json'), 'utf-8')) as PackageJson; + expect(rootPackageJson.scripts).toHaveProperty('build', 'lerna run build'); + expect(rootPackageJson.scripts).toHaveProperty('test', 'lerna run test'); + expect(rootPackageJson.scripts).toHaveProperty('lint', 'lerna run lint'); + expect(() => packageManagerRun({script: 'build'}, execAppOptions)).not.toThrow(); + expect(() => packageManagerRun({script: 'test'}, execAppOptions)).not.toThrow(); + expect(() => packageManagerRun({script: 'lint'}, execAppOptions)).not.toThrow(); + expect(rootPackageJson.workspaces).toContain('libs/*'); + expect(rootPackageJson.workspaces).toContain('apps/*'); + }); }); diff --git a/packages/@o3r/workspace/schematics/ng-add/helpers/npm-workspace.ts b/packages/@o3r/workspace/schematics/ng-add/helpers/npm-workspace.ts index 55a93fdf15..0d71abad06 100644 --- a/packages/@o3r/workspace/schematics/ng-add/helpers/npm-workspace.ts +++ b/packages/@o3r/workspace/schematics/ng-add/helpers/npm-workspace.ts @@ -2,7 +2,8 @@ import { chain } from '@angular-devkit/schematics'; import type { Rule, SchematicContext, Tree } from '@angular-devkit/schematics'; import { SchematicsException } from '@angular-devkit/schematics'; import type { PackageJson } from 'type-fest'; -import { DEFAULT_ROOT_FOLDERS, isNxContext, setupSchematicsParamsForProject, WorkspaceLayout, WorkspaceSchematics } from '@o3r/schematics'; +import { DEFAULT_ROOT_FOLDERS, getPackageManager, isNxContext, setupSchematicsParamsForProject, WorkspaceLayout, WorkspaceSchematics } from '@o3r/schematics'; +import type { MonorepoManager } from '../schema'; /** * Update root package.json to include workspaces @@ -75,3 +76,43 @@ export function filterPackageJsonScripts(tree: Tree, _context: SchematicContext) tree.overwrite(rootPackageJsonPath, JSON.stringify(rootPackageJsonObject, null, 2)); return tree; } + +/** + * Add a monorepo manager at the root of the project + * @param o3rWorkspacePackageJson the @o3r/workspace package.json + * @param manager the monorepo manager + */ +export function addMonorepoManager(o3rWorkspacePackageJson: PackageJson & { generatorDependencies: Record }, manager: MonorepoManager): Rule { + return (tree: Tree, _context: SchematicContext) => { + if (manager === 'lerna') { + const rootPackageJsonPath = '/package.json'; + if (!tree.exists(rootPackageJsonPath)) { + throw new SchematicsException('Root package.json does not exist'); + } + + const rootPackageJsonObject = tree.readJson(rootPackageJsonPath) as PackageJson; + rootPackageJsonObject.devDependencies = { + ...rootPackageJsonObject.devDependencies, + 'lerna': o3rWorkspacePackageJson.generatorDependencies.lerna + }; + rootPackageJsonObject.scripts = { + ...rootPackageJsonObject.scripts, + 'build': 'lerna run build', + 'test': 'lerna run test', + 'lint': 'lerna run lint' + }; + + const lernaJson: { $schema: string; version: string; npmClient?: string } = { + '$schema': 'https://github.com/lerna/lerna/blob/main/packages/lerna/schemas/lerna-schema.json', + 'version': rootPackageJsonObject.version || '0.0.0-placeholder' + }; + if (getPackageManager() === 'yarn') { + lernaJson.npmClient = 'yarn'; + } + tree.create('/lerna.json', JSON.stringify(lernaJson, null, 2)); + + tree.overwrite(rootPackageJsonPath, JSON.stringify(rootPackageJsonObject, null, 2)); + } + return tree; + }; +} diff --git a/packages/@o3r/workspace/schematics/ng-add/project-setup.ts b/packages/@o3r/workspace/schematics/ng-add/project-setup.ts index 3a1e20a296..53dca2f2d1 100644 --- a/packages/@o3r/workspace/schematics/ng-add/project-setup.ts +++ b/packages/@o3r/workspace/schematics/ng-add/project-setup.ts @@ -10,11 +10,12 @@ 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'; -import { addWorkspacesToProject, filterPackageJsonScripts } from './helpers/npm-workspace'; +import { addMonorepoManager, addWorkspacesToProject, filterPackageJsonScripts } from './helpers/npm-workspace'; import { generateRenovateConfig } from './helpers/renovate'; import type { NgAddSchematicsSchema } from './schema'; import { shouldOtterLinterBeInstalled } from './helpers/linter'; import { updateGitIgnore } from './helpers/gitignore-update'; +import type { PackageJson } from 'type-fest'; /** * Enable all the otter features requested by the user @@ -34,7 +35,7 @@ export const prepareProject = (options: NgAddSchematicsSchema): Rule => { const ownSchematicsFolder = path.resolve(__dirname, '..'); const ownPackageJsonPath = path.resolve(ownSchematicsFolder, '..', 'package.json'); const depsInfo = getO3rPeerDeps(ownPackageJsonPath); - const ownPackageJsonContent = JSON.parse(fs.readFileSync(ownPackageJsonPath, { encoding: 'utf-8' })); + const ownPackageJsonContent = JSON.parse(fs.readFileSync(ownPackageJsonPath, { encoding: 'utf-8' })) as PackageJson & { generatorDependencies: Record }; return async (tree, context) => { if (!ownPackageJsonContent) { @@ -74,7 +75,8 @@ export const prepareProject = (options: NgAddSchematicsSchema): Rule => { ngAddToRun: internalPackagesToInstallWithNgAdd }), !options.skipLinter && installOtterLinter ? applyEsLintFix() : noop(), - addWorkspacesToProject() + addWorkspacesToProject(), + addMonorepoManager(ownPackageJsonContent, options.monorepoManager) ])(tree, context); }; }; diff --git a/packages/@o3r/workspace/schematics/ng-add/schema.json b/packages/@o3r/workspace/schematics/ng-add/schema.json index f1f8be29f0..45e518b40c 100644 --- a/packages/@o3r/workspace/schematics/ng-add/schema.json +++ b/packages/@o3r/workspace/schematics/ng-add/schema.json @@ -50,6 +50,15 @@ "type": "boolean", "description": "Use a pinned version for otter packages", "default": false + }, + "monorepoManager": { + "description": "Which monorepo manager to use", + "type": "string", + "default": "lerna", + "enum": [ + "lerna", + "none" + ] } }, "additionalProperties": true, diff --git a/packages/@o3r/workspace/schematics/ng-add/schema.ts b/packages/@o3r/workspace/schematics/ng-add/schema.ts index 4dfdb7c0f8..488bdd009b 100644 --- a/packages/@o3r/workspace/schematics/ng-add/schema.ts +++ b/packages/@o3r/workspace/schematics/ng-add/schema.ts @@ -1,6 +1,7 @@ import type { SchematicOptionObject } from '@o3r/schematics'; -export type PresetNames = 'basic' | 'cms'; +/** Monorepo manager to use */ +export type MonorepoManager = 'lerna' | 'none'; export interface NgAddSchematicsSchema extends SchematicOptionObject { /** Skip the linter process */ @@ -17,4 +18,7 @@ export interface NgAddSchematicsSchema extends SchematicOptionObject { /** Use a pinned version for otter packages */ exactO3rVersion?: boolean; + + /** Monorepo manager to use */ + monorepoManager: MonorepoManager; } diff --git a/packages/@o3r/workspace/src/cli/set-version.cts b/packages/@o3r/workspace/src/cli/set-version.cts index 6d55a552a5..afb029b930 100644 --- a/packages/@o3r/workspace/src/cli/set-version.cts +++ b/packages/@o3r/workspace/src/cli/set-version.cts @@ -7,7 +7,7 @@ import * as path from 'node:path'; import * as winston from 'winston'; import { clean } from 'semver'; -const defaultIncludedFiles = ['**/package.json', '!/**/templates/**/package.json', '!**/node_modules/**/package.json']; +const defaultIncludedFiles = ['**/package.json', '!/**/templates/**/package.json', '!**/node_modules/**/package.json', '**/lerna.json']; const collect = (pattern: string, patterns: string[]) => { if (patterns === defaultIncludedFiles && pattern) {