@aerogel/cli 0.0.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/.eslintrc.js +7 -0
- package/bin/ag +4 -0
- package/dist/aerogel-cli.cjs.js +2 -0
- package/dist/aerogel-cli.cjs.js.map +1 -0
- package/dist/aerogel-cli.d.ts +10 -0
- package/dist/aerogel-cli.esm.js +2 -0
- package/dist/aerogel-cli.esm.js.map +1 -0
- package/noeldemartin.config.js +4 -0
- package/package.json +46 -0
- package/src/cli.ts +23 -0
- package/src/commands/Command.ts +59 -0
- package/src/commands/create.test.ts +25 -0
- package/src/commands/create.ts +69 -0
- package/src/commands/generate-component.test.ts +33 -0
- package/src/commands/generate-component.ts +65 -0
- package/src/commands/generate-model.test.ts +34 -0
- package/src/commands/generate-model.ts +73 -0
- package/src/lib/App.ts +25 -0
- package/src/lib/File.mock.ts +66 -0
- package/src/lib/File.ts +66 -0
- package/src/lib/Log.mock.ts +28 -0
- package/src/lib/Log.test.ts +21 -0
- package/src/lib/Log.ts +79 -0
- package/src/lib/Shell.mock.ts +20 -0
- package/src/lib/Shell.ts +28 -0
- package/src/lib/Template.ts +55 -0
- package/src/lib/utils.test.ts +33 -0
- package/src/lib/utils.ts +44 -0
- package/src/main.ts +3 -0
- package/src/testing/setup.ts +42 -0
- package/src/types/ts-reset.d.ts +1 -0
- package/templates/app/.github/workflows/ci.yml +17 -0
- package/templates/app/.nvmrc +1 -0
- package/templates/app/cypress/e2e/app.cy.ts +9 -0
- package/templates/app/cypress/support/e2e.ts +3 -0
- package/templates/app/cypress/tsconfig.json +12 -0
- package/templates/app/cypress.config.ts +8 -0
- package/templates/app/index.html +12 -0
- package/templates/app/package.json +44 -0
- package/templates/app/postcss.config.js +6 -0
- package/templates/app/src/App.vue +10 -0
- package/templates/app/src/assets/styles.css +3 -0
- package/templates/app/src/lang/en.yaml +3 -0
- package/templates/app/src/main.test.ts +9 -0
- package/templates/app/src/main.ts +9 -0
- package/templates/app/src/types/globals.d.ts +3 -0
- package/templates/app/src/types/shims.d.ts +7 -0
- package/templates/app/src/types/ts-reset.d.ts +1 -0
- package/templates/app/tailwind.config.js +5 -0
- package/templates/app/tsconfig.json +18 -0
- package/templates/app/vite.config.ts +21 -0
- package/templates/component/[component.name].vue +3 -0
- package/templates/component-story/[component.name].story.vue +7 -0
- package/templates/model/[model.name].schema.ts +7 -0
- package/templates/model/[model.name].ts +3 -0
- package/tsconfig.json +11 -0
- package/vite.config.ts +14 -0
package/.eslintrc.js
ADDED
package/bin/ag
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:!0});var e=require("commander"),t=require("@babel/runtime/helpers/defineProperty"),s=require("@noeldemartin/utils"),n=require("fs"),r=require("path");require("core-js/modules/esnext.async-iterator.for-each.js"),require("core-js/modules/esnext.iterator.constructor.js"),require("core-js/modules/esnext.iterator.for-each.js");var i=require("chalk"),o=require("readline");require("core-js/modules/esnext.async-iterator.reduce.js"),require("core-js/modules/esnext.iterator.reduce.js");var a=require("mustache"),c=require("child_process");function l(e){return e&&"object"==typeof e&&"default"in e?e:{default:e}}require("core-js/modules/esnext.async-iterator.map.js"),require("core-js/modules/esnext.iterator.map.js");var d=l(t);var u=s.facade(new class{exists(e){return n.existsSync(e)}read(e){return this.isFile(e)?n.readFileSync(e).toString():null}getFiles(e){const t=n.readdirSync(e,{withFileTypes:!0}),s=[];for(const n of t){const t=r.resolve(e,n.name);n.isDirectory()?s.push(...this.getFiles(t)):s.push(t)}return s}isDirectory(e){return this.exists(e)&&n.lstatSync(e).isDirectory()}isFile(e){return this.exists(e)&&n.lstatSync(e).isFile()}isEmptyDirectory(e){return!!this.isDirectory(e)&&0===this.getFiles(e).length}makeDirectory(e){n.mkdirSync(e,{recursive:!0})}write(e,t){n.existsSync(r.dirname(e))||n.mkdirSync(r.dirname(e),{recursive:!0}),n.writeFileSync(e,t)}});var p=s.facade(new class{constructor(){d.default(this,"renderInfo",i.hex("#00ffff")),d.default(this,"renderSuccess",i.hex("#00ff00")),d.default(this,"renderError",i.hex("#ff0000"))}async animate(e,t){var s=this;const n=function(){let t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:"";const n=s.renderInfo(s.renderMarkdown(e)+".".repeat(r%4))+t;s.stdout(n)};let r=0;n();const i=setInterval((()=>(r++,n())),1e3),o=await t();return clearInterval(i),n("\n"),o}info(e){s.arrayFrom(e).forEach((e=>{this.log(this.renderInfo(this.renderMarkdown(e)))}))}error(e){s.arrayFrom(e).forEach((e=>{this.log(this.renderError(this.renderMarkdown(e)))}))}fail(e){this.error(e),process.exit(1)}success(e){s.arrayFrom(e).forEach((e=>{this.log(this.renderSuccess(this.renderMarkdown(e)))}))}renderMarkdown(e){const t=s.stringMatchAll(e,/\*\*(.*)\*\*/g);for(const s of t)e=e.replace(s[0],i.bold(s[1]));return e}log(e){s.arrayFrom(e).forEach((e=>console.log(e)))}stdout(e){o.cursorTo(process.stdout,0),o.clearLine(process.stdout,0),process.stdout.write(e)}});class m{static instantiate(e,t,s){return new m(e).instantiate(t,s)}constructor(e){d.default(this,"path",void 0),this.path=e}instantiate(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};const s=this.getFilenameReplacements(t),r=[];e=`${e}/`.replace(/\/\//,"/");for(const i of u.getFiles(this.path)){const o=Object.entries(s).reduce(((e,t)=>{let[s,n]=t;return e.replaceAll(s,n)}),i.substring(this.path.length+1)),c=n.readFileSync(i).toString();u.write(e+o,a.render(c,t,void 0,["<%","%>"])),r.push(e+o)}return r}getFilenameReplacements(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"";return Object.entries(e).reduce(((e,n)=>{let[r,i]=n;return"object"==typeof i?Object.assign(e,this.getFilenameReplacements(i,`${r}.`)):e[`[${t}${r}]`]=s.toString(i),e}),{})}}function h(e){return r.resolve(__dirname,"../",e)}class f{constructor(e){d.default(this,"name",void 0),this.name=e}create(e){!u.exists(e)||u.isDirectory(e)&&u.isEmptyDirectory(e)||p.fail(`Folder at '${e}' already exists!`),m.instantiate(h("templates/app"),e,{app:{name:this.name,slug:s.stringToSlug(this.name)}})}}class g{static define(e){var t=this;e=e.command(this.command).description(this.description);for(const[t,s]of this.parameters)e=e.argument(`<${t}>`,s);for(const[t,s]of Object.entries(this.options)){const n="string"==typeof s?s:s.description,r="string"==typeof s?"string":s.type??"string";e=e.option("boolean"===r?`--${t}`:`--${t} <${r}>`,n)}e=e.action((function(){for(var e=arguments.length,s=new Array(e),n=0;n<e;n++)s[n]=arguments[n];return t.run.call(t,...s)}))}static async run(){for(var e=arguments.length,t=new Array(e),s=0;s<e;s++)t[s]=arguments[s];const n=new this(...t);await n.run()}async run(){}assertAerogelOrDirectory(e){const t=u.read("package.json");if(t?.includes("@aerogel/core"))return;if(e&&u.isDirectory(e))return;const s=e?`${e} folder does not exist.`:"package.json does not contain @aerogel/core.";p.fail(`${s} Are you sure this is an Aerogel app?`)}}d.default(g,"command",""),d.default(g,"description",""),d.default(g,"parameters",[]),d.default(g,"options",{});var y=s.facade(new class{constructor(){d.default(this,"cwd",null)}setWorkingDirectory(e){this.cwd=e}async run(e){await new Promise(((t,s)=>{c.exec(e,{cwd:this.cwd??void 0},(e=>{e?s(e):t()}))}))}});class w extends g{constructor(e,t){super(),d.default(this,"path",void 0),d.default(this,"options",void 0),this.path=e,this.options=t}async run(){const e=this.path,t=this.options.name??"Aerogel App";y.setWorkingDirectory(e),await this.createApp(t,e),await this.installDependencies(),await this.initializeGit(),p.success(["",`That's it! You can start working on **${t}** doing the following:`,` cd ${e}`," npm run dev","","Have fun!"])}async createApp(e,t){p.info(`Creating **${e}**...`),new f(e).create(t)}async installDependencies(){await p.animate("Installing dependencies",(async()=>{await y.run("npm install")}))}async initializeGit(){await p.animate("Initializing git",(async()=>{await y.run("git init"),await y.run("git add ."),await y.run('git commit -m "Start"')}))}}d.default(w,"command","create"),d.default(w,"description","Create AerogelJS app"),d.default(w,"parameters",[["path","Application path"]]),d.default(w,"options",{name:"Application name"});class v extends g{constructor(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};super(),d.default(this,"name",void 0),d.default(this,"options",void 0),this.name=e,this.options=t}async run(){this.assertAerogelOrDirectory("src/components"),this.options.story&&this.assertHistoireInstalled(),u.exists(`src/components/${this.name}.vue`)&&p.fail(`${this.name} component already exists!`);const e=m.instantiate(h("templates/component"),"src/components",{component:{name:this.name}});if(this.options.story){const t=m.instantiate(h("templates/component-story"),"src/components",{component:{name:this.name}});e.push(...t)}const t=e.map((e=>`- ${e}`)).join("\n");p.info(`${this.name} component created successfully! The following files were created:\n\n${t}`)}assertHistoireInstalled(){u.exists("src/main.histoire.ts")||p.fail("Histoire is not installed yet!")}}d.default(v,"command","generate:component"),d.default(v,"description","Generate an AerogelJS component"),d.default(v,"parameters",[["name","Component name"]]),d.default(v,"options",{story:{description:"Create component story using Histoire",type:"boolean"}});class x extends g{constructor(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};super(),d.default(this,"name",void 0),d.default(this,"options",void 0),this.name=e,this.options=t}async run(){this.assertAerogelOrDirectory("src/models"),u.exists(`src/models/${this.name}.ts`)&&p.fail(`${this.name} model already exists!`);const e=m.instantiate(h("templates/model"),"src/models",{model:{name:this.name,fieldsDefinition:this.getFieldsDefinition()},soukaiImports:this.options.fields?"FieldType, defineModelSchema":"defineModelSchema"}).map((e=>`- ${e}`)).join("\n");p.info(`${this.name} model created successfully! The following files were created:\n\n${e}`)}getFieldsDefinition(){if(!this.options.fields)return" //";return function(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};const s=e.split("\n"),n=t.indent??0;let r=0,i="";for(const e of s){const t=e.trim(),s=0===t.length;if(0===i.length){if(s)continue;r=e.indexOf(t[0]??""),i+=`${" ".repeat(n)}${t}\n`;continue}if(s){i+="\n";continue}const o=e.indexOf(t[0]??"");i+=`${" ".repeat(n+o-r)}${t}\n`}return i.trimEnd()}(this.options.fields.split(",").map((e=>{const[t,n]=e.split(":");return{name:t,type:s.stringToStudlyCase(n??"string")}})).reduce(((e,t)=>e+`\n${t.name}: FieldType.${t.type},`),""),{indent:8})}}d.default(x,"command","generate:model"),d.default(x,"description","Generate an AerogelJS model"),d.default(x,"parameters",[["name","Model name"]]),d.default(x,"options",{fields:"Create model with the given fields"});var $=s.facade(new class{run(t){const s=new e.Command;s.name("ag").description("AerogelJS CLI").version("0.0.0"),w.define(s),v.define(s),x.define(s),s.parse(t)}});exports.CLI=$;
|
|
2
|
+
//# sourceMappingURL=aerogel-cli.cjs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"aerogel-cli.cjs.js","sources":[],"sourcesContent":[],"names":[],"mappings":""}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{Command as e}from"commander";import t from"@babel/runtime/helpers/esm/defineProperty";import{facade as s,arrayFrom as n,stringMatchAll as i,toString as r,stringToSlug as o,stringToStudlyCase as a}from"@noeldemartin/utils";import{existsSync as c,readFileSync as l,readdirSync as m,lstatSync as p,mkdirSync as d,writeFileSync as h}from"fs";import{resolve as u,dirname as f}from"path";import"core-js/modules/esnext.async-iterator.for-each.js";import"core-js/modules/esnext.iterator.constructor.js";import"core-js/modules/esnext.iterator.for-each.js";import{hex as g,bold as y}from"chalk";import{cursorTo as w,clearLine as v}from"readline";import"core-js/modules/esnext.async-iterator.reduce.js";import"core-js/modules/esnext.iterator.reduce.js";import{render as $}from"mustache";import{exec as x}from"child_process";import"core-js/modules/esnext.async-iterator.map.js";import"core-js/modules/esnext.iterator.map.js";var j=s(new class{exists(e){return c(e)}read(e){return this.isFile(e)?l(e).toString():null}getFiles(e){const t=m(e,{withFileTypes:!0}),s=[];for(const n of t){const t=u(e,n.name);n.isDirectory()?s.push(...this.getFiles(t)):s.push(t)}return s}isDirectory(e){return this.exists(e)&&p(e).isDirectory()}isFile(e){return this.exists(e)&&p(e).isFile()}isEmptyDirectory(e){return!!this.isDirectory(e)&&0===this.getFiles(e).length}makeDirectory(e){d(e,{recursive:!0})}write(e,t){c(f(e))||d(f(e),{recursive:!0}),h(e,t)}});var D=s(new class{constructor(){t(this,"renderInfo",g("#00ffff")),t(this,"renderSuccess",g("#00ff00")),t(this,"renderError",g("#ff0000"))}async animate(e,t){var s=this;const n=function(){let t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:"";const n=s.renderInfo(s.renderMarkdown(e)+".".repeat(i%4))+t;s.stdout(n)};let i=0;n();const r=setInterval((()=>(i++,n())),1e3),o=await t();return clearInterval(r),n("\n"),o}info(e){n(e).forEach((e=>{this.log(this.renderInfo(this.renderMarkdown(e)))}))}error(e){n(e).forEach((e=>{this.log(this.renderError(this.renderMarkdown(e)))}))}fail(e){this.error(e),process.exit(1)}success(e){n(e).forEach((e=>{this.log(this.renderSuccess(this.renderMarkdown(e)))}))}renderMarkdown(e){const t=i(e,/\*\*(.*)\*\*/g);for(const s of t)e=e.replace(s[0],y(s[1]));return e}log(e){n(e).forEach((e=>console.log(e)))}stdout(e){w(process.stdout,0),v(process.stdout,0),process.stdout.write(e)}});class A{static instantiate(e,t,s){return new A(e).instantiate(t,s)}constructor(e){t(this,"path",void 0),this.path=e}instantiate(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};const s=this.getFilenameReplacements(t),n=[];e=`${e}/`.replace(/\/\//,"/");for(const i of j.getFiles(this.path)){const r=Object.entries(s).reduce(((e,t)=>{let[s,n]=t;return e.replaceAll(s,n)}),i.substring(this.path.length+1)),o=l(i).toString();j.write(e+r,$(o,t,void 0,["<%","%>"])),n.push(e+r)}return n}getFilenameReplacements(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"";return Object.entries(e).reduce(((e,s)=>{let[n,i]=s;return"object"==typeof i?Object.assign(e,this.getFilenameReplacements(i,`${n}.`)):e[`[${t}${n}]`]=r(i),e}),{})}}function F(e){return u(__dirname,"../",e)}class k{constructor(e){t(this,"name",void 0),this.name=e}create(e){!j.exists(e)||j.isDirectory(e)&&j.isEmptyDirectory(e)||D.fail(`Folder at '${e}' already exists!`),A.instantiate(F("templates/app"),e,{app:{name:this.name,slug:o(this.name)}})}}class I{static define(e){var t=this;e=e.command(this.command).description(this.description);for(const[t,s]of this.parameters)e=e.argument(`<${t}>`,s);for(const[t,s]of Object.entries(this.options)){const n="string"==typeof s?s:s.description,i="string"==typeof s?"string":s.type??"string";e=e.option("boolean"===i?`--${t}`:`--${t} <${i}>`,n)}e=e.action((function(){for(var e=arguments.length,s=new Array(e),n=0;n<e;n++)s[n]=arguments[n];return t.run.call(t,...s)}))}static async run(){for(var e=arguments.length,t=new Array(e),s=0;s<e;s++)t[s]=arguments[s];const n=new this(...t);await n.run()}async run(){}assertAerogelOrDirectory(e){const t=j.read("package.json");if(t?.includes("@aerogel/core"))return;if(e&&j.isDirectory(e))return;const s=e?`${e} folder does not exist.`:"package.json does not contain @aerogel/core.";D.fail(`${s} Are you sure this is an Aerogel app?`)}}t(I,"command",""),t(I,"description",""),t(I,"parameters",[]),t(I,"options",{});var S=s(new class{constructor(){t(this,"cwd",null)}setWorkingDirectory(e){this.cwd=e}async run(e){await new Promise(((t,s)=>{x(e,{cwd:this.cwd??void 0},(e=>{e?s(e):t()}))}))}});class b extends I{constructor(e,s){super(),t(this,"path",void 0),t(this,"options",void 0),this.path=e,this.options=s}async run(){const e=this.path,t=this.options.name??"Aerogel App";S.setWorkingDirectory(e),await this.createApp(t,e),await this.installDependencies(),await this.initializeGit(),D.success(["",`That's it! You can start working on **${t}** doing the following:`,` cd ${e}`," npm run dev","","Have fun!"])}async createApp(e,t){D.info(`Creating **${e}**...`),new k(e).create(t)}async installDependencies(){await D.animate("Installing dependencies",(async()=>{await S.run("npm install")}))}async initializeGit(){await D.animate("Initializing git",(async()=>{await S.run("git init"),await S.run("git add ."),await S.run('git commit -m "Start"')}))}}t(b,"command","create"),t(b,"description","Create AerogelJS app"),t(b,"parameters",[["path","Application path"]]),t(b,"options",{name:"Application name"});class E extends I{constructor(e){let s=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};super(),t(this,"name",void 0),t(this,"options",void 0),this.name=e,this.options=s}async run(){this.assertAerogelOrDirectory("src/components"),this.options.story&&this.assertHistoireInstalled(),j.exists(`src/components/${this.name}.vue`)&&D.fail(`${this.name} component already exists!`);const e=A.instantiate(F("templates/component"),"src/components",{component:{name:this.name}});if(this.options.story){const t=A.instantiate(F("templates/component-story"),"src/components",{component:{name:this.name}});e.push(...t)}const t=e.map((e=>`- ${e}`)).join("\n");D.info(`${this.name} component created successfully! The following files were created:\n\n${t}`)}assertHistoireInstalled(){j.exists("src/main.histoire.ts")||D.fail("Histoire is not installed yet!")}}t(E,"command","generate:component"),t(E,"description","Generate an AerogelJS component"),t(E,"parameters",[["name","Component name"]]),t(E,"options",{story:{description:"Create component story using Histoire",type:"boolean"}});class O extends I{constructor(e){let s=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};super(),t(this,"name",void 0),t(this,"options",void 0),this.name=e,this.options=s}async run(){this.assertAerogelOrDirectory("src/models"),j.exists(`src/models/${this.name}.ts`)&&D.fail(`${this.name} model already exists!`);const e=A.instantiate(F("templates/model"),"src/models",{model:{name:this.name,fieldsDefinition:this.getFieldsDefinition()},soukaiImports:this.options.fields?"FieldType, defineModelSchema":"defineModelSchema"}).map((e=>`- ${e}`)).join("\n");D.info(`${this.name} model created successfully! The following files were created:\n\n${e}`)}getFieldsDefinition(){if(!this.options.fields)return" //";return function(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};const s=e.split("\n"),n=t.indent??0;let i=0,r="";for(const e of s){const t=e.trim(),s=0===t.length;if(0===r.length){if(s)continue;i=e.indexOf(t[0]??""),r+=`${" ".repeat(n)}${t}\n`;continue}if(s){r+="\n";continue}const o=e.indexOf(t[0]??"");r+=`${" ".repeat(n+o-i)}${t}\n`}return r.trimEnd()}(this.options.fields.split(",").map((e=>{const[t,s]=e.split(":");return{name:t,type:a(s??"string")}})).reduce(((e,t)=>e+`\n${t.name}: FieldType.${t.type},`),""),{indent:8})}}t(O,"command","generate:model"),t(O,"description","Generate an AerogelJS model"),t(O,"parameters",[["name","Model name"]]),t(O,"options",{fields:"Create model with the given fields"});var C=s(new class{run(t){const s=new e;s.name("ag").description("AerogelJS CLI").version("0.0.0"),b.define(s),E.define(s),O.define(s),s.parse(t)}});export{C as CLI};
|
|
2
|
+
//# sourceMappingURL=aerogel-cli.esm.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"aerogel-cli.esm.js","sources":[],"sourcesContent":[],"names":[],"mappings":""}
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@aerogel/cli",
|
|
3
|
+
"description": "Aerogel CLI",
|
|
4
|
+
"version": "0.0.0",
|
|
5
|
+
"main": "dist/aerogel-cli.cjs.js",
|
|
6
|
+
"module": "dist/aerogel-cli.esm.js",
|
|
7
|
+
"types": "dist/aerogel-cli.d.ts",
|
|
8
|
+
"sideEffects": false,
|
|
9
|
+
"bin": {
|
|
10
|
+
"ag": "bin/ag"
|
|
11
|
+
},
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "rm dist -rf && npm run build:js && npm run build:types",
|
|
14
|
+
"build:js": "noeldemartin-build-javascript",
|
|
15
|
+
"build:types": "noeldemartin-build-types",
|
|
16
|
+
"lint": "noeldemartin-lint src",
|
|
17
|
+
"publish-next": "noeldemartin-publish-next",
|
|
18
|
+
"test": "vitest --run"
|
|
19
|
+
},
|
|
20
|
+
"engines": {
|
|
21
|
+
"node": ">=18.x"
|
|
22
|
+
},
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "https://github.com/NoelDeMartin/aerogeljs.git"
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"javascript",
|
|
29
|
+
"aerogel",
|
|
30
|
+
"cli"
|
|
31
|
+
],
|
|
32
|
+
"author": "Noel De Martin",
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"bugs": {
|
|
35
|
+
"url": "https://github.com/NoelDeMartin/aerogeljs/issues"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@noeldemartin/utils": "0.4.0-next.ac00beaecf32bb02ed8e335225d7948d946d73bd",
|
|
39
|
+
"chalk": "^4.1.2",
|
|
40
|
+
"commander": "^11.0.0",
|
|
41
|
+
"mustache": "^4.2.0"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@types/node": "^20.4.0"
|
|
45
|
+
}
|
|
46
|
+
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { CreateCommand } from '@/commands/create';
|
|
3
|
+
import { facade } from '@noeldemartin/utils';
|
|
4
|
+
import { GenerateComponentCommand } from '@/commands/generate-component';
|
|
5
|
+
import { GenerateModelCommand } from '@/commands/generate-model';
|
|
6
|
+
|
|
7
|
+
export class CLIService {
|
|
8
|
+
|
|
9
|
+
public run(argv?: string[]): void {
|
|
10
|
+
const program = new Command();
|
|
11
|
+
|
|
12
|
+
program.name('ag').description('AerogelJS CLI').version('0.0.0');
|
|
13
|
+
|
|
14
|
+
CreateCommand.define(program);
|
|
15
|
+
GenerateComponentCommand.define(program);
|
|
16
|
+
GenerateModelCommand.define(program);
|
|
17
|
+
|
|
18
|
+
program.parse(argv);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export default facade(new CLIService());
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import File from '@/lib/File';
|
|
2
|
+
import Log from '@/lib/Log';
|
|
3
|
+
import type { Constructor } from '@noeldemartin/utils';
|
|
4
|
+
import type { Command as CommanderCommand } from 'commander';
|
|
5
|
+
|
|
6
|
+
export type CommandConstructor<T extends Command = Command> = Constructor<T>;
|
|
7
|
+
export type CommandOptions = Record<string, string | { description: string; type?: string }>;
|
|
8
|
+
|
|
9
|
+
export default class Command {
|
|
10
|
+
|
|
11
|
+
public static command: string = '';
|
|
12
|
+
public static description: string = '';
|
|
13
|
+
public static parameters: [string, string][] = [];
|
|
14
|
+
public static options: CommandOptions = {};
|
|
15
|
+
|
|
16
|
+
public static define(program: CommanderCommand): void {
|
|
17
|
+
program = program.command(this.command).description(this.description);
|
|
18
|
+
|
|
19
|
+
for (const [name, description] of this.parameters) {
|
|
20
|
+
program = program.argument(`<${name}>`, description);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
for (const [name, definition] of Object.entries(this.options)) {
|
|
24
|
+
const description = typeof definition === 'string' ? definition : definition.description;
|
|
25
|
+
const type = typeof definition === 'string' ? 'string' : definition.type ?? 'string';
|
|
26
|
+
|
|
27
|
+
program = program.option(type === 'boolean' ? `--${name}` : `--${name} <${type}>`, description);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
program = program.action((...args) => this.run.call(this, ...args));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
public static async run<T extends CommandConstructor>(this: T, ...args: ConstructorParameters<T>): Promise<void> {
|
|
34
|
+
const instance = new this(...args);
|
|
35
|
+
|
|
36
|
+
await instance.run();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
public async run(): Promise<void> {
|
|
40
|
+
//
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
protected assertAerogelOrDirectory(path?: string): void {
|
|
44
|
+
const packageJson = File.read('package.json');
|
|
45
|
+
|
|
46
|
+
if (packageJson?.includes('@aerogel/core')) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (path && File.isDirectory(path)) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const message = path ? `${path} folder does not exist.` : 'package.json does not contain @aerogel/core.';
|
|
55
|
+
|
|
56
|
+
Log.fail(`${message} Are you sure this is an Aerogel app?`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { describe, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import FileMock from '@/lib/File.mock';
|
|
4
|
+
import ShellMock from '@/lib/Shell.mock';
|
|
5
|
+
|
|
6
|
+
import { CreateCommand } from './create';
|
|
7
|
+
|
|
8
|
+
describe('Create command', () => {
|
|
9
|
+
|
|
10
|
+
it('works', async () => {
|
|
11
|
+
// Act
|
|
12
|
+
await CreateCommand.run('./app', { name: 'My App' });
|
|
13
|
+
|
|
14
|
+
// Assert
|
|
15
|
+
FileMock.expectCreated('./app/package.json').toContain('"name": "my-app"');
|
|
16
|
+
FileMock.expectCreated('./app/index.html').toContain('My App');
|
|
17
|
+
FileMock.expectCreated('./app/src/App.vue').toContain(
|
|
18
|
+
'<h1 class="text-4xl font-semibold">{{ $t(\'home.title\') }}</h1>',
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
ShellMock.expectRan('npm install');
|
|
22
|
+
ShellMock.expectRan('git init');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import App from '@/lib/App';
|
|
2
|
+
import Command from '@/commands/Command';
|
|
3
|
+
import Log from '@/lib/Log';
|
|
4
|
+
import Shell from '@/lib/Shell';
|
|
5
|
+
|
|
6
|
+
export interface Options {
|
|
7
|
+
name?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export class CreateCommand extends Command {
|
|
11
|
+
|
|
12
|
+
public static command: string = 'create';
|
|
13
|
+
public static description: string = 'Create AerogelJS app';
|
|
14
|
+
public static parameters: [string, string][] = [['path', 'Application path']];
|
|
15
|
+
public static options: Record<string, string> = {
|
|
16
|
+
name: 'Application name',
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
private path: string;
|
|
20
|
+
private options: Options;
|
|
21
|
+
|
|
22
|
+
constructor(path: string, options: Options) {
|
|
23
|
+
super();
|
|
24
|
+
|
|
25
|
+
this.path = path;
|
|
26
|
+
this.options = options;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
public async run(): Promise<void> {
|
|
30
|
+
const path = this.path;
|
|
31
|
+
const name = this.options.name ?? 'Aerogel App';
|
|
32
|
+
|
|
33
|
+
Shell.setWorkingDirectory(path);
|
|
34
|
+
|
|
35
|
+
await this.createApp(name, path);
|
|
36
|
+
await this.installDependencies();
|
|
37
|
+
await this.initializeGit();
|
|
38
|
+
|
|
39
|
+
Log.success([
|
|
40
|
+
'',
|
|
41
|
+
`That's it! You can start working on **${name}** doing the following:`,
|
|
42
|
+
` cd ${path}`,
|
|
43
|
+
' npm run dev',
|
|
44
|
+
'',
|
|
45
|
+
'Have fun!',
|
|
46
|
+
]);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
protected async createApp(name: string, path: string): Promise<void> {
|
|
50
|
+
Log.info(`Creating **${name}**...`);
|
|
51
|
+
|
|
52
|
+
new App(name).create(path);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
protected async installDependencies(): Promise<void> {
|
|
56
|
+
await Log.animate('Installing dependencies', async () => {
|
|
57
|
+
await Shell.run('npm install');
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
protected async initializeGit(): Promise<void> {
|
|
62
|
+
await Log.animate('Initializing git', async () => {
|
|
63
|
+
await Shell.run('git init');
|
|
64
|
+
await Shell.run('git add .');
|
|
65
|
+
await Shell.run('git commit -m "Start"');
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { describe, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import FileMock from '@/lib/File.mock';
|
|
4
|
+
|
|
5
|
+
import { GenerateComponentCommand } from './generate-component';
|
|
6
|
+
|
|
7
|
+
describe('Generate component command', () => {
|
|
8
|
+
|
|
9
|
+
it('generates components', async () => {
|
|
10
|
+
// Arrange
|
|
11
|
+
FileMock.stub('package.json', '@aerogel/core');
|
|
12
|
+
|
|
13
|
+
// Act
|
|
14
|
+
await GenerateComponentCommand.run('FooBar');
|
|
15
|
+
|
|
16
|
+
// Assert
|
|
17
|
+
FileMock.expectCreated('src/components/FooBar.vue').toContain('<div>FooBar</div>');
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('generates components with stories', async () => {
|
|
21
|
+
// Arrange
|
|
22
|
+
FileMock.stub('package.json', '@aerogel/core');
|
|
23
|
+
FileMock.stub('src/main.histoire.ts');
|
|
24
|
+
|
|
25
|
+
// Act
|
|
26
|
+
await GenerateComponentCommand.run('FooBar', { story: true });
|
|
27
|
+
|
|
28
|
+
// Assert
|
|
29
|
+
FileMock.expectCreated('src/components/FooBar.vue').toContain('<div>FooBar</div>');
|
|
30
|
+
FileMock.expectCreated('src/components/FooBar.story.vue').toContain('<FooBar />');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import Command from '@/commands/Command';
|
|
2
|
+
import File from '@/lib/File';
|
|
3
|
+
import Log from '@/lib/Log';
|
|
4
|
+
import Template from '@/lib/Template';
|
|
5
|
+
import { basePath } from '@/lib/utils';
|
|
6
|
+
import type { CommandOptions } from '@/commands/Command';
|
|
7
|
+
|
|
8
|
+
export interface Options {
|
|
9
|
+
story?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class GenerateComponentCommand extends Command {
|
|
13
|
+
|
|
14
|
+
public static command: string = 'generate:component';
|
|
15
|
+
public static description: string = 'Generate an AerogelJS component';
|
|
16
|
+
public static parameters: [string, string][] = [['name', 'Component name']];
|
|
17
|
+
public static options: CommandOptions = {
|
|
18
|
+
story: {
|
|
19
|
+
description: 'Create component story using Histoire',
|
|
20
|
+
type: 'boolean',
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
private name: string;
|
|
25
|
+
private options: Options;
|
|
26
|
+
|
|
27
|
+
constructor(name: string, options: Options = {}) {
|
|
28
|
+
super();
|
|
29
|
+
|
|
30
|
+
this.name = name;
|
|
31
|
+
this.options = options;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
public async run(): Promise<void> {
|
|
35
|
+
this.assertAerogelOrDirectory('src/components');
|
|
36
|
+
this.options.story && this.assertHistoireInstalled();
|
|
37
|
+
|
|
38
|
+
if (File.exists(`src/components/${this.name}.vue`)) {
|
|
39
|
+
Log.fail(`${this.name} component already exists!`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const files = Template.instantiate(basePath('templates/component'), 'src/components', {
|
|
43
|
+
component: { name: this.name },
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
if (this.options.story) {
|
|
47
|
+
const storyFiles = Template.instantiate(basePath('templates/component-story'), 'src/components', {
|
|
48
|
+
component: { name: this.name },
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
files.push(...storyFiles);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const filesList = files.map((file) => `- ${file}`).join('\n');
|
|
55
|
+
|
|
56
|
+
Log.info(`${this.name} component created successfully! The following files were created:\n\n${filesList}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
protected assertHistoireInstalled(): void {
|
|
60
|
+
if (!File.exists('src/main.histoire.ts')) {
|
|
61
|
+
Log.fail('Histoire is not installed yet!');
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { describe, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import FileMock from '@/lib/File.mock';
|
|
4
|
+
import { formatCodeBlock } from '@/lib/utils';
|
|
5
|
+
|
|
6
|
+
import { GenerateModelCommand } from './generate-model';
|
|
7
|
+
|
|
8
|
+
describe('Generate model command', () => {
|
|
9
|
+
|
|
10
|
+
it('generates models', async () => {
|
|
11
|
+
// Arrange
|
|
12
|
+
FileMock.stub('package.json', '@aerogel/core');
|
|
13
|
+
|
|
14
|
+
// Act
|
|
15
|
+
await GenerateModelCommand.run('FooBar', {
|
|
16
|
+
fields: 'name:string,age:number,birth:Date',
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
// Assert
|
|
20
|
+
FileMock.expectCreated('src/models/FooBar.ts').toContain('import Model from \'./FooBar.schema\'');
|
|
21
|
+
FileMock.expectCreated('src/models/FooBar.schema.ts').toContain(
|
|
22
|
+
formatCodeBlock(`
|
|
23
|
+
defineModelSchema({
|
|
24
|
+
fields: {
|
|
25
|
+
name: FieldType.String,
|
|
26
|
+
age: FieldType.Number,
|
|
27
|
+
birth: FieldType.Date,
|
|
28
|
+
},
|
|
29
|
+
})
|
|
30
|
+
`),
|
|
31
|
+
);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { stringToStudlyCase } from '@noeldemartin/utils';
|
|
2
|
+
|
|
3
|
+
import Command from '@/commands/Command';
|
|
4
|
+
import File from '@/lib/File';
|
|
5
|
+
import Log from '@/lib/Log';
|
|
6
|
+
import Template from '@/lib/Template';
|
|
7
|
+
import { basePath, formatCodeBlock } from '@/lib/utils';
|
|
8
|
+
import type { CommandOptions } from '@/commands/Command';
|
|
9
|
+
|
|
10
|
+
interface Options {
|
|
11
|
+
fields?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class GenerateModelCommand extends Command {
|
|
15
|
+
|
|
16
|
+
public static command: string = 'generate:model';
|
|
17
|
+
public static description: string = 'Generate an AerogelJS model';
|
|
18
|
+
public static parameters: [string, string][] = [['name', 'Model name']];
|
|
19
|
+
public static options: CommandOptions = {
|
|
20
|
+
fields: 'Create model with the given fields',
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
private name: string;
|
|
24
|
+
private options: Options;
|
|
25
|
+
|
|
26
|
+
constructor(name: string, options: Options = {}) {
|
|
27
|
+
super();
|
|
28
|
+
|
|
29
|
+
this.name = name;
|
|
30
|
+
this.options = options;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
public async run(): Promise<void> {
|
|
34
|
+
this.assertAerogelOrDirectory('src/models');
|
|
35
|
+
|
|
36
|
+
if (File.exists(`src/models/${this.name}.ts`)) {
|
|
37
|
+
Log.fail(`${this.name} model already exists!`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const files = Template.instantiate(basePath('templates/model'), 'src/models', {
|
|
41
|
+
model: {
|
|
42
|
+
name: this.name,
|
|
43
|
+
fieldsDefinition: this.getFieldsDefinition(),
|
|
44
|
+
},
|
|
45
|
+
soukaiImports: this.options.fields ? 'FieldType, defineModelSchema' : 'defineModelSchema',
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const filesList = files.map((file) => `- ${file}`).join('\n');
|
|
49
|
+
|
|
50
|
+
Log.info(`${this.name} model created successfully! The following files were created:\n\n${filesList}`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
protected getFieldsDefinition(): string {
|
|
54
|
+
if (!this.options.fields) {
|
|
55
|
+
return ' //';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const code = this.options.fields
|
|
59
|
+
.split(',')
|
|
60
|
+
.map((field) => {
|
|
61
|
+
const [name, type] = field.split(':');
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
name,
|
|
65
|
+
type: stringToStudlyCase(type ?? 'string'),
|
|
66
|
+
};
|
|
67
|
+
})
|
|
68
|
+
.reduce((definition, field) => definition + `\n${field.name}: FieldType.${field.type},`, '');
|
|
69
|
+
|
|
70
|
+
return formatCodeBlock(code, { indent: 8 });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
}
|
package/src/lib/App.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { stringToSlug } from '@noeldemartin/utils';
|
|
2
|
+
|
|
3
|
+
import File from '@/lib/File';
|
|
4
|
+
import Log from '@/lib/Log';
|
|
5
|
+
import Template from '@/lib/Template';
|
|
6
|
+
import { basePath } from '@/lib/utils';
|
|
7
|
+
|
|
8
|
+
export default class App {
|
|
9
|
+
|
|
10
|
+
constructor(public name: string) {}
|
|
11
|
+
|
|
12
|
+
public create(path: string): void {
|
|
13
|
+
if (File.exists(path) && (!File.isDirectory(path) || !File.isEmptyDirectory(path))) {
|
|
14
|
+
Log.fail(`Folder at '${path}' already exists!`);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
Template.instantiate(basePath('templates/app'), path, {
|
|
18
|
+
app: {
|
|
19
|
+
name: this.name,
|
|
20
|
+
slug: stringToSlug(this.name),
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { expect } from 'vitest';
|
|
2
|
+
import { facade } from '@noeldemartin/utils';
|
|
3
|
+
import type { Assertion } from 'vitest';
|
|
4
|
+
|
|
5
|
+
import { FileService } from './File';
|
|
6
|
+
|
|
7
|
+
export class FileMockService extends FileService {
|
|
8
|
+
|
|
9
|
+
private virtualFilesystem: Record<string, string | { directory: true }> = {};
|
|
10
|
+
|
|
11
|
+
public exists(path: string): boolean {
|
|
12
|
+
return super.exists(path) || path in this.virtualFilesystem;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
public isDirectory(path: string): boolean {
|
|
16
|
+
return (
|
|
17
|
+
super.isDirectory(path) ||
|
|
18
|
+
(path in this.virtualFilesystem && typeof this.virtualFilesystem[path] === 'object')
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
public isFile(path: string): boolean {
|
|
23
|
+
return (
|
|
24
|
+
super.isFile(path) || (path in this.virtualFilesystem && typeof this.virtualFilesystem[path] === 'string')
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
public makeDirectory(path: string): void {
|
|
29
|
+
this.virtualFilesystem[path] = { directory: true };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
public read(path: string): string | null {
|
|
33
|
+
if (path in this.virtualFilesystem && typeof this.virtualFilesystem[path] === 'string') {
|
|
34
|
+
return this.virtualFilesystem[path] as string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return super.read(path);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
public write(path: string, contents: string): void {
|
|
41
|
+
this.virtualFilesystem[path] = contents;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
public reset(): void {
|
|
45
|
+
this.virtualFilesystem = {};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
public expectCreated(path: string, expectContent?: (contents: string) => void): Assertion<string> {
|
|
49
|
+
expect(typeof this.virtualFilesystem[path] === 'string', `expected '${path}' file to have been created`).toBe(
|
|
50
|
+
true,
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
const contents = (this.virtualFilesystem[path] as string) ?? '';
|
|
54
|
+
|
|
55
|
+
expectContent?.(contents);
|
|
56
|
+
|
|
57
|
+
return expect(contents);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
public stub(path: string, contents: string = ''): void {
|
|
61
|
+
this.virtualFilesystem[path] = contents;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export default facade(new FileMockService());
|