feat: CLI-утилита для генерации файлов из шаблонов
Реализовано: - Генерация файлов из папки .templates с подстановкой переменных - Цветной вывод в терминал с иконками и деревом файлов - Валидация аргументов и проверка существования папок - Поддержка --dry-run, --overwrite, произвольных переменных
This commit is contained in:
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
node_modules/
|
||||
dist/
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
1
.templates/component/{{name.pascalCase}}/index.ts
Normal file
1
.templates/component/{{name.pascalCase}}/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './{{name.pascalCase}}'
|
||||
@@ -0,0 +1,4 @@
|
||||
|
||||
.wrapper {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { FC } from 'react'
|
||||
import styles from './{{name.pascalCase}}.module.css'
|
||||
import cl from 'clsx'
|
||||
|
||||
interface IOwnProps {
|
||||
className?: string
|
||||
}
|
||||
|
||||
export const {{name.pascalCase}}: FC<IOwnProps> = ({className}) => {
|
||||
return (
|
||||
<div className={cl(styles.wrapper, className)}>
|
||||
{{name.pascalCase}}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
592
package-lock.json
generated
Normal file
592
package-lock.json
generated
Normal file
@@ -0,0 +1,592 @@
|
||||
{
|
||||
"name": "@gromlab/create",
|
||||
"version": "0.1.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@gromlab/create",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"archy": "^1.0.0",
|
||||
"boxen": "^5.1.2",
|
||||
"chalk": "^4.1.2",
|
||||
"change-case-all": "^2.1.0",
|
||||
"directory-tree": "^3.5.2",
|
||||
"figures": "^3.2.0"
|
||||
},
|
||||
"bin": {
|
||||
"gromlab-create": "dist/cli.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/archy": "^0.0.31",
|
||||
"@types/node": "^20.11.24",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/archy": {
|
||||
"version": "0.0.31",
|
||||
"resolved": "https://registry.npmjs.org/@types/archy/-/archy-0.0.31.tgz",
|
||||
"integrity": "sha512-v+dxizsFVyXgD3EpFuqT9YjdEjbJmPxNf1QIX9ohZOhxh1ZF2yhqv3vYaeum9lg3VghhxS5S0a6yldN9J9lPEQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "20.19.30",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.30.tgz",
|
||||
"integrity": "sha512-WJtwWJu7UdlvzEAUm484QNg5eAoq5QR08KDNx7g45Usrs2NtOPiX8ugDqmKdXkyL03rBqU5dYNYVQetEpBHq2g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-align": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz",
|
||||
"integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"string-width": "^4.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-convert": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/archy": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz",
|
||||
"integrity": "sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/array-back": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/array-back/-/array-back-3.1.0.tgz",
|
||||
"integrity": "sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/boxen": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/boxen/-/boxen-5.1.2.tgz",
|
||||
"integrity": "sha512-9gYgQKXx+1nP8mP7CzFyaUARhg7D3n1dF/FnErWmu9l6JvGpNUN278h0aSb+QjoiKSWG+iZ3uHrcqk0qrY9RQQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-align": "^3.0.0",
|
||||
"camelcase": "^6.2.0",
|
||||
"chalk": "^4.1.0",
|
||||
"cli-boxes": "^2.2.1",
|
||||
"string-width": "^4.2.2",
|
||||
"type-fest": "^0.20.2",
|
||||
"widest-line": "^3.1.0",
|
||||
"wrap-ansi": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/camelcase": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz",
|
||||
"integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/chalk": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.1.0",
|
||||
"supports-color": "^7.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/change-case": {
|
||||
"version": "5.4.4",
|
||||
"resolved": "https://registry.npmjs.org/change-case/-/change-case-5.4.4.tgz",
|
||||
"integrity": "sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/change-case-all": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/change-case-all/-/change-case-all-2.1.0.tgz",
|
||||
"integrity": "sha512-v6b0WWWkZUMHVuYk82l+WROgkUm4qEN2w5hKRNWtEOYwWqUGoi8C6xH0l1RLF1EoWqDFK6MFclmN3od6ws3/uw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"change-case": "^5.2.0",
|
||||
"sponge-case": "^2.0.2",
|
||||
"swap-case": "^3.0.2",
|
||||
"title-case": "^3.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/cli-boxes": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.1.tgz",
|
||||
"integrity": "sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-name": "~1.1.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/command-line-args": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/command-line-args/-/command-line-args-5.2.1.tgz",
|
||||
"integrity": "sha512-H4UfQhZyakIjC74I9d34fGYDwk3XpSr17QhEd0Q3I9Xq1CETHo4Hcuo87WyWHpAF1aSLjLRf5lD9ZGX2qStUvg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"array-back": "^3.1.0",
|
||||
"find-replace": "^3.0.0",
|
||||
"lodash.camelcase": "^4.3.0",
|
||||
"typical": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/command-line-usage": {
|
||||
"version": "6.1.3",
|
||||
"resolved": "https://registry.npmjs.org/command-line-usage/-/command-line-usage-6.1.3.tgz",
|
||||
"integrity": "sha512-sH5ZSPr+7UStsloltmDh7Ce5fb8XPlHyoPzTpyyMuYCtervL65+ubVZ6Q61cFtFl62UyJlc8/JwERRbAFPUqgw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"array-back": "^4.0.2",
|
||||
"chalk": "^2.4.2",
|
||||
"table-layout": "^1.0.2",
|
||||
"typical": "^5.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/command-line-usage/node_modules/ansi-styles": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
|
||||
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-convert": "^1.9.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/command-line-usage/node_modules/array-back": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/array-back/-/array-back-4.0.2.tgz",
|
||||
"integrity": "sha512-NbdMezxqf94cnNfWLL7V/im0Ub+Anbb0IoZhvzie8+4HJ4nMQuzHuy49FkGYCJK2yAloZ3meiB6AVMClbrI1vg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/command-line-usage/node_modules/chalk": {
|
||||
"version": "2.4.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
|
||||
"integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^3.2.1",
|
||||
"escape-string-regexp": "^1.0.5",
|
||||
"supports-color": "^5.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/command-line-usage/node_modules/color-convert": {
|
||||
"version": "1.9.3",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
|
||||
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-name": "1.1.3"
|
||||
}
|
||||
},
|
||||
"node_modules/command-line-usage/node_modules/color-name": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
|
||||
"integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/command-line-usage/node_modules/has-flag": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
|
||||
"integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/command-line-usage/node_modules/supports-color": {
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
|
||||
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"has-flag": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/command-line-usage/node_modules/typical": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/typical/-/typical-5.2.0.tgz",
|
||||
"integrity": "sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/deep-extend": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
|
||||
"integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/directory-tree": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/directory-tree/-/directory-tree-3.6.0.tgz",
|
||||
"integrity": "sha512-rTMWs+zxr0QEbzQKRfwV6SeEy+zIHFkorskI4bhG2o7ayr82c+FC7yWg3yLpurgp6Hs2NGy1NWrKIaDodr2r8A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"command-line-args": "^5.2.0",
|
||||
"command-line-usage": "^6.1.1"
|
||||
},
|
||||
"bin": {
|
||||
"directory-tree": "bin/index.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/escape-string-regexp": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
|
||||
"integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/figures": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz",
|
||||
"integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"escape-string-regexp": "^1.0.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/find-replace": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/find-replace/-/find-replace-3.0.0.tgz",
|
||||
"integrity": "sha512-6Tb2myMioCAgv5kfvP5/PkZZ/ntTpVK39fHY7WkWBgvbeE+VHd/tZuZ4mrC+bxh4cfOZeYKVPaJIZtZXV7GNCQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"array-back": "^3.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/is-fullwidth-code-point": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/lodash.camelcase": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
|
||||
"integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/reduce-flatten": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/reduce-flatten/-/reduce-flatten-2.0.0.tgz",
|
||||
"integrity": "sha512-EJ4UNY/U1t2P/2k6oqotuX2Cc3T6nxJwsM0N0asT7dhrtH1ltUxDn4NalSYmPE2rCkVpcf/X6R0wDwcFpzhd4w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/sponge-case": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/sponge-case/-/sponge-case-2.0.3.tgz",
|
||||
"integrity": "sha512-i4h9ZGRfxV6Xw3mpZSFOfbXjf0cQcYmssGWutgNIfFZ2VM+YIWfD71N/kjjwK6X/AAHzBr+rciEcn/L34S8TGw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/string-width": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-ansi": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"has-flag": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/swap-case": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/swap-case/-/swap-case-3.0.3.tgz",
|
||||
"integrity": "sha512-6p4op8wE9CQv7uDFzulI6YXUw4lD9n4oQierdbFThEKVWVQcbQcUjdP27W8XE7V4QnWmnq9jueSHceyyQnqQVA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/table-layout": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/table-layout/-/table-layout-1.0.2.tgz",
|
||||
"integrity": "sha512-qd/R7n5rQTRFi+Zf2sk5XVVd9UQl6ZkduPFC3S7WEGJAmetDTjY3qPN50eSKzwuzEyQKy5TN2TiZdkIjos2L6A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"array-back": "^4.0.1",
|
||||
"deep-extend": "~0.6.0",
|
||||
"typical": "^5.2.0",
|
||||
"wordwrapjs": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/table-layout/node_modules/array-back": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/array-back/-/array-back-4.0.2.tgz",
|
||||
"integrity": "sha512-NbdMezxqf94cnNfWLL7V/im0Ub+Anbb0IoZhvzie8+4HJ4nMQuzHuy49FkGYCJK2yAloZ3meiB6AVMClbrI1vg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/table-layout/node_modules/typical": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/typical/-/typical-5.2.0.tgz",
|
||||
"integrity": "sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/title-case": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/title-case/-/title-case-3.0.3.tgz",
|
||||
"integrity": "sha512-e1zGYRvbffpcHIrnuqT0Dh+gEJtDaxDSoG4JAIpq4oDFyooziLBIiYQv0GBT4FUAnUop5uZ1hiIAj7oAF6sOCA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/type-fest": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
|
||||
"integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
|
||||
"license": "(MIT OR CC0-1.0)",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/typical": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/typical/-/typical-4.0.0.tgz",
|
||||
"integrity": "sha512-VAH4IvQ7BDFYglMd7BPRDfLgxZZX4O4TFcRDA6EN5X7erNJJq+McIEp8np9aVtxrCJ6qx4GTYVfOWNjcqwZgRw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/widest-line": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz",
|
||||
"integrity": "sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"string-width": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/wordwrapjs": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/wordwrapjs/-/wordwrapjs-4.0.1.tgz",
|
||||
"integrity": "sha512-kKlNACbvHrkpIw6oPeYDSmdCTu2hdMHoyXLTcUKala++lx5Y+wjJ/e474Jqv5abnVmwxw08DiTuHmw69lJGksA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"reduce-flatten": "^2.0.0",
|
||||
"typical": "^5.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/wordwrapjs/node_modules/typical": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/typical/-/typical-5.2.0.tgz",
|
||||
"integrity": "sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
"string-width": "^4.1.0",
|
||||
"strip-ansi": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
30
package.json
Normal file
30
package.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "@gromlab/create",
|
||||
"version": "0.1.0",
|
||||
"description": "Template-based file generator CLI",
|
||||
"bin": {
|
||||
"gromlab-create": "dist/cli.js"
|
||||
},
|
||||
"main": "dist/cli.js",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsc -p tsconfig.json",
|
||||
"dev": "tsc -p tsconfig.json -w",
|
||||
"prepare": "npm run build"
|
||||
},
|
||||
"dependencies": {
|
||||
"archy": "^1.0.0",
|
||||
"boxen": "^5.1.2",
|
||||
"chalk": "^4.1.2",
|
||||
"change-case-all": "^2.1.0",
|
||||
"directory-tree": "^3.5.2",
|
||||
"figures": "^3.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/archy": "^0.0.31",
|
||||
"@types/node": "^20.11.24",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
101
src/args.ts
Normal file
101
src/args.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import { ParsedArgs } from './types';
|
||||
|
||||
export function printHelp() {
|
||||
const lines = [
|
||||
'Использование:',
|
||||
' npx @gromlab/create <template> [name] [options]',
|
||||
'',
|
||||
'Опции:',
|
||||
' --<var> <value> Переменная шаблона (поддерживается любой --key <value>)',
|
||||
' [name] Сокращение для --name',
|
||||
' --templates <path> Папка шаблонов (по умолчанию: .templates)',
|
||||
' --templates-path Алиас для --templates',
|
||||
' --out <path> Папка вывода (по умолчанию: текущая директория)',
|
||||
' --overwrite Перезаписывать существующие файлы',
|
||||
' --dry-run Показать результат без записи на диск',
|
||||
' -h, --help Показать эту справку',
|
||||
'',
|
||||
'Примеры:',
|
||||
' npx @gromlab/create component --name test',
|
||||
' npx @gromlab/create component --name test --out src/components',
|
||||
' npx @gromlab/create component --name test --templates /path/to/.templates'
|
||||
];
|
||||
console.log(lines.join('\n'));
|
||||
}
|
||||
|
||||
function consumeValue(args: string[], index: number, key: string): string {
|
||||
const next = args[index + 1];
|
||||
if (!next || next.startsWith('-')) {
|
||||
throw new Error(`Missing value for --${key}`);
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
export function parseArgs(argv: string[]): ParsedArgs {
|
||||
const parsed: ParsedArgs = {
|
||||
vars: {},
|
||||
overwrite: false,
|
||||
dryRun: false,
|
||||
help: false,
|
||||
extra: []
|
||||
};
|
||||
const args = argv.slice(2);
|
||||
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
const arg = args[i];
|
||||
|
||||
if (arg === '-h' || arg === '--help') {
|
||||
parsed.help = true;
|
||||
continue;
|
||||
}
|
||||
if (arg === '--overwrite') {
|
||||
parsed.overwrite = true;
|
||||
continue;
|
||||
}
|
||||
if (arg === '--dry-run') {
|
||||
parsed.dryRun = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg.startsWith('--')) {
|
||||
const eqIndex = arg.indexOf('=');
|
||||
const key = eqIndex === -1 ? arg.slice(2) : arg.slice(2, eqIndex);
|
||||
const inlineValue = eqIndex === -1 ? undefined : arg.slice(eqIndex + 1);
|
||||
|
||||
if (!key) continue;
|
||||
|
||||
if (key === 'templates' || key === 'templatesPath' || key === 'templates-path') {
|
||||
const value = inlineValue ?? consumeValue(args, i, key);
|
||||
if (inlineValue === undefined) i++;
|
||||
parsed.templatesPath = value;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (key === 'out' || key === 'output') {
|
||||
const value = inlineValue ?? consumeValue(args, i, key);
|
||||
if (inlineValue === undefined) i++;
|
||||
parsed.outDir = value;
|
||||
continue;
|
||||
}
|
||||
|
||||
const value = inlineValue ?? consumeValue(args, i, key);
|
||||
if (inlineValue === undefined) i++;
|
||||
parsed.vars[key] = value;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!parsed.templateName) {
|
||||
parsed.templateName = arg;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!parsed.positionalName) {
|
||||
parsed.positionalName = arg;
|
||||
continue;
|
||||
}
|
||||
|
||||
parsed.extra.push(arg);
|
||||
}
|
||||
|
||||
return parsed;
|
||||
}
|
||||
121
src/cli.ts
Normal file
121
src/cli.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
#!/usr/bin/env node
|
||||
import * as path from 'path';
|
||||
import { parseArgs, printHelp } from './args';
|
||||
import { printError, printWarnings, printSummary } from './output';
|
||||
import { PlanItem } from './types';
|
||||
import { normalizeArgs, resolveTemplateContext } from './validation';
|
||||
import { buildPlan, getCollisions, getExistingDirs, getRoots, getTopLevelDirs, writePlan } from './plan';
|
||||
|
||||
function resolvePath(baseDir: string, inputPath: string): string {
|
||||
if (path.isAbsolute(inputPath)) return path.normalize(inputPath);
|
||||
return path.resolve(baseDir, inputPath);
|
||||
}
|
||||
|
||||
function run() {
|
||||
let parsed;
|
||||
try {
|
||||
parsed = parseArgs(process.argv);
|
||||
} catch (error) {
|
||||
console.error(String(error));
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
if (parsed.help) {
|
||||
printHelp();
|
||||
return;
|
||||
}
|
||||
|
||||
const normalizedResult = normalizeArgs(parsed);
|
||||
if (normalizedResult.error) {
|
||||
printError(normalizedResult.error.title, normalizedResult.error.details, normalizedResult.error.hint);
|
||||
if (normalizedResult.error.showHelp) {
|
||||
printHelp();
|
||||
}
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
const normalized = normalizedResult.normalized;
|
||||
|
||||
const cwd = process.cwd();
|
||||
const templatesDir = resolvePath(cwd, normalized.templatesPath ?? '.templates');
|
||||
const outDir = resolvePath(cwd, normalized.outDir ?? '.');
|
||||
|
||||
const templateResult = resolveTemplateContext(
|
||||
templatesDir,
|
||||
normalized.templateName!,
|
||||
normalized.vars
|
||||
);
|
||||
if (templateResult.error) {
|
||||
printError(templateResult.error.title, templateResult.error.details, templateResult.error.hint);
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
const templateContext = templateResult.context!;
|
||||
const plan: PlanItem[] = buildPlan(templateContext.templateDir, outDir, normalized.vars, templateContext.files);
|
||||
const topLevelDirs = getTopLevelDirs(outDir, plan);
|
||||
const existingDirs = getExistingDirs(outDir, topLevelDirs);
|
||||
|
||||
const redWarnings: string[][] = [];
|
||||
const warnings: string[][] = [];
|
||||
|
||||
if (existingDirs.length > 0) {
|
||||
if (!normalized.overwrite && !normalized.dryRun) {
|
||||
printError(
|
||||
'Папка назначения уже существует',
|
||||
existingDirs.map((dir) => path.join(outDir, dir)),
|
||||
'Используйте --overwrite для перезаписи'
|
||||
);
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
const warningLines = [
|
||||
'Папка назначения уже существует:',
|
||||
...existingDirs.map((dir) => ` - ${path.join(outDir, dir)}`),
|
||||
'Используйте --overwrite для перезаписи.'
|
||||
];
|
||||
redWarnings.push(warningLines);
|
||||
}
|
||||
|
||||
if (existingDirs.length === 0) {
|
||||
const collisions = getCollisions(plan);
|
||||
|
||||
if (collisions.length > 0 && !normalized.overwrite) {
|
||||
if (normalized.dryRun) {
|
||||
warnings.push([
|
||||
'Файлы уже существуют:',
|
||||
...collisions.map((target) => ` - ${path.relative(outDir, target)}`),
|
||||
'Используйте --overwrite для перезаписи.'
|
||||
]);
|
||||
} else {
|
||||
printError(
|
||||
'Файлы уже существуют',
|
||||
collisions.map((target) => path.relative(outDir, target)),
|
||||
'Используйте --overwrite для перезаписи'
|
||||
);
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const roots = getRoots(outDir, plan);
|
||||
|
||||
if (normalized.dryRun) {
|
||||
printSummary(plan, outDir, normalized.vars, true, normalized.templateName!, roots);
|
||||
printWarnings(redWarnings, warnings);
|
||||
return;
|
||||
}
|
||||
|
||||
writePlan(plan, normalized.vars, normalized.overwrite);
|
||||
|
||||
printSummary(plan, outDir, normalized.vars, false, normalized.templateName!, roots);
|
||||
printWarnings(redWarnings, warnings);
|
||||
}
|
||||
|
||||
try {
|
||||
run();
|
||||
} catch (error) {
|
||||
console.error(String(error));
|
||||
process.exitCode = 1;
|
||||
}
|
||||
176
src/output.ts
Normal file
176
src/output.ts
Normal file
@@ -0,0 +1,176 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import archy = require('archy');
|
||||
import chalk = require('chalk');
|
||||
import directoryTree = require('directory-tree');
|
||||
import figures = require('figures');
|
||||
import { PlanItem } from './types';
|
||||
|
||||
type TreeNode = {
|
||||
name: string;
|
||||
children: Map<string, TreeNode>;
|
||||
isFile: boolean;
|
||||
};
|
||||
|
||||
function buildTreeFromPaths(rootLabel: string, paths: string[]): string {
|
||||
const root: TreeNode = { name: rootLabel, children: new Map(), isFile: false };
|
||||
|
||||
for (const rawPath of paths) {
|
||||
const parts = rawPath.split(path.sep).filter(Boolean);
|
||||
let current = root;
|
||||
parts.forEach((part, index) => {
|
||||
const isLast = index === parts.length - 1;
|
||||
let child = current.children.get(part);
|
||||
if (!child) {
|
||||
child = { name: part, children: new Map(), isFile: isLast };
|
||||
current.children.set(part, child);
|
||||
}
|
||||
if (isLast) {
|
||||
child.isFile = true;
|
||||
}
|
||||
current = child;
|
||||
});
|
||||
}
|
||||
|
||||
const toArchyNode = (node: TreeNode): archy.Data => {
|
||||
const entries = Array.from(node.children.values()).sort((a, b) => a.name.localeCompare(b.name));
|
||||
const isDir = node.children.size > 0;
|
||||
return {
|
||||
label: isDir ? chalk.cyan(`${node.name}/`) : chalk.white(node.name),
|
||||
nodes: entries.map((child) => toArchyNode(child))
|
||||
};
|
||||
};
|
||||
|
||||
return archy(toArchyNode(root));
|
||||
}
|
||||
|
||||
function buildTreeFromDirectory(rootPath: string): string {
|
||||
const tree = directoryTree(rootPath, { attributes: ['type'] }) as directoryTree.DirectoryTree | null;
|
||||
if (!tree) return '';
|
||||
|
||||
const toArchyNode = (node: directoryTree.DirectoryTree): archy.Data => {
|
||||
const nodes = (node.children ?? [])
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
.map((child) => toArchyNode(child));
|
||||
const isDir = node.type === 'directory';
|
||||
const label = isDir ? chalk.cyan(`${node.name}/`) : chalk.white(node.name);
|
||||
return { label, nodes };
|
||||
};
|
||||
|
||||
return archy(toArchyNode(tree));
|
||||
}
|
||||
|
||||
export function printError(title: string, details?: string[], hint?: string) {
|
||||
console.error('');
|
||||
console.error(chalk.red(`${figures.cross} ${title}`));
|
||||
if (details && details.length > 0) {
|
||||
console.error('');
|
||||
for (const detail of details) {
|
||||
console.error(chalk.red(` ${figures.pointer} ${detail}`));
|
||||
}
|
||||
}
|
||||
if (hint) {
|
||||
console.error('');
|
||||
console.error(chalk.dim(` ${figures.info} ${hint}`));
|
||||
}
|
||||
console.error('');
|
||||
}
|
||||
|
||||
function printRedLines(lines: string[]) {
|
||||
for (const line of lines) {
|
||||
console.log(chalk.red(line));
|
||||
}
|
||||
}
|
||||
|
||||
export function printWarnings(redBlocks: string[][], blocks: string[][]) {
|
||||
if (redBlocks.length === 0 && blocks.length === 0) return;
|
||||
console.log('');
|
||||
console.log(chalk.yellow(`${figures.warning} Предупреждения:`));
|
||||
for (const lines of redBlocks) {
|
||||
printRedLines(lines);
|
||||
}
|
||||
for (const lines of blocks) {
|
||||
for (const line of lines) {
|
||||
console.log(chalk.yellow(line));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function buildVariablesList(vars: Record<string, string>): string[] {
|
||||
const entries = Object.entries(vars);
|
||||
if (entries.length === 0) return [chalk.dim(' (нет)')];
|
||||
|
||||
return entries.map(([key, value]) =>
|
||||
` ${chalk.dim('--')}${chalk.blue(key)}${chalk.dim(':')} ${chalk.green(value)}`
|
||||
);
|
||||
}
|
||||
|
||||
export function printSummary(
|
||||
plan: PlanItem[],
|
||||
outDir: string,
|
||||
vars: Record<string, string>,
|
||||
isDryRun: boolean,
|
||||
templateName: string,
|
||||
roots: string[]
|
||||
) {
|
||||
const nameValue = vars.name;
|
||||
const displayName = nameValue ?? templateName;
|
||||
|
||||
console.log('');
|
||||
|
||||
// Заголовок с иконкой
|
||||
const statusIcon = isDryRun ? figures.info : figures.tick;
|
||||
const statusColor = isDryRun ? chalk.blue : chalk.green;
|
||||
const statusText = isDryRun ? 'Планируется генерация' : 'Успешно создан';
|
||||
|
||||
console.log(statusColor(`${statusIcon} ${statusText}: `) + chalk.bold.white(displayName));
|
||||
console.log('');
|
||||
|
||||
// Путь/пути
|
||||
if (roots.length === 1) {
|
||||
console.log(chalk.dim(' Путь: ') + chalk.underline(roots[0]));
|
||||
} else if (roots.length > 1) {
|
||||
console.log(chalk.dim(' Пути:'));
|
||||
for (const rootPath of roots) {
|
||||
console.log(` ${figures.pointer} ${chalk.underline(rootPath)}`);
|
||||
}
|
||||
}
|
||||
console.log('');
|
||||
|
||||
// Дерево файлов
|
||||
console.log(chalk.dim(' Структура файлов:'));
|
||||
console.log('');
|
||||
|
||||
if (roots.length === 0) {
|
||||
console.log(chalk.dim(' (пусто)'));
|
||||
} else {
|
||||
for (const rootPath of roots) {
|
||||
const treeOutput = (!isDryRun && fs.existsSync(rootPath))
|
||||
? buildTreeFromDirectory(rootPath)
|
||||
: buildTreeFromPaths(path.basename(rootPath) || rootPath, plan
|
||||
.map((item) => path.relative(rootPath, item.target))
|
||||
.filter((rel) => rel && !rel.startsWith('..')));
|
||||
|
||||
// Добавляем отступ к дереву
|
||||
const indentedTree = treeOutput
|
||||
.trimEnd()
|
||||
.split('\n')
|
||||
.map(line => ' ' + line)
|
||||
.join('\n');
|
||||
console.log(indentedTree);
|
||||
|
||||
if (roots.length > 1) {
|
||||
console.log('');
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log('');
|
||||
|
||||
// Переменные
|
||||
console.log(chalk.dim(' Переменные:'));
|
||||
const varsLines = buildVariablesList(vars);
|
||||
for (const line of varsLines) {
|
||||
console.log(' ' + line);
|
||||
}
|
||||
console.log('');
|
||||
}
|
||||
71
src/plan.ts
Normal file
71
src/plan.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { renderTemplate } from './templateUtils';
|
||||
import { PlanItem } from './types';
|
||||
|
||||
export function buildPlan(
|
||||
templateDir: string,
|
||||
outDir: string,
|
||||
vars: Record<string, string>,
|
||||
files: string[]
|
||||
): PlanItem[] {
|
||||
return files.map((source) => {
|
||||
const relPath = path.relative(templateDir, source);
|
||||
const targetRelPath = renderTemplate(relPath, vars);
|
||||
if (!targetRelPath) {
|
||||
throw new Error(`Rendered path is empty for template file: ${relPath}`);
|
||||
}
|
||||
return {
|
||||
source,
|
||||
target: path.join(outDir, targetRelPath)
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function getTopLevelDirs(outDir: string, plan: PlanItem[]): string[] {
|
||||
const topLevelDirs = new Set<string>();
|
||||
for (const item of plan) {
|
||||
const relPath = path.relative(outDir, item.target);
|
||||
const parts = relPath.split(path.sep).filter(Boolean);
|
||||
if (parts.length > 1) {
|
||||
topLevelDirs.add(parts[0]);
|
||||
}
|
||||
}
|
||||
return Array.from(topLevelDirs);
|
||||
}
|
||||
|
||||
export function getExistingDirs(outDir: string, topLevelDirs: string[]): string[] {
|
||||
return topLevelDirs.filter((dir) => {
|
||||
const fullPath = path.join(outDir, dir);
|
||||
return fs.existsSync(fullPath) && fs.statSync(fullPath).isDirectory();
|
||||
});
|
||||
}
|
||||
|
||||
export function getCollisions(plan: PlanItem[]): string[] {
|
||||
return plan
|
||||
.map((item) => item.target)
|
||||
.filter((target) => fs.existsSync(target));
|
||||
}
|
||||
|
||||
export function getRoots(outDir: string, plan: PlanItem[]): string[] {
|
||||
const rootPaths = new Set<string>();
|
||||
for (const item of plan) {
|
||||
const relPath = path.relative(outDir, item.target);
|
||||
const parts = relPath.split(path.sep).filter(Boolean);
|
||||
if (parts.length <= 1) {
|
||||
rootPaths.add(outDir);
|
||||
} else {
|
||||
rootPaths.add(path.join(outDir, parts[0]));
|
||||
}
|
||||
}
|
||||
return Array.from(rootPaths);
|
||||
}
|
||||
|
||||
export function writePlan(plan: PlanItem[], vars: Record<string, string>, overwrite: boolean) {
|
||||
for (const item of plan) {
|
||||
const content = fs.readFileSync(item.source, 'utf8');
|
||||
const rendered = renderTemplate(content, vars);
|
||||
fs.mkdirSync(path.dirname(item.target), { recursive: true });
|
||||
fs.writeFileSync(item.target, rendered, { flag: overwrite ? 'w' : 'wx' });
|
||||
}
|
||||
}
|
||||
72
src/templateUtils.ts
Normal file
72
src/templateUtils.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { camelCase, pascalCase, snakeCase, kebabCase, constantCase, upperCase, lowerCase } from 'change-case-all';
|
||||
|
||||
export const CASE_MODIFIERS: Record<string, (input: string) => string> = {
|
||||
pascalCase,
|
||||
camelCase,
|
||||
snakeCase,
|
||||
kebabCase,
|
||||
screamingSnakeCase: constantCase,
|
||||
upperCase,
|
||||
lowerCase,
|
||||
upperCaseAll: (value: string) => value.replace(/[-_\s]+/g, '').toUpperCase(),
|
||||
lowerCaseAll: (value: string) => value.replace(/[-_\s]+/g, '').toLowerCase()
|
||||
};
|
||||
|
||||
const VARIABLE_PATTERN = /{{\s*([a-zA-Z0-9_]+)(?:\.([a-zA-Z0-9_]+))?\s*}}/g;
|
||||
|
||||
export function readDirRecursive(dir: string): string[] {
|
||||
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||
const files: string[] = [];
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
files.push(...readDirRecursive(fullPath));
|
||||
} else if (entry.isFile()) {
|
||||
files.push(fullPath);
|
||||
}
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
export function collectTemplateVariables(templateDir: string): Set<string> {
|
||||
const vars = new Set<string>();
|
||||
const files = readDirRecursive(templateDir);
|
||||
for (const file of files) {
|
||||
const relPath = path.relative(templateDir, file);
|
||||
{
|
||||
const pathRegex = /{{\s*([a-zA-Z0-9_]+)(?:\.[a-zA-Z0-9_]+)?\s*}}/g;
|
||||
let match: RegExpExecArray | null;
|
||||
while ((match = pathRegex.exec(relPath)) !== null) {
|
||||
vars.add(match[1]);
|
||||
}
|
||||
}
|
||||
{
|
||||
const content = fs.readFileSync(file, 'utf8');
|
||||
const contentRegex = /{{\s*([a-zA-Z0-9_]+)(?:\.[a-zA-Z0-9_]+)?\s*}}/g;
|
||||
let match: RegExpExecArray | null;
|
||||
while ((match = contentRegex.exec(content)) !== null) {
|
||||
vars.add(match[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return vars;
|
||||
}
|
||||
|
||||
export function renderTemplate(input: string, vars: Record<string, string>): string {
|
||||
return input.replace(VARIABLE_PATTERN, (_match, varName: string, modifier: string | undefined) => {
|
||||
const value = vars[varName];
|
||||
if (value === undefined) return '';
|
||||
if (modifier && CASE_MODIFIERS[modifier]) {
|
||||
return CASE_MODIFIERS[modifier](value);
|
||||
}
|
||||
return value;
|
||||
});
|
||||
}
|
||||
|
||||
export function listTemplateNames(templatesDir: string): string[] {
|
||||
if (!fs.existsSync(templatesDir)) return [];
|
||||
const entries = fs.readdirSync(templatesDir, { withFileTypes: true });
|
||||
return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
|
||||
}
|
||||
28
src/types.ts
Normal file
28
src/types.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
export type ParsedArgs = {
|
||||
templateName?: string;
|
||||
positionalName?: string;
|
||||
templatesPath?: string;
|
||||
outDir?: string;
|
||||
vars: Record<string, string>;
|
||||
overwrite: boolean;
|
||||
dryRun: boolean;
|
||||
help: boolean;
|
||||
extra: string[];
|
||||
};
|
||||
|
||||
export type PlanItem = {
|
||||
source: string;
|
||||
target: string;
|
||||
};
|
||||
|
||||
export type ValidationError = {
|
||||
title: string;
|
||||
details?: string[];
|
||||
hint?: string;
|
||||
showHelp?: boolean;
|
||||
};
|
||||
|
||||
export type TemplateContext = {
|
||||
templateDir: string;
|
||||
files: string[];
|
||||
};
|
||||
109
src/validation.ts
Normal file
109
src/validation.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { collectTemplateVariables, listTemplateNames, readDirRecursive } from './templateUtils';
|
||||
import { ParsedArgs, ValidationError, TemplateContext } from './types';
|
||||
|
||||
export function normalizeArgs(parsed: ParsedArgs): { normalized: ParsedArgs; error?: ValidationError } {
|
||||
const normalized: ParsedArgs = {
|
||||
...parsed,
|
||||
vars: { ...parsed.vars },
|
||||
extra: [...parsed.extra]
|
||||
};
|
||||
|
||||
if (!normalized.templateName) {
|
||||
return {
|
||||
normalized,
|
||||
error: {
|
||||
title: 'Требуется имя шаблона',
|
||||
showHelp: true
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (normalized.positionalName && normalized.vars.name) {
|
||||
return {
|
||||
normalized,
|
||||
error: {
|
||||
title: 'name задан дважды',
|
||||
details: ['позиционно и через --name']
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (normalized.positionalName && !normalized.vars.name) {
|
||||
normalized.vars.name = normalized.positionalName;
|
||||
}
|
||||
|
||||
if (normalized.extra.length > 0) {
|
||||
return {
|
||||
normalized,
|
||||
error: {
|
||||
title: 'Неожиданные аргументы',
|
||||
details: normalized.extra
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return { normalized };
|
||||
}
|
||||
|
||||
export function resolveTemplateContext(
|
||||
templatesDir: string,
|
||||
templateName: string,
|
||||
vars: Record<string, string>
|
||||
): { context?: TemplateContext; error?: ValidationError } {
|
||||
if (!fs.existsSync(templatesDir) || !fs.statSync(templatesDir).isDirectory()) {
|
||||
return {
|
||||
error: {
|
||||
title: 'Папка шаблонов не найдена',
|
||||
details: [templatesDir]
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const availableTemplates = listTemplateNames(templatesDir);
|
||||
if (availableTemplates.length === 0) {
|
||||
return {
|
||||
error: {
|
||||
title: 'В папке шаблонов нет шаблонов'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (!availableTemplates.includes(templateName)) {
|
||||
return {
|
||||
error: {
|
||||
title: `Шаблон не найден: ${templateName}`,
|
||||
details: availableTemplates,
|
||||
hint: 'Доступные шаблоны указаны выше'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const templateDir = path.join(templatesDir, templateName);
|
||||
const requiredVars = collectTemplateVariables(templateDir);
|
||||
const missingVars = Array.from(requiredVars).filter((name) => {
|
||||
const value = vars[name];
|
||||
return value === undefined || value.length === 0;
|
||||
});
|
||||
|
||||
if (missingVars.length > 0) {
|
||||
return {
|
||||
error: {
|
||||
title: 'Не заданы переменные шаблона',
|
||||
details: missingVars.map((name) => `--${name} <value>`)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const files = readDirRecursive(templateDir);
|
||||
if (files.length === 0) {
|
||||
return {
|
||||
error: {
|
||||
title: 'Шаблон пустой'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return { context: { templateDir, files } };
|
||||
}
|
||||
16
tsconfig.json
Normal file
16
tsconfig.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "CommonJS",
|
||||
"moduleResolution": "Node",
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user