@qlover/create-app 0.6.3 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/dist/configs/node-lib/eslint.config.js +3 -3
  3. package/dist/configs/react-app/eslint.config.js +3 -3
  4. package/dist/index.cjs +1 -1
  5. package/dist/index.js +1 -1
  6. package/dist/templates/pack-app/eslint.config.js +3 -3
  7. package/dist/templates/pack-app/package.json +1 -1
  8. package/dist/templates/react-app/__tests__/__mocks__/I18nService.ts +13 -0
  9. package/dist/templates/react-app/__tests__/__mocks__/MockAppConfit.ts +48 -0
  10. package/dist/templates/react-app/__tests__/__mocks__/MockDialogHandler.ts +16 -0
  11. package/dist/templates/react-app/__tests__/__mocks__/MockLogger.ts +14 -0
  12. package/dist/templates/react-app/__tests__/__mocks__/createMockGlobals.ts +92 -0
  13. package/dist/templates/react-app/__tests__/setup/index.ts +51 -0
  14. package/dist/templates/react-app/__tests__/src/App.test.tsx +139 -0
  15. package/dist/templates/react-app/__tests__/src/base/cases/AppConfig.test.ts +288 -0
  16. package/dist/templates/react-app/__tests__/src/base/cases/AppError.test.ts +102 -0
  17. package/dist/templates/react-app/__tests__/src/base/cases/DialogHandler.test.ts +228 -0
  18. package/dist/templates/react-app/__tests__/src/base/cases/I18nKeyErrorPlugin.test.ts +207 -0
  19. package/dist/templates/react-app/__tests__/src/base/cases/InversifyContainer.test.ts +181 -0
  20. package/dist/templates/react-app/__tests__/src/base/cases/PublicAssetsPath.test.ts +61 -0
  21. package/dist/templates/react-app/__tests__/src/base/cases/RequestLogger.test.ts +199 -0
  22. package/dist/templates/react-app/__tests__/src/base/cases/RequestStatusCatcher.test.ts +192 -0
  23. package/dist/templates/react-app/__tests__/src/base/cases/RouterLoader.test.ts +235 -0
  24. package/dist/templates/react-app/__tests__/src/base/services/I18nService.test.ts +224 -0
  25. package/dist/templates/react-app/__tests__/src/core/IOC.test.ts +257 -0
  26. package/dist/templates/react-app/__tests__/src/core/bootstraps/BootstrapsApp.test.ts +72 -0
  27. package/dist/templates/react-app/__tests__/src/main.integration.test.tsx +62 -0
  28. package/dist/templates/react-app/__tests__/src/main.test.tsx +46 -0
  29. package/dist/templates/react-app/__tests__/src/uikit/components/BaseHeader.test.tsx +88 -0
  30. package/dist/templates/react-app/config/app.router.ts +155 -0
  31. package/dist/templates/react-app/config/common.ts +9 -1
  32. package/dist/templates/react-app/docs/en/test-guide.md +782 -0
  33. package/dist/templates/react-app/docs/zh/test-guide.md +782 -0
  34. package/dist/templates/react-app/package.json +9 -20
  35. package/dist/templates/react-app/public/locales/en/common.json +1 -1
  36. package/dist/templates/react-app/public/locales/zh/common.json +1 -1
  37. package/dist/templates/react-app/src/base/cases/AppConfig.ts +16 -9
  38. package/dist/templates/react-app/src/base/cases/PublicAssetsPath.ts +7 -1
  39. package/dist/templates/react-app/src/base/services/I18nService.ts +15 -4
  40. package/dist/templates/react-app/src/base/services/RouteService.ts +43 -7
  41. package/dist/templates/react-app/src/core/bootstraps/BootstrapApp.ts +31 -10
  42. package/dist/templates/react-app/src/core/bootstraps/BootstrapsRegistry.ts +1 -1
  43. package/dist/templates/react-app/src/core/globals.ts +1 -3
  44. package/dist/templates/react-app/src/core/registers/RegisterCommon.ts +5 -3
  45. package/dist/templates/react-app/src/main.tsx +6 -1
  46. package/dist/templates/react-app/src/pages/404.tsx +0 -1
  47. package/dist/templates/react-app/src/pages/500.tsx +1 -1
  48. package/dist/templates/react-app/src/pages/base/RedirectPathname.tsx +3 -1
  49. package/dist/templates/react-app/src/uikit/components/BaseHeader.tsx +9 -2
  50. package/dist/templates/react-app/src/uikit/components/LocaleLink.tsx +5 -3
  51. package/dist/templates/react-app/src/uikit/hooks/useI18nGuard.ts +4 -6
  52. package/dist/templates/react-app/tsconfig.json +2 -1
  53. package/dist/templates/react-app/tsconfig.test.json +13 -0
  54. package/dist/templates/react-app/vite.config.ts +3 -2
  55. package/package.json +3 -3
package/CHANGELOG.md CHANGED
@@ -1,5 +1,34 @@
1
1
  # @qlover/create-app
2
2
 
3
+ ## 0.7.1
4
+
5
+ ### Patch Changes
6
+
7
+ #### ♻️ Refactors
8
+
9
+ - **docs:** update references from @qlover/eslint-plugin-fe-dev to @qlover/eslint-plugin ([3c682cd](https://github.com/qlover/fe-base/commit/3c682cdbac17cc0e96166a45934899bad7c565cc)) ([#489](https://github.com/qlover/fe-base/pull/489))
10
+ - Replaced all instances of @qlover/eslint-plugin-fe-dev with @qlover/eslint-plugin in documentation and configuration files to reflect the new plugin structure.
11
+ - Ensured consistency across README and documentation files in both English and Chinese.
12
+ - Updated ESLint configuration files to use the new plugin name, enhancing clarity and maintainability.
13
+
14
+ ## 0.7.0
15
+
16
+ ### Minor Changes
17
+
18
+ #### ✨ Features
19
+
20
+ - **router:** implement locale routing and enhance route configuration ([c6ceec2](https://github.com/qlover/fe-base/commit/c6ceec28f83a6c3638872bb4bbb4d6534b02fca8)) ([#476](https://github.com/qlover/fe-base/pull/476))
21
+ - Added support for locale-based routing in the application, allowing routes to include language prefixes.
22
+ - Introduced `baseNoLocaleRoutes` to manage routes without localization, improving flexibility in route handling.
23
+ - Updated `I18nService` to conditionally cache user language based on locale routing settings.
24
+ - Enhanced `RouteService` to handle locale routes and added methods for redirection and language validation.
25
+ - Modified `RegisterCommon` to utilize the appropriate route configuration based on locale settings.
26
+ - Updated various components and hooks to integrate with the new routing logic, ensuring seamless navigation and language handling.
27
+
28
+ These changes aim to improve the user experience by providing localized routes and better language management.
29
+
30
+ Co-authored-by: QRJ <renjie.qin@brain.im>
31
+
3
32
  ## 0.6.3
4
33
 
5
34
  ### Patch Changes
@@ -4,7 +4,7 @@ import prettierConfig from '../../packages/node-lib/.prettierrc.js';
4
4
  import prettier from 'eslint-plugin-prettier';
5
5
  import tseslint from '@typescript-eslint/eslint-plugin';
6
6
  import tsparser from '@typescript-eslint/parser';
7
- import * as feDev from '@qlover/eslint-plugin-fe-dev';
7
+ import qloverEslint from '@qlover/eslint-plugin';
8
8
 
9
9
  export default [
10
10
  {
@@ -29,7 +29,7 @@ export default [
29
29
  plugins: {
30
30
  prettier,
31
31
  '@typescript-eslint': tseslint,
32
- 'fe-dev': feDev
32
+ '@qlover-eslint': qloverEslint
33
33
  },
34
34
  rules: {
35
35
  ...js.configs.recommended.rules,
@@ -44,7 +44,7 @@ export default [
44
44
  }
45
45
  }
46
46
  ],
47
- 'fe-dev/ts-class-method-return': 'error'
47
+ '@qlover-eslint/ts-class-method-return': 'error'
48
48
  }
49
49
  }
50
50
  ];
@@ -2,7 +2,7 @@ import js from '@eslint/js';
2
2
  import globals from 'globals';
3
3
  import vitest from 'eslint-plugin-vitest';
4
4
  import * as eslintChain from '@qlover/fe-standard/eslint/index.js';
5
- import * as feDev from '@qlover/eslint-plugin-fe-dev';
5
+ import qloverEslint from '@qlover/eslint-plugin';
6
6
  import reactHooks from 'eslint-plugin-react-hooks';
7
7
  import reactRefresh from 'eslint-plugin-react-refresh';
8
8
  import tseslint from 'typescript-eslint';
@@ -71,10 +71,10 @@ export default tseslint.config([
71
71
  plugins: {
72
72
  'react-hooks': reactHooks,
73
73
  'react-refresh': reactRefresh,
74
- 'fe-dev': feDev
74
+ '@qlover-eslint': qloverEslint
75
75
  },
76
76
  rules: {
77
- 'fe-dev/ts-class-method-return': 'error',
77
+ '@qlover-eslint/ts-class-method-return': 'error',
78
78
  '@typescript-eslint/explicit-function-return-type': 'off',
79
79
  '@typescript-eslint/no-empty-object-type': 'off',
80
80
  '@typescript-eslint/no-unused-vars': [
package/dist/index.cjs CHANGED
@@ -8,4 +8,4 @@ ${t}`,dn=Object.getOwnPropertyDescriptor(Function.prototype,"toString"),_n=Objec
8
8
  `))this.#e+=Math.max(1,Math.ceil(Ye(D,{countAnsiEscapeCodes:!0})/t))}get isEnabled(){return this.#a&&!this.#F}set isEnabled(t){if(typeof t!="boolean")throw new TypeError("The `isEnabled` option must be a boolean");this.#a=t}get isSilent(){return this.#F}set isSilent(t){if(typeof t!="boolean")throw new TypeError("The `isSilent` option must be a boolean");this.#F=t}frame(){let t=Date.now();(this.#i===-1||t-this.#c>=this.interval)&&(this.#i=++this.#i%this.#D.frames.length,this.#c=t);let{frames:r}=this.#D,u=r[this.#i];this.color&&(u=d[this.color](u));let i=typeof this.#s=="string"&&this.#s!==""?this.#s+" ":"",D=typeof this.text=="string"?" "+this.text:"",o=typeof this.#o=="string"&&this.#o!==""?" "+this.#o:"";return i+u+D+o}clear(){if(!this.#a||!this.#u.isTTY)return this;this.#u.cursorTo(0);for(let t=0;t<this.#n;t++)t>0&&this.#u.moveCursor(0,-1),this.#u.clearLine(1);return(this.#l||this.lastIndent!==this.#l)&&this.#u.cursorTo(this.#l),this.lastIndent=this.#l,this.#n=0,this}render(){return this.#F?this:(this.clear(),this.#u.write(this.frame()),this.#n=this.#e,this)}start(t){return t&&(this.text=t),this.#F?this:this.#a?this.isSpinning?this:(this.#t.hideCursor&&$e.hide(this.#u),this.#t.discardStdin&&V.default.stdin.isTTY&&(this.#r=!0,Je.start()),this.render(),this.#f=setInterval(this.render.bind(this),this.interval),this):(this.text&&this.#u.write(`- ${this.text}
9
9
  `),this)}stop(){return this.#a?(clearInterval(this.#f),this.#f=void 0,this.#i=0,this.clear(),this.#t.hideCursor&&$e.show(this.#u),this.#t.discardStdin&&V.default.stdin.isTTY&&this.#r&&(Je.stop(),this.#r=!1),this):this}succeed(t){return this.stopAndPersist({symbol:H.success,text:t})}fail(t){return this.stopAndPersist({symbol:H.error,text:t})}warn(t){return this.stopAndPersist({symbol:H.warning,text:t})}info(t){return this.stopAndPersist({symbol:H.info,text:t})}stopAndPersist(t={}){if(this.#F)return this;let r=t.prefixText??this.#s,u=this.#E(r," "),i=t.symbol??" ",D=t.text??this.text,s=typeof D=="string"?(i?" ":"")+D:"",a=t.suffixText??this.#o,f=this.#g(a," "),p=u+i+s+f+`
10
10
  `;return this.stop(),this.#u.write(p),this}};function tr(e){return new Ze(e)}async function rr(e,t){let r=typeof e=="function",u=typeof e.then=="function";if(!r&&!u)throw new TypeError("Parameter `action` must be a Function or a Promise");let{successText:i,failText:D}=typeof t=="object"?t:{successText:void 0,failText:void 0},o=tr(t).start();try{let a=await(r?e(o):e);return o.succeed(i===void 0?void 0:typeof i=="string"?i:i(a)),a}catch(s){throw o.fail(D===void 0?void 0:typeof D=="string"?D:D(s)),s}}var HD=require("fs");var P=require("path"),me=require("fs"),cr=h(lr(),1),Dt=require("fs");var de=require("fs"),X=class{static ensureDir(t){(0,de.existsSync)(t)||(0,de.mkdirSync)(t,{recursive:!0})}};var{copyFile:rs,stat:us}=Dt.promises,_e=class e{constructor(t,r=e.IGNORE_FILE){this.ignoreTargetPath=t;this.ignoreFile=r}static IGNORE_FILE=".gitignore.template";getIg(t=this.ignoreTargetPath){let r=(0,P.join)(t,this.ignoreFile);if(!(0,me.existsSync)(r))return;let D=(0,me.readFileSync)(r,"utf8").split(`
11
- `).map(o=>o.trim()).filter(o=>o&&!o.startsWith("#"));return(0,cr.default)().add(D)}async copyFiles(t,r,u,i){let D=await Dt.promises.readdir(t);await Promise.all(D.map(async o=>{let s=(0,P.join)(t,o),a=(0,P.join)(r,o);if(u&&u.ignores(o))return;if(X.ensureDir((0,P.dirname)(a)),(await us(s)).isDirectory())await this.copyFiles(s,a,u,i);else{if(i&&await i(s,a))return;await rs(s,a)}}))}copyPaths({sourcePath:t,targetPath:r,copyCallback:u}){X.ensureDir(r);let i=this.getIg();return this.copyFiles(t,r,i,u)}};var L=require("fs"),kD=h(LD(),1),xe=class{constructor(){}isJSONFilePath(t){return t.endsWith(".json")||t.endsWith(".json.template")}isTemplateFilePath(t){return t.endsWith(".template")}getRealTemplateFilePath(t){return t.replace(".template","")}readFile(t){return(0,L.readFileSync)(t,"utf-8")}readJSONFile(t){return JSON.parse(this.readFile(t))}writeFile(t,r){(0,L.writeFileSync)(this.getRealTemplateFilePath(t),r,{encoding:"utf-8"})}replaceFile(t,r){let u=this.readFile(t);return Object.keys(r).forEach(i=>{let D=r[i];u=u.replace(new RegExp(`\\[TPL:${i}\\]`,"g"),typeof D=="string"?D:JSON.stringify(D))}),u}mergeJSONFile(t,r){let u=this.readJSONFile(t),i=(0,kD.default)(r,u);this.writeFile(t,JSON.stringify(i,null,2))}composeConfigFile(t,r,u){if(this.isTemplateFilePath(r)){let i=this.replaceFile(r,t);if(this.isJSONFilePath(r)&&this.isJSONFilePath(u)){let D=this.getRealTemplateFilePath(u);return(0,L.existsSync)(D)?(this.mergeJSONFile(D,JSON.parse(i)),!0):(this.writeFile(D,i),!0)}return this.writeFile(u,i),!0}return!1}};var $D=["pack-app"],ye=class{ora;context;subPackages;copyer;compose;constructor(t){let r=t.options?.templateRootPath;if(!r)throw new Error("template path not exit");if(!(0,HD.existsSync)(r))throw new Error("template path not exit");this.ora=rr,this.context=new UD.FeScriptContext(t),this.subPackages=["node-lib","react-app"],this.copyer=new _e((0,v.join)(this.context.options.configsRootPath,"_common")),this.compose=new xe}get logger(){return this.context.logger}async steps(t){try{return await WD.default.prompt(t)}catch(r){throw r.isTtyError,this.logger.error(r),r}}async action({label:t,task:r}){let u=r();u instanceof Promise||(u=Promise.resolve(u));let i=t;return this.ora(u,i),u}isPackageTemplate(t){return $D.includes(t)}async getGeneratorContext(){let t=Ot(this.subPackages,$D),r=await this.steps(t);if(this.isPackageTemplate(r.template)){let u=wt(this.subPackages),i=await this.steps(u);Object.assign(r,i)}return r.targetPath=(0,v.join)(process.cwd(),r.projectName),r.releasePath=r.releasePath||"src",r}async generate(){let t=await this.getGeneratorContext();if(this.logger.debug("context is:",t,this.context.options.templateRootPath),t.subPackages){await this.action({label:"Generate Directories(subPackages)",task:async()=>{await this.generateTemplateDir(t),await this.generateSubPackages(t),await this.generateConfigs(t,t.targetPath,"_common")}});return}await this.action({label:"Generate Directory",task:async()=>{await this.generateTemplateDir(t),await this.generateConfigs(t,t.targetPath,"_common"),await this.generateConfigs(t,t.targetPath,t.template)}})}async generateConfigs(t,r,u){let i=(s,a)=>(this.logger.debug("copyCallback",s,a),this.compose.composeConfigFile(t,s,a)),{configsRootPath:D,config:o}=this.context.options;if(!o){this.logger.debug("no copy config files");return}await this.copyer.copyPaths({sourcePath:(0,v.join)(D,u),targetPath:r,copyCallback:i})}generateTemplateDir(t){return this.copyer.copyPaths({sourcePath:(0,v.join)(this.context.options.templateRootPath,t.template),targetPath:t.targetPath})}async generateSubPackages(t){let{packagesNames:r="packages",subPackages:u=[],targetPath:i=""}=t,{templateRootPath:D}=this.context.options;for(let o of u){let s=(0,v.join)(D,o),a=(0,v.join)(i,r,o);this.logger.debug("copy sub package",s,a),await this.copyer.copyPaths({sourcePath:s,targetPath:a})}}};var bt={name:"@qlover/create-app",version:"0.6.3",description:"Create a new app with a single command",private:!1,type:"module",files:["dist","package.json","README.md","CHANGELOG.md"],bin:{"create-app":"dist/index.js"},scripts:{build:"tsup","create:app":"node ./dist/index.js"},repository:{type:"git",url:"git+https://github.com/qlover/fe-base.git",directory:"packages/create-app"},homepage:"https://github.com/qlover/fe-base#readme",keywords:["create-app","fe-scripts","scripts"],author:"qlover",license:"ISC",publishConfig:{access:"public"},devDependencies:{"@qlover/logger":"workspace:*",ignore:"^7.0.3",lodash:"^4.17.21",ora:"^8.1.1"},dependencies:{"@qlover/scripts-context":"workspace:*",commander:"^13.1.0",inquirer:"^12.3.2"}};function Ul(){let e=new YD.Command;return e.version(bt.version,"-v, --version","Show version").description(bt.description).option("-d, --dry-run","Do not touch or write anything, but show the commands").option("-V, --verbose","Show more information").option("--config","Copy config files (default: true)",!0).option("--no-config","Do not copy config files"),e.parse(),e.opts()}async function Wl(){let{dryRun:e,verbose:t,...r}=Ul(),u=(0,vt.resolve)("./templates"),i=(0,vt.resolve)("./configs");(0,xt.existsSync)(u)||(console.error("Template is empty!"),process.exit(1)),(0,xt.existsSync)(i)||(console.error("Configs is empty!"),process.exit(1)),await new ye({dryRun:e,verbose:t,options:{...r,templateRootPath:u,configsRootPath:i}}).generate()}Wl().catch(e=>{console.error(e),process.exit(1)});
11
+ `).map(o=>o.trim()).filter(o=>o&&!o.startsWith("#"));return(0,cr.default)().add(D)}async copyFiles(t,r,u,i){let D=await Dt.promises.readdir(t);await Promise.all(D.map(async o=>{let s=(0,P.join)(t,o),a=(0,P.join)(r,o);if(u&&u.ignores(o))return;if(X.ensureDir((0,P.dirname)(a)),(await us(s)).isDirectory())await this.copyFiles(s,a,u,i);else{if(i&&await i(s,a))return;await rs(s,a)}}))}copyPaths({sourcePath:t,targetPath:r,copyCallback:u}){X.ensureDir(r);let i=this.getIg();return this.copyFiles(t,r,i,u)}};var L=require("fs"),kD=h(LD(),1),xe=class{constructor(){}isJSONFilePath(t){return t.endsWith(".json")||t.endsWith(".json.template")}isTemplateFilePath(t){return t.endsWith(".template")}getRealTemplateFilePath(t){return t.replace(".template","")}readFile(t){return(0,L.readFileSync)(t,"utf-8")}readJSONFile(t){return JSON.parse(this.readFile(t))}writeFile(t,r){(0,L.writeFileSync)(this.getRealTemplateFilePath(t),r,{encoding:"utf-8"})}replaceFile(t,r){let u=this.readFile(t);return Object.keys(r).forEach(i=>{let D=r[i];u=u.replace(new RegExp(`\\[TPL:${i}\\]`,"g"),typeof D=="string"?D:JSON.stringify(D))}),u}mergeJSONFile(t,r){let u=this.readJSONFile(t),i=(0,kD.default)(r,u);this.writeFile(t,JSON.stringify(i,null,2))}composeConfigFile(t,r,u){if(this.isTemplateFilePath(r)){let i=this.replaceFile(r,t);if(this.isJSONFilePath(r)&&this.isJSONFilePath(u)){let D=this.getRealTemplateFilePath(u);return(0,L.existsSync)(D)?(this.mergeJSONFile(D,JSON.parse(i)),!0):(this.writeFile(D,i),!0)}return this.writeFile(u,i),!0}return!1}};var $D=["pack-app"],ye=class{ora;context;subPackages;copyer;compose;constructor(t){let r=t.options?.templateRootPath;if(!r)throw new Error("template path not exit");if(!(0,HD.existsSync)(r))throw new Error("template path not exit");this.ora=rr,this.context=new UD.FeScriptContext(t),this.subPackages=["node-lib","react-app"],this.copyer=new _e((0,v.join)(this.context.options.configsRootPath,"_common")),this.compose=new xe}get logger(){return this.context.logger}async steps(t){try{return await WD.default.prompt(t)}catch(r){throw r.isTtyError,this.logger.error(r),r}}async action({label:t,task:r}){let u=r();u instanceof Promise||(u=Promise.resolve(u));let i=t;return this.ora(u,i),u}isPackageTemplate(t){return $D.includes(t)}async getGeneratorContext(){let t=Ot(this.subPackages,$D),r=await this.steps(t);if(this.isPackageTemplate(r.template)){let u=wt(this.subPackages),i=await this.steps(u);Object.assign(r,i)}return r.targetPath=(0,v.join)(process.cwd(),r.projectName),r.releasePath=r.releasePath||"src",r}async generate(){let t=await this.getGeneratorContext();if(this.logger.debug("context is:",t,this.context.options.templateRootPath),t.subPackages){await this.action({label:"Generate Directories(subPackages)",task:async()=>{await this.generateTemplateDir(t),await this.generateSubPackages(t),await this.generateConfigs(t,t.targetPath,"_common")}});return}await this.action({label:"Generate Directory",task:async()=>{await this.generateTemplateDir(t),await this.generateConfigs(t,t.targetPath,"_common"),await this.generateConfigs(t,t.targetPath,t.template)}})}async generateConfigs(t,r,u){let i=(s,a)=>(this.logger.debug("copyCallback",s,a),this.compose.composeConfigFile(t,s,a)),{configsRootPath:D,config:o}=this.context.options;if(!o){this.logger.debug("no copy config files");return}await this.copyer.copyPaths({sourcePath:(0,v.join)(D,u),targetPath:r,copyCallback:i})}generateTemplateDir(t){return this.copyer.copyPaths({sourcePath:(0,v.join)(this.context.options.templateRootPath,t.template),targetPath:t.targetPath})}async generateSubPackages(t){let{packagesNames:r="packages",subPackages:u=[],targetPath:i=""}=t,{templateRootPath:D}=this.context.options;for(let o of u){let s=(0,v.join)(D,o),a=(0,v.join)(i,r,o);this.logger.debug("copy sub package",s,a),await this.copyer.copyPaths({sourcePath:s,targetPath:a})}}};var bt={name:"@qlover/create-app",version:"0.7.1",description:"Create a new app with a single command",private:!1,type:"module",files:["dist","package.json","README.md","CHANGELOG.md"],bin:{"create-app":"dist/index.js"},scripts:{build:"tsup","create:app":"node ./dist/index.js"},repository:{type:"git",url:"git+https://github.com/qlover/fe-base.git",directory:"packages/create-app"},homepage:"https://github.com/qlover/fe-base#readme",keywords:["create-app","fe-scripts","scripts"],author:"qlover",license:"ISC",publishConfig:{access:"public"},devDependencies:{"@qlover/logger":"workspace:*",ignore:"^7.0.3",lodash:"^4.17.21",ora:"^8.1.1"},dependencies:{"@qlover/scripts-context":"workspace:*",commander:"^13.1.0",inquirer:"^12.3.2"}};function Ul(){let e=new YD.Command;return e.version(bt.version,"-v, --version","Show version").description(bt.description).option("-d, --dry-run","Do not touch or write anything, but show the commands").option("-V, --verbose","Show more information").option("--config","Copy config files (default: true)",!0).option("--no-config","Do not copy config files"),e.parse(),e.opts()}async function Wl(){let{dryRun:e,verbose:t,...r}=Ul(),u=(0,vt.resolve)("./templates"),i=(0,vt.resolve)("./configs");(0,xt.existsSync)(u)||(console.error("Template is empty!"),process.exit(1)),(0,xt.existsSync)(i)||(console.error("Configs is empty!"),process.exit(1)),await new ye({dryRun:e,verbose:t,options:{...r,templateRootPath:u,configsRootPath:i}}).generate()}Wl().catch(e=>{console.error(e),process.exit(1)});
package/dist/index.js CHANGED
@@ -8,4 +8,4 @@ ${t}`,cn=Object.getOwnPropertyDescriptor(Function.prototype,"toString"),fn=Objec
8
8
  `))this.#e+=Math.max(1,Math.ceil(Le(D,{countAnsiEscapeCodes:!0})/t))}get isEnabled(){return this.#a&&!this.#F}set isEnabled(t){if(typeof t!="boolean")throw new TypeError("The `isEnabled` option must be a boolean");this.#a=t}get isSilent(){return this.#F}set isSilent(t){if(typeof t!="boolean")throw new TypeError("The `isSilent` option must be a boolean");this.#F=t}frame(){let t=Date.now();(this.#i===-1||t-this.#c>=this.interval)&&(this.#i=++this.#i%this.#D.frames.length,this.#c=t);let{frames:r}=this.#D,u=r[this.#i];this.color&&(u=E[this.color](u));let i=typeof this.#s=="string"&&this.#s!==""?this.#s+" ":"",D=typeof this.text=="string"?" "+this.text:"",o=typeof this.#o=="string"&&this.#o!==""?" "+this.#o:"";return i+u+D+o}clear(){if(!this.#a||!this.#u.isTTY)return this;this.#u.cursorTo(0);for(let t=0;t<this.#n;t++)t>0&&this.#u.moveCursor(0,-1),this.#u.clearLine(1);return(this.#l||this.lastIndent!==this.#l)&&this.#u.cursorTo(this.#l),this.lastIndent=this.#l,this.#n=0,this}render(){return this.#F?this:(this.clear(),this.#u.write(this.frame()),this.#n=this.#e,this)}start(t){return t&&(this.text=t),this.#F?this:this.#a?this.isSpinning?this:(this.#t.hideCursor&&je.hide(this.#u),this.#t.discardStdin&&ce.stdin.isTTY&&(this.#r=!0,We.start()),this.render(),this.#f=setInterval(this.render.bind(this),this.interval),this):(this.text&&this.#u.write(`- ${this.text}
9
9
  `),this)}stop(){return this.#a?(clearInterval(this.#f),this.#f=void 0,this.#i=0,this.clear(),this.#t.hideCursor&&je.show(this.#u),this.#t.discardStdin&&ce.stdin.isTTY&&this.#r&&(We.stop(),this.#r=!1),this):this}succeed(t){return this.stopAndPersist({symbol:k.success,text:t})}fail(t){return this.stopAndPersist({symbol:k.error,text:t})}warn(t){return this.stopAndPersist({symbol:k.warning,text:t})}info(t){return this.stopAndPersist({symbol:k.info,text:t})}stopAndPersist(t={}){if(this.#F)return this;let r=t.prefixText??this.#s,u=this.#E(r," "),i=t.symbol??" ",D=t.text??this.text,s=typeof D=="string"?(i?" ":"")+D:"",a=t.suffixText??this.#o,f=this.#g(a," "),p=u+i+s+f+`
10
10
  `;return this.stop(),this.#u.write(p),this}};function zt(e){return new He(e)}async function Kt(e,t){let r=typeof e=="function",u=typeof e.then=="function";if(!r&&!u)throw new TypeError("Parameter `action` must be a Function or a Promise");let{successText:i,failText:D}=typeof t=="object"?t:{successText:void 0,failText:void 0},o=zt(t).start();try{let a=await(r?e(o):e);return o.succeed(i===void 0?void 0:typeof i=="string"?i:i(a)),a}catch(s){throw o.fail(D===void 0?void 0:typeof D=="string"?D:D(s)),s}}import{existsSync as Hl}from"fs";var Dr=ue(ir(),1);import{dirname as Jn,join as Ze}from"path";import{existsSync as Zn,readFileSync as Qn}from"fs";import{promises as nr}from"fs";import{existsSync as Kn,mkdirSync as Xn}from"fs";var H=class{static ensureDir(t){Kn(t)||Xn(t,{recursive:!0})}};var{copyFile:es,stat:ts}=nr,Ce=class e{constructor(t,r=e.IGNORE_FILE){this.ignoreTargetPath=t;this.ignoreFile=r}static IGNORE_FILE=".gitignore.template";getIg(t=this.ignoreTargetPath){let r=Ze(t,this.ignoreFile);if(!Zn(r))return;let D=Qn(r,"utf8").split(`
11
- `).map(o=>o.trim()).filter(o=>o&&!o.startsWith("#"));return(0,Dr.default)().add(D)}async copyFiles(t,r,u,i){let D=await nr.readdir(t);await Promise.all(D.map(async o=>{let s=Ze(t,o),a=Ze(r,o);if(u&&u.ignores(o))return;if(H.ensureDir(Jn(a)),(await ts(s)).isDirectory())await this.copyFiles(s,a,u,i);else{if(i&&await i(s,a))return;await es(s,a)}}))}copyPaths({sourcePath:t,targetPath:r,copyCallback:u}){H.ensureDir(r);let i=this.getIg();return this.copyFiles(t,r,i,u)}};var ID=ue(qD(),1);import{readFileSync as Ll,writeFileSync as kl,existsSync as $l}from"fs";var me=class{constructor(){}isJSONFilePath(t){return t.endsWith(".json")||t.endsWith(".json.template")}isTemplateFilePath(t){return t.endsWith(".template")}getRealTemplateFilePath(t){return t.replace(".template","")}readFile(t){return Ll(t,"utf-8")}readJSONFile(t){return JSON.parse(this.readFile(t))}writeFile(t,r){kl(this.getRealTemplateFilePath(t),r,{encoding:"utf-8"})}replaceFile(t,r){let u=this.readFile(t);return Object.keys(r).forEach(i=>{let D=r[i];u=u.replace(new RegExp(`\\[TPL:${i}\\]`,"g"),typeof D=="string"?D:JSON.stringify(D))}),u}mergeJSONFile(t,r){let u=this.readJSONFile(t),i=(0,ID.default)(r,u);this.writeFile(t,JSON.stringify(i,null,2))}composeConfigFile(t,r,u){if(this.isTemplateFilePath(r)){let i=this.replaceFile(r,t);if(this.isJSONFilePath(r)&&this.isJSONFilePath(u)){let D=this.getRealTemplateFilePath(u);return $l(D)?(this.mergeJSONFile(D,JSON.parse(i)),!0):(this.writeFile(D,i),!0)}return this.writeFile(u,i),!0}return!1}};var jD=["pack-app"],Be=class{ora;context;subPackages;copyer;compose;constructor(t){let r=t.options?.templateRootPath;if(!r)throw new Error("template path not exit");if(!Hl(r))throw new Error("template path not exit");this.ora=Kt,this.context=new Ul(t),this.subPackages=["node-lib","react-app"],this.copyer=new Ce(N(this.context.options.configsRootPath,"_common")),this.compose=new me}get logger(){return this.context.logger}async steps(t){try{return await Wl.prompt(t)}catch(r){throw r.isTtyError,this.logger.error(r),r}}async action({label:t,task:r}){let u=r();u instanceof Promise||(u=Promise.resolve(u));let i=t;return this.ora(u,i),u}isPackageTemplate(t){return jD.includes(t)}async getGeneratorContext(){let t=_t(this.subPackages,jD),r=await this.steps(t);if(this.isPackageTemplate(r.template)){let u=mt(this.subPackages),i=await this.steps(u);Object.assign(r,i)}return r.targetPath=N(process.cwd(),r.projectName),r.releasePath=r.releasePath||"src",r}async generate(){let t=await this.getGeneratorContext();if(this.logger.debug("context is:",t,this.context.options.templateRootPath),t.subPackages){await this.action({label:"Generate Directories(subPackages)",task:async()=>{await this.generateTemplateDir(t),await this.generateSubPackages(t),await this.generateConfigs(t,t.targetPath,"_common")}});return}await this.action({label:"Generate Directory",task:async()=>{await this.generateTemplateDir(t),await this.generateConfigs(t,t.targetPath,"_common"),await this.generateConfigs(t,t.targetPath,t.template)}})}async generateConfigs(t,r,u){let i=(s,a)=>(this.logger.debug("copyCallback",s,a),this.compose.composeConfigFile(t,s,a)),{configsRootPath:D,config:o}=this.context.options;if(!o){this.logger.debug("no copy config files");return}await this.copyer.copyPaths({sourcePath:N(D,u),targetPath:r,copyCallback:i})}generateTemplateDir(t){return this.copyer.copyPaths({sourcePath:N(this.context.options.templateRootPath,t.template),targetPath:t.targetPath})}async generateSubPackages(t){let{packagesNames:r="packages",subPackages:u=[],targetPath:i=""}=t,{templateRootPath:D}=this.context.options;for(let o of u){let s=N(D,o),a=N(i,r,o);this.logger.debug("copy sub package",s,a),await this.copyer.copyPaths({sourcePath:s,targetPath:a})}}};var Et={name:"@qlover/create-app",version:"0.6.3",description:"Create a new app with a single command",private:!1,type:"module",files:["dist","package.json","README.md","CHANGELOG.md"],bin:{"create-app":"dist/index.js"},scripts:{build:"tsup","create:app":"node ./dist/index.js"},repository:{type:"git",url:"git+https://github.com/qlover/fe-base.git",directory:"packages/create-app"},homepage:"https://github.com/qlover/fe-base#readme",keywords:["create-app","fe-scripts","scripts"],author:"qlover",license:"ISC",publishConfig:{access:"public"},devDependencies:{"@qlover/logger":"workspace:*",ignore:"^7.0.3",lodash:"^4.17.21",ora:"^8.1.1"},dependencies:{"@qlover/scripts-context":"workspace:*",commander:"^13.1.0",inquirer:"^12.3.2"}};function zl(){let e=new Vl;return e.version(Et.version,"-v, --version","Show version").description(Et.description).option("-d, --dry-run","Do not touch or write anything, but show the commands").option("-V, --verbose","Show more information").option("--config","Copy config files (default: true)",!0).option("--no-config","Do not copy config files"),e.parse(),e.opts()}async function Kl(){let{dryRun:e,verbose:t,...r}=zl(),u=ND("./templates"),i=ND("./configs");GD(u)||(console.error("Template is empty!"),process.exit(1)),GD(i)||(console.error("Configs is empty!"),process.exit(1)),await new Be({dryRun:e,verbose:t,options:{...r,templateRootPath:u,configsRootPath:i}}).generate()}Kl().catch(e=>{console.error(e),process.exit(1)});
11
+ `).map(o=>o.trim()).filter(o=>o&&!o.startsWith("#"));return(0,Dr.default)().add(D)}async copyFiles(t,r,u,i){let D=await nr.readdir(t);await Promise.all(D.map(async o=>{let s=Ze(t,o),a=Ze(r,o);if(u&&u.ignores(o))return;if(H.ensureDir(Jn(a)),(await ts(s)).isDirectory())await this.copyFiles(s,a,u,i);else{if(i&&await i(s,a))return;await es(s,a)}}))}copyPaths({sourcePath:t,targetPath:r,copyCallback:u}){H.ensureDir(r);let i=this.getIg();return this.copyFiles(t,r,i,u)}};var ID=ue(qD(),1);import{readFileSync as Ll,writeFileSync as kl,existsSync as $l}from"fs";var me=class{constructor(){}isJSONFilePath(t){return t.endsWith(".json")||t.endsWith(".json.template")}isTemplateFilePath(t){return t.endsWith(".template")}getRealTemplateFilePath(t){return t.replace(".template","")}readFile(t){return Ll(t,"utf-8")}readJSONFile(t){return JSON.parse(this.readFile(t))}writeFile(t,r){kl(this.getRealTemplateFilePath(t),r,{encoding:"utf-8"})}replaceFile(t,r){let u=this.readFile(t);return Object.keys(r).forEach(i=>{let D=r[i];u=u.replace(new RegExp(`\\[TPL:${i}\\]`,"g"),typeof D=="string"?D:JSON.stringify(D))}),u}mergeJSONFile(t,r){let u=this.readJSONFile(t),i=(0,ID.default)(r,u);this.writeFile(t,JSON.stringify(i,null,2))}composeConfigFile(t,r,u){if(this.isTemplateFilePath(r)){let i=this.replaceFile(r,t);if(this.isJSONFilePath(r)&&this.isJSONFilePath(u)){let D=this.getRealTemplateFilePath(u);return $l(D)?(this.mergeJSONFile(D,JSON.parse(i)),!0):(this.writeFile(D,i),!0)}return this.writeFile(u,i),!0}return!1}};var jD=["pack-app"],Be=class{ora;context;subPackages;copyer;compose;constructor(t){let r=t.options?.templateRootPath;if(!r)throw new Error("template path not exit");if(!Hl(r))throw new Error("template path not exit");this.ora=Kt,this.context=new Ul(t),this.subPackages=["node-lib","react-app"],this.copyer=new Ce(N(this.context.options.configsRootPath,"_common")),this.compose=new me}get logger(){return this.context.logger}async steps(t){try{return await Wl.prompt(t)}catch(r){throw r.isTtyError,this.logger.error(r),r}}async action({label:t,task:r}){let u=r();u instanceof Promise||(u=Promise.resolve(u));let i=t;return this.ora(u,i),u}isPackageTemplate(t){return jD.includes(t)}async getGeneratorContext(){let t=_t(this.subPackages,jD),r=await this.steps(t);if(this.isPackageTemplate(r.template)){let u=mt(this.subPackages),i=await this.steps(u);Object.assign(r,i)}return r.targetPath=N(process.cwd(),r.projectName),r.releasePath=r.releasePath||"src",r}async generate(){let t=await this.getGeneratorContext();if(this.logger.debug("context is:",t,this.context.options.templateRootPath),t.subPackages){await this.action({label:"Generate Directories(subPackages)",task:async()=>{await this.generateTemplateDir(t),await this.generateSubPackages(t),await this.generateConfigs(t,t.targetPath,"_common")}});return}await this.action({label:"Generate Directory",task:async()=>{await this.generateTemplateDir(t),await this.generateConfigs(t,t.targetPath,"_common"),await this.generateConfigs(t,t.targetPath,t.template)}})}async generateConfigs(t,r,u){let i=(s,a)=>(this.logger.debug("copyCallback",s,a),this.compose.composeConfigFile(t,s,a)),{configsRootPath:D,config:o}=this.context.options;if(!o){this.logger.debug("no copy config files");return}await this.copyer.copyPaths({sourcePath:N(D,u),targetPath:r,copyCallback:i})}generateTemplateDir(t){return this.copyer.copyPaths({sourcePath:N(this.context.options.templateRootPath,t.template),targetPath:t.targetPath})}async generateSubPackages(t){let{packagesNames:r="packages",subPackages:u=[],targetPath:i=""}=t,{templateRootPath:D}=this.context.options;for(let o of u){let s=N(D,o),a=N(i,r,o);this.logger.debug("copy sub package",s,a),await this.copyer.copyPaths({sourcePath:s,targetPath:a})}}};var Et={name:"@qlover/create-app",version:"0.7.1",description:"Create a new app with a single command",private:!1,type:"module",files:["dist","package.json","README.md","CHANGELOG.md"],bin:{"create-app":"dist/index.js"},scripts:{build:"tsup","create:app":"node ./dist/index.js"},repository:{type:"git",url:"git+https://github.com/qlover/fe-base.git",directory:"packages/create-app"},homepage:"https://github.com/qlover/fe-base#readme",keywords:["create-app","fe-scripts","scripts"],author:"qlover",license:"ISC",publishConfig:{access:"public"},devDependencies:{"@qlover/logger":"workspace:*",ignore:"^7.0.3",lodash:"^4.17.21",ora:"^8.1.1"},dependencies:{"@qlover/scripts-context":"workspace:*",commander:"^13.1.0",inquirer:"^12.3.2"}};function zl(){let e=new Vl;return e.version(Et.version,"-v, --version","Show version").description(Et.description).option("-d, --dry-run","Do not touch or write anything, but show the commands").option("-V, --verbose","Show more information").option("--config","Copy config files (default: true)",!0).option("--no-config","Do not copy config files"),e.parse(),e.opts()}async function Kl(){let{dryRun:e,verbose:t,...r}=zl(),u=ND("./templates"),i=ND("./configs");GD(u)||(console.error("Template is empty!"),process.exit(1)),GD(i)||(console.error("Configs is empty!"),process.exit(1)),await new Be({dryRun:e,verbose:t,options:{...r,templateRootPath:u,configsRootPath:i}}).generate()}Kl().catch(e=>{console.error(e),process.exit(1)});
@@ -7,7 +7,7 @@ import prettierConfig from './.prettierrc.js';
7
7
  import prettier from 'eslint-plugin-prettier';
8
8
  import tseslint from '@typescript-eslint/eslint-plugin';
9
9
  import tsparser from '@typescript-eslint/parser';
10
- import * as feDev from '@qlover/eslint-plugin-fe-dev';
10
+ import qloverEslint from '@qlover/eslint-plugin';
11
11
  import vitestPlugin from 'eslint-plugin-vitest';
12
12
 
13
13
  const allowedGlobals = {
@@ -43,7 +43,7 @@ export default [
43
43
  'react-refresh': reactRefresh,
44
44
  prettier,
45
45
  '@typescript-eslint': tseslint,
46
- 'fe-dev': feDev
46
+ '@qlover-eslint': qloverEslint
47
47
  },
48
48
  rules: {
49
49
  ...js.configs.recommended.rules,
@@ -68,7 +68,7 @@ export default [
68
68
  }
69
69
  ],
70
70
  'react-hooks/exhaustive-deps': 'off',
71
- 'fe-dev/ts-class-method-return': 'error'
71
+ '@qlover-eslint/ts-class-method-return': 'error'
72
72
  }
73
73
  },
74
74
  {
@@ -38,7 +38,7 @@
38
38
  "@eslint/js": "^9.13.0",
39
39
  "@nx/js": "20.6.4",
40
40
  "@qlover/env-loader": "latest",
41
- "@qlover/eslint-plugin-fe-dev": "latest",
41
+ "@qlover/eslint-plugin": "latest",
42
42
  "@qlover/fe-scripts": "latest",
43
43
  "@qlover/fe-standard": "latest",
44
44
  "@qlover/scripts-context": "latest",
@@ -0,0 +1,13 @@
1
+ import { vi } from 'vitest';
2
+ import { I18nService } from '@/base/services/I18nService';
3
+
4
+ export class MockI18nService extends I18nService {
5
+ constructor() {
6
+ super('/');
7
+ }
8
+
9
+ t = vi.fn((key: string) => key);
10
+ changeLanguage = vi.fn();
11
+ changeLoading = vi.fn();
12
+ onBefore = vi.fn();
13
+ }
@@ -0,0 +1,48 @@
1
+ import { EnvConfigInterface } from '@qlover/corekit-bridge';
2
+ import { name, version } from '../../package.json';
3
+
4
+ export class MockAppConfig implements EnvConfigInterface {
5
+ appName = name;
6
+
7
+ appVersion = version;
8
+
9
+ env: string = import.meta.env.MODE;
10
+
11
+ userTokenStorageKey = '__fe_user_token__';
12
+
13
+ userInfoStorageKey = '__fe_user_info__';
14
+
15
+ openAiModels = [
16
+ 'gpt-4o-mini',
17
+ 'gpt-3.5-turbo',
18
+ 'gpt-3.5-turbo-2',
19
+ 'gpt-4',
20
+ 'gpt-4-32k'
21
+ ];
22
+
23
+ openAiBaseUrl = '';
24
+
25
+ openAiToken = '';
26
+
27
+ openAiTokenPrefix = '';
28
+
29
+ openAiRequireToken = true;
30
+
31
+ loginUser = '';
32
+
33
+ loginPassword = '';
34
+
35
+ feApiBaseUrl = '';
36
+
37
+ userApiBaseUrl = '';
38
+
39
+ aiApiBaseUrl = 'https://api.openai.com/v1';
40
+
41
+ aiApiToken = '';
42
+
43
+ aiApiTokenPrefix = 'Bearer';
44
+
45
+ aiApiRequireToken = true;
46
+
47
+ bootHref = '';
48
+ }
@@ -0,0 +1,16 @@
1
+ import { InteractionHubInterface } from '@/base/port/InteractionHubInterface';
2
+ import { AntdStaticApiInterface } from '@brain-toolkit/antd-theme-override/react';
3
+ import { vi } from 'vitest';
4
+
5
+ export class MockDialogHandler
6
+ implements InteractionHubInterface, AntdStaticApiInterface
7
+ {
8
+ setMessage = vi.fn();
9
+ setModal = vi.fn();
10
+ setNotification = vi.fn();
11
+ success = vi.fn();
12
+ error = vi.fn();
13
+ info = vi.fn();
14
+ warn = vi.fn();
15
+ confirm = vi.fn();
16
+ }
@@ -0,0 +1,14 @@
1
+ import { LoggerInterface } from '@qlover/logger';
2
+ import { vi } from 'vitest';
3
+
4
+ export class MockLogger implements LoggerInterface {
5
+ info = vi.fn();
6
+ error = vi.fn();
7
+ debug = vi.fn();
8
+ warn = vi.fn();
9
+ trace = vi.fn();
10
+ log = vi.fn();
11
+ fatal = vi.fn();
12
+ addAppender = vi.fn();
13
+ context = vi.fn();
14
+ }
@@ -0,0 +1,92 @@
1
+ import { vi } from 'vitest';
2
+ import { MockLogger } from './MockLogger';
3
+ import { MockAppConfig } from './MockAppConfit';
4
+ import { MockDialogHandler } from './MockDialogHandler';
5
+
6
+ export function createMockGlobals() {
7
+ const mockLogger = new MockLogger();
8
+ const mockAppConfig = new MockAppConfig();
9
+ const mockDialogHandler = new MockDialogHandler();
10
+
11
+ // Mock JSON serializer
12
+ const mockJSON = {
13
+ serialize: vi.fn((data) => JSON.stringify(data)),
14
+ deserialize: vi.fn((data) => JSON.parse(data)),
15
+ stringify: vi.fn((data) => JSON.stringify(data)),
16
+ parse: vi.fn((data) => JSON.parse(data))
17
+ };
18
+
19
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
20
+ const storageOptions: Record<string, any> = {
21
+ localStorage: {},
22
+ cookieStorage: {},
23
+ localStorageEncrypt: {}
24
+ };
25
+
26
+ // Mock localStorage with actual storage simulation
27
+ const mockLocalStorageData = storageOptions.localStorage ?? {};
28
+ const mockLocalStorage = {
29
+ get: vi.fn((key: string) => mockLocalStorageData[key]),
30
+ set: vi.fn((key: string, value: unknown) => {
31
+ mockLocalStorageData[key] = value;
32
+ }),
33
+ getItem: vi.fn((key: string) => mockLocalStorageData[key]),
34
+ setItem: vi.fn((key: string, value: unknown) => {
35
+ mockLocalStorageData[key] = value;
36
+ }),
37
+ remove: vi.fn((key: string) => {
38
+ delete mockLocalStorageData[key];
39
+ }),
40
+ removeItem: vi.fn((key: string) => {
41
+ delete mockLocalStorageData[key];
42
+ }),
43
+ clear: vi.fn(() => {
44
+ Object.keys(mockLocalStorageData).forEach((key) => {
45
+ delete mockLocalStorageData[key];
46
+ });
47
+ }),
48
+ has: vi.fn((key: string) => key in mockLocalStorageData),
49
+ keys: vi.fn(() => Object.keys(mockLocalStorageData)),
50
+ size: vi.fn(() => Object.keys(mockLocalStorageData).length)
51
+ };
52
+
53
+ // Mock localStorageEncrypt (same as localStorage for now)
54
+ const mockLocalStorageEncrypt = { ...mockLocalStorage };
55
+
56
+ // Mock cookieStorage
57
+ const mockCookieStorageData = storageOptions.cookieStorage ?? {};
58
+ const mockCookieStorage = {
59
+ get: vi.fn((key: string) => mockCookieStorageData[key]),
60
+ set: vi.fn((key: string, value: unknown, _options?: unknown) => {
61
+ mockCookieStorageData[key] = value;
62
+ }),
63
+ getItem: vi.fn((key: string) => mockCookieStorageData[key]),
64
+ setItem: vi.fn((key: string, value: unknown, _options?: unknown) => {
65
+ mockCookieStorageData[key] = value;
66
+ }),
67
+ remove: vi.fn((key: string) => {
68
+ delete mockCookieStorageData[key];
69
+ }),
70
+ removeItem: vi.fn((key: string) => {
71
+ delete mockCookieStorageData[key];
72
+ }),
73
+ clear: vi.fn(() => {
74
+ Object.keys(mockCookieStorageData).forEach((key) => {
75
+ delete mockCookieStorageData[key];
76
+ });
77
+ }),
78
+ has: vi.fn((key: string) => key in mockCookieStorageData),
79
+ keys: vi.fn(() => Object.keys(mockCookieStorageData)),
80
+ size: vi.fn(() => Object.keys(mockCookieStorageData).length)
81
+ };
82
+
83
+ return {
84
+ logger: mockLogger,
85
+ appConfig: mockAppConfig,
86
+ dialogHandler: mockDialogHandler,
87
+ JSON: mockJSON,
88
+ localStorage: mockLocalStorage,
89
+ localStorageEncrypt: mockLocalStorageEncrypt,
90
+ cookieStorage: mockCookieStorage
91
+ };
92
+ }
@@ -0,0 +1,51 @@
1
+ import { createMockGlobals } from '../__mocks__/createMockGlobals';
2
+
3
+ // 设置测试环境
4
+ beforeEach(() => {
5
+ // 清理DOM
6
+ document.body.innerHTML = '';
7
+
8
+ // 创建root元素(如果不存在)
9
+ if (!document.getElementById('root')) {
10
+ const rootElement = document.createElement('div');
11
+ rootElement.id = 'root';
12
+ document.body.appendChild(rootElement);
13
+ }
14
+ });
15
+
16
+ afterEach(() => {
17
+ // 清理所有mock
18
+ vi.clearAllMocks();
19
+ });
20
+
21
+ // 全局测试配置
22
+ global.ResizeObserver = vi.fn().mockImplementation(() => ({
23
+ observe: vi.fn(),
24
+ unobserve: vi.fn(),
25
+ disconnect: vi.fn()
26
+ }));
27
+
28
+ // Mock window.matchMedia
29
+ Object.defineProperty(window, 'matchMedia', {
30
+ writable: true,
31
+ value: vi.fn().mockImplementation((query) => ({
32
+ matches: false,
33
+ media: query,
34
+ onchange: null,
35
+ addListener: vi.fn(), // deprecated
36
+ removeListener: vi.fn(), // deprecated
37
+ addEventListener: vi.fn(),
38
+ removeEventListener: vi.fn(),
39
+ dispatchEvent: vi.fn()
40
+ }))
41
+ });
42
+
43
+ // Mock IntersectionObserver
44
+ global.IntersectionObserver = vi.fn().mockImplementation(() => ({
45
+ observe: vi.fn(),
46
+ unobserve: vi.fn(),
47
+ disconnect: vi.fn()
48
+ }));
49
+
50
+ // Mock globals
51
+ vi.mock('@/core/globals', () => createMockGlobals());
@@ -0,0 +1,139 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import App from '@/App';
3
+ import { routerPrefix } from '@config/common';
4
+
5
+ // Mock the dependencies
6
+ vi.mock('react-router-dom', async () => {
7
+ const actual = await vi.importActual('react-router-dom');
8
+ return {
9
+ ...actual,
10
+ createBrowserRouter: vi.fn(() => ({
11
+ routes: [],
12
+ basename: routerPrefix
13
+ })),
14
+ RouterProvider: vi.fn(({ router }) => (
15
+ <div data-testid="router-provider">
16
+ <div data-testid="router-basename">{router.basename}</div>
17
+ <div data-testid="router-routes-count">
18
+ {router.routes?.length || 0}
19
+ </div>
20
+ </div>
21
+ ))
22
+ };
23
+ });
24
+
25
+ vi.mock('@brain-toolkit/antd-theme-override/react', () => ({
26
+ AntdThemeProvider: vi.fn(({ children, theme }) => (
27
+ <div data-testid="antd-theme-provider">
28
+ <div data-testid="theme-key">{theme?.cssVar?.key}</div>
29
+ <div data-testid="theme-prefix">{theme?.cssVar?.prefix}</div>
30
+ {children}
31
+ </div>
32
+ ))
33
+ }));
34
+
35
+ vi.mock('@/core/IOC', () => ({
36
+ IOC: vi.fn((service) => {
37
+ if (service === 'RouteService') {
38
+ return {
39
+ routes: [
40
+ { path: '/', component: 'HomePage' },
41
+ { path: '/about', component: 'AboutPage' }
42
+ ]
43
+ };
44
+ }
45
+ if (service === 'DialogHandler') {
46
+ return {};
47
+ }
48
+ return {};
49
+ })
50
+ }));
51
+
52
+ vi.mock('@/uikit/hooks/useStore', () => ({
53
+ useStore: vi.fn((service, selector) => {
54
+ if (service && selector) {
55
+ return [
56
+ { path: '/', component: 'HomePage' },
57
+ { path: '/about', component: 'AboutPage' }
58
+ ];
59
+ }
60
+ return [];
61
+ })
62
+ }));
63
+
64
+ vi.mock('@/base/cases/RouterLoader', () => ({
65
+ RouterLoader: vi.fn().mockImplementation(() => ({
66
+ toRoute: vi.fn((route) => ({
67
+ path: route.path,
68
+ element: <div data-testid={`route-${route.path}`}>{route.component}</div>
69
+ }))
70
+ }))
71
+ }));
72
+
73
+ // Mock the CSS import
74
+ vi.mock('@/styles/css/index.css', () => ({}));
75
+
76
+ describe('App Component', () => {
77
+ beforeEach(() => {
78
+ vi.clearAllMocks();
79
+ });
80
+
81
+ it('should render without crashing', () => {
82
+ render(<App />);
83
+
84
+ // Check if the main app structure is rendered
85
+ expect(screen.getByTestId('antd-theme-provider')).toBeDefined();
86
+ expect(screen.getByTestId('router-provider')).toBeDefined();
87
+ });
88
+
89
+ it('should configure theme provider correctly', () => {
90
+ render(<App />);
91
+
92
+ // Check theme configuration
93
+ expect(screen.getByTestId('theme-key').textContent).toBe('fe-theme');
94
+ expect(screen.getByTestId('theme-prefix').textContent).toBe('fe');
95
+ });
96
+
97
+ it('should configure router with correct basename', () => {
98
+ render(<App />);
99
+
100
+ // Check router configuration
101
+ expect(screen.getByTestId('router-basename')).toBeDefined();
102
+ expect(screen.getByTestId('router-basename').textContent).toBe(
103
+ routerPrefix
104
+ );
105
+ });
106
+
107
+ it('should render router provider with routes', () => {
108
+ render(<App />);
109
+
110
+ // Check that router provider is rendered
111
+ expect(screen.getByTestId('router-provider')).toBeDefined();
112
+ expect(screen.getByTestId('router-routes-count')).toBeDefined();
113
+ });
114
+
115
+ it('should have proper component structure', () => {
116
+ const { container } = render(<App />);
117
+
118
+ // Check the overall structure
119
+ expect(container.firstChild).toBeDefined();
120
+
121
+ // Check that theme provider wraps router provider
122
+ const themeProvider = screen.getByTestId('antd-theme-provider');
123
+ const routerProvider = screen.getByTestId('router-provider');
124
+
125
+ expect(themeProvider.contains(routerProvider)).toBe(true);
126
+ });
127
+
128
+ it('should handle empty routes gracefully', async () => {
129
+ // Mock useStore to return empty routes
130
+ const { useStore } = await import('@/uikit/hooks/useStore');
131
+ vi.mocked(useStore).mockReturnValueOnce([]);
132
+
133
+ render(<App />);
134
+
135
+ // Should still render without crashing
136
+ expect(screen.getByTestId('antd-theme-provider')).toBeDefined();
137
+ expect(screen.getByTestId('router-provider')).toBeDefined();
138
+ });
139
+ });