@qlover/create-app 0.9.0 → 0.10.0

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 (57) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/index.cjs +1 -1
  3. package/dist/index.js +1 -1
  4. package/dist/templates/next-app/.env.template +9 -10
  5. package/dist/templates/next-app/eslint.config.mjs +5 -0
  6. package/dist/templates/next-app/next.config.ts +1 -1
  7. package/dist/templates/next-app/src/app/[locale]/login/page.tsx +1 -1
  8. package/dist/templates/next-app/src/app/[locale]/page.tsx +1 -1
  9. package/dist/templates/next-app/src/app/[locale]/register/page.tsx +1 -1
  10. package/dist/templates/next-app/src/app/api/locales/json/route.ts +2 -1
  11. package/dist/templates/next-app/src/i18n/request.ts +2 -2
  12. package/dist/templates/react-app/__tests__/__mocks__/{MockAppConfit.ts → MockAppConfig.ts} +1 -1
  13. package/dist/templates/react-app/__tests__/__mocks__/components/TestApp.tsx +10 -17
  14. package/dist/templates/react-app/__tests__/__mocks__/components/TestBootstrapsProvider.tsx +27 -8
  15. package/dist/templates/react-app/__tests__/__mocks__/createMockGlobals.ts +1 -1
  16. package/dist/templates/react-app/__tests__/__mocks__/i18nextHttpBackend.ts +110 -0
  17. package/dist/templates/react-app/__tests__/__mocks__/testIOC/TestIOCRegister.ts +3 -2
  18. package/dist/templates/react-app/__tests__/setup/setupGlobal.ts +13 -0
  19. package/dist/templates/react-app/__tests__/src/base/services/I18nService.test.ts +3 -1
  20. package/dist/templates/react-app/config/IOCIdentifier.ts +9 -6
  21. package/dist/templates/react-app/config/common.ts +38 -0
  22. package/dist/templates/react-app/config/feapi.mock.json +5 -12
  23. package/dist/templates/react-app/eslint.config.mjs +6 -5
  24. package/dist/templates/react-app/package.json +1 -1
  25. package/dist/templates/react-app/src/base/apis/userApi/UserApi.ts +22 -13
  26. package/dist/templates/react-app/src/base/apis/userApi/UserApiBootstarp.ts +3 -3
  27. package/dist/templates/react-app/src/base/apis/userApi/UserApiType.ts +17 -12
  28. package/dist/templates/react-app/src/base/cases/I18nKeyErrorPlugin.ts +19 -2
  29. package/dist/templates/react-app/src/base/port/RouteServiceInterface.ts +2 -4
  30. package/dist/templates/react-app/src/base/port/UserServiceInterface.ts +15 -9
  31. package/dist/templates/react-app/src/base/services/BaseLayoutService.ts +55 -0
  32. package/dist/templates/react-app/src/base/services/I18nService.ts +1 -0
  33. package/dist/templates/react-app/src/base/services/UserBootstrap.ts +43 -0
  34. package/dist/templates/react-app/src/base/services/UserGatewayPlugin.ts +16 -0
  35. package/dist/templates/react-app/src/base/services/UserService.ts +51 -80
  36. package/dist/templates/react-app/src/core/bootstraps/BootstrapClient.ts +8 -3
  37. package/dist/templates/react-app/src/core/bootstraps/BootstrapsRegistry.ts +6 -6
  38. package/dist/templates/react-app/src/core/bootstraps/SaveAppInfo.ts +28 -0
  39. package/dist/templates/react-app/src/core/clientIoc/ClientIOCRegister.ts +24 -18
  40. package/dist/templates/react-app/src/core/globals.ts +10 -11
  41. package/dist/templates/react-app/src/main.tsx +1 -1
  42. package/dist/templates/react-app/src/pages/auth/Layout.tsx +4 -4
  43. package/dist/templates/react-app/src/pages/auth/RegisterPage.tsx +1 -1
  44. package/dist/templates/react-app/src/pages/base/Layout.tsx +3 -3
  45. package/dist/templates/react-app/src/uikit/components/BaseLayoutProvider.tsx +44 -0
  46. package/dist/templates/react-app/src/uikit/components/LogoutButton.tsx +1 -3
  47. package/dist/templates/react-app/src/uikit/hooks/useNavigateBridge.ts +9 -0
  48. package/dist/templates/react-app/src/uikit/hooks/{useI18nGuard.ts → useRouterI18nGuard.ts} +7 -4
  49. package/dist/templates/react-app/tsconfig.app.json +4 -2
  50. package/dist/templates/react-app/tsconfig.node.json +4 -0
  51. package/dist/templates/react-app/tsconfig.test.json +3 -1
  52. package/package.json +3 -3
  53. package/dist/templates/react-app/__tests__/src/base/cases/AppError.test.ts +0 -102
  54. package/dist/templates/react-app/__tests__/src/main.integration.test.tsx +0 -61
  55. package/dist/templates/react-app/src/base/services/ProcesserExecutor.ts +0 -57
  56. package/dist/templates/react-app/src/uikit/components/ProcessExecutorProvider.tsx +0 -28
  57. package/dist/templates/react-app/src/uikit/components/UserAuthProvider.tsx +0 -16
package/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # @qlover/create-app
2
2
 
3
+ ## 0.10.0
4
+
5
+ ### Minor Changes
6
+
7
+ #### ✨ Features
8
+
9
+ - **react-app:** Update TypeScript configurations and enhance user services ([6b33d18](https://github.com/qlover/fe-base/commit/6b33d18bdec4d0943a615d746e929348ea965031)) ([#545](https://github.com/qlover/fe-base/pull/545))
10
+ - Added path mappings for corekit-bridge and fe-corekit in TypeScript configuration files.
11
+ - Refactored integration tests to use updated import paths and improved error handling.
12
+ - Introduced a new BaseLayoutService for managing user authentication and layout rendering.
13
+ - Enhanced UserService with credential management and user information validation.
14
+ - Removed obsolete ProcesserExecutor and UserAuthProvider components to streamline the codebase.
15
+ - Updated mock API configurations for improved endpoint handling and response structures.
16
+ - Added new hooks for router internationalization and layout management.
17
+
3
18
  ## 0.9.0
4
19
 
5
20
  ### Minor Changes
package/dist/index.cjs CHANGED
@@ -8,4 +8,4 @@ ${t}`,_n=Object.getOwnPropertyDescriptor(Function.prototype,"toString"),mn=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,l=this.#g(a," "),p=u+i+s+l+`
10
10
  `;return this.stop(),this.#u.write(p),this}};function rr(e){return new Ze(e)}async function ur(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=rr(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 bt=require("fs");var P=require("path"),me=require("fs"),fr=h(cr(),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:us,stat:is}=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,fr.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 is(s)).isDirectory())await this.copyFiles(s,a,u,i);else{if(i&&await i(s,a))return;await us(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"),$D=h(kD(),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,$D.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 UD=["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,bt.existsSync)(r))throw new Error("template path not exit");this.ora=ur,this.context=new WD.ScriptContext("create-app",t),this.subPackages=["node-lib","react-app","next-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 HD.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 UD.includes(t)}async getGeneratorContext(){let t=wt(this.subPackages,UD),r=await this.steps(t);if(this.isPackageTemplate(r.template)){let u=Pt(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=(a,l)=>(this.logger.debug("copyCallback",a,l),this.compose.composeConfigFile(t,a,l)),{configsRootPath:D,config:o}=this.context.options;if(!o){this.logger.debug("no copy config files");return}let s=(0,v.join)(D,u);if(!(0,bt.existsSync)(s)){this.logger.debug(`Config path not found: ${s}`);return}await this.copyer.copyPaths({sourcePath:s,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 vt={name:"@qlover/create-app",version:"0.9.0",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:{lint:"eslint src --fix",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 Wl(){let e=new YD.Command;return e.version(vt.version,"-v, --version","Show version").description(vt.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 VD(e=process.cwd()){let{dryRun:t,verbose:r,...u}=Wl(),i=(0,xt.resolve)(e,"./templates"),D=(0,xt.resolve)(e,"./configs");(0,yt.existsSync)(i)||(console.error("Template is empty!"),process.exit(1)),(0,yt.existsSync)(D)||(console.error("Configs is empty!"),process.exit(1)),await new ye({dryRun:t,verbose:r,options:{...u,templateRootPath:i,configsRootPath:D}}).generate()}VD(__dirname).catch(e=>{console.error(e),process.exit(1)});
11
+ `).map(o=>o.trim()).filter(o=>o&&!o.startsWith("#"));return(0,fr.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 is(s)).isDirectory())await this.copyFiles(s,a,u,i);else{if(i&&await i(s,a))return;await us(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"),$D=h(kD(),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,$D.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 UD=["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,bt.existsSync)(r))throw new Error("template path not exit");this.ora=ur,this.context=new WD.ScriptContext("create-app",t),this.subPackages=["node-lib","react-app","next-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 HD.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 UD.includes(t)}async getGeneratorContext(){let t=wt(this.subPackages,UD),r=await this.steps(t);if(this.isPackageTemplate(r.template)){let u=Pt(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=(a,l)=>(this.logger.debug("copyCallback",a,l),this.compose.composeConfigFile(t,a,l)),{configsRootPath:D,config:o}=this.context.options;if(!o){this.logger.debug("no copy config files");return}let s=(0,v.join)(D,u);if(!(0,bt.existsSync)(s)){this.logger.debug(`Config path not found: ${s}`);return}await this.copyer.copyPaths({sourcePath:s,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 vt={name:"@qlover/create-app",version:"0.10.0",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:{lint:"eslint src --fix",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 Wl(){let e=new YD.Command;return e.version(vt.version,"-v, --version","Show version").description(vt.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 VD(e=process.cwd()){let{dryRun:t,verbose:r,...u}=Wl(),i=(0,xt.resolve)(e,"./templates"),D=(0,xt.resolve)(e,"./configs");(0,yt.existsSync)(i)||(console.error("Template is empty!"),process.exit(1)),(0,yt.existsSync)(D)||(console.error("Configs is empty!"),process.exit(1)),await new ye({dryRun:t,verbose:r,options:{...u,templateRootPath:i,configsRootPath:D}}).generate()}VD(__dirname).catch(e=>{console.error(e),process.exit(1)});
package/dist/index.js CHANGED
@@ -8,4 +8,4 @@ ${t}`,pD=Object.getOwnPropertyDescriptor(Function.prototype,"toString"),hD=Objec
8
8
  `))this.#e+=Math.max(1,Math.ceil(Le(n,{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.#n.frames.length,this.#c=t);let{frames:r}=this.#n,u=r[this.#i];this.color&&(u=E[this.color](u));let i=typeof this.#s=="string"&&this.#s!==""?this.#s+" ":"",n=typeof this.text=="string"?" "+this.text:"",o=typeof this.#o=="string"&&this.#o!==""?" "+this.#o:"";return i+u+n+o}clear(){if(!this.#a||!this.#u.isTTY)return this;this.#u.cursorTo(0);for(let t=0;t<this.#D;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.#D=0,this}render(){return this.#F?this:(this.clear(),this.#u.write(this.frame()),this.#D=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??" ",n=t.text??this.text,s=typeof n=="string"?(i?" ":"")+n:"",a=t.suffixText??this.#o,l=this.#g(a," "),p=u+i+s+l+`
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:n}=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(n===void 0?void 0:typeof n=="string"?n:n(s)),s}}import{existsSync as Nn}from"fs";var nr=ue(ir(),1);import{dirname as QD,join as Ze}from"path";import{existsSync as es,readFileSync as ts}from"fs";import{promises as Dr}from"fs";import{existsSync as JD,mkdirSync as ZD}from"fs";var H=class{static ensureDir(t){JD(t)||ZD(t,{recursive:!0})}};var{copyFile:rs,stat:us}=Dr,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(!es(r))return;let n=ts(r,"utf8").split(`
11
- `).map(o=>o.trim()).filter(o=>o&&!o.startsWith("#"));return(0,nr.default)().add(n)}async copyFiles(t,r,u,i){let n=await Dr.readdir(t);await Promise.all(n.map(async o=>{let s=Ze(t,o),a=Ze(r,o);if(u&&u.ignores(o))return;if(H.ensureDir(QD(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}){H.ensureDir(r);let i=this.getIg();return this.copyFiles(t,r,i,u)}};var jn=ue(In(),1);import{readFileSync as $l,writeFileSync as Ul,existsSync as Wl}from"fs";var _e=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 $l(t,"utf-8")}readJSONFile(t){return JSON.parse(this.readFile(t))}writeFile(t,r){Ul(this.getRealTemplateFilePath(t),r,{encoding:"utf-8"})}replaceFile(t,r){let u=this.readFile(t);return Object.keys(r).forEach(i=>{let n=r[i];u=u.replace(new RegExp(`\\[TPL:${i}\\]`,"g"),typeof n=="string"?n:JSON.stringify(n))}),u}mergeJSONFile(t,r){let u=this.readJSONFile(t),i=(0,jn.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 n=this.getRealTemplateFilePath(u);return Wl(n)?(this.mergeJSONFile(n,JSON.parse(i)),!0):(this.writeFile(n,i),!0)}return this.writeFile(u,i),!0}return!1}};var Gn=["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(!Nn(r))throw new Error("template path not exit");this.ora=Kt,this.context=new Hl("create-app",t),this.subPackages=["node-lib","react-app","next-app"],this.copyer=new Ce(N(this.context.options.configsRootPath,"_common")),this.compose=new _e}get logger(){return this.context.logger}async steps(t){try{return await Yl.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 Gn.includes(t)}async getGeneratorContext(){let t=mt(this.subPackages,Gn),r=await this.steps(t);if(this.isPackageTemplate(r.template)){let u=_t(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=(a,l)=>(this.logger.debug("copyCallback",a,l),this.compose.composeConfigFile(t,a,l)),{configsRootPath:n,config:o}=this.context.options;if(!o){this.logger.debug("no copy config files");return}let s=N(n,u);if(!Nn(s)){this.logger.debug(`Config path not found: ${s}`);return}await this.copyer.copyPaths({sourcePath:s,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:n}=this.context.options;for(let o of u){let s=N(n,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.9.0",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:{lint:"eslint src --fix",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 Kl(){let e=new zl;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 kn(e=process.cwd()){let{dryRun:t,verbose:r,...u}=Kl(),i=Mn(e,"./templates"),n=Mn(e,"./configs");Ln(i)||(console.error("Template is empty!"),process.exit(1)),Ln(n)||(console.error("Configs is empty!"),process.exit(1)),await new Be({dryRun:t,verbose:r,options:{...u,templateRootPath:i,configsRootPath:n}}).generate()}import{fileURLToPath as Xl}from"url";import{dirname as Jl}from"path";var Zl=Xl(import.meta.url),Ql=Jl(Zl);kn(Ql).catch(e=>{console.error(e),process.exit(1)});
11
+ `).map(o=>o.trim()).filter(o=>o&&!o.startsWith("#"));return(0,nr.default)().add(n)}async copyFiles(t,r,u,i){let n=await Dr.readdir(t);await Promise.all(n.map(async o=>{let s=Ze(t,o),a=Ze(r,o);if(u&&u.ignores(o))return;if(H.ensureDir(QD(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}){H.ensureDir(r);let i=this.getIg();return this.copyFiles(t,r,i,u)}};var jn=ue(In(),1);import{readFileSync as $l,writeFileSync as Ul,existsSync as Wl}from"fs";var _e=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 $l(t,"utf-8")}readJSONFile(t){return JSON.parse(this.readFile(t))}writeFile(t,r){Ul(this.getRealTemplateFilePath(t),r,{encoding:"utf-8"})}replaceFile(t,r){let u=this.readFile(t);return Object.keys(r).forEach(i=>{let n=r[i];u=u.replace(new RegExp(`\\[TPL:${i}\\]`,"g"),typeof n=="string"?n:JSON.stringify(n))}),u}mergeJSONFile(t,r){let u=this.readJSONFile(t),i=(0,jn.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 n=this.getRealTemplateFilePath(u);return Wl(n)?(this.mergeJSONFile(n,JSON.parse(i)),!0):(this.writeFile(n,i),!0)}return this.writeFile(u,i),!0}return!1}};var Gn=["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(!Nn(r))throw new Error("template path not exit");this.ora=Kt,this.context=new Hl("create-app",t),this.subPackages=["node-lib","react-app","next-app"],this.copyer=new Ce(N(this.context.options.configsRootPath,"_common")),this.compose=new _e}get logger(){return this.context.logger}async steps(t){try{return await Yl.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 Gn.includes(t)}async getGeneratorContext(){let t=mt(this.subPackages,Gn),r=await this.steps(t);if(this.isPackageTemplate(r.template)){let u=_t(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=(a,l)=>(this.logger.debug("copyCallback",a,l),this.compose.composeConfigFile(t,a,l)),{configsRootPath:n,config:o}=this.context.options;if(!o){this.logger.debug("no copy config files");return}let s=N(n,u);if(!Nn(s)){this.logger.debug(`Config path not found: ${s}`);return}await this.copyer.copyPaths({sourcePath:s,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:n}=this.context.options;for(let o of u){let s=N(n,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.10.0",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:{lint:"eslint src --fix",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 Kl(){let e=new zl;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 kn(e=process.cwd()){let{dryRun:t,verbose:r,...u}=Kl(),i=Mn(e,"./templates"),n=Mn(e,"./configs");Ln(i)||(console.error("Template is empty!"),process.exit(1)),Ln(n)||(console.error("Configs is empty!"),process.exit(1)),await new Be({dryRun:t,verbose:r,options:{...u,templateRootPath:i,configsRootPath:n}}).generate()}import{fileURLToPath as Xl}from"url";import{dirname as Jl}from"path";var Zl=Xl(import.meta.url),Ql=Jl(Zl);kn(Ql).catch(e=>{console.error(e),process.exit(1)});
@@ -1,26 +1,25 @@
1
1
  # ci
2
2
  NPM_TOKEN=
3
3
  GITHUB_TOKEN=
4
-
5
4
  # fe-scripts
6
5
  FE_RELEASE_BRANCH=master
7
6
  FE_RELEASE=false
8
7
  FE_RELEASE_ENV=localhost
9
-
10
- # ======== server
11
- APP_HOST=http://localhost:3100
8
+ # ===== server
9
+ APP_HOST=
12
10
  SUPABASE_URL=
13
11
  SUPABASE_ANON_KEY=
14
12
  JWT_SECRET=
15
-
16
- # browser
13
+ CEREBRAS_API_KEY=
14
+ CEREBRAS_BASE_URL=
15
+ # ===== browser
17
16
  NEXT_PUBLIC_USER_TOKEN_STORAGE_KEY=fe_user_token
18
17
  NEXT_PUBLIC_OPEN_AI_MODELS='["gpt-4o-mini","gpt-3.5-turbo","gpt-3.5-turbo-2","gpt-4","gpt-4-32k"]'
19
18
  NEXT_PUBLIC_OPEN_AI_BASE_URL=
20
19
  NEXT_PUBLIC_OPEN_AI_TOKEN=sk-proj-1234567890
21
20
  NEXT_PUBLIC_OPEN_AI_TOKEN_PREFIX=Bearer
22
21
  NEXT_PUBLIC_OPEN_AI_REQUIRE_TOKEN=true
23
- NEXT_PUBLIC_LOGIN_USER=admin
24
- NEXT_PUBLIC_LOGIN_PASSWORD=prod-123456
25
- NEXT_PUBLIC_FE_API_BASE_URL=https://feapi.example.com/
26
- NEXT_PUBLIC_STRING_ENCRYPT_KEY=YourSecretKey123!@#
22
+ NEXT_PUBLIC_LOGIN_USER=xxxx
23
+ NEXT_PUBLIC_LOGIN_PASSWORD=xxx
24
+ NEXT_PUBLIC_FE_API_BASE_URL=https://xxx.example.com/
25
+ NEXT_PUBLIC_STRING_ENCRYPT_KEY=xxx!@#
@@ -173,6 +173,11 @@ const eslintConfig = [
173
173
  singleQuote: true,
174
174
  trailingComma: 'none',
175
175
  endOfLine: 'lf'
176
+ },
177
+ {
178
+ // 仅用于单独部署时对 eslint prettier 插件自动查找 prettierrc 时报错
179
+ // 注意: vscode 等编辑器会失效, 作为单独项目开发时可以去掉
180
+ usePrettierrc: false
176
181
  }
177
182
  ],
178
183
  // 默认禁用 export default
@@ -2,7 +2,7 @@ import createNextIntlPlugin from 'next-intl/plugin';
2
2
  import { generateLocales } from './make/generateLocales';
3
3
  import type { NextConfig } from 'next';
4
4
 
5
- const withNextIntl = createNextIntlPlugin();
5
+ const withNextIntl = createNextIntlPlugin('./src/i18n/request.ts');
6
6
 
7
7
  // 在构建开始时生成本地化文件
8
8
  generateLocales().catch((error) => {
@@ -17,7 +17,7 @@ export async function generateStaticParams() {
17
17
  }
18
18
 
19
19
  // Allow Next.js to statically generate this page if possible (default behavior)
20
- export const dynamic = 'auto'; // Enable static generation when possible, fallback to dynamic if needed
20
+ // Note: 'auto' is not a valid value in Next.js 15, removed to use default behavior
21
21
 
22
22
  // Optional: Use revalidate if you want ISR (Incremental Static Regeneration)
23
23
  // export const revalidate = 3600; // Rebuild every hour (optional)
@@ -23,7 +23,7 @@ export async function generateStaticParams() {
23
23
  }
24
24
 
25
25
  // Allow Next.js to statically generate this page if possible (default behavior)
26
- export const dynamic = 'auto'; // Enable static generation when possible, fallback to dynamic if needed
26
+ // Note: 'auto' is not a valid value in Next.js 15, removed to use default behavior
27
27
 
28
28
  // Optional: Use revalidate if you want ISR (Incremental Static Regeneration)
29
29
  // export const revalidate = 3600; // Rebuild every hour (optional)
@@ -17,7 +17,7 @@ export async function generateStaticParams() {
17
17
  }
18
18
 
19
19
  // Allow Next.js to statically generate this page if possible (default behavior)
20
- export const dynamic = 'auto'; // Enable static generation when possible, fallback to dynamic if needed
20
+ // Note: 'auto' is not a valid value in Next.js 15, removed to use default behavior
21
21
 
22
22
  // Optional: Use revalidate if you want ISR (Incremental Static Regeneration)
23
23
  // export const revalidate = 3600; // Rebuild every hour (optional)
@@ -5,7 +5,8 @@ import { ApiLocaleService } from '@/server/services/ApiLocaleService';
5
5
  import { i18nConfig } from '@config/i18n';
6
6
  import type { LocaleType } from '@config/i18n';
7
7
 
8
- export const revalidate = i18nConfig.localeCacheTime;
8
+ // Use literal value instead of imported config to ensure static analysis
9
+ export const revalidate = 60; // Cache time in seconds (matches i18nConfig.localeCacheTime)
9
10
 
10
11
  export async function GET(req: NextRequest) {
11
12
  const searchParams = Object.fromEntries(req.nextUrl.searchParams.entries());
@@ -27,7 +27,7 @@ export default getRequestConfig(async ({ requestLocale }) => {
27
27
  onError: (error) => {
28
28
  if (error.message.includes('MISSING_MESSAGE')) {
29
29
  console.warn(`[i18n] Missing translation: ${error.message}`);
30
- return error.key; // 返回 key 作为 fallback 文本
30
+ return error.message; // 返回 key 作为 fallback 文本
31
31
  }
32
32
  throw error; // 其他错误仍然抛出
33
33
  }
@@ -42,7 +42,7 @@ export default getRequestConfig(async ({ requestLocale }) => {
42
42
  onError: (error) => {
43
43
  if (error.message.includes('MISSING_MESSAGE')) {
44
44
  console.warn(`[i18n] Missing translation: ${error.message}`);
45
- return error.key; // 返回 key 作为 fallback 文本
45
+ return error.message; // 返回 key 作为 fallback 文本
46
46
  }
47
47
  throw error; // 其他错误仍然抛出
48
48
  }
@@ -6,7 +6,7 @@ export class MockAppConfig implements EnvConfigInterface {
6
6
 
7
7
  appVersion = version;
8
8
 
9
- env: string = import.meta.env.MODE;
9
+ env: string = 'test';
10
10
 
11
11
  userTokenStorageKey = '__fe_user_token__';
12
12
 
@@ -1,18 +1,7 @@
1
- import { TestBootstrapsProvider } from './TestBootstrapsProvider';
2
-
3
- interface TestAppProps {
4
- children: React.ReactNode;
5
- /**
6
- * Initial URL path for the router
7
- * @default ['/en/']
8
- */
9
- routerInitialEntries?: string[];
10
- /**
11
- * Initial index of the entries array
12
- * @default 0
13
- */
14
- routerInitialIndex?: number;
15
- }
1
+ import {
2
+ TestBootstrapsProvider,
3
+ type TestBootstrapsProviderProps
4
+ } from './TestBootstrapsProvider';
16
5
 
17
6
  /**
18
7
  * TestApp - Complete test wrapper with IOC and Router
@@ -31,13 +20,17 @@ interface TestAppProps {
31
20
  export function TestApp({
32
21
  children,
33
22
  routerInitialEntries,
34
- routerInitialIndex
35
- }: TestAppProps) {
23
+ routerInitialIndex,
24
+ bootHref,
25
+ appConfig
26
+ }: TestBootstrapsProviderProps) {
36
27
  return (
37
28
  <TestBootstrapsProvider
38
29
  data-testid="TestApp"
39
30
  routerInitialEntries={routerInitialEntries}
40
31
  routerInitialIndex={routerInitialIndex}
32
+ bootHref={bootHref}
33
+ appConfig={appConfig}
41
34
  >
42
35
  {children}
43
36
  </TestBootstrapsProvider>
@@ -1,12 +1,9 @@
1
+ import { appConfig as globalsAppConfig } from '@/core/globals';
1
2
  import { IOCContext } from '@/uikit/contexts/IOCContext';
2
3
  import { TestRouter } from './TestRouter';
3
4
  import { testIOC } from '../testIOC/TestIOC';
4
-
5
- export function TestBootstrapsProvider({
6
- children,
7
- routerInitialEntries,
8
- routerInitialIndex
9
- }: {
5
+ import type { EnvConfigInterface } from '@qlover/corekit-bridge';
6
+ export interface TestBootstrapsProviderProps {
10
7
  children: React.ReactNode;
11
8
  /**
12
9
  * Initial URL path for the router
@@ -18,8 +15,30 @@ export function TestBootstrapsProvider({
18
15
  * @default 0
19
16
  */
20
17
  routerInitialIndex?: number;
21
- }) {
22
- const IOC = testIOC.create();
18
+
19
+ /**
20
+ * The boot href
21
+ *
22
+ * @default `https://localhost.test:3000/en/`
23
+ */
24
+ bootHref?: string;
25
+
26
+ appConfig?: EnvConfigInterface;
27
+ }
28
+
29
+ const defaultBootHref = 'https://localhost.test:3000/en/';
30
+
31
+ export function TestBootstrapsProvider({
32
+ children,
33
+ routerInitialEntries,
34
+ routerInitialIndex,
35
+ bootHref,
36
+ appConfig
37
+ }: TestBootstrapsProviderProps) {
38
+ const IOC = testIOC.create({
39
+ pathname: bootHref ?? defaultBootHref,
40
+ appConfig: appConfig ?? globalsAppConfig
41
+ });
23
42
 
24
43
  return (
25
44
  <IOCContext.Provider data-testid="TestBootstrapsProvider" value={IOC}>
@@ -1,5 +1,5 @@
1
1
  import { vi } from 'vitest';
2
- import { MockAppConfig } from './MockAppConfit';
2
+ import { MockAppConfig } from './MockAppConfig';
3
3
  import { MockDialogHandler } from './MockDialogHandler';
4
4
  import { MockLogger } from './MockLogger';
5
5
 
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Mock i18next-http-backend for testing
3
+ *
4
+ * This mock prevents network requests while still allowing i18next to work properly.
5
+ * It loads translation data directly from JSON files, enabling translation testing.
6
+ */
7
+
8
+ import enCommon from '../../public/locales/en/common.json';
9
+ import zhCommon from '../../public/locales/zh/common.json';
10
+ import type { i18n as I18nType } from 'i18next';
11
+
12
+ // Translation resources loaded from JSON files
13
+ const resources: Record<string, Record<string, Record<string, string>>> = {
14
+ en: {
15
+ common: enCommon as Record<string, string>
16
+ },
17
+ zh: {
18
+ common: zhCommon as Record<string, string>
19
+ }
20
+ };
21
+
22
+ /**
23
+ * Mock HttpApi backend class for i18next
24
+ * Implements the backend interface to avoid network requests
25
+ */
26
+ export class MockHttpBackend {
27
+ type = 'backend';
28
+
29
+ /**
30
+ * Initialize the backend
31
+ * This is called by i18next when .use() is called
32
+ */
33
+ init(
34
+ _services: unknown,
35
+ _backendOptions: unknown,
36
+ _i18nextOptions: unknown,
37
+ i18nextInstance: I18nType
38
+ ): void {
39
+ // Preload resources directly into i18next
40
+ if (
41
+ i18nextInstance &&
42
+ typeof i18nextInstance.addResourceBundle === 'function'
43
+ ) {
44
+ // Add resources for all languages and namespaces
45
+ Object.keys(resources).forEach((lng) => {
46
+ Object.keys(resources[lng]).forEach((ns) => {
47
+ i18nextInstance.addResourceBundle(
48
+ lng,
49
+ ns,
50
+ resources[lng][ns],
51
+ true,
52
+ true
53
+ );
54
+ });
55
+ });
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Read translation data for a language and namespace
61
+ * This method is called by i18next to load translations
62
+ */
63
+ read(
64
+ language: string,
65
+ namespace: string,
66
+ callback: (error: Error | null, data?: Record<string, string>) => void
67
+ ): void {
68
+ try {
69
+ const data = resources[language]?.[namespace];
70
+ if (data) {
71
+ // Return immediately with the data
72
+ callback(null, data);
73
+ } else {
74
+ callback(new Error(`Translation not found: ${language}/${namespace}`));
75
+ }
76
+ } catch (error) {
77
+ callback(error as Error);
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Load URL - called by i18next-http-backend to load from URL
83
+ * We intercept this and return data from memory instead
84
+ */
85
+ loadUrl(
86
+ _url: string,
87
+ callback: (error: Error | null, data?: Record<string, string>) => void
88
+ ): void {
89
+ // Extract language and namespace from URL if possible
90
+ // Format: /locales/{{lng}}/{{ns}}.json
91
+ const match = _url.match(/locales\/([^/]+)\/([^/]+)\.json/);
92
+ if (match) {
93
+ const [, lng, ns] = match;
94
+ const data = resources[lng]?.[ns];
95
+ if (data) {
96
+ callback(null, data);
97
+ return;
98
+ }
99
+ }
100
+ // Fallback: return empty object
101
+ callback(null, {});
102
+ }
103
+
104
+ /**
105
+ * Create - not used in our mock
106
+ */
107
+ create(): void {
108
+ // No-op
109
+ }
110
+ }
@@ -1,5 +1,5 @@
1
1
  import { baseNoLocaleRoutes, baseRoutes } from '@config/app.router';
2
- import { useLocaleRoutes } from '@config/common';
2
+ import { routerPrefix, useLocaleRoutes } from '@config/common';
3
3
  import { I } from '@config/IOCIdentifier';
4
4
  import { themeConfig } from '@config/theme';
5
5
  import { ThemeService } from '@qlover/corekit-bridge';
@@ -54,7 +54,8 @@ export class TestIOCRegister
54
54
  {
55
55
  routes: useLocaleRoutes ? baseRoutes : baseNoLocaleRoutes,
56
56
  logger: ioc.get(I.Logger),
57
- hasLocalRoutes: useLocaleRoutes
57
+ hasLocalRoutes: useLocaleRoutes,
58
+ routerPrefix: routerPrefix
58
59
  }
59
60
  )
60
61
  );
@@ -49,3 +49,16 @@ global.IntersectionObserver = vi.fn().mockImplementation(() => ({
49
49
 
50
50
  // Mock globals
51
51
  vi.mock('@/core/globals', () => createMockGlobals());
52
+
53
+ // Mock i18next-http-backend to avoid network requests in tests
54
+ // This prevents timeout issues when I18nService tries to load language files
55
+ // The mock loads translations directly from JSON files, enabling translation testing
56
+ vi.mock('i18next-http-backend', async () => {
57
+ const { MockHttpBackend } = await import('@__mocks__/i18nextHttpBackend');
58
+ return { default: MockHttpBackend };
59
+ });
60
+
61
+ // Mock i18next-browser-languagedetector to avoid browser API calls
62
+ vi.mock('i18next-browser-languagedetector', () => ({
63
+ default: {}
64
+ }));
@@ -205,7 +205,8 @@ describe('I18nService', () => {
205
205
  const result = service.t(key);
206
206
  expect(result).toBe('translated_test.key');
207
207
  expect(i18n.t).toHaveBeenCalledWith(key, {
208
- lng: 'en'
208
+ lng: 'en',
209
+ nsSeparator: false
209
210
  });
210
211
  });
211
212
 
@@ -216,6 +217,7 @@ describe('I18nService', () => {
216
217
  expect(result).toBe('translated_test.key');
217
218
  expect(i18n.t).toHaveBeenCalledWith(key, {
218
219
  lng: 'en',
220
+ nsSeparator: false,
219
221
  ...params
220
222
  });
221
223
  });
@@ -6,8 +6,6 @@ import type { ExecutorPageBridgeInterface } from '@/base/port/ExecutorPageBridge
6
6
  import type { JSONStoragePageBridgeInterface } from '@/base/port/JSONStoragePageBridgeInterface';
7
7
  import type { RequestPageBridgeInterface } from '@/base/port/RequestPageBridgeInterface';
8
8
  import type { I18nService } from '@/base/services/I18nService';
9
- import type { MessageService } from '@/base/services/MessageService';
10
- import type { ProcesserExecutor } from '@/base/services/ProcesserExecutor';
11
9
  import type { RouteService } from '@/base/services/RouteService';
12
10
  import type { UserService } from '@/base/services/UserService';
13
11
  import type * as CorekitBridge from '@qlover/corekit-bridge';
@@ -30,10 +28,17 @@ export const IOCIdentifier = Object.freeze({
30
28
  AntdStaticApiInterface: 'AntdStaticApiInterface',
31
29
  RequestCatcherInterface: 'RequestCatcherInterface',
32
30
  I18nServiceInterface: 'I18nServiceInterface',
33
- ProcesserExecutorInterface: 'ProcesserExecutorInterface',
34
31
  RouteServiceInterface: 'RouteServiceInterface',
32
+ /**
33
+ * User service interface
34
+ *
35
+ * This interface is implemented by the corekit-bridge UserServiceInterface interface, used to manage user information and authentication.
36
+ *
37
+ * @example ```
38
+ * const userService = useIOC(IOCIdentifier.UserServiceInterface);
39
+ * ```
40
+ */
35
41
  UserServiceInterface: 'UserServiceInterface',
36
- MessageServiceInterface: 'MessageServiceInterface',
37
42
  I18nKeyErrorPlugin: 'I18nKeyErrorPlugin',
38
43
  FeApiCommonPlugin: 'FeApiCommonPlugin',
39
44
  ApiMockPlugin: 'ApiMockPlugin',
@@ -73,10 +78,8 @@ export interface IOCIdentifierMap {
73
78
  [IOCIdentifier.AntdStaticApiInterface]: DialogHandler;
74
79
  [IOCIdentifier.RequestCatcherInterface]: RequestStatusCatcher;
75
80
  [IOCIdentifier.I18nServiceInterface]: I18nService;
76
- [IOCIdentifier.ProcesserExecutorInterface]: ProcesserExecutor;
77
81
  [IOCIdentifier.RouteServiceInterface]: RouteService;
78
82
  [IOCIdentifier.UserServiceInterface]: UserService;
79
- [IOCIdentifier.MessageServiceInterface]: MessageService;
80
83
  [IOCIdentifier.I18nKeyErrorPlugin]: I18nKeyErrorPlugin;
81
84
  [IOCIdentifier.FeApiCommonPlugin]: CorekitBridge.RequestCommonPlugin;
82
85
  [IOCIdentifier.ApiMockPlugin]: CorekitBridge.ApiMockPlugin;
@@ -35,6 +35,8 @@ export const loggerStyles = {
35
35
  *
36
36
  * - 需要以 / 开头
37
37
  * - 但是不能只有 /
38
+ *
39
+ * **TODO: 未来可能需要修改为支持 vercel 环境使用前缀**
38
40
  */
39
41
  export const routerPrefix = '/router-root';
40
42
 
@@ -45,3 +47,39 @@ export const routerPrefix = '/router-root';
45
47
  * - false: 不使用本地化路由,直接使用路径 (例如: /home)
46
48
  */
47
49
  export const useLocaleRoutes = true;
50
+
51
+ /**
52
+ * 注入到浏览器全局变量中需要忽略的变量
53
+ *
54
+ * 应用 `@/core/globals.ts` 中的变量
55
+ *
56
+ * 可能 appConfig 有敏感信息,需要忽略
57
+ *
58
+ * - 可以直接忽略整个 appConfig 对象, 例如: 'appConfig'
59
+ * - 也可以忽略单个属性, 例如: 'appConfig.openAiTokenPrefix', 'appConfig.openAiToken'
60
+ *
61
+ * @example 忽略 appConfig 对象
62
+ * ```typescript
63
+ * export const omitInjectedGlobals = [
64
+ * 'appConfig'
65
+ * ];
66
+ * ```
67
+ *
68
+ * @example 忽略 appConfig 中的 openAiTokenPrefix 和 openAiToken 属性
69
+ * ```typescript
70
+ * export const omitInjectedGlobals = [
71
+ * 'appConfig.openAiTokenPrefix',
72
+ * 'appConfig.openAiToken'
73
+ * ];
74
+ * ```
75
+ */
76
+ export const omitInjectedGlobals = [
77
+ 'appConfig.openAiTokenPrefix',
78
+ 'appConfig.openAiToken',
79
+ 'appConfig.loginPassword',
80
+ 'appConfig.loginUser',
81
+ 'appConfig.aiApiTokenPrefix',
82
+ 'appConfig.openAiBaseUrl',
83
+ 'appConfig.aiApiBaseUrl',
84
+ 'appConfig.aiApiToken'
85
+ ];
@@ -3,29 +3,19 @@
3
3
  "mock": true,
4
4
  "noUrl": true
5
5
  },
6
- "GET https://feapi.example.com/api/userinfo": {
7
- "name": "John Doe",
8
- "email": "john.doe@example.com",
9
- "picture": "https://randomuser.me/api/portraits/men/1.jpg"
10
- },
6
+
11
7
  "GET /api/userinfo": {
12
8
  "name": "John Doe",
13
9
  "email": "john.doe@example.com",
14
10
  "picture": "https://randomuser.me/api/portraits/men/1.jpg"
15
11
  },
16
- "POST https://feapi.example.com/api/login": {
17
- "token": "asdfasdf123123asdfasdf"
18
- },
19
12
  "POST /api/login": {
20
13
  "token": "/api/login-token-adfasdfasdf"
21
14
  },
22
- "POST https://feapi.example.com/api/register": {
23
- "token": "asdfasdf123123asdfasdf"
24
- },
25
15
  "POST /api/register": {
26
16
  "token": "asdfasdf123123asdfasdf"
27
17
  },
28
- "POST https://api.openai.com/v1/chat/completions": {
18
+ "POST /chat/completions": {
29
19
  "id": "chatcmpl-1234567890",
30
20
  "object": "chat.completion",
31
21
  "created": 1721702400,
@@ -37,5 +27,8 @@
37
27
  }
38
28
  }
39
29
  ]
30
+ },
31
+ "POST /api/logout": {
32
+ "redirect": "/login"
40
33
  }
41
34
  }