@qlover/create-app 0.10.6 → 0.11.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # @qlover/create-app
2
2
 
3
+ ## 0.11.0
4
+
5
+ ### Minor Changes
6
+
7
+ #### 📝 Documentation
8
+
9
+ - **tests:** add testing-guide type tests ([16870e0](https://github.com/qlover/fe-base/commit/16870e043b130f98cdd98152d816b6b9a7f05719)) ([#558](https://github.com/qlover/fe-base/pull/558))
10
+
3
11
  ## 0.10.6
4
12
 
5
13
  ### Patch Changes
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&&z.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&&z.default.stdin.isTTY&&this.#r&&(Je.stop(),this.#r=!1),this):this}succeed(t){return this.stopAndPersist({symbol:Y.success,text:t})}fail(t){return this.stopAndPersist({symbol:Y.error,text:t})}warn(t){return this.stopAndPersist({symbol:Y.warning,text:t})}info(t){return this.stopAndPersist({symbol:Y.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 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 bt=require("fs");var S=require("path"),A=require("fs"),cr=h(lr(),1);var _e=require("fs"),J=class{static ensureDir(t){(0,_e.existsSync)(t)||(0,_e.mkdirSync)(t,{recursive:!0})}};var{copyFile:rs,stat:us}=A.promises,me=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,S.join)(t,this.ignoreFile);if(!(0,A.existsSync)(r))return;let D=(0,A.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 A.promises.readdir(t);await Promise.all(D.map(async o=>{let s=(0,S.join)(t,o),a=(0,S.join)(r,o);if(u&&u.ignores(o))return;if(J.ensureDir((0,S.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}){J.ensureDir(r);let i=this.getIg();return this.copyFiles(t,r,i,u)}};var k=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,k.readFileSync)(t,"utf-8")}readJSONFile(t){return JSON.parse(this.readFile(t))}writeFile(t,r){(0,k.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,k.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,bt.existsSync)(r))throw new Error("template path not exit");this.ora=rr,this.context=new UD.ScriptContext("create-app",t),this.subPackages=["node-lib","react-app","next-app"],this.copyer=new me((0,x.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,x.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,x.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,x.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,x.join)(D,o),a=(0,x.join)(i,r,o);this.logger.debug("copy sub package",s,a),await this.copyer.copyPaths({sourcePath:s,targetPath:a})}}};var At={name:"@qlover/create-app",version:"0.10.6",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 Ul(){let e=new HD.Command;return e.version(At.version,"-v, --version","Show version").description(At.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 YD(e=process.cwd()){let{dryRun:t,verbose:r,...u}=Ul(),i=(0,vt.resolve)(e,"./templates"),D=(0,vt.resolve)(e,"./configs");(0,xt.existsSync)(i)||(console.error("Template is empty!"),process.exit(1)),(0,xt.existsSync)(D)||(console.error("Configs is empty!"),process.exit(1)),await new ye({dryRun:t,verbose:r,options:{...u,templateRootPath:i,configsRootPath:D}}).generate()}YD(__dirname).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 A.promises.readdir(t);await Promise.all(D.map(async o=>{let s=(0,S.join)(t,o),a=(0,S.join)(r,o);if(u&&u.ignores(o))return;if(J.ensureDir((0,S.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}){J.ensureDir(r);let i=this.getIg();return this.copyFiles(t,r,i,u)}};var k=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,k.readFileSync)(t,"utf-8")}readJSONFile(t){return JSON.parse(this.readFile(t))}writeFile(t,r){(0,k.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,k.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,bt.existsSync)(r))throw new Error("template path not exit");this.ora=rr,this.context=new UD.ScriptContext("create-app",t),this.subPackages=["node-lib","react-app","next-app"],this.copyer=new me((0,x.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,x.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,x.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,x.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,x.join)(D,o),a=(0,x.join)(i,r,o);this.logger.debug("copy sub package",s,a),await this.copyer.copyPaths({sourcePath:s,targetPath:a})}}};var At={name:"@qlover/create-app",version:"0.11.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 Ul(){let e=new HD.Command;return e.version(At.version,"-v, --version","Show version").description(At.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 YD(e=process.cwd()){let{dryRun:t,verbose:r,...u}=Ul(),i=(0,vt.resolve)(e,"./templates"),D=(0,vt.resolve)(e,"./configs");(0,xt.existsSync)(i)||(console.error("Template is empty!"),process.exit(1)),(0,xt.existsSync)(D)||(console.error("Configs is empty!"),process.exit(1)),await new ye({dryRun:t,verbose:r,options:{...u,templateRootPath:i,configsRootPath:D}}).generate()}YD(__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 Dr=ue(ir(),1);import{dirname as QD,join as Ze}from"path";import{existsSync as es,readFileSync as ts,promises as nr}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}=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(!es(r))return;let n=ts(r,"utf8").split(`
11
- `).map(o=>o.trim()).filter(o=>o&&!o.startsWith("#"));return(0,Dr.default)().add(n)}async copyFiles(t,r,u,i){let n=await nr.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 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 $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 me}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=_t(this.subPackages,Gn),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=(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.6",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,Dr.default)().add(n)}async copyFiles(t,r,u,i){let n=await nr.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 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 $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 me}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=_t(this.subPackages,Gn),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=(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.11.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)});
@@ -627,7 +627,20 @@ describe('AsyncService', () => {
627
627
 
628
628
  ### Type Safety Testing
629
629
 
630
+ TypeScript project tests should not only verify runtime behavior but also ensure the correctness of the type system. Vitest provides the `expectTypeOf` utility for compile-time type checking.
631
+
632
+ #### Why Type Testing?
633
+
634
+ 1. **Type Inference Validation**: Ensure TypeScript correctly infers complex types
635
+ 2. **Generic Constraint Checking**: Verify generic parameter constraints
636
+ 3. **Type Compatibility**: Ensure type definitions match actual usage
637
+ 4. **API Contract Guarantee**: Prevent breaking changes in type definitions
638
+
639
+ #### Basic Type Testing
640
+
630
641
  ```typescript
642
+ import { describe, it, expectTypeOf } from 'vitest';
643
+
631
644
  describe('TypeSafetyTests', () => {
632
645
  it('should maintain type safety', () => {
633
646
  const processor = new DataProcessor<User>();
@@ -636,9 +649,190 @@ describe('TypeSafetyTests', () => {
636
649
  expectTypeOf(processor.process).parameter(0).toEqualTypeOf<User>();
637
650
  expectTypeOf(processor.process).returns.toEqualTypeOf<ProcessedUser>();
638
651
  });
652
+
653
+ it('should infer correct return types', () => {
654
+ const result = getData();
655
+
656
+ // Verify return type
657
+ expectTypeOf(result).toEqualTypeOf<{ id: number; name: string }>();
658
+ expectTypeOf(result).not.toEqualTypeOf<{ id: string; name: string }>();
659
+ });
660
+
661
+ it('should validate parameter types', () => {
662
+ function processUser(user: User): void {
663
+ // implementation
664
+ }
665
+
666
+ // Verify parameter type
667
+ expectTypeOf(processUser).parameter(0).toMatchTypeOf<{ id: number }>();
668
+ expectTypeOf(processUser).parameter(0).toHaveProperty('id');
669
+ });
670
+ });
671
+ ```
672
+
673
+ #### Generic Type Testing
674
+
675
+ ```typescript
676
+ describe('Generic Type Tests', () => {
677
+ it('should work with generic constraints', () => {
678
+ class Storage<T extends { id: number }> {
679
+ store(item: T): T {
680
+ return item;
681
+ }
682
+ }
683
+
684
+ const storage = new Storage<User>();
685
+
686
+ // Verify generic type
687
+ expectTypeOf(storage.store).parameter(0).toMatchTypeOf<User>();
688
+ expectTypeOf(storage.store).returns.toMatchTypeOf<User>();
689
+ });
690
+
691
+ it('should validate complex generic types', () => {
692
+ type ApiResponse<T> = {
693
+ data: T;
694
+ status: number;
695
+ message?: string;
696
+ };
697
+
698
+ const response: ApiResponse<User[]> = {
699
+ data: [],
700
+ status: 200
701
+ };
702
+
703
+ // Verify nested generic type
704
+ expectTypeOf(response).toMatchTypeOf<ApiResponse<User[]>>();
705
+ expectTypeOf(response.data).toEqualTypeOf<User[]>();
706
+ });
707
+ });
708
+ ```
709
+
710
+ #### Union and Intersection Type Testing
711
+
712
+ ```typescript
713
+ describe('Union and Intersection Types', () => {
714
+ it('should handle union types correctly', () => {
715
+ type Result = Success | Error;
716
+ type Success = { status: 'success'; data: string };
717
+ type Error = { status: 'error'; message: string };
718
+
719
+ function handleResult(result: Result): void {
720
+ // implementation
721
+ }
722
+
723
+ // Verify union type
724
+ expectTypeOf(handleResult).parameter(0).toMatchTypeOf<Success>();
725
+ expectTypeOf(handleResult).parameter(0).toMatchTypeOf<Error>();
726
+ });
727
+
728
+ it('should handle intersection types correctly', () => {
729
+ type Timestamped = { createdAt: Date; updatedAt: Date };
730
+ type UserWithTimestamp = User & Timestamped;
731
+
732
+ const user: UserWithTimestamp = {
733
+ id: 1,
734
+ name: 'John',
735
+ createdAt: new Date(),
736
+ updatedAt: new Date()
737
+ };
738
+
739
+ // Verify intersection type contains all properties
740
+ expectTypeOf(user).toHaveProperty('id');
741
+ expectTypeOf(user).toHaveProperty('name');
742
+ expectTypeOf(user).toHaveProperty('createdAt');
743
+ expectTypeOf(user).toHaveProperty('updatedAt');
744
+ });
639
745
  });
640
746
  ```
641
747
 
748
+ #### Type Narrowing Testing
749
+
750
+ ```typescript
751
+ describe('Type Narrowing Tests', () => {
752
+ it('should validate type guards', () => {
753
+ function isString(value: unknown): value is string {
754
+ return typeof value === 'string';
755
+ }
756
+
757
+ const value: unknown = 'test';
758
+
759
+ if (isString(value)) {
760
+ // Within this scope, value should be narrowed to string type
761
+ expectTypeOf(value).toEqualTypeOf<string>();
762
+ }
763
+ });
764
+
765
+ it('should validate discriminated unions', () => {
766
+ type Shape =
767
+ | { kind: 'circle'; radius: number }
768
+ | { kind: 'rectangle'; width: number; height: number };
769
+
770
+ function getArea(shape: Shape): number {
771
+ if (shape.kind === 'circle') {
772
+ // In this branch, shape should be narrowed to circle type
773
+ expectTypeOf(shape).toHaveProperty('radius');
774
+ expectTypeOf(shape).not.toHaveProperty('width');
775
+ return Math.PI * shape.radius ** 2;
776
+ } else {
777
+ // In this branch, shape should be narrowed to rectangle type
778
+ expectTypeOf(shape).toHaveProperty('width');
779
+ expectTypeOf(shape).toHaveProperty('height');
780
+ return shape.width * shape.height;
781
+ }
782
+ }
783
+ });
784
+ });
785
+ ```
786
+
787
+ #### Practical Recommendations
788
+
789
+ 1. **Combine with Runtime Tests**: Type tests should complement runtime tests
790
+
791
+ ```typescript
792
+ describe('Combined Runtime and Type Tests', () => {
793
+ it('should validate both runtime behavior and types', () => {
794
+ function add(a: number, b: number): number {
795
+ return a + b;
796
+ }
797
+
798
+ // Type test
799
+ expectTypeOf(add).parameter(0).toEqualTypeOf<number>();
800
+ expectTypeOf(add).returns.toEqualTypeOf<number>();
801
+
802
+ // Runtime test
803
+ expect(add(1, 2)).toBe(3);
804
+ expect(add(-1, 1)).toBe(0);
805
+ });
806
+ });
807
+ ```
808
+
809
+ 2. **Test Type Inference**: Ensure TypeScript correctly infers types, avoid overusing `any`
810
+
811
+ ```typescript
812
+ describe('Type Inference Tests', () => {
813
+ it('should infer types correctly', () => {
814
+ const data = { id: 1, name: 'John' };
815
+
816
+ // Verify inferred type
817
+ expectTypeOf(data).toEqualTypeOf<{ id: number; name: string }>();
818
+ expectTypeOf(data.id).toEqualTypeOf<number>();
819
+ expectTypeOf(data.name).toEqualTypeOf<string>();
820
+ });
821
+ });
822
+ ```
823
+
824
+ 3. **Use TypeScript Compiler Checks**: Run `tsc --noEmit` in CI to ensure no type errors
825
+
826
+ ```bash
827
+ # Add script in package.json
828
+ {
829
+ "scripts": {
830
+ "type-check": "tsc --noEmit",
831
+ "test": "pnpm type-check && vitest run"
832
+ }
833
+ }
834
+ ```
835
+
642
836
  ---
643
837
 
644
838
  ## Performance Testing
@@ -627,7 +627,20 @@ describe('AsyncService', () => {
627
627
 
628
628
  ### 类型安全测试
629
629
 
630
+ TypeScript 项目的测试不仅要验证运行时行为,还应该确保类型系统的正确性。Vitest 提供了 `expectTypeOf` 工具来进行编译时类型检查。
631
+
632
+ #### 为什么需要类型测试?
633
+
634
+ 1. **类型推断验证**:确保 TypeScript 能正确推断复杂类型
635
+ 2. **泛型约束检查**:验证泛型参数的约束条件
636
+ 3. **类型兼容性**:确保类型定义与实际使用匹配
637
+ 4. **API 契约保证**:防止类型定义的破坏性变更
638
+
639
+ #### 基础类型测试
640
+
630
641
  ```typescript
642
+ import { describe, it, expectTypeOf } from 'vitest';
643
+
631
644
  describe('TypeSafetyTests', () => {
632
645
  it('should maintain type safety', () => {
633
646
  const processor = new DataProcessor<User>();
@@ -636,9 +649,190 @@ describe('TypeSafetyTests', () => {
636
649
  expectTypeOf(processor.process).parameter(0).toEqualTypeOf<User>();
637
650
  expectTypeOf(processor.process).returns.toEqualTypeOf<ProcessedUser>();
638
651
  });
652
+
653
+ it('should infer correct return types', () => {
654
+ const result = getData();
655
+
656
+ // 验证返回值类型
657
+ expectTypeOf(result).toEqualTypeOf<{ id: number; name: string }>();
658
+ expectTypeOf(result).not.toEqualTypeOf<{ id: string; name: string }>();
659
+ });
660
+
661
+ it('should validate parameter types', () => {
662
+ function processUser(user: User): void {
663
+ // implementation
664
+ }
665
+
666
+ // 验证参数类型
667
+ expectTypeOf(processUser).parameter(0).toMatchTypeOf<{ id: number }>();
668
+ expectTypeOf(processUser).parameter(0).toHaveProperty('id');
669
+ });
670
+ });
671
+ ```
672
+
673
+ #### 泛型类型测试
674
+
675
+ ```typescript
676
+ describe('Generic Type Tests', () => {
677
+ it('should work with generic constraints', () => {
678
+ class Storage<T extends { id: number }> {
679
+ store(item: T): T {
680
+ return item;
681
+ }
682
+ }
683
+
684
+ const storage = new Storage<User>();
685
+
686
+ // 验证泛型类型
687
+ expectTypeOf(storage.store).parameter(0).toMatchTypeOf<User>();
688
+ expectTypeOf(storage.store).returns.toMatchTypeOf<User>();
689
+ });
690
+
691
+ it('should validate complex generic types', () => {
692
+ type ApiResponse<T> = {
693
+ data: T;
694
+ status: number;
695
+ message?: string;
696
+ };
697
+
698
+ const response: ApiResponse<User[]> = {
699
+ data: [],
700
+ status: 200
701
+ };
702
+
703
+ // 验证嵌套泛型类型
704
+ expectTypeOf(response).toMatchTypeOf<ApiResponse<User[]>>();
705
+ expectTypeOf(response.data).toEqualTypeOf<User[]>();
706
+ });
707
+ });
708
+ ```
709
+
710
+ #### 联合类型与交叉类型测试
711
+
712
+ ```typescript
713
+ describe('Union and Intersection Types', () => {
714
+ it('should handle union types correctly', () => {
715
+ type Result = Success | Error;
716
+ type Success = { status: 'success'; data: string };
717
+ type Error = { status: 'error'; message: string };
718
+
719
+ function handleResult(result: Result): void {
720
+ // implementation
721
+ }
722
+
723
+ // 验证联合类型
724
+ expectTypeOf(handleResult).parameter(0).toMatchTypeOf<Success>();
725
+ expectTypeOf(handleResult).parameter(0).toMatchTypeOf<Error>();
726
+ });
727
+
728
+ it('should handle intersection types correctly', () => {
729
+ type Timestamped = { createdAt: Date; updatedAt: Date };
730
+ type UserWithTimestamp = User & Timestamped;
731
+
732
+ const user: UserWithTimestamp = {
733
+ id: 1,
734
+ name: 'John',
735
+ createdAt: new Date(),
736
+ updatedAt: new Date()
737
+ };
738
+
739
+ // 验证交叉类型包含所有属性
740
+ expectTypeOf(user).toHaveProperty('id');
741
+ expectTypeOf(user).toHaveProperty('name');
742
+ expectTypeOf(user).toHaveProperty('createdAt');
743
+ expectTypeOf(user).toHaveProperty('updatedAt');
744
+ });
639
745
  });
640
746
  ```
641
747
 
748
+ #### 类型窄化测试
749
+
750
+ ```typescript
751
+ describe('Type Narrowing Tests', () => {
752
+ it('should validate type guards', () => {
753
+ function isString(value: unknown): value is string {
754
+ return typeof value === 'string';
755
+ }
756
+
757
+ const value: unknown = 'test';
758
+
759
+ if (isString(value)) {
760
+ // 在此作用域内,value 应该被窄化为 string 类型
761
+ expectTypeOf(value).toEqualTypeOf<string>();
762
+ }
763
+ });
764
+
765
+ it('should validate discriminated unions', () => {
766
+ type Shape =
767
+ | { kind: 'circle'; radius: number }
768
+ | { kind: 'rectangle'; width: number; height: number };
769
+
770
+ function getArea(shape: Shape): number {
771
+ if (shape.kind === 'circle') {
772
+ // 在此分支,shape 应该被窄化为 circle 类型
773
+ expectTypeOf(shape).toHaveProperty('radius');
774
+ expectTypeOf(shape).not.toHaveProperty('width');
775
+ return Math.PI * shape.radius ** 2;
776
+ } else {
777
+ // 在此分支,shape 应该被窄化为 rectangle 类型
778
+ expectTypeOf(shape).toHaveProperty('width');
779
+ expectTypeOf(shape).toHaveProperty('height');
780
+ return shape.width * shape.height;
781
+ }
782
+ }
783
+ });
784
+ });
785
+ ```
786
+
787
+ #### 实用建议
788
+
789
+ 1. **结合运行时测试**:类型测试应该与运行时测试相辅相成
790
+
791
+ ```typescript
792
+ describe('Combined Runtime and Type Tests', () => {
793
+ it('should validate both runtime behavior and types', () => {
794
+ function add(a: number, b: number): number {
795
+ return a + b;
796
+ }
797
+
798
+ // 类型测试
799
+ expectTypeOf(add).parameter(0).toEqualTypeOf<number>();
800
+ expectTypeOf(add).returns.toEqualTypeOf<number>();
801
+
802
+ // 运行时测试
803
+ expect(add(1, 2)).toBe(3);
804
+ expect(add(-1, 1)).toBe(0);
805
+ });
806
+ });
807
+ ```
808
+
809
+ 2. **测试类型推断**:确保 TypeScript 能正确推断类型,避免过度使用 `any`
810
+
811
+ ```typescript
812
+ describe('Type Inference Tests', () => {
813
+ it('should infer types correctly', () => {
814
+ const data = { id: 1, name: 'John' };
815
+
816
+ // 验证推断的类型
817
+ expectTypeOf(data).toEqualTypeOf<{ id: number; name: string }>();
818
+ expectTypeOf(data.id).toEqualTypeOf<number>();
819
+ expectTypeOf(data.name).toEqualTypeOf<string>();
820
+ });
821
+ });
822
+ ```
823
+
824
+ 3. **使用 TypeScript 编译器检查**:在 CI 中运行 `tsc --noEmit` 确保没有类型错误
825
+
826
+ ```bash
827
+ # 在 package.json 中添加脚本
828
+ {
829
+ "scripts": {
830
+ "type-check": "tsc --noEmit",
831
+ "test": "pnpm type-check && vitest run"
832
+ }
833
+ }
834
+ ```
835
+
642
836
  ---
643
837
 
644
838
  ## 性能测试
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qlover/create-app",
3
- "version": "0.10.6",
3
+ "version": "0.11.0",
4
4
  "description": "Create a new app with a single command",
5
5
  "private": false,
6
6
  "type": "module",