From 4f3cd7465fb7355495c82149da90c3189fffe4fa Mon Sep 17 00:00:00 2001 From: cipchk Date: Fri, 20 Oct 2023 18:26:08 +0800 Subject: [PATCH] feat(cli): support multiple projects --- package.json | 2 +- schematics/application/index.spec.ts | 69 +++++++++++++++ schematics/application/index.ts | 121 ++++++++++++++++----------- schematics/ng-add/index.ts | 23 ++++- schematics/ng-add/schema.json | 7 +- schematics/ng-add/schema.ts | 1 + schematics/utils/versions.ts | 29 +++---- schematics/utils/workspace.ts | 64 ++++++++------ 8 files changed, 219 insertions(+), 97 deletions(-) diff --git a/package.json b/package.json index 4718ec380a..065e6e8be7 100644 --- a/package.json +++ b/package.json @@ -156,7 +156,7 @@ "less-bundle-promise": "^1.0.11", "ng-alain-codelyzer": "^0.0.1", "ng-alain-sts": "^0.0.2", - "ng-alain-plugin-theme": "^15.0.1", + "ng-alain-plugin-theme": "^16.0.0", "tsconfig-paths": "^4.2.0", "@nguniversal/builders": "^16.2.0", "@types/express": "^4.17.17", diff --git a/schematics/application/index.spec.ts b/schematics/application/index.spec.ts index 8137c9f094..746d273c3a 100644 --- a/schematics/application/index.spec.ts +++ b/schematics/application/index.spec.ts @@ -115,4 +115,73 @@ describe('NgAlainSchematic: application', () => { expect(content).not.toContain(`json-schema`); }); }); + + describe('#multiple-projects', () => { + let runner: SchematicTestRunner; + let tree: UnitTestTree; + let projectName = 'mgr'; + beforeEach(async () => { + const baseRunner = createNgRunner(); + const workspaceTree = await baseRunner.runSchematic('workspace', { + name: 'workspace', + newProjectRoot: 'projects', + version: '16.0.0' + }); + await baseRunner.runSchematic( + 'application', + { + name: 'h5', + inlineStyle: false, + inlineTemplate: false, + routing: false, + style: 'css', + skipTests: false, + skipPackageJson: false + }, + workspaceTree + ); + tree = await baseRunner.runSchematic( + 'application', + { + name: projectName, + inlineStyle: false, + inlineTemplate: false, + routing: false, + style: 'css', + skipTests: false, + skipPackageJson: false + }, + workspaceTree + ); + runner = createAlainRunner(); + }); + it(`should be working`, async () => { + tree = await runner.runSchematic( + 'ng-add', + { + skipPackageJson: false, + project: projectName + }, + tree + ); + const content = tree.readContent(`/projects/${projectName}/src/app/shared/index.ts`); + expect(content).toContain(`json-schema`); + expect(tree.exists(`/projects/h5/src/app/shared/index.ts`)).toBe(false); + }); + it(`should be throw error when not found project name`, async () => { + try { + tree = await runner.runSchematic( + 'ng-add', + { + skipPackageJson: false, + project: `${projectName}invalid` + }, + tree + ); + expect(true).toBe(false); + } catch (ex) { + expect(ex.message).toContain(`Not found under the projects node of angular.json`); + } + }); + }); }); diff --git a/schematics/application/index.ts b/schematics/application/index.ts index c29d7ff5a6..47cbf511df 100644 --- a/schematics/application/index.ts +++ b/schematics/application/index.ts @@ -14,7 +14,7 @@ import { Tree, url } from '@angular-devkit/schematics'; -import { getWorkspace, updateWorkspace } from '@schematics/angular/utility/workspace'; +import { updateWorkspace } from '@schematics/angular/utility/workspace'; import { Schema as ApplicationOptions } from './schema'; import { getLangData } from '../core/lang.config'; @@ -27,19 +27,21 @@ import { addHtmlToBody, addPackage, addSchematicCollections, - addStylePreprocessorOptionsToAllProject, + addStylePreprocessorOptions, BUILD_TARGET_BUILD, BUILD_TARGET_SERVE, DEFAULT_WORKSPACE_PATH, + getNgAlainJson, getProject, getProjectFromWorkspace, - getProjectName, + isMulitProject, readContent, readJSON, readPackage, VERSION, writeFile, writeJSON, + writeNgAlainJson, writePackage, ZORROVERSION } from '../utils'; @@ -47,6 +49,8 @@ import { addImportNotation } from '../utils/less'; import { addESLintRule, UpgradeMainVersions } from '../utils/versions'; let project: ProjectDefinition; +let projectName: string; +let mulitProject = false; /** Remove files to be overwrite */ function removeOrginalFiles(): Rule { @@ -70,41 +74,25 @@ function removeOrginalFiles(): Rule { }; } -function fixAngularJson(options: ApplicationOptions): Rule { +function fixAngularJson(): Rule { return updateWorkspace(async workspace => { - const p = getProjectFromWorkspace(workspace, options.project); + const p = getProjectFromWorkspace(workspace, projectName); // Add proxy.conf.js const serveTarget = p.targets?.get(BUILD_TARGET_SERVE); if (serveTarget.options == null) serveTarget.options = {}; serveTarget.options.proxyConfig = 'proxy.conf.js'; - // // 调整budgets, error in angular 15.1 - // const budgets = (getProjectTarget(p, BUILD_TARGET_BUILD, 'configurations').production as JsonObject) - // .budgets as Array<{ - // type: string; - // maximumWarning: string; - // maximumError: string; - // }>; - // if (budgets && budgets.length > 0) { - // const initial = budgets.find(w => w.type === 'initial'); - // if (initial) { - // initial.maximumWarning = '2mb'; - // initial.maximumError = '3mb'; - // } - // } - - addStylePreprocessorOptionsToAllProject(workspace); + addStylePreprocessorOptions(workspace, projectName); addSchematicCollections(workspace); - addFileReplacements(workspace); + addFileReplacements(workspace, projectName); }); } /** * Fix https://github.com/ng-alain/ng-alain/issues/2359 */ -function fixBrowserBuilderBudgets(options: ApplicationOptions): Rule { +function fixBrowserBuilderBudgets(): Rule { return async (tree: Tree) => { - const projectName = getProjectName(await getWorkspace(tree), options.project); const json = readJSON(tree, DEFAULT_WORKSPACE_PATH); const budgets = json.projects[projectName].architect.build.configurations.production.budgets as Array<{ type: string; @@ -122,7 +110,7 @@ function fixBrowserBuilderBudgets(options: ApplicationOptions): Rule { }; } -function addDependenciesToPackageJson(options: ApplicationOptions): Rule { +function addDependenciesToPackageJson(): Rule { return (tree: Tree) => { UpgradeMainVersions(tree); // 3rd @@ -135,33 +123,46 @@ function addRunScriptToPackageJson(): Rule { return (tree: Tree) => { const json = readPackage(tree, 'scripts'); if (json == null) return tree; + + const commandPrefix = mulitProject ? `${projectName}:` : ''; + const commandFragment = mulitProject ? ` ${projectName}` : ''; json.scripts['ng-high-memory'] = `node --max_old_space_size=8000 ./node_modules/@angular/cli/bin/ng`; - json.scripts.start = `ng s -o`; - json.scripts.hmr = `ng s -o --hmr`; - json.scripts.build = `npm run ng-high-memory build`; - json.scripts.analyze = `npm run ng-high-memory build -- --source-map`; - json.scripts['analyze:view'] = `source-map-explorer dist/**/*.js`; - json.scripts['test-coverage'] = `ng test --code-coverage --watch=false`; - json.scripts['color-less'] = `ng-alain-plugin-theme -t=colorLess`; - json.scripts.theme = `ng-alain-plugin-theme -t=themeCss`; - json.scripts.icon = `ng g ng-alain:plugin icon`; + json.scripts[`${commandPrefix}start`] = `ng s${commandFragment} -o`; + json.scripts[`${commandPrefix}hmr`] = `ng s${commandFragment} -o --hmr`; + json.scripts[`${commandPrefix}build`] = `npm run ng-high-memory build${commandFragment}`; + json.scripts[`${commandPrefix}analyze`] = `npm run ng-high-memory build${commandFragment} -- --source-map`; + json.scripts[`${commandPrefix}analyze:view`] = `source-map-explorer dist/${ + mulitProject ? `${projectName}/` : '' + }**/*.js`; + json.scripts[`${commandPrefix}test-coverage`] = `ng test${commandFragment} --code-coverage --watch=false`; + const themeCommand = mulitProject ? ` -n=${projectName}` : ''; + json.scripts[`${commandPrefix}color-less`] = `ng-alain-plugin-theme -t=colorLess${themeCommand}`; + json.scripts[`${commandPrefix}theme`] = `ng-alain-plugin-theme -t=themeCss${themeCommand}`; + json.scripts[`${commandPrefix}icon`] = `ng g ng-alain:plugin icon${ + mulitProject ? ` --project ${projectName}` : '' + }`; json.scripts.prepare = `husky install`; writePackage(tree, json); return tree; }; } -function addPathsToTsConfig(): Rule { +function addPathsToTsConfig(project: ProjectDefinition): Rule { return (tree: Tree) => { - const json = readJSON(tree, 'tsconfig.json', 'compilerOptions'); + if (project == null) return; + const tsconfigPath = project.targets?.get(BUILD_TARGET_BUILD)?.options?.tsConfig as string; + if (tsconfigPath == null) return; + const json = readJSON(tree, tsconfigPath); if (json == null) return tree; if (!json.compilerOptions) json.compilerOptions = {}; if (!json.compilerOptions.paths) json.compilerOptions.paths = {}; const paths = json.compilerOptions.paths; - paths['@shared'] = ['src/app/shared/index']; - paths['@core'] = ['src/app/core/index']; - paths['@env/*'] = ['src/environments/*']; - writeJSON(tree, 'tsconfig.json', json); + const commandPrefix = mulitProject ? `projects/${projectName}/` : ''; + paths['@shared'] = [`${commandPrefix}src/app/shared/index`]; + paths['@core'] = [`${commandPrefix}src/app/core/index`]; + paths['@env/*'] = [`${commandPrefix}src/environments/*`]; + paths['@_mock'] = ['_mock/index']; + writeJSON(tree, tsconfigPath, json); return tree; }; } @@ -241,9 +242,13 @@ function addSchematics(options: ApplicationOptions): Rule { } function forceLess(): Rule { - return () => { - addAssetsToTarget([{ type: 'style', value: 'src/styles.less' }], 'add', [BUILD_TARGET_BUILD], null!, true); - }; + return addAssetsToTarget( + [{ type: 'style', value: `${mulitProject ? `projects/${projectName}/` : ''}src/styles.less` }], + 'add', + [BUILD_TARGET_BUILD], + projectName, + false + ); } function addStyle(): Rule { @@ -370,24 +375,39 @@ function fixVsCode(): Rule { }; } +function fixNgAlainJson(): Rule { + return (tree: Tree) => { + const json = getNgAlainJson(tree); + if (json == null) return; + + if (typeof json.projects !== 'object') json.projects = {}; + if (!json.projects[projectName]) json.projects[projectName] = {}; + + writeNgAlainJson(tree, json); + }; +} + export default function (options: ApplicationOptions): Rule { return async (tree: Tree, context: SchematicContext) => { - project = (await getProject(tree, options.project)).project; - context.logger.info(`Generating NG-ALAIN scaffold...`); + const res = await getProject(tree, options.project); + mulitProject = isMulitProject(tree); + project = res.project; + projectName = res.name; + context.logger.info(`Generating NG-ALAIN scaffold to ${projectName} project...`); return chain([ // @delon/* dependencies - addDependenciesToPackageJson(options), + addDependenciesToPackageJson(), // Configuring CommonJS dependencies // https://angular.io/guide/build#configuring-commonjs-dependencies addAllowedCommonJsDependencies([]), addAllowSyntheticDefaultImports(), // ci addRunScriptToPackageJson(), - addPathsToTsConfig(), + addPathsToTsConfig(project), // code style addCodeStylesToPackageJson(), addSchematics(options), - addESLintRule(context, false), + addESLintRule(res.name), addImportNotation(), // files removeOrginalFiles(), @@ -396,8 +416,9 @@ export default function (options: ApplicationOptions): Rule { addStyle(), fixLang(options), fixVsCode(), - fixAngularJson(options), - fixBrowserBuilderBudgets(options) + fixAngularJson(), + fixBrowserBuilderBudgets(), + fixNgAlainJson() ]); }; } diff --git a/schematics/ng-add/index.ts b/schematics/ng-add/index.ts index c15bb4a953..ba48b3e644 100644 --- a/schematics/ng-add/index.ts +++ b/schematics/ng-add/index.ts @@ -58,6 +58,10 @@ function isYarn(tree: Tree): boolean { return readJSON(tree, DEFAULT_WORKSPACE_PATH)?.cli?.packageManager === 'yarn'; } +function isValidProjectName(tree: Tree, name: string): boolean { + return Object.keys(readJSON(tree, DEFAULT_WORKSPACE_PATH)?.projects ?? {}).indexOf(name) !== -1; +} + function finished(): Rule { return (_: Tree, context: SchematicContext) => { context.addTask(new NodePackageInstallTask()); @@ -75,7 +79,9 @@ NG-ALAIN documentation site: https://ng-alain.com export default function (options: NgAddOptions): Rule { return (tree: Tree, context: SchematicContext) => { if (!isYarn(tree)) { - context.logger.warn(`TIPS:: Please use yarn instead of NPM to install dependencies`); + context.logger.warn( + `TIPS:: Please use yarn instead of NPM to install dependencies, Pls refer to https://ng-alain.com/docs/getting-started/en#Installation` + ); } const nodeVersion = getNodeMajorVersion(); @@ -89,8 +95,19 @@ export default function (options: NgAddOptions): Rule { const pkg = readPackage(tree); - if (pkg.devDependencies['ng-alain']) { - throw new SchematicsException(`Already an NG-ALAIN project and can't be executed again: ng add ng-alain`); + if (options.project) { + if (!isValidProjectName(tree, options.project)) { + throw new SchematicsException(`Not found under the projects node of angular.json: ${options.project}`); + } + } else { + if (pkg.devDependencies['ng-alain']) { + throw new SchematicsException(`Already an NG-ALAIN project and can't be executed again: ng add ng-alain`); + } + if (!tree.exists('src/index.html')) { + throw new SchematicsException( + `NG-ALAIN must be attached to a new Angular project, so you need to use 'ng new projectName' to build a new Angular project, or specify the project name to be attached` + ); + } } let ngCoreVersion = pkg.dependencies['@angular/core'] as string; diff --git a/schematics/ng-add/schema.json b/schematics/ng-add/schema.json index f76a08f363..d3acbc4d30 100644 --- a/schematics/ng-add/schema.json +++ b/schematics/ng-add/schema.json @@ -1,9 +1,14 @@ { "$schema": "http://json-schema.org/draft-07/schema", "$id": "SchematicsNgAlainAdd", - "title": "NgAlain Add Options Schema", + "title": "NG-ALAIN Add Options Schema", "type": "object", "properties": { + "project": { + "type": "string", + "description": "The name of the project name.", + "x-prompt": "In which project do you want to create NG-ALAIN? (If you enter project name, it means using in multiple-projects)" + }, "defaultLanguage": { "type": "string", "description": "Specify default language [https://ng-alain.com/docs/i18n].", diff --git a/schematics/ng-add/schema.ts b/schematics/ng-add/schema.ts index 4ea858d7dc..43e7585d6d 100644 --- a/schematics/ng-add/schema.ts +++ b/schematics/ng-add/schema.ts @@ -1,4 +1,5 @@ export interface Schema { + project?: string; form?: boolean; mock?: boolean; defaultLanguage?: string; diff --git a/schematics/utils/versions.ts b/schematics/utils/versions.ts index bedd2b19fb..6dd54c4ce0 100644 --- a/schematics/utils/versions.ts +++ b/schematics/utils/versions.ts @@ -1,10 +1,9 @@ -import { Tree, Rule, SchematicContext } from '@angular-devkit/schematics'; +import { Tree, Rule } from '@angular-devkit/schematics'; import { updateWorkspace } from '@schematics/angular/utility/workspace'; import { VERSION } from './lib-versions'; -import { logInfo } from './log'; import { addPackage } from './package'; -import { BUILD_TARGET_LINT } from './workspace'; +import { BUILD_TARGET_LINT, getProjectFromWorkspace } from './workspace'; /** * 修复主要依赖的版本号 @@ -44,21 +43,19 @@ export function UpgradeMainVersions(tree: Tree, version: string = VERSION): void addPackage(tree, [`rxjs@DEP-0.0.0-PLACEHOLDER`, `ng-zorro-antd@DEP-0.0.0-PLACEHOLDER`]); } -export function addESLintRule(context: SchematicContext, showLog: Boolean = true): Rule { +export function addESLintRule(projectName: string): Rule { return updateWorkspace(async workspace => { - workspace.projects.forEach(project => { - if (project.targets.has(BUILD_TARGET_LINT)) { - project.targets.delete(BUILD_TARGET_LINT); + const project = getProjectFromWorkspace(workspace, projectName); + if (project == null) return; + + if (project.targets.has(BUILD_TARGET_LINT)) { + project.targets.delete(BUILD_TARGET_LINT); + } + project.targets.set(BUILD_TARGET_LINT, { + builder: '@angular-eslint/builder:lint', + options: { + lintFilePatterns: ['src/**/*.ts', 'src/**/*.html'] } - project.targets.set(BUILD_TARGET_LINT, { - builder: '@angular-eslint/builder:lint', - options: { - lintFilePatterns: ['src/**/*.ts', 'src/**/*.html'] - } - }); }); - if (showLog) { - logInfo(context, `Update 'lint' node in angular.json`); - } }); } diff --git a/schematics/utils/workspace.ts b/schematics/utils/workspace.ts index 093cc883f7..659aa948a0 100644 --- a/schematics/utils/workspace.ts +++ b/schematics/utils/workspace.ts @@ -34,6 +34,15 @@ export function getNgAlainJson(tree: Tree): NgAlainDefinition | undefined { return readJSON(tree, NG_ALAIN_JSON); } +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function writeNgAlainJson(tree: Tree, json: any): any { + return writeJSON(tree, NG_ALAIN_JSON, json); +} + +export function isMulitProject(tree: Tree): boolean { + return !tree.exists('src/main.ts'); +} + export async function getProject( tree: Tree, projectName?: string @@ -67,7 +76,9 @@ export function addAssetsToTarget( list.length = 0; } if (behavior === 'add') { - list.push(item.value); + if (!list.includes(item.value)) { + list.push(item.value); + } } else { const idx = list.indexOf(item.value); if (idx !== -1) { @@ -154,19 +165,20 @@ export function getProjectTarget( return options; } -export function addStylePreprocessorOptionsToAllProject(workspace: WorkspaceDefinition): void { - workspace.projects.forEach(project => { - const build = project.targets.get(BUILD_TARGET_BUILD); - if (build == null || build.options == null) return; - if (build.options.stylePreprocessorOptions == null) { - build.options.stylePreprocessorOptions = {}; - } - let includePaths: string[] = build.options.stylePreprocessorOptions['includePaths'] ?? []; - if (!Array.isArray(includePaths)) includePaths = []; - if (includePaths.includes(`node_modules/`)) return; - includePaths.push(`node_modules/`); - build.options.stylePreprocessorOptions['includePaths'] = includePaths; - }); +export function addStylePreprocessorOptions(workspace: WorkspaceDefinition, projectName: string): void { + const project = getProjectFromWorkspace(workspace, projectName); + if (project == null) return; + + const build = project.targets.get(BUILD_TARGET_BUILD); + if (build == null || build.options == null) return; + if (build.options.stylePreprocessorOptions == null) { + build.options.stylePreprocessorOptions = {}; + } + let includePaths: string[] = build.options.stylePreprocessorOptions['includePaths'] ?? []; + if (!Array.isArray(includePaths)) includePaths = []; + if (includePaths.includes(`node_modules/`)) return; + includePaths.push(`node_modules/`); + build.options.stylePreprocessorOptions['includePaths'] = includePaths; } export function addSchematicCollections(workspace: WorkspaceDefinition): void { @@ -180,17 +192,17 @@ export function addSchematicCollections(workspace: WorkspaceDefinition): void { workspace.extensions.cli['schematicCollections'] = schematicCollections; } -export function addFileReplacements(workspace: WorkspaceDefinition): void { - workspace.projects.forEach(project => { - const build = project.targets.get(BUILD_TARGET_BUILD); - if (build == null || build.options == null) return; - if (build.configurations == null) build.configurations = {}; - if (build.configurations.production == null) build.configurations.production = {}; - if (!Array.isArray(build.configurations.production.fileReplacements)) - build.configurations.production.fileReplacements = []; - build.configurations.production.fileReplacements.push({ - replace: 'src/environments/environment.ts', - with: 'src/environments/environment.prod.ts' - }); +export function addFileReplacements(workspace: WorkspaceDefinition, projectName: string): void { + const project = getProjectFromWorkspace(workspace, projectName); + if (project == null) return; + const build = project.targets.get(BUILD_TARGET_BUILD); + if (build == null || build.options == null) return; + if (build.configurations == null) build.configurations = {}; + if (build.configurations.production == null) build.configurations.production = {}; + if (!Array.isArray(build.configurations.production.fileReplacements)) + build.configurations.production.fileReplacements = []; + build.configurations.production.fileReplacements.push({ + replace: 'src/environments/environment.ts', + with: 'src/environments/environment.prod.ts' }); }