UNPKG

ng-alain

Version:

Schematics specific to NG-ALAIN

481 lines (444 loc) 16.6 kB
import { strings } from '@angular-devkit/core'; import { ProjectDefinition } from '@angular-devkit/core/src/workspace'; import { apply, chain, filter, MergeStrategy, mergeWith, move, noop, Rule, SchematicContext, template, Tree, url } from '@angular-devkit/schematics'; import { updateWorkspace } from '@schematics/angular/utility/workspace'; import { getLangData } from '../core/lang.config'; import { addAllowedCommonJsDependencies, addAllowSyntheticDefaultImports, addAssetsToTarget, addFileReplacements, addHeadStyle, addHtmlToBody, addPackage, addSchematicCollections, addStylePreprocessorOptions, addStyleResources, addViTestConfig, BUILD_TARGET_BUILD, BUILD_TARGET_SERVE, DEFAULT_WORKSPACE_PATH, getNgAlainJson, getProject, getProjectFromWorkspace, isMulitProject, modifyJSON, readContent, readJSON, readPackage, VERSION, writeFile, writeJSON, writeNgAlainJson, writePackage, ZORROVERSION } from '../utils'; import { Schema as ApplicationOptions } from './schema'; import { addESLintRule, UpgradeMainVersions } from '../utils/versions'; let project: ProjectDefinition; let projectName: string; let mulitProject = false; /** Remove files to be overwrite */ function removeOrginalFiles(): Rule { return (tree: Tree) => { [ `${project.root}/README.md`, `${project.root}/public/favicon.ico`, `${project.sourceRoot}/main.ts`, `${project.sourceRoot}/styles.less`, `${project.sourceRoot}/app/app.spec.ts`, `${project.sourceRoot}/app/app.ts`, `${project.sourceRoot}/app/app.html`, `${project.sourceRoot}/app/app.less`, `${project.sourceRoot}/app/app.config.ts`, `${project.sourceRoot}/app/app.routes.ts` ] .filter(p => tree.exists(p)) .forEach(p => tree.delete(p)); }; } function fixAngularJson(): Rule { return updateWorkspace(async workspace => { 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'; addStyleResources(workspace, projectName); addStylePreprocessorOptions(workspace, projectName); addSchematicCollections(workspace); addFileReplacements(workspace, projectName); addViTestConfig(workspace, projectName); }); } /** * Fix https://github.com/ng-alain/ng-alain/issues/2359 */ function fixBrowserBuilderBudgets(): Rule { return async (tree: Tree) => { const json = readJSON(tree, DEFAULT_WORKSPACE_PATH); const budgets = json.projects[projectName].architect.build.configurations.production.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'; writeJSON(tree, DEFAULT_WORKSPACE_PATH, json); } } }; } function addDependenciesToPackageJson(): Rule { return (tree: Tree) => { UpgradeMainVersions(tree); // 3rd addPackage(tree, ['screenfull@^6.0.2']); return tree; }; } 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[commandFragment ? commandFragment.trim() : 'start'] = `ng s${commandFragment} -o`; json.scripts[`${commandPrefix}hmr`] = `ng s${commandFragment} -o --hmr`; json.scripts[`${commandPrefix}build`] = `ng b${commandFragment}`; json.scripts[`${commandPrefix}analyze`] = `ng b${commandFragment} --source-map`; json.scripts[`${commandPrefix}analyze:view`] = `source-map-explorer dist/${mulitProject ? `${projectName}/` : ''}**/*.js --no-border-checks`; json.scripts[`${commandPrefix}test-coverage`] = `ng test${commandFragment} -c coverage --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}` : ''}`; writePackage(tree, json); return tree; }; } function addPathsToTsConfig(): Rule { return (tree: Tree) => { const tsconfigPath = project.targets?.get(BUILD_TARGET_BUILD)?.options?.tsConfig as string; if (tsconfigPath == null) { console.warn(`Cannot find tsconfig file in project ${projectName}`); return tree; } const commandPrefix = mulitProject ? `projects/${projectName}/` : ''; const tsConfigPath = 'tsconfig.json'; modifyJSON(tree, tsConfigPath, { path: ['compilerOptions', 'baseUrl'], value: './' }); const basePath = ['compilerOptions', 'paths']; modifyJSON(tree, tsConfigPath, { path: basePath, value: {} }); modifyJSON(tree, tsConfigPath, { path: [...basePath, `@shared`], value: [`${commandPrefix}src/app/shared/index`] }); modifyJSON(tree, tsConfigPath, { path: [...basePath, `@core`], value: [`${commandPrefix}src/app/core/index`] }); modifyJSON(tree, tsConfigPath, { path: [...basePath, `@env/*`], value: [`${commandPrefix}src/environments/*`] }); modifyJSON(tree, tsConfigPath, { path: [...basePath, `@_mock`], value: [`_mock/index`] }); return tree; }; } function addCodeStylesToPackageJson(): Rule { return (tree: Tree) => { const json = readPackage(tree); if (json == null) return tree; json.scripts.lint = `npm run lint:ts && npm run lint:style`; json.scripts['lint:ts'] = `ng lint`; json.scripts['lint:style'] = `npx stylelint \\"src/**/*.less\\" --fix`; json.scripts['prepare'] = 'husky install'; // remove prettier node if (json.prettier) { delete json.prettier; } writePackage(tree, json); // fix polyfills.ts const polyfillsPath = `${project.sourceRoot}/polyfills.ts`; if (tree.exists(polyfillsPath)) { const polyfillsContent = `/* eslint-disable import/no-unassigned-import */\n${readContent(tree, polyfillsPath)}`; writeFile(tree, polyfillsPath, polyfillsContent); } // dependencies addPackage( tree, [ `husky@^9.1.7`, `lint-staged@^16.4.0`, `prettier@^3.8.3`, `stylelint@^17.9.1`, `stylelint-config-standard@^40.0.0`, `stylelint-declaration-block-no-ignored-properties@^3.0.0`, `stylelint-config-clean-order@^8.0.1`, `stylelint-order@^8.1.1` ], 'devDependencies' ); return tree; }; } function addSchematics(options: ApplicationOptions): Rule { return updateWorkspace(async workspace => { const p = getProjectFromWorkspace(workspace, options.project); const schematics = p.extensions.schematics; schematics['ng-alain:module'] = { routing: true }; schematics['ng-alain:list'] = { skipTests: false }; schematics['ng-alain:edit'] = { skipTests: false, modal: true }; schematics['ng-alain:view'] = { skipTests: false, modal: true }; schematics['ng-alain:curd'] = { skipTests: false }; schematics['@schematics/angular:module'] = { routing: true }; schematics['@schematics/angular:component'] = { skipTests: false, flat: false, inlineStyle: true, inlineTemplate: false, ...schematics['@schematics/angular:component'] }; schematics['@schematics/angular:directive'] = { skipTests: false }; schematics['@schematics/angular:service'] = { skipTests: false }; }); } function forceLess(): Rule { return addAssetsToTarget( [{ type: 'style', value: `${mulitProject ? `projects/${projectName}/` : ''}src/styles.less` }], 'add', [BUILD_TARGET_BUILD], projectName, false ); } function addStyle(): Rule { return (tree: Tree) => { addHeadStyle( tree, project, ` <style type="text/css">.preloader{position:fixed;top:0;left:0;width:100%;height:100%;overflow:hidden;background:#49a9ee;z-index:9999;transition:opacity .65s}.preloader-hidden-add{opacity:1;display:block}.preloader-hidden-add-active{opacity:0}.preloader-hidden{display:none}.cs-loader{position:absolute;top:0;left:0;height:100%;width:100%}.cs-loader-inner{transform:translateY(-50%);top:50%;position:absolute;width:100%;color:#fff;text-align:center}.cs-loader-inner label{font-size:20px;opacity:0;display:inline-block}@keyframes lol{0%{opacity:0;transform:translateX(-300px)}33%{opacity:1;transform:translateX(0)}66%{opacity:1;transform:translateX(0)}100%{opacity:0;transform:translateX(300px)}}.cs-loader-inner label:nth-child(6){animation:lol 3s infinite ease-in-out}.cs-loader-inner label:nth-child(5){animation:lol 3s .1s infinite ease-in-out}.cs-loader-inner label:nth-child(4){animation:lol 3s .2s infinite ease-in-out}.cs-loader-inner label:nth-child(3){animation:lol 3s .3s infinite ease-in-out}.cs-loader-inner label:nth-child(2){animation:lol 3s .4s infinite ease-in-out}.cs-loader-inner label:nth-child(1){animation:lol 3s .5s infinite ease-in-out}</style>` ); addHtmlToBody( tree, project, ` <div class="preloader"><div class="cs-loader"><div class="cs-loader-inner"><label> ●</label><label> ●</label><label> ●</label><label> ●</label><label> ●</label><label> ●</label></div></div></div>\n` ); // // add styles // [`${project.sourceRoot}/styles/index.less`, `${project.sourceRoot}/styles/theme.less`].forEach(p => { // overwriteFile({ tree, filePath: p, content: path.join(overwriteDataFileRoot, p), overwrite: true }); // }); return tree; }; } function addFilesToRoot(options: ApplicationOptions): Rule { return chain([ mergeWith( apply(url('./files/src'), [ options.i18n ? noop() : filter(p => p.indexOf('i18n') === -1), options.form ? noop() : filter(p => p.indexOf('json-schema') === -1), template({ utils: strings, ...options, dot: '.', VERSION, ZORROVERSION }), move(project.sourceRoot) ]), MergeStrategy.Overwrite ), mergeWith( apply(url('./files/root'), [ options.i18n ? noop() : filter(p => p.indexOf('i18n') === -1), options.form ? noop() : filter(p => p.indexOf('json-schema') === -1), template({ utils: strings, ...options, dot: '.', VERSION, ZORROVERSION }) // move('/') ]), MergeStrategy.Overwrite ) ]); } function fixLang(options: ApplicationOptions): Rule { return (tree: Tree, context: SchematicContext) => { if (options.i18n) return; const langs = getLangData(options.defaultLanguage!); if (!langs) return; context.logger.info(`Translating template into ${options.defaultLanguage} language, please wait...`); tree.visit(p => { if (~p.indexOf(`/node_modules/`)) return; fixLangInHtml(tree, p, langs); }); }; } function fixLangInHtml(tree: Tree, p: string, langs: unknown): void { let html = tree.get(p)!.content.toString('utf8'); let matchCount = 0; // {{(status ? 'menu.fullscreen.exit' : 'menu.fullscreen') | i18n }} // {{ (status ? 'menu.fullscreen.exit' : 'menu.fullscreen') | i18n }} html = html.replace(/\{\{[ ]?\(status \? '([^']+)' : '([^']+)'\) \| i18n \}\}/g, (_word, key1, key2) => { ++matchCount; return `{{ status ? '${langs[key1] || key1}' : '${langs[key2] || key2}' }}`; }); // {{ 'app.register-result.msg' | i18n: { email } }} html = html.replace(/\{\{[ ]?'([^']+)'[ ]? \| i18n: \{ [^ ]+ \} \}\}/g, (_word, key) => { ++matchCount; return langs[key] || key; }); // {{ 'app.register-result.msg' | i18n: params }} html = html.replace(/\{\{[ ]?'([^']+)'[ ]? \| i18n: [^ ]+ \}\}/g, (_word, key) => { ++matchCount; return langs[key] || key; }); // {{ 'Please enter mobile number!' | i18n }} html = html.replace(/\{\{[ ]?'([^']+)' \| i18n[ ]?\}\}/g, (_word, key) => { ++matchCount; return langs[key] || key; }); // [nzTitle]="'app.login.tab-login-credentials' | i18n" html = html.replace(/'([^']+)' \| i18n[ ]?/g, (_word, key) => { ++matchCount; const value = langs[key] || key; return `'${value}'`; }); // 'app.register.get-verification-code' | i18n html = html.replace(/'([^']+)' \| i18n/g, (_word, key) => { ++matchCount; return langs[key] || key; }); // removed `header-i18n` if (~html.indexOf(`<header-i18n showLangText="false" class="langs" />`)) { ++matchCount; html = html.replace(`<header-i18n showLangText="false" class="langs" />`, ``); } if (matchCount > 0) { tree.overwrite(p, html); } } 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); }; } function addTailwindcss(options: ApplicationOptions): Rule { if (!options.tailwindcss) return noop(); const sourceRoot = project.sourceRoot!; const prefix = mulitProject ? `projects/${projectName}/` : ''; return chain([ (tree: Tree) => { // Add devDependencies addPackage( tree, [ 'tailwindcss@^4.3.0', '@tailwindcss/postcss@^4.1.12', 'postcss@^8.5.3', 'postcss-less@^6.0.0' ], 'devDependencies' ); // Create .postcssrc.json tree.create('.postcssrc.json', `${JSON.stringify({ plugins: { '@tailwindcss/postcss': {} } }, null, 2)}\n`); // Create src/tailwind.css tree.create(`${sourceRoot}/tailwind.css`, `@layer theme, base, ng-alain, utilities;\n\n@import 'tailwindcss';\n`); // Wrap styles.less in @layer ng-alain const stylesLessPath = `${sourceRoot}/styles.less`; if (tree.exists(stylesLessPath)) { const content = tree.read(stylesLessPath)!.toString('utf8'); const wrappedContent = `/* stylelint-disable no-invalid-position-at-import-rule */\n@layer ng-alain {\n${content}}\n`; tree.overwrite(stylesLessPath, wrappedContent); } // Create/update .vscode/extensions.json with tailwindcss recommendation const extPath = '.vscode/extensions.json'; const extId = 'bradlc.vscode-tailwindcss'; if (tree.exists(extPath)) { const json = readJSON(tree, extPath); if (!json.recommendations) json.recommendations = []; if (!json.recommendations.includes(extId)) json.recommendations.push(extId); writeJSON(tree, extPath, json); } else { tree.create(extPath, `${JSON.stringify({ recommendations: [extId] }, null, 2)}\n`); } return tree; }, // Add src/tailwind.css to angular.json styles addAssetsToTarget( [{ type: 'style', value: `${prefix}src/tailwind.css` }], 'add', [BUILD_TARGET_BUILD], projectName, false ) ]); } export default function (options: ApplicationOptions): Rule { return async (tree: Tree, context: SchematicContext) => { 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(), // Configuring CommonJS dependencies // https://angular.io/guide/build#configuring-commonjs-dependencies addAllowedCommonJsDependencies([]), addAllowSyntheticDefaultImports(), // ci addRunScriptToPackageJson(), addPathsToTsConfig(), // code style addCodeStylesToPackageJson(), addSchematics(options), addESLintRule(res.name), // files removeOrginalFiles(), addFilesToRoot(options), forceLess(), addStyle(), addTailwindcss(options), fixLang(options), fixAngularJson(), fixBrowserBuilderBudgets(), fixNgAlainJson() ]); }; }