Compare commits

...

185 Commits

Author SHA1 Message Date
02dcbdaf13 chore(master): release 3.1.0 2026-03-23 17:21:02 +02:00
970e8e0b37 build: fix build warnings & reduce bundle size 2026-03-23 17:17:23 +02:00
c860e4644a fix: interactive inputs with existing config/cli options 2026-03-23 17:14:25 +02:00
bb0248f91a feat: update all ouput logging 2026-03-23 17:02:33 +02:00
972d199fbb feat: config validation 2026-03-23 16:53:55 +02:00
b5fd1df821 feat: scaffoldignore 2026-03-23 16:41:08 +02:00
f6408f221d feat: select, confirm and number input types 2026-03-23 16:32:41 +02:00
0a4ead17c0 feat: after scaffold hook 2026-03-23 16:28:40 +02:00
7926b15053 feat: init command 2026-03-23 16:22:43 +02:00
9cdea1c5ea build: add lint-staged 2026-03-23 16:18:21 +02:00
4a79961ae5 build: update node version 2026-03-23 14:39:31 +02:00
51f422cc90 chore(master): release 3.0.0 2026-03-23 14:25:33 +02:00
93f3a4caaf chore(deps): update docs dependencies 2026-03-23 12:37:27 +02:00
04e7e895d7 feat: add .scaffold.{ext} as auto-detected file 2026-03-23 12:34:48 +02:00
68e6d17fa9 feat: auto-detect config file
Release-As: 3.0.0
2026-03-23 12:29:57 +02:00
d64dd4f0e7 feat: predefined data inputs 2026-03-23 12:27:55 +02:00
519ef273ac feat: interactive inputs 2026-03-23 12:16:24 +02:00
1431fda3db docs: fixes 2026-03-23 12:16:24 +02:00
2229a9cda1 refactor: reorganize and simplify all logic 2026-03-23 11:57:05 +02:00
1f80a50185 docs: update README.md 2026-03-23 11:49:18 +02:00
e82827d909 build: update workflows 2026-03-23 11:45:52 +02:00
2e49448e59 docs: update docs build 2026-03-23 11:39:47 +02:00
0ffd7ef788 build: migrate to vite+vitest 2026-03-23 10:45:57 +02:00
d16fb17c38 test: add comprehensive tests 2026-03-23 10:38:00 +02:00
af33c059b9 fix: string helpers to words parts conversion 2026-03-23 10:37:54 +02:00
d487d36b04 chore(deps): update dependencies 2026-03-23 10:24:27 +02:00
429f12d1b8 chore(master): release 2.3.3 2025-06-19 07:50:55 +03:00
dcba30689b chore: update formatting & lints 2025-06-19 01:28:15 +03:00
7745385573 chore: update dependencies 2025-06-19 01:28:03 +03:00
29f2afe097 test: add cli precedence test 2025-06-19 01:27:50 +03:00
4b0b4e7380 fix: config CLI precedence over file 2025-06-19 00:51:40 +03:00
Chen Asraf
c1536839e3 chore(master): release 2.3.2 2024-10-27 03:32:33 +02:00
7e029fd122 chore: update deps 2024-10-27 01:18:18 +02:00
41f4ca52f1 fix: template config from CLI 2024-10-27 01:12:43 +02:00
Chen Asraf
78d6bf186d chore(master): release 2.3.1 2024-10-03 14:16:18 +03:00
Chen Asraf
80c92bfe84 fix: strip tmpDir from output dir (#108)
* fix: strip tmpDir from output dir

* ci: test PRs

* fix: cmd

* fix: use relative path for replacement

* chore: remove todo
2024-10-03 14:12:30 +03:00
162cc8cec1 ci: remove develop branch 2024-09-18 00:27:51 +03:00
Chen Asraf
db6177c200 chore(develop): release 2.3.0 2024-09-18 00:12:00 +03:00
ae64db846f chore: add coverage script 2024-09-18 00:12:00 +03:00
89dc43c73d fix: exclude globs
refactor: split main file iteration to 2 steps
2024-09-17 23:57:48 +03:00
2c43dc4daf build: update target 2024-09-17 23:57:48 +03:00
f4c907e6c9 ci: fix build 2024-09-17 23:57:48 +03:00
a275e688d4 ci: use release-please 2024-09-17 23:57:48 +03:00
ff4ebf0a5b test: add color tests 2024-09-17 23:57:48 +03:00
ab9322e1ab feat: remove chalk dependency 2024-09-17 23:57:48 +03:00
semantic-release-bot
35f0d014d9 chore(release): 2.2.2 [skip ci]
## [2.2.2](https://github.com/chenasraf/simple-scaffold/compare/v2.2.1...v2.2.2) (2024-08-27)

### Bug Fixes

* homepage url ([daaefaf](daaefaf54e))
2024-08-27 13:07:31 +00:00
8ad8cb4be1 chore: update dependencies 2024-08-27 16:06:56 +03:00
daaefaf54e fix: homepage url 2024-08-27 16:06:56 +03:00
aefba4b773 docs: fix property names 2024-08-27 16:06:56 +03:00
dependabot[bot]
8457f0996a build(deps): bump ws
Bumps the npm_and_yarn group with 1 update in the /docs directory: [ws](https://github.com/websockets/ws).


Updates `ws` from 7.5.9 to 7.5.10
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/compare/7.5.9...7.5.10)

---
updated-dependencies:
- dependency-name: ws
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-27 16:06:56 +03:00
semantic-release-bot
adc95809ba chore(release): 2.2.1 [skip ci]
## [2.2.1](https://github.com/chenasraf/simple-scaffold/compare/v2.2.0...v2.2.1) (2024-04-21)

### Bug Fixes

* beforeWrite from config files ([98b326c](98b326c843))
* use console.info for handlebars parse warning ([19e7b0f](19e7b0f0c3))
2024-04-21 20:38:09 +00:00
98b326c843 fix: beforeWrite from config files 2024-04-21 23:37:35 +03:00
ddc115a037 chore: update dependencies 2024-04-21 23:37:35 +03:00
19e7b0f0c3 fix: use console.info for handlebars parse warning 2024-04-21 23:37:35 +03:00
dependabot[bot]
f883571daa build(deps): bump express from 4.18.2 to 4.19.2 in /docs
Bumps [express](https://github.com/expressjs/express) from 4.18.2 to 4.19.2.
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/master/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.18.2...4.19.2)

---
updated-dependencies:
- dependency-name: express
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-21 23:37:35 +03:00
Chen Asraf
be3068a533 ci: update doc workflow permissions 2024-04-21 23:37:35 +03:00
dependabot[bot]
8acc660dea build(deps): bump the npm_and_yarn group across 1 directory with 2 updates
Bumps the npm_and_yarn group with 2 updates in the /docs directory: [follow-redirects](https://github.com/follow-redirects/follow-redirects) and [webpack-dev-middleware](https://github.com/webpack/webpack-dev-middleware).


Updates `follow-redirects` from 1.15.5 to 1.15.6
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.5...v1.15.6)

Updates `webpack-dev-middleware` from 5.3.3 to 5.3.4
- [Release notes](https://github.com/webpack/webpack-dev-middleware/releases)
- [Changelog](https://github.com/webpack/webpack-dev-middleware/blob/v5.3.4/CHANGELOG.md)
- [Commits](https://github.com/webpack/webpack-dev-middleware/compare/v5.3.3...v5.3.4)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-type: indirect
  dependency-group: npm_and_yarn-security-group
- dependency-name: webpack-dev-middleware
  dependency-type: indirect
  dependency-group: npm_and_yarn-security-group
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-21 23:37:35 +03:00
semantic-release-bot
df6c351cb0 chore(release): 2.2.0 [skip ci]
# [2.2.0](https://github.com/chenasraf/simple-scaffold/compare/v2.1.0...v2.2.0) (2024-02-23)

### Features

* `list` command ([d579c09](d579c09c11))
* add `--before-write` cli option ([#89](https://github.com/chenasraf/simple-scaffold/issues/89)) ([5f810e2](5f810e2116))
2024-02-23 22:42:32 +00:00
Chen Asraf
5f810e2116 feat: add --before-write cli option (#89)
* feat: add `--before-write` cli option

* refactor: cleanup before write wrapper

* docs: add before-write docs
2024-02-24 00:41:56 +02:00
d579c09c11 feat: list command 2024-02-24 00:41:56 +02:00
3765398ab9 ci: attemp to fix pack cmd 2024-02-24 00:41:56 +02:00
semantic-release-bot
9be8a8b71b chore(release): 2.1.0 [skip ci]
# [2.1.0](https://github.com/chenasraf/simple-scaffold/compare/v2.0.2...v2.1.0) (2024-02-12)

### Features

* support directory in --config flag ([e48b832](e48b832e0b))
* support providing name in config ([4e7ac34](4e7ac34db9))
2024-02-12 23:39:35 +00:00
5cb8c3c081 docs: update navbar logo 2024-02-13 01:39:04 +02:00
3b52c255f0 chore: dry 2024-02-13 01:39:04 +02:00
80cf2076b0 chore: update dependencies 2024-02-13 01:39:04 +02:00
4fd710b763 test: fix tests 2024-02-13 01:39:04 +02:00
4e7ac34db9 feat: support providing name in config 2024-02-13 01:39:04 +02:00
e48b832e0b feat: support directory in --config flag 2024-02-13 01:39:04 +02:00
06ffa656ae docs: update templates page 2024-02-13 01:39:04 +02:00
919fd54ebb docs: usage page order 2024-02-13 01:39:04 +02:00
semantic-release-bot
dbc3283d5a chore(release): 2.0.2 [skip ci]
## [2.0.2](https://github.com/chenasraf/simple-scaffold/compare/v2.0.1...v2.0.2) (2024-02-04)

### Bug Fixes

* try to await scaffold before finally ([1b70897](1b70897f98))
2024-02-04 14:20:58 +00:00
Chen Asraf
0e567ec56f chore: show handlebars fail on default log level
fixes #86
2024-02-04 16:20:25 +02:00
semantic-release-bot
0ec671fe83 chore(release): 2.0.2-pre.1 [skip ci]
## [2.0.2-pre.1](https://github.com/chenasraf/simple-scaffold/compare/v2.0.1...v2.0.2-pre.1) (2024-02-03)

### Bug Fixes

* try to await scaffold before finally ([b0a6bf7](b0a6bf7021))
2024-02-04 16:20:25 +02:00
1b70897f98 fix: try to await scaffold before finally 2024-02-04 16:20:25 +02:00
43c9e8748f docs: update readme 2024-02-04 16:20:25 +02:00
65b14bc707 docs: update logo 2024-02-04 16:20:25 +02:00
f583af662c docs: update readme 2024-02-04 16:20:25 +02:00
600cc78186 docs: add logo, update docs 2024-02-04 16:20:25 +02:00
b4aea804cb docs: update readme 2024-02-04 16:20:25 +02:00
795635dc61 docs: update readme 2024-02-04 16:20:25 +02:00
6a026ce1a1 chore: version output + docs update 2024-02-04 16:20:25 +02:00
268e4d973c docs: fix github link 2024-02-04 16:20:25 +02:00
a403d9df9b ci: clean up release config 2024-02-04 16:20:25 +02:00
298beff355 docs: fix edit link 2024-02-04 16:20:25 +02:00
8f1d58f2c2 ci: fix pack step order 2024-02-04 16:20:25 +02:00
b10d69d2fe chore: update package.json spec 2024-02-04 16:20:25 +02:00
semantic-release-bot
069c890ca3 chore(release): 2.0.1 [skip ci]
## [2.0.1](https://github.com/chenasraf/simple-scaffold/compare/v2.0.0...v2.0.1) (2024-02-02)

### Bug Fixes

* log level flag ([5d7f449](5d7f449050))
* rm tmp dir too early ([4aa52c8](4aa52c84bd))
2024-02-02 00:48:01 +00:00
5d7f449050 fix: log level flag 2024-02-02 02:47:27 +02:00
cd29bd0521 docs: fix migration docs 2024-02-02 02:47:27 +02:00
c9e5dad746 test: fix tests 2024-02-02 02:47:27 +02:00
18f73b3eca build: try to pack in semantic-release 2024-02-02 02:47:27 +02:00
4aa52c84bd fix: rm tmp dir too early 2024-02-02 02:47:27 +02:00
f748125ae6 docs: update changelog 2024-02-02 02:47:27 +02:00
semantic-release-bot
43afe60ce8 chore(release): 2.0.0 [skip ci]
# [2.0.0](https://github.com/chenasraf/simple-scaffold/compare/v1.10.0...v2.0.0) (2024-01-31)

* fix!: version number ([bc0a18d](bc0a18dce0))

### BREAKING CHANGES

* see changelog
2024-01-31 22:27:49 +00:00
bc0a18dce0 fix!: version number
BREAKING CHANGE: see changelog
2024-02-01 00:27:12 +02:00
semantic-release-bot
179fabc579 chore(release): 1.10.0 [skip ci]
# [1.10.0](https://github.com/chenasraf/simple-scaffold/compare/v1.9.0...v1.10.0) (2024-01-31)

### Bug Fixes

* remove gh flag ([939200c](939200c9f2))
* tests ([ff92fd7](ff92fd7607))

### Features

* try multiple default config files ([89aacb5](89aacb58fd))
2024-01-31 22:22:21 +00:00
909fe5bbea fix!: version number 2024-02-01 00:21:49 +02:00
semantic-release-bot
bc7d687977 chore(release): 1.10.0 [skip ci]
# [1.10.0](https://github.com/chenasraf/simple-scaffold/compare/v1.9.0...v1.10.0) (2024-01-31)

### Bug Fixes

* remove gh flag ([939200c](939200c9f2))
* tests ([ff92fd7](ff92fd7607))

### Features

* try multiple default config files ([89aacb5](89aacb58fd))
2024-01-31 22:15:13 +00:00
e6d9816a2f ci: fix release 2024-02-01 00:14:32 +02:00
e2353134d4 chore: update deps 2024-01-31 23:55:39 +02:00
6c7e3e5068 build: fix versioning 2024-01-31 23:55:39 +02:00
81dd6e3b19 docs: fix usage index link 2024-01-31 23:55:39 +02:00
b852a956ba docs: update badges 2024-01-31 23:55:39 +02:00
0ecc2590c7 docs: update badges 2024-01-31 23:55:39 +02:00
af4b753a6d docs: fix intro gif 2024-01-31 23:55:39 +02:00
semantic-release-bot
a8162f2315 chore(release): 2.0.0-pre.3 [skip ci]
# [2.0.0-pre.3](https://github.com/chenasraf/simple-scaffold/compare/v2.0.0-pre.2...v2.0.0-pre.3) (2024-01-30)

### Bug Fixes

* tests ([c219d38](c219d382e6))
2024-01-31 23:55:39 +02:00
ff92fd7607 fix: tests 2024-01-31 23:55:39 +02:00
0dc1116141 feat!: rename createSubFolder and subFolderNameHelper 2024-01-31 23:55:39 +02:00
f36cf4c2f2 docs: regenerate changelog
d
2024-01-31 23:55:39 +02:00
Chen Asraf
dd58c7fdda Update README.md 2024-01-31 23:55:39 +02:00
semantic-release-bot
058f1d4afa chore(release): 2.0.0-pre.2 [skip ci]
# [2.0.0-pre.2](https://github.com/chenasraf/simple-scaffold/compare/v2.0.0-pre.1...v2.0.0-pre.2) (2024-01-29)
2024-01-31 23:55:39 +02:00
583be2d9d2 docs: fix readme doc links
a
2024-01-31 23:55:39 +02:00
semantic-release-bot
18f1fac119 chore(release): 2.0.0-pre.1 [skip ci]
# [2.0.0-pre.1](https://github.com/chenasraf/simple-scaffold/compare/v1.9.0...v2.0.0-pre.1) (2024-01-29)

### Bug Fixes

* remove gh flag ([e66d6ba](e66d6ba86a))

### Features

* try multiple default config files ([f25cda7](f25cda738b))
2024-01-31 23:55:39 +02:00
693b8c8ea4 ci: fix docs 2024-01-31 23:55:39 +02:00
901d5d76b4 ci: fix docs 2024-01-31 23:55:39 +02:00
fac571f588 ci: fix docs 2024-01-31 23:55:39 +02:00
a10f412337 ci: use tag versions 2024-01-31 23:55:39 +02:00
819cd20644 docs: gtag + update deps 2024-01-31 23:55:39 +02:00
dc4c940559 ci: fix docs command 2024-01-31 23:55:39 +02:00
816e2f9aa0 ci: fix 2024-01-31 23:55:39 +02:00
825bd096c2 chore: fix docs & formatting 2024-01-31 23:55:39 +02:00
939200c9f2 fix: remove gh flag 2024-01-31 23:55:39 +02:00
0b7653de72 ci: fix docs build dir 2024-01-31 23:55:39 +02:00
89aacb58fd feat: try multiple default config files 2024-01-31 23:55:39 +02:00
9ce2845ace feat!: separate git/github/config flags
feat: separate git/github/config

t

test

cleanup
2024-01-31 23:55:39 +02:00
5373495f80 docs: update 2024-01-31 23:55:39 +02:00
70a080cc8e docs: update docs 2024-01-31 23:55:39 +02:00
ef7811d7e2 docs: update readme image 2024-01-31 23:55:39 +02:00
8b6b958480 docs: update docs, remove generated files from git 2024-01-31 23:55:39 +02:00
2c73207784 docs: update 2024-01-31 23:55:39 +02:00
5252642f6b docs: docusaurus initial commit 2024-01-31 23:55:39 +02:00
361778a188 feat!: rename verbose to logLevel 2024-01-31 23:55:39 +02:00
a54b1d6297 chore: update deps 2024-01-31 23:55:39 +02:00
ae69eb52db chore: update deps 2024-01-31 23:55:39 +02:00
3b60f1816d docs: update docs 2024-01-31 23:55:39 +02:00
c04d1cc42a chore!: remove Name from default data 2024-01-31 23:55:39 +02:00
c6b185cd8f feat!: remove url colon syntax 2024-01-31 23:55:39 +02:00
5abf528b81 chore!: update massarg 2024-01-31 23:55:39 +02:00
aeb3680ddf ci: update docs build 2024-01-31 23:55:39 +02:00
40e2381d05 ci: update build 2024-01-31 23:55:39 +02:00
semantic-release-bot
4d1a6e1f0d chore(release): 1.9.0 [skip ci]
# [1.9.0](https://github.com/chenasraf/simple-scaffold/compare/v1.8.0...v1.9.0) (2024-01-02)

### Features

* add --recurse-submodules to git clone ([cbaf130](cbaf130a0c))
2024-01-02 00:20:20 +00:00
daae9c10f9 ci: fix actions 2024-01-02 02:19:46 +02:00
758719de5d chore: update dependencies 2024-01-02 02:19:46 +02:00
1903055d46 ci: update build steps 2024-01-02 02:19:46 +02:00
Claudio Barca
cbaf130a0c feat: add --recurse-submodules to git clone 2024-01-02 02:19:46 +02:00
e26fe2a3d4 ci: update build process 2024-01-02 02:19:46 +02:00
7e1acf0607 ci: update docs build, semantic release
docs: update config files entry
2024-01-02 02:19:46 +02:00
semantic-release-bot
f666c357f4 chore(release): 1.8.0 [skip ci]
## [1.8.0](https://github.com/chenasraf/simple-scaffold/compare/v1.7.2...v1.8.0) (2023-11-29)

### Bug Fixes

* **config:** fn config load ([457c904](457c90470b)), closes [#63](https://github.com/chenasraf/simple-scaffold/issues/63)
2023-11-29 22:16:52 +00:00
f5d55f234a docs: update configuration files docs 2023-11-30 00:12:11 +02:00
semantic-release-bot
746f924a22 chore(release): 1.8.0-pre.1 [skip ci]
## [1.8.0-pre.1](https://github.com/chenasraf/simple-scaffold/compare/v1.7.2...v1.8.0-pre.1) (2023-11-27)

### Bug Fixes

* **config:** fn config load ([457c904](457c90470b)), closes [#63](https://github.com/chenasraf/simple-scaffold/issues/63)
2023-11-30 00:12:11 +02:00
807c3e27e2 ci: update release config 2023-11-28 00:49:28 +02:00
b048841dac chore: update dependencies 2023-11-28 00:49:28 +02:00
457c90470b fix(config): fn config load
closes #63
2023-11-28 00:49:28 +02:00
dependabot[bot]
0fa1ad4db7 build(deps-dev): bump @babel/traverse from 7.21.5 to 7.23.2
Bumps [@babel/traverse](https://github.com/babel/babel/tree/HEAD/packages/babel-traverse) from 7.21.5 to 7.23.2.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.23.2/packages/babel-traverse)

---
updated-dependencies:
- dependency-name: "@babel/traverse"
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-28 00:49:28 +02:00
semantic-release-bot
d62eeeb8e4 chore(release): 1.7.2 [skip ci]
## [1.7.2](https://github.com/chenasraf/simple-scaffold/compare/v1.7.1...v1.7.2) (2023-08-20)

### Bug Fixes

* windows path resolution ([98ee000](98ee00031f))
2023-08-20 10:09:48 +00:00
Chen Asraf
565d1f33aa Merge pull request #61 from chenasraf/pre
v1.7.2
2023-08-20 13:08:25 +03:00
semantic-release-bot
9f58fff2cf chore(release): 1.7.2-pre.1 [skip ci]
## [1.7.2-pre.1](https://github.com/chenasraf/simple-scaffold/compare/v1.7.1...v1.7.2-pre.1) (2023-08-15)

### Bug Fixes

* windows path resolution ([98ee000](98ee00031f))
2023-08-15 19:34:02 +00:00
dbba81d053 ci: trigger on pre branch 2023-08-15 22:32:25 +03:00
9f5716e1b9 chore: bump version number 2023-08-15 22:31:38 +03:00
Chen Asraf
4cbb79bb3b Merge pull request #60 from chenasraf/fix/windows
[BUGFIX] Windows path resolution
2023-08-15 22:28:04 +03:00
98ee00031f fix: windows path resolution 2023-08-14 00:19:50 +03:00
8c3369a00d chore: formatting 2023-08-14 00:19:36 +03:00
20ef0cea9b chore: update logs 2023-08-14 00:08:16 +03:00
Chen Asraf
3413151358 test: add tests
chore: use node:* modules for builtins
2023-06-12 23:26:25 +03:00
Chen Asraf
d2a2fda1b1 chore: update dependencies 2023-06-12 23:18:50 +03:00
Chen Asraf
486c07a55b Merge branch 'master' into develop 2023-06-08 08:13:52 +03:00
semantic-release-bot
de05bca546 chore(release): 1.7.1 [skip ci]
## [1.7.1](https://github.com/chenasraf/simple-scaffold/compare/v1.7.0...v1.7.1) (2023-06-07)

### Bug Fixes

* local config file load error ([2b74239](2b7423993b))
2023-06-07 23:56:29 +00:00
Chen Asraf
72d4cf58c5 Merge pull request #58 from chenasraf/develop
v1.7.1
2023-06-08 02:55:06 +03:00
Chen Asraf
2cf31e827e build: fix tsconfig 2023-06-08 02:52:45 +03:00
Chen Asraf
3714e8b3bd build: update release rules, add tests 2023-06-08 02:50:50 +03:00
semantic-release-bot
77be7c09d5 chore(release): 1.7.1-develop.1 [skip ci]
## [1.7.1-develop.1](https://github.com/chenasraf/simple-scaffold/compare/v1.7.0...v1.7.1-develop.1) (2023-06-07)

### Bug Fixes

* local config file load error ([2b74239](2b7423993b))
2023-06-07 21:51:53 +00:00
Chen Asraf
1246b51cda Merge branch 'master' into develop
# Conflicts:
#	CHANGELOG.md
#	package.json
2023-06-08 00:50:24 +03:00
semantic-release-bot
fea6c0fb16 chore(release): 1.7.0-develop.7 [skip ci]
## [1.7.0-develop.7](https://github.com/chenasraf/simple-scaffold/compare/v1.7.0-develop.6...v1.7.0-develop.7) (2023-06-07)

### Bug Fixes

* local config file load error ([2b74239](2b7423993b))
2023-06-07 20:22:57 +00:00
Chen Asraf
2b7423993b fix: local config file load error 2023-06-07 23:21:12 +03:00
semantic-release-bot
a47ba1186c chore(release): 1.7.0-develop.6 [skip ci]
## [1.7.0-develop.6](https://github.com/chenasraf/simple-scaffold/compare/v1.7.0-develop.5...v1.7.0-develop.6) (2023-05-27)
2023-05-27 23:16:21 +00:00
Chen Asraf
06b1552dca docs: update README.md 2023-05-28 02:14:36 +03:00
semantic-release-bot
4868925511 chore(release): 1.7.0 [skip ci]
## [1.7.0](https://github.com/chenasraf/simple-scaffold/compare/v1.6.0...v1.7.0) (2023-05-17)

### Features

* function config file ([02a8ba1](02a8ba16cd))

### Bug Fixes

* use path.normalize ([565090a](565090a951))
2023-05-17 17:30:07 +00:00
Chen Asraf
79cfdbed38 Merge pull request #57 from chenasraf/develop
release
2023-05-17 20:28:02 +03:00
Chen Asraf
4f27b7b934 test: add tests 2023-05-13 02:36:26 +03:00
semantic-release-bot
bee430a40d chore(release): 1.7.0-develop.5 [skip ci]
## [1.7.0-develop.5](https://github.com/chenasraf/simple-scaffold/compare/v1.7.0-develop.4...v1.7.0-develop.5) (2023-05-12)
2023-05-12 22:59:55 +00:00
Chen Asraf
3dfc920455 test: fix + add tests 2023-05-13 01:58:21 +03:00
Chen Asraf
68307d1378 refactor: remove lodash dependency 2023-05-13 01:29:59 +03:00
Chen Asraf
33e1d569a3 build: update workflows 2023-05-13 01:29:40 +03:00
semantic-release-bot
c446439b1a chore(release): 1.7.0-develop.4 [skip ci]
## [1.7.0-develop.4](https://github.com/chenasraf/simple-scaffold/compare/v1.7.0-develop.3...v1.7.0-develop.4) (2023-05-11)
2023-05-11 21:03:55 +00:00
Chen Asraf
773fd00674 build: update package asset name 2023-05-12 00:02:36 +03:00
96 changed files with 22385 additions and 6357 deletions

View File

@@ -1,23 +0,0 @@
name: Documentation
on:
push:
branches: [master, develop]
jobs:
docs:
runs-on: ubuntu-latest
# if: "!contains(github.event.head_commit.message, '[skip ci]') && !contains(github.event.head_commit.message, '[skip docs]')"
if: "!contains(github.event.head_commit.message, '[skip docs]')"
steps:
- uses: actions/checkout@v3
- uses: pnpm/action-setup@v2
with:
# node-version: "18.x"
run_install: |
- recursive: true
args: [--frozen-lockfile, --strict-peer-dependencies]
- run: pnpm build-docs
- uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./docs

View File

@@ -1,19 +0,0 @@
name: Pull Requests
on:
pull_request:
branches: [master, develop]
jobs:
build:
runs-on: ubuntu-latest
if: "!contains(github.event.head_commit.message, '[skip ci]')"
steps:
- uses: actions/checkout@v3
- uses: pnpm/action-setup@v2
with:
# node-version: "18.x"
run_install: |
- recursive: true
args: [--frozen-lockfile, --strict-peer-dependencies]
- run: pnpm build
- run: pnpm test

View File

@@ -1,38 +1,100 @@
name: Test & Build
name: Release
on:
pull_request:
branches:
- master
push:
branches: [master, develop, feat/*, fix/*]
branches:
- master
permissions:
contents: write
pull-requests: write
id-token: write
jobs:
test:
name: Test
runs-on: ubuntu-latest
if: "!contains(github.event.head_commit.message, '[skip ci]')"
steps:
- uses: actions/checkout@v3
- uses: pnpm/action-setup@v2
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
# node-version: "18.x"
run_install: |
- recursive: true
args: [--frozen-lockfile, --strict-peer-dependencies]
node-version: 20
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm test
if: "!contains(github.event.head_commit.message, '[skip test]')"
build:
name: Build
runs-on: ubuntu-latest
if: "!contains(github.event.head_commit.message, '[skip ci]')"
needs: test
steps:
- uses: actions/checkout@v3
- uses: pnpm/action-setup@v2
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: "18.x"
run_install: |
- recursive: true
args: [--frozen-lockfile, --strict-peer-dependencies]
node-version: 20
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm build
- run: cd ./dist && pnpm pack --pack-destination=../
- run: pnpm semantic-release
if: "!contains(github.event.head_commit.message, '[skip publish]')"
env:
NPM_TOKEN: "${{ secrets.NPM_TOKEN }}"
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
release:
name: Release Please
if: github.event_name == 'push'
needs:
- build
- test
runs-on: ubuntu-latest
outputs:
release_created: ${{ steps.release.outputs.release_created }}
steps:
- uses: googleapis/release-please-action@v4
id: release
with:
token: ${{ secrets.RELEASE_PLEASE_TOKEN }}
release-type: node
target-branch: master
publish:
name: NPM Publish
needs: release
runs-on: ubuntu-latest
if: ${{ needs.release.outputs.release_created }}
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
registry-url: "https://registry.npmjs.org"
- run: npm --version
- run: pnpm install --frozen-lockfile
- run: pnpm build
- run: cd dist && npm publish --provenance --access public
docs:
name: Deploy Documentation
needs: release
runs-on: ubuntu-latest
if: ${{ needs.release.outputs.release_created }}
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: cd docs && pnpm install --frozen-lockfile
- run: pnpm docs:build
- name: Deploy to GitHub Pages
uses: peaceiris/actions-gh-pages@v4
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./docs/build

2
.gitignore vendored
View File

@@ -64,5 +64,5 @@ examples/test-output/**/*
dist/
.DS_Store
tmp/
docs/
.nvmrc

1
.husky/pre-commit Normal file
View File

@@ -0,0 +1 @@
pnpm lint-staged

5
.prettierignore Normal file
View File

@@ -0,0 +1,5 @@
docs/docs/api/
examples/
.github/
CHANGELOG.md
pnpm-lock.yaml

View File

@@ -1,8 +1,9 @@
{
"semi": false,
"singleQuote": false,
"trailingComma": "all",
"printWidth": 120,
"tabWidth": 2,
"printWidth": 100,
"overrides": [
{
"files": "*.md",

View File

@@ -14,9 +14,6 @@
"variabletoken"
],
"[markdown]": {
"editor.rulers": [
87,
100
],
},
"editor.rulers": [87, 100]
}
}

6
.vscode/tasks.json vendored
View File

@@ -14,7 +14,7 @@
"problemMatcher": []
},
{
"command": "yarn typedoc --watch",
"command": "pnpm typedoc --watch",
"label": "typedoc --watch",
"type": "shell",
"problemMatcher": []
@@ -32,8 +32,8 @@
"problemMatcher": []
},
{
"command": "yarn test --watchAll",
"label": "yarn test --watchAll",
"command": "pnpm test --watchAll",
"label": "pnpm test --watchAll",
"type": "shell",
"problemMatcher": []
},

View File

@@ -1,178 +1,706 @@
# Change Log
## [1.7.0-develop.3](https://github.com/chenasraf/simple-scaffold/compare/v1.7.0-develop.2...v1.7.0-develop.3) (2023-05-11)
## [1.7.0-develop.2](https://github.com/chenasraf/simple-scaffold/compare/v1.7.0-develop.1...v1.7.0-develop.2) (2023-05-10)
## [1.7.0-develop.1](https://github.com/chenasraf/simple-scaffold/compare/v1.6.0...v1.7.0-develop.1) (2023-05-09)
## [3.1.0](https://github.com/chenasraf/simple-scaffold/compare/v3.0.0...v3.1.0) (2026-03-23)
### Features
* function config file ([02a8ba1](https://github.com/chenasraf/simple-scaffold/commit/02a8ba16cd6ee31806532845cb5ddbe0f5abf7de))
* after scaffold hook ([0a4ead1](https://github.com/chenasraf/simple-scaffold/commit/0a4ead17c013b5410e8eec8000e83fa7b7186fbb))
* config validation ([972d199](https://github.com/chenasraf/simple-scaffold/commit/972d199fbb7167506f76eba697fbea6acea9de56))
* init command ([7926b15](https://github.com/chenasraf/simple-scaffold/commit/7926b15053726d5eb9e5772b5f04c3989e567b2c))
* scaffoldignore ([b5fd1df](https://github.com/chenasraf/simple-scaffold/commit/b5fd1df821eba7e13e5ceedc53da550c8dd7cd8e))
* select, confirm and number input types ([f6408f2](https://github.com/chenasraf/simple-scaffold/commit/f6408f221d84d84c5671737af63ff8079093fd27))
* update all ouput logging ([bb0248f](https://github.com/chenasraf/simple-scaffold/commit/bb0248f91a012c297e408f15f0846c5272166543))
### Bug Fixes
* use path.normalize ([565090a](https://github.com/chenasraf/simple-scaffold/commit/565090a951e13dd222f2f802df717e7cb6ca0a73))
* interactive inputs with existing config/cli options ([c860e46](https://github.com/chenasraf/simple-scaffold/commit/c860e4644a3f3d4bd6b7dab9974accdf8bae9463))
## [1.6.0](https://github.com/chenasraf/simple-scaffold/compare/v1.5.0...v1.6.0) (2023-05-05)
## [3.0.0](https://github.com/chenasraf/simple-scaffold/compare/v2.3.3...v3.0.0) (2026-03-23)
### Features
* node.js function for remote configs ([ce5adbe](https://github.com/chenasraf/simple-scaffold/commit/ce5adbe0f898a86db6046d7f66d83dfcaa519ad2))
* add .scaffold.{ext} as auto-detected file ([04e7e89](https://github.com/chenasraf/simple-scaffold/commit/04e7e895d7d97e29cf1ded2f2d0ac8e6a237b997))
* auto-detect config file ([68e6d17](https://github.com/chenasraf/simple-scaffold/commit/68e6d17fa9898dffbc620eba0140748a90bc007f))
* interactive inputs ([519ef27](https://github.com/chenasraf/simple-scaffold/commit/519ef273ac3db4b7a1e71c8e1c456aa1334d6fbd))
* predefined data inputs ([d64dd4f](https://github.com/chenasraf/simple-scaffold/commit/d64dd4f0e775d3bff3efb074d0af23d54edcbaab))
### Bug Fixes
* move dependency to dev dependency ([d916d88](https://github.com/chenasraf/simple-scaffold/commit/d916d88384054e6c6b40e6299073f1d1acb4d29d))
* string helpers to words parts conversion ([af33c05](https://github.com/chenasraf/simple-scaffold/commit/af33c059b91d3f463a5d174ab3a0119c577880c5))
## Change Log
## [2.3.3](https://github.com/chenasraf/simple-scaffold/compare/v2.3.2...v2.3.3) (2025-06-18)
# [1.5.0](https://github.com/chenasraf/simple-scaffold/compare/v1.6.0-develop.1...v1.5.0) (2023-05-04)
## [1.6.0-develop.1](https://github.com/chenasraf/simple-scaffold/compare/v1.5.0...v1.6.0-develop.1) (2023-05-04)
### Bug Fixes
* config CLI precedence over file ([4b0b4e7](https://github.com/chenasraf/simple-scaffold/commit/4b0b4e73803ff741120b18767ded88db324a8844))
## [2.3.2](https://github.com/chenasraf/simple-scaffold/compare/v2.3.1...v2.3.2) (2024-10-27)
### Bug Fixes
* template config from CLI ([41f4ca5](https://github.com/chenasraf/simple-scaffold/commit/41f4ca52f12d3477e1a9a15757dc816fb99b6743))
## [2.3.1](https://github.com/chenasraf/simple-scaffold/compare/v2.3.0...v2.3.1) (2024-10-03)
### Bug Fixes
* strip tmpDir from output dir ([#108](https://github.com/chenasraf/simple-scaffold/issues/108)) ([80c92bf](https://github.com/chenasraf/simple-scaffold/commit/80c92bfe84dc896412ef98bce222e1d26cdb4e91))
## [2.3.0](https://github.com/chenasraf/simple-scaffold/compare/v2.2.2...v2.3.0) (2024-09-17)
### Features
- node.js function for remote configs
([ce5adbe](https://github.com/chenasraf/simple-scaffold/commit/ce5adbe0f898a86db6046d7f66d83dfcaa519ad2))
* remove chalk dependency ([ab9322e](https://github.com/chenasraf/simple-scaffold/commit/ab9322e1ab9c0a07cdab7275f3398286dee67a64))
### Bug Fixes
- move dependency to dev dependency
([d916d88](https://github.com/chenasraf/simple-scaffold/commit/d916d88384054e6c6b40e6299073f1d1acb4d29d))
* exclude globs ([89dc43c](https://github.com/chenasraf/simple-scaffold/commit/89dc43c73d9a8640f45ae77e5c89e4f08f7f99ad))
## [1.5.0](https://github.com/chenasraf/simple-scaffold/compare/v1.5.0-develop.1...v1.5.0) (2023-05-02)
## [2.2.2](https://github.com/chenasraf/simple-scaffold/compare/v2.2.1...v2.2.2) (2024-08-27)
### Bug Fixes
* homepage url ([daaefaf](https://github.com/chenasraf/simple-scaffold/commit/daaefaf54e8c8887e6f210d02fd5f96c6ff4aa21))
## [2.2.1](https://github.com/chenasraf/simple-scaffold/compare/v2.2.0...v2.2.1) (2024-04-21)
### Bug Fixes
* beforeWrite from config files ([98b326c](https://github.com/chenasraf/simple-scaffold/commit/98b326c84346162f379af46bc5aefb69df8be515))
* use console.info for handlebars parse warning ([19e7b0f](https://github.com/chenasraf/simple-scaffold/commit/19e7b0f0c35c1b79a98781bdec9f54354123d8e0))
# [2.2.0](https://github.com/chenasraf/simple-scaffold/compare/v2.1.0...v2.2.0) (2024-02-23)
## [1.5.0-develop.1](https://github.com/chenasraf/simple-scaffold/compare/v1.4.0...v1.5.0-develop.1) (2023-05-02)
### Features
- add github remote templates
([f961c13](https://github.com/chenasraf/simple-scaffold/commit/f961c13da15320b42540773ed958cdc3f97e4502))
- support for remote template configs
([05487f4](https://github.com/chenasraf/simple-scaffold/commit/05487f4d1e3b05f1d695242bb54427ee2fbdf247))
* `list` command ([d579c09](https://github.com/chenasraf/simple-scaffold/commit/d579c09c11f2149fe7bb4515297c1287fa67083e))
* add `--before-write` cli option ([#89](https://github.com/chenasraf/simple-scaffold/issues/89)) ([5f810e2](https://github.com/chenasraf/simple-scaffold/commit/5f810e21160816bc683cc0f375de318ff874871c))
# [2.1.0](https://github.com/chenasraf/simple-scaffold/compare/v2.0.2...v2.1.0) (2024-02-12)
## [1.4.0](https://github.com/chenasraf/simple-scaffold/compare/v1.3.2...v1.4.0) (2023-04-28)
### Features
- add `--key` | `-k` to config loader
([6c5ba0b](https://github.com/chenasraf/simple-scaffold/commit/6c5ba0bc916fb1d59240d2eaa1abedc74527a974))
* support directory in --config flag ([e48b832](https://github.com/chenasraf/simple-scaffold/commit/e48b832e0b72a084d33fa2cbcca332e8209a734f))
* support providing name in config ([4e7ac34](https://github.com/chenasraf/simple-scaffold/commit/4e7ac34db9bf67d012bbd1c06c1a26bc5ac93559))
## [2.0.2](https://github.com/chenasraf/simple-scaffold/compare/v2.0.1...v2.0.2) (2024-02-04)
## [1.3.2](https://github.com/chenasraf/simple-scaffold/compare/v1.3.1...v1.3.2) (2023-04-28)
### Bug Fixes
- release build
([2c23fa9](https://github.com/chenasraf/simple-scaffold/commit/2c23fa9dbb310cd0a31f09606798f96b95d66779))
- release build asset
([0bef2df](https://github.com/chenasraf/simple-scaffold/commit/0bef2df5f3aa800ad5f1094c0996108db9acce51))
* try to await scaffold before finally ([1b70897](https://github.com/chenasraf/simple-scaffold/commit/1b70897f9840e6365ff800490fbb813b9840177d))
## [2.0.1](https://github.com/chenasraf/simple-scaffold/compare/v2.0.0...v2.0.1) (2024-02-02)
## [1.3.1](https://github.com/chenasraf/simple-scaffold/compare/v1.3.0...v1.3.1) (2023-04-28)
### Bug Fixes
- docs
([6e19a86](https://github.com/chenasraf/simple-scaffold/commit/6e19a86190dd924058a48448aa6463569ef1125f))
- remove old peer-dep
([c7e2ef8](https://github.com/chenasraf/simple-scaffold/commit/c7e2ef862cb658feb1071ac120b185d8b34d6dd3))
* log level flag ([5d7f449](https://github.com/chenasraf/simple-scaffold/commit/5d7f449050e50a6e4b2d00b7a2215cdb5fc9b611))
* rm tmp dir too early ([4aa52c8](https://github.com/chenasraf/simple-scaffold/commit/4aa52c84bd8cf302031e9f7f6407466aa736beb7))
## [1.3.0](https://github.com/chenasraf/simple-scaffold/compare/v1.2.0...v1.3.0) (2023-04-25)
# [2.0.0](https://github.com/chenasraf/simple-scaffold/compare/v1.9.0...v2.0.0) (2024-01-31)
### Features
* fix!: version number ([bc0a18d](https://github.com/chenasraf/simple-scaffold/commit/bc0a18dce01fefec6187192cb20c9303f7f7dbfa))
* remove gh flag ([939200c](https://github.com/chenasraf/simple-scaffold/commit/939200c9f21be240485ea602a73b983ba2f47aaf))
* tests ([ff92fd7](https://github.com/chenasraf/simple-scaffold/commit/ff92fd7607f1b86f36fc6b62652fdfc81cb391a3))
* try multiple default config files ([89aacb5](https://github.com/chenasraf/simple-scaffold/commit/89aacb58fd90a892f4994c758c61c43b2a6b1fba))
* Update README.md ([e012d51](https://github.com/chenasraf/simple-scaffold/commit/e012d51))
* docs: fix readme doc links ([55e561b](https://github.com/chenasraf/simple-scaffold/commit/55e561b))
* chore: fix docs & formatting ([b4f0731](https://github.com/chenasraf/simple-scaffold/commit/b4f0731))
* chore: update deps ([22ad5d4](https://github.com/chenasraf/simple-scaffold/commit/22ad5d4))
* chore: update deps ([b2373aa](https://github.com/chenasraf/simple-scaffold/commit/b2373aa))
* chore(release): 2.0.0-pre.1 [skip ci] ([add794a](https://github.com/chenasraf/simple-scaffold/commit/add794a))
* ci: fix ([29a7aa3](https://github.com/chenasraf/simple-scaffold/commit/29a7aa3))
* ci: fix docs ([d4cb767](https://github.com/chenasraf/simple-scaffold/commit/d4cb767))
* ci: fix docs ([570b00d](https://github.com/chenasraf/simple-scaffold/commit/570b00d))
* ci: fix docs ([7ef3421](https://github.com/chenasraf/simple-scaffold/commit/7ef3421))
* ci: fix docs build dir ([96c1d5a](https://github.com/chenasraf/simple-scaffold/commit/96c1d5a))
* ci: fix docs command ([5cf5692](https://github.com/chenasraf/simple-scaffold/commit/5cf5692))
* ci: update build ([d168dc1](https://github.com/chenasraf/simple-scaffold/commit/d168dc1))
* ci: update docs build ([9830df8](https://github.com/chenasraf/simple-scaffold/commit/9830df8))
* ci: use tag versions ([b6fed83](https://github.com/chenasraf/simple-scaffold/commit/b6fed83))
* docs: docusaurus initial commit ([a955c4d](https://github.com/chenasraf/simple-scaffold/commit/a955c4d))
* docs: gtag + update deps ([e0c0f5c](https://github.com/chenasraf/simple-scaffold/commit/e0c0f5c))
* docs: update ([4821be6](https://github.com/chenasraf/simple-scaffold/commit/4821be6))
* docs: update ([c95477d](https://github.com/chenasraf/simple-scaffold/commit/c95477d))
* docs: update docs ([f59111f](https://github.com/chenasraf/simple-scaffold/commit/f59111f))
* docs: update docs ([3b0fc7a](https://github.com/chenasraf/simple-scaffold/commit/3b0fc7a))
* docs: update docs, remove generated files from git ([f0a080c](https://github.com/chenasraf/simple-scaffold/commit/f0a080c))
* docs: update readme image ([8478d36](https://github.com/chenasraf/simple-scaffold/commit/8478d36))
* fix: remove gh flag ([e66d6ba](https://github.com/chenasraf/simple-scaffold/commit/e66d6ba))
* feat: try multiple default config files ([f25cda7](https://github.com/chenasraf/simple-scaffold/commit/f25cda7))
* chore!: remove `Name` from default data ([0bb282c](https://github.com/chenasraf/simple-scaffold/commit/0bb282c))
* chore!: update massarg ([55877f0](https://github.com/chenasraf/simple-scaffold/commit/55877f0))
* feat!: remove url colon syntax ([b57be8e](https://github.com/chenasraf/simple-scaffold/commit/b57be8e))
* feat!: rename verbose to logLevel ([17fdf0c](https://github.com/chenasraf/simple-scaffold/commit/17fdf0c))
* feat!: separate git/github/config flags ([995b433](https://github.com/chenasraf/simple-scaffold/commit/995b433))
- load scaffold config from files
([c398976](https://github.com/chenasraf/simple-scaffold/commit/c3989769fee445c9183ff5e5b3892c4e9fb66a9e))
### Bug Fixes
- config option should not be mandatory
([3db6a91](https://github.com/chenasraf/simple-scaffold/commit/3db6a918f13d9300efa2fcb4a356d004475ab91c))
- export config file type
([4302eb5](https://github.com/chenasraf/simple-scaffold/commit/4302eb5ce35ed6cf1dc80dfb92790c3fdd96f963))
## 1.9.0 (2024-01-02)
## [1.2.0](https://github.com/chenasraf/simple-scaffold/compare/v1.1.4...v1.2.0) (2023-04-24)
* chore: update dependencies ([758719d](https://github.com/chenasraf/simple-scaffold/commit/758719d))
* chore(release): 1.9.0 [skip ci] ([4d1a6e1](https://github.com/chenasraf/simple-scaffold/commit/4d1a6e1))
* ci: fix actions ([daae9c1](https://github.com/chenasraf/simple-scaffold/commit/daae9c1))
* ci: update build process ([e26fe2a](https://github.com/chenasraf/simple-scaffold/commit/e26fe2a))
* ci: update build steps ([1903055](https://github.com/chenasraf/simple-scaffold/commit/1903055))
* ci: update docs build, semantic release ([7e1acf0](https://github.com/chenasraf/simple-scaffold/commit/7e1acf0))
* feat: add --recurse-submodules to git clone ([cbaf130](https://github.com/chenasraf/simple-scaffold/commit/cbaf130))
### Features
- append-data cli flag
([3c5c2de](https://github.com/chenasraf/simple-scaffold/commit/3c5c2ded02f61ff086e81ea4a7f40529bdff1c9d))
### Bug Fixes
## 1.8.0 (2023-11-29)
- ci node version
([767d34c](https://github.com/chenasraf/simple-scaffold/commit/767d34c684516d4cea865b25e87c27c779bb79ce))
- github action node version
([7c19c53](https://github.com/chenasraf/simple-scaffold/commit/7c19c533376dc6904231e5cc51c7a4b2658c66e0))
- github action node version
([94fec76](https://github.com/chenasraf/simple-scaffold/commit/94fec766165f7540c578dbf2d0aeeb6ea3969ad8))
- semantic-release build dir
([f7956dd](https://github.com/chenasraf/simple-scaffold/commit/f7956ddc786018905c48ccf1f21a3bb4657c3d75))
- support quote wrapping in append-data
([4fecca8](https://github.com/chenasraf/simple-scaffold/commit/4fecca848347312d45d704f82f2bcb3822da9b06))
* chore: update dependencies ([b048841](https://github.com/chenasraf/simple-scaffold/commit/b048841))
* chore(release): 1.8.0 [skip ci] ([f666c35](https://github.com/chenasraf/simple-scaffold/commit/f666c35)), closes [#63](https://github.com/chenasraf/simple-scaffold/issues/63)
* chore(release): 1.8.0-pre.1 [skip ci] ([746f924](https://github.com/chenasraf/simple-scaffold/commit/746f924)), closes [#63](https://github.com/chenasraf/simple-scaffold/issues/63)
* docs: update configuration files docs ([f5d55f2](https://github.com/chenasraf/simple-scaffold/commit/f5d55f2))
* ci: update release config ([807c3e2](https://github.com/chenasraf/simple-scaffold/commit/807c3e2))
* fix(config): fn config load ([457c904](https://github.com/chenasraf/simple-scaffold/commit/457c904)), closes [#63](https://github.com/chenasraf/simple-scaffold/issues/63)
* build(deps-dev): bump @babel/traverse from 7.21.5 to 7.23.2 ([0fa1ad4](https://github.com/chenasraf/simple-scaffold/commit/0fa1ad4))
## [1.1.3](https://github.com/chenasraf/simple-scaffold/compare/v1.1.2...v1.1.3) (2023-03-11)
### Bug Fixes
- base path
([943717a](https://github.com/chenasraf/simple-scaffold/commit/943717a76998ec0609f2072c886df6b4775f2ea2))
- binary files + add tests
([e450ad2](https://github.com/chenasraf/simple-scaffold/commit/e450ad242ed70ae928b19964da38cdcb1b6cf659))
## <small>1.7.2 (2023-08-20)</small>
## [1.1.0](https://github.com/chenasraf/simple-scaffold/compare/v1.0.4...v1.1.0) (2022-04-21)
* chore(release): 1.7.2 [skip ci] ([d62eeeb](https://github.com/chenasraf/simple-scaffold/commit/d62eeeb))
## [1.0.3](https://github.com/chenasraf/simple-scaffold/compare/v1.0.2...v1.0.3) (2022-03-03)
## [1.0.1-pre.1](https://github.com/chenasraf/simple-scaffold/compare/v1.0.0...v1.0.1-pre.1) (2022-02-17)
## [0.7.5](https://github.com/chenasraf/simple-scaffold/compare/v0.7.4...v0.7.5) (2021-09-26)
## <small>1.7.2-pre.1 (2023-08-15)</small>
## [0.7.4](https://github.com/chenasraf/simple-scaffold/compare/v0.7.3...v0.7.4) (2021-09-26)
* chore: bump version number ([9f5716e](https://github.com/chenasraf/simple-scaffold/commit/9f5716e))
* chore: formatting ([8c3369a](https://github.com/chenasraf/simple-scaffold/commit/8c3369a))
* chore: update dependencies ([d2a2fda](https://github.com/chenasraf/simple-scaffold/commit/d2a2fda))
* chore: update logs ([20ef0ce](https://github.com/chenasraf/simple-scaffold/commit/20ef0ce))
* chore(release): 1.7.2-pre.1 [skip ci] ([9f58fff](https://github.com/chenasraf/simple-scaffold/commit/9f58fff))
* ci: trigger on pre branch ([dbba81d](https://github.com/chenasraf/simple-scaffold/commit/dbba81d))
* fix: windows path resolution ([98ee000](https://github.com/chenasraf/simple-scaffold/commit/98ee000))
* test: add tests ([3413151](https://github.com/chenasraf/simple-scaffold/commit/3413151))
## [0.7.3](https://github.com/chenasraf/simple-scaffold/compare/v0.7.2...v0.7.3) (2021-09-26)
## [0.7.2](https://github.com/chenasraf/simple-scaffold/compare/v0.6.1...v0.7.2) (2021-04-19)
## [0.6.1](https://github.com/chenasraf/simple-scaffold/compare/v0.6.0...v0.6.1) (2021-02-01)
## <small>1.7.1 (2023-06-07)</small>
### Bug Fixes
* chore(release): 1.7.1 [skip ci] ([de05bca](https://github.com/chenasraf/simple-scaffold/commit/de05bca))
* build: fix tsconfig ([2cf31e8](https://github.com/chenasraf/simple-scaffold/commit/2cf31e8))
* build: update release rules, add tests ([3714e8b](https://github.com/chenasraf/simple-scaffold/commit/3714e8b))
- binary files
([7c0c347](https://github.com/chenasraf/simple-scaffold/commit/7c0c3470020d7c166ea68a8effa6df65ec38f2c8))
## [0.6.0](https://github.com/chenasraf/simple-scaffold/compare/v0.5.0...v0.6.0) (2021-02-01)
### Bug Fixes
## <small>1.7.1-develop.1 (2023-06-07)</small>
- support deeper file structure
([4afafa5](https://github.com/chenasraf/simple-scaffold/commit/4afafa5a4af2e3f4b0af54f20811ecb2c8d98560))
* chore(release): 1.7.1-develop.1 [skip ci] ([77be7c0](https://github.com/chenasraf/simple-scaffold/commit/77be7c0))
## [0.5.0](https://github.com/chenasraf/simple-scaffold/compare/v0.4.5...v0.5.0) (2019-02-27)
## [0.4.5](https://github.com/chenasraf/simple-scaffold/compare/v0.4.4...v0.4.5) (2019-02-27)
## [0.4.4](https://github.com/chenasraf/simple-scaffold/compare/v0.4.3...v0.4.4) (2019-02-27)
## 1.7.0 (2023-05-17)
## [0.4.3](https://github.com/chenasraf/simple-scaffold/compare/v0.4.2...v0.4.3) (2019-02-27)
* chore(release): 1.7.0 [skip ci] ([4868925](https://github.com/chenasraf/simple-scaffold/commit/4868925))
## [0.4.2](https://github.com/chenasraf/simple-scaffold/compare/v0.4.1...v0.4.2) (2019-02-25)
## [0.4.1](https://github.com/chenasraf/simple-scaffold/compare/v0.3.1...v0.4.1) (2019-02-25)
## [0.3.1](https://github.com/chenasraf/simple-scaffold/compare/v0.3.0...v0.3.1) (2018-01-15)
## 1.7.0-develop.7 (2023-06-07)
## [0.3.0](https://github.com/chenasraf/simple-scaffold/compare/v0.2.0...v0.3.0) (2018-01-15)
* chore(release): 1.7.0-develop.7 [skip ci] ([fea6c0f](https://github.com/chenasraf/simple-scaffold/commit/fea6c0f))
* fix: local config file load error ([2b74239](https://github.com/chenasraf/simple-scaffold/commit/2b74239))
## [0.2.0](https://github.com/chenasraf/simple-scaffold/compare/v0.1.5...v0.2.0) (2018-01-05)
## [0.1.5](https://github.com/chenasraf/simple-scaffold/compare/v0.1.4...v0.1.5) (2018-01-01)
## [0.1.4](https://github.com/chenasraf/simple-scaffold/compare/v0.1.3...v0.1.4) (2018-01-01)
## 1.7.0-develop.6 (2023-05-27)
## [0.1.3](https://github.com/chenasraf/simple-scaffold/compare/v0.1.2...v0.1.3) (2018-01-01)
* chore(release): 1.7.0-develop.6 [skip ci] ([a47ba11](https://github.com/chenasraf/simple-scaffold/commit/a47ba11))
* docs: update README.md ([06b1552](https://github.com/chenasraf/simple-scaffold/commit/06b1552))
* test: add tests ([4f27b7b](https://github.com/chenasraf/simple-scaffold/commit/4f27b7b))
## 0.1.2 (2018-01-01)
## 1.7.0 (2023-05-17)
* chore(release): 1.7.0 [skip ci] ([4868925](https://github.com/chenasraf/simple-scaffold/commit/4868925))
## 1.7.0-develop.5 (2023-05-12)
* chore(release): 1.7.0-develop.5 [skip ci] ([bee430a](https://github.com/chenasraf/simple-scaffold/commit/bee430a))
* test: fix + add tests ([3dfc920](https://github.com/chenasraf/simple-scaffold/commit/3dfc920))
* refactor: remove lodash dependency ([68307d1](https://github.com/chenasraf/simple-scaffold/commit/68307d1))
* build: update workflows ([33e1d56](https://github.com/chenasraf/simple-scaffold/commit/33e1d56))
## 1.7.0-develop.4 (2023-05-11)
* chore(release): 1.7.0-develop.4 [skip ci] ([c446439](https://github.com/chenasraf/simple-scaffold/commit/c446439))
* build: update package asset name ([773fd00](https://github.com/chenasraf/simple-scaffold/commit/773fd00))
## 1.7.0-develop.3 (2023-05-11)
* chore: cleanup ([9393c54](https://github.com/chenasraf/simple-scaffold/commit/9393c54))
* chore(release): 1.7.0-develop.3 [skip ci] ([22763f6](https://github.com/chenasraf/simple-scaffold/commit/22763f6))
* build: add packageManager key to package.json ([dac5527](https://github.com/chenasraf/simple-scaffold/commit/dac5527))
* build: update package link ([a54b1f9](https://github.com/chenasraf/simple-scaffold/commit/a54b1f9))
* build: update workflows ([339459c](https://github.com/chenasraf/simple-scaffold/commit/339459c))
* build: update workflows ([62b4a1c](https://github.com/chenasraf/simple-scaffold/commit/62b4a1c))
* build: use pnpm ([5844c08](https://github.com/chenasraf/simple-scaffold/commit/5844c08))
## 1.7.0-develop.2 (2023-05-10)
* chore(release): 1.7.0-develop.2 [skip ci] ([263bf0b](https://github.com/chenasraf/simple-scaffold/commit/263bf0b))
* build: update release.config.js ([e0ed371](https://github.com/chenasraf/simple-scaffold/commit/e0ed371))
## 1.7.0-develop.1 (2023-05-09)
* chore: cleanups ([c027e37](https://github.com/chenasraf/simple-scaffold/commit/c027e37))
* chore: cleanups ([2251a9c](https://github.com/chenasraf/simple-scaffold/commit/2251a9c))
* chore(release): 1.7.0-develop.1 [skip ci] ([87934fb](https://github.com/chenasraf/simple-scaffold/commit/87934fb))
* build: fix build ([19b7ed5](https://github.com/chenasraf/simple-scaffold/commit/19b7ed5))
* feat: function config file ([02a8ba1](https://github.com/chenasraf/simple-scaffold/commit/02a8ba1))
* fix: use path.normalize ([565090a](https://github.com/chenasraf/simple-scaffold/commit/565090a))
* docs: update docs ([943aad1](https://github.com/chenasraf/simple-scaffold/commit/943aad1))
* docs: update readme + author ([be92047](https://github.com/chenasraf/simple-scaffold/commit/be92047))
* test: move scaffold.config.js ([9fb4762](https://github.com/chenasraf/simple-scaffold/commit/9fb4762))
## 1.6.0 (2023-05-05)
* chore(release): 1.6.0 [skip ci] ([0940e84](https://github.com/chenasraf/simple-scaffold/commit/0940e84))
* chore(release): bump version number ([77e477e](https://github.com/chenasraf/simple-scaffold/commit/77e477e))
* build: fix package.json version update ([6c8eb02](https://github.com/chenasraf/simple-scaffold/commit/6c8eb02))
* build: use pnpm instead of yarn ([08b0488](https://github.com/chenasraf/simple-scaffold/commit/08b0488))
* docs: fix changelog title ([5ba6034](https://github.com/chenasraf/simple-scaffold/commit/5ba6034))
* docs: update docs ([95dafdf](https://github.com/chenasraf/simple-scaffold/commit/95dafdf))
## 1.6.0-develop.1 (2023-05-04)
* chore(release): 1.5.0-develop.2 [skip ci] ([81743f1](https://github.com/chenasraf/simple-scaffold/commit/81743f1))
* chore(release): 1.5.0-develop.3 [skip ci] ([4bf9674](https://github.com/chenasraf/simple-scaffold/commit/4bf9674))
* chore(release): 1.6.0-develop.1 [skip ci] ([4c3f1c9](https://github.com/chenasraf/simple-scaffold/commit/4c3f1c9))
* docs: fix cli.md page ([2ca91a7](https://github.com/chenasraf/simple-scaffold/commit/2ca91a7))
* docs: fix link ([ba984b6](https://github.com/chenasraf/simple-scaffold/commit/ba984b6))
* docs: update docs ([260ce6a](https://github.com/chenasraf/simple-scaffold/commit/260ce6a))
* docs: update docs ([7b6260c](https://github.com/chenasraf/simple-scaffold/commit/7b6260c))
* docs: update package description ([1799971](https://github.com/chenasraf/simple-scaffold/commit/1799971))
* feat: node.js function for remote configs ([ce5adbe](https://github.com/chenasraf/simple-scaffold/commit/ce5adbe))
* build: remove unnecessary dependency ([99318f7](https://github.com/chenasraf/simple-scaffold/commit/99318f7))
* build: separate test & build ([9489f14](https://github.com/chenasraf/simple-scaffold/commit/9489f14))
* fix: move dependency to dev dependency ([d916d88](https://github.com/chenasraf/simple-scaffold/commit/d916d88))
## 1.5.0 (2023-05-02)
* chore: bump version number ([7f10db0](https://github.com/chenasraf/simple-scaffold/commit/7f10db0))
* chore(release): 1.5.0 [skip ci] ([7be79fd](https://github.com/chenasraf/simple-scaffold/commit/7be79fd))
* docs: update help text ([1a3fd3d](https://github.com/chenasraf/simple-scaffold/commit/1a3fd3d))
## 1.5.0-develop.1 (2023-05-02)
* chore: fix package version ([10ea6b4](https://github.com/chenasraf/simple-scaffold/commit/10ea6b4))
* chore(release): 1.5.0-develop.1 [skip ci] ([93f5b4a](https://github.com/chenasraf/simple-scaffold/commit/93f5b4a))
* docs: add docs for remote templates ([b74411d](https://github.com/chenasraf/simple-scaffold/commit/b74411d))
* feat: add github remote templates ([f961c13](https://github.com/chenasraf/simple-scaffold/commit/f961c13))
* feat: support for remote template configs ([05487f4](https://github.com/chenasraf/simple-scaffold/commit/05487f4))
* build: always build docs ([ce39918](https://github.com/chenasraf/simple-scaffold/commit/ce39918))
* build: fix git/github step order ([c50518a](https://github.com/chenasraf/simple-scaffold/commit/c50518a))
## 1.4.0 (2023-04-28)
* chore(release): 1.4.0 [skip ci] ([83d3807](https://github.com/chenasraf/simple-scaffold/commit/83d3807))
* feat: add `--key` | `-k` to config loader ([6c5ba0b](https://github.com/chenasraf/simple-scaffold/commit/6c5ba0b))
## <small>1.3.2 (2023-04-28)</small>
* chore(release): 1.3.2 [skip ci] ([2c4eccd](https://github.com/chenasraf/simple-scaffold/commit/2c4eccd))
* fix: release build ([2c23fa9](https://github.com/chenasraf/simple-scaffold/commit/2c23fa9))
* fix: release build asset ([0bef2df](https://github.com/chenasraf/simple-scaffold/commit/0bef2df))
## <small>1.3.1 (2023-04-28)</small>
* chore: bump version number + fix changelog ([eba7897](https://github.com/chenasraf/simple-scaffold/commit/eba7897))
* chore(release): 1.3.1 [skip ci] ([398a5d7](https://github.com/chenasraf/simple-scaffold/commit/398a5d7))
* fix: docs ([6e19a86](https://github.com/chenasraf/simple-scaffold/commit/6e19a86))
* fix: remove old peer-dep ([c7e2ef8](https://github.com/chenasraf/simple-scaffold/commit/c7e2ef8))
* docs: fix doc links ([36dd27e](https://github.com/chenasraf/simple-scaffold/commit/36dd27e))
## 1.3.0 (2023-04-25)
* chore(release): 1.3.0 [skip ci] ([dfdbca5](https://github.com/chenasraf/simple-scaffold/commit/dfdbca5))
* docs: add changelog to typedoc ([873fa77](https://github.com/chenasraf/simple-scaffold/commit/873fa77))
* docs: add table of contents ([8a2207b](https://github.com/chenasraf/simple-scaffold/commit/8a2207b))
* docs: clean up css ([1d6643b](https://github.com/chenasraf/simple-scaffold/commit/1d6643b))
* docs: fix CHANGELOG.md ([0c8e6e7](https://github.com/chenasraf/simple-scaffold/commit/0c8e6e7))
* docs: move migration to pages, fix urls ([1460868](https://github.com/chenasraf/simple-scaffold/commit/1460868))
* docs: remove unnecessary nested menus ([42568e0](https://github.com/chenasraf/simple-scaffold/commit/42568e0))
* docs: reorganize file structure ([f41ebfb](https://github.com/chenasraf/simple-scaffold/commit/f41ebfb))
* docs: split into files ([a4498f9](https://github.com/chenasraf/simple-scaffold/commit/a4498f9))
* docs: update config doc ([5a2b187](https://github.com/chenasraf/simple-scaffold/commit/5a2b187))
* docs: update readme ([f28280e](https://github.com/chenasraf/simple-scaffold/commit/f28280e))
* docs: use js for typedoc config ([9b86499](https://github.com/chenasraf/simple-scaffold/commit/9b86499))
* build: only generate docs on master ([1e0b731](https://github.com/chenasraf/simple-scaffold/commit/1e0b731))
* build: remove unnecessary yarn pack ([b3f7912](https://github.com/chenasraf/simple-scaffold/commit/b3f7912))
* build: update changelog sections ([1ce4a41](https://github.com/chenasraf/simple-scaffold/commit/1ce4a41))
* fix: config option should not be mandatory ([3db6a91](https://github.com/chenasraf/simple-scaffold/commit/3db6a91))
* fix: export config file type ([4302eb5](https://github.com/chenasraf/simple-scaffold/commit/4302eb5))
* feat: load scaffold config from files ([c398976](https://github.com/chenasraf/simple-scaffold/commit/c398976))
## 1.2.0 (2023-04-24)
* chore: bump version number ([8e432bf](https://github.com/chenasraf/simple-scaffold/commit/8e432bf))
* chore: bump version number [skip-ci] ([029f260](https://github.com/chenasraf/simple-scaffold/commit/029f260))
* chore: update dependencies ([20400bd](https://github.com/chenasraf/simple-scaffold/commit/20400bd))
* chore: update FUNDING.yml ([1bfcafa](https://github.com/chenasraf/simple-scaffold/commit/1bfcafa))
* chore(release): 1.2.0 [skip ci] ([7da786a](https://github.com/chenasraf/simple-scaffold/commit/7da786a))
* fix: ci node version ([767d34c](https://github.com/chenasraf/simple-scaffold/commit/767d34c))
* fix: github action node version ([7c19c53](https://github.com/chenasraf/simple-scaffold/commit/7c19c53))
* fix: github action node version ([94fec76](https://github.com/chenasraf/simple-scaffold/commit/94fec76))
* fix: semantic-release build dir ([f7956dd](https://github.com/chenasraf/simple-scaffold/commit/f7956dd))
* fix: support quote wrapping in append-data ([4fecca8](https://github.com/chenasraf/simple-scaffold/commit/4fecca8))
* build: add missing dependencies ([75641e5](https://github.com/chenasraf/simple-scaffold/commit/75641e5))
* build: add missing dependencies ([f4c745b](https://github.com/chenasraf/simple-scaffold/commit/f4c745b))
* build: fix build ([47b4c42](https://github.com/chenasraf/simple-scaffold/commit/47b4c42))
* build: fix docs build ([7a4c0ab](https://github.com/chenasraf/simple-scaffold/commit/7a4c0ab))
* build: semantic-release ([2050ea3](https://github.com/chenasraf/simple-scaffold/commit/2050ea3))
* build: update dependencies & fix build ([59a46b0](https://github.com/chenasraf/simple-scaffold/commit/59a46b0))
* build: update github action versions ([222e1a0](https://github.com/chenasraf/simple-scaffold/commit/222e1a0))
* docs: update docs ([7ef6d58](https://github.com/chenasraf/simple-scaffold/commit/7ef6d58))
* docs: update domain ([8f5bee8](https://github.com/chenasraf/simple-scaffold/commit/8f5bee8))
* docs: update spacing ([ed385ec](https://github.com/chenasraf/simple-scaffold/commit/ed385ec))
* docs: update table css ([833ea9d](https://github.com/chenasraf/simple-scaffold/commit/833ea9d))
* docs: update typedoc & remove custom theme ([8fb508f](https://github.com/chenasraf/simple-scaffold/commit/8fb508f))
* docs: update typedoc version ([c334396](https://github.com/chenasraf/simple-scaffold/commit/c334396))
* feat: append-data cli flag ([3c5c2de](https://github.com/chenasraf/simple-scaffold/commit/3c5c2de))
* Bump json5 from 2.2.0 to 2.2.3 ([e28c4db](https://github.com/chenasraf/simple-scaffold/commit/e28c4db))
* Bump minimatch from 3.0.4 to 3.1.2 ([ee4e52c](https://github.com/chenasraf/simple-scaffold/commit/ee4e52c))
## <small>1.1.3 (2023-03-11)</small>
* chore: bump version number [publish] ([d7d2b13](https://github.com/chenasraf/simple-scaffold/commit/d7d2b13))
* fix: base path ([943717a](https://github.com/chenasraf/simple-scaffold/commit/943717a))
* fix: binary files + add tests ([e450ad2](https://github.com/chenasraf/simple-scaffold/commit/e450ad2))
* add gaid to docs ([9bd6219](https://github.com/chenasraf/simple-scaffold/commit/9bd6219))
* fix build ([0364247](https://github.com/chenasraf/simple-scaffold/commit/0364247))
* fix build ([ac2c0d7](https://github.com/chenasraf/simple-scaffold/commit/ac2c0d7))
* fix doc deps ([12f8bca](https://github.com/chenasraf/simple-scaffold/commit/12f8bca))
* fix docs build ([0042c12](https://github.com/chenasraf/simple-scaffold/commit/0042c12))
* fix docs build process ([35262b5](https://github.com/chenasraf/simple-scaffold/commit/35262b5))
* fix gtag ([643431d](https://github.com/chenasraf/simple-scaffold/commit/643431d))
* fix workflow [skip publish] ([0d359b1](https://github.com/chenasraf/simple-scaffold/commit/0d359b1))
* formatting updates ([b569f2b](https://github.com/chenasraf/simple-scaffold/commit/b569f2b))
* improve docs build process ([3b77e69](https://github.com/chenasraf/simple-scaffold/commit/3b77e69))
* improve docs build process ([3cf9359](https://github.com/chenasraf/simple-scaffold/commit/3cf9359))
* improve docs build process ([8957b59](https://github.com/chenasraf/simple-scaffold/commit/8957b59))
* npm audit fix ([5571aba](https://github.com/chenasraf/simple-scaffold/commit/5571aba))
* remove unnecessary install ([11edb0d](https://github.com/chenasraf/simple-scaffold/commit/11edb0d))
* revert docs build in ci ([ac8af8e](https://github.com/chenasraf/simple-scaffold/commit/ac8af8e))
* try fix docs build process ([96b93d8](https://github.com/chenasraf/simple-scaffold/commit/96b93d8))
* try to fix docs [skip publish] ([af65eca](https://github.com/chenasraf/simple-scaffold/commit/af65eca))
* Typdoc (#37) [skip publish] ([f2a75c9](https://github.com/chenasraf/simple-scaffold/commit/f2a75c9)), closes [#37](https://github.com/chenasraf/simple-scaffold/issues/37)
* update docs ([a002402](https://github.com/chenasraf/simple-scaffold/commit/a002402))
* update docs ([27a6ba4](https://github.com/chenasraf/simple-scaffold/commit/27a6ba4))
* update docs ([923b531](https://github.com/chenasraf/simple-scaffold/commit/923b531))
* update docs ([bf10fb8](https://github.com/chenasraf/simple-scaffold/commit/bf10fb8))
* update docs [skip publish] ([4e2fa01](https://github.com/chenasraf/simple-scaffold/commit/4e2fa01))
* update docs + formatting update ([2841ebd](https://github.com/chenasraf/simple-scaffold/commit/2841ebd))
* update docs + spell checker ([7c69010](https://github.com/chenasraf/simple-scaffold/commit/7c69010))
* update docs GA ([869911b](https://github.com/chenasraf/simple-scaffold/commit/869911b))
* Update FUNDING.yml ([a8e9f71](https://github.com/chenasraf/simple-scaffold/commit/a8e9f71))
* update help command ([8c8cede](https://github.com/chenasraf/simple-scaffold/commit/8c8cede))
* update intro gif ([1b37a8e](https://github.com/chenasraf/simple-scaffold/commit/1b37a8e))
* update intro gif ([576798c](https://github.com/chenasraf/simple-scaffold/commit/576798c))
* Update README.md ([5308ef9](https://github.com/chenasraf/simple-scaffold/commit/5308ef9))
* Update README.md ([6fa2a8b](https://github.com/chenasraf/simple-scaffold/commit/6fa2a8b))
* Update README.md ([adba649](https://github.com/chenasraf/simple-scaffold/commit/adba649))
* Update README.md ([cd68ab4](https://github.com/chenasraf/simple-scaffold/commit/cd68ab4))
* Update README.md ([bdc23e7](https://github.com/chenasraf/simple-scaffold/commit/bdc23e7))
* Update README.md ([6160a04](https://github.com/chenasraf/simple-scaffold/commit/6160a04))
* update test desc ([40606ed](https://github.com/chenasraf/simple-scaffold/commit/40606ed))
* docs: fix typo ([1d0c20c](https://github.com/chenasraf/simple-scaffold/commit/1d0c20c))
## 1.1.0 (2022-04-21)
* Add keywords to package.json [skip ci] ([4a4e024](https://github.com/chenasraf/simple-scaffold/commit/4a4e024))
* Bump minimist from 1.2.5 to 1.2.6 ([56be5f3](https://github.com/chenasraf/simple-scaffold/commit/56be5f3))
* bump version number ([dffa81f](https://github.com/chenasraf/simple-scaffold/commit/dffa81f))
* update package.json ([3c57638](https://github.com/chenasraf/simple-scaffold/commit/3c57638))
* Update README.md [skip ci] ([86a7a2c](https://github.com/chenasraf/simple-scaffold/commit/86a7a2c))
* Update README.md [skip ci] ([d3259c4](https://github.com/chenasraf/simple-scaffold/commit/d3259c4))
* v1.1 (#35) ([a3deda2](https://github.com/chenasraf/simple-scaffold/commit/a3deda2)), closes [#35](https://github.com/chenasraf/simple-scaffold/issues/35)
* chore: use rimraf + add error debug log [skip ci] ([b2799d0](https://github.com/chenasraf/simple-scaffold/commit/b2799d0))
* docs: Update README.md [skip ci] ([f4997c6](https://github.com/chenasraf/simple-scaffold/commit/f4997c6))
## <small>1.0.3 (2022-03-03)</small>
* bump version number ([cb6e06f](https://github.com/chenasraf/simple-scaffold/commit/cb6e06f))
* bump version number [skip ci] ([21c4ab6](https://github.com/chenasraf/simple-scaffold/commit/21c4ab6))
* fix transform of windows-style paths ([56f1340](https://github.com/chenasraf/simple-scaffold/commit/56f1340))
* fixed more windows paths, updated tests ([52cb3e7](https://github.com/chenasraf/simple-scaffold/commit/52cb3e7))
* import/file cleanup ([d6e1693](https://github.com/chenasraf/simple-scaffold/commit/d6e1693))
* improved test ([f07df79](https://github.com/chenasraf/simple-scaffold/commit/f07df79))
* refactor handlebarsParse - remove redundant arg ([89d7897](https://github.com/chenasraf/simple-scaffold/commit/89d7897))
* remove unnecessary package [skip ci] ([1783ddf](https://github.com/chenasraf/simple-scaffold/commit/1783ddf))
* update README.md ([e26a434](https://github.com/chenasraf/simple-scaffold/commit/e26a434))
* updated tests ([a043a05](https://github.com/chenasraf/simple-scaffold/commit/a043a05))
## <small>1.0.1-pre.1 (2022-02-17)</small>
* add build step ([f1698d2](https://github.com/chenasraf/simple-scaffold/commit/f1698d2))
* add cmd args ([7cdf5e4](https://github.com/chenasraf/simple-scaffold/commit/7cdf5e4))
* add dry run option ([aeddd44](https://github.com/chenasraf/simple-scaffold/commit/aeddd44))
* add export for cmd_util ([6b57406](https://github.com/chenasraf/simple-scaffold/commit/6b57406))
* add subFolderNameHelper arg ([81ba5f5](https://github.com/chenasraf/simple-scaffold/commit/81ba5f5))
* add tests ([c42a58c](https://github.com/chenasraf/simple-scaffold/commit/c42a58c))
* added --quiet flag ([4f81654](https://github.com/chenasraf/simple-scaffold/commit/4f81654))
* added custom helpers ([d03d0e0](https://github.com/chenasraf/simple-scaffold/commit/d03d0e0))
* bump alpha version number ([d797e5b](https://github.com/chenasraf/simple-scaffold/commit/d797e5b))
* Bump ansi-regex from 5.0.0 to 5.0.1 ([36f8b87](https://github.com/chenasraf/simple-scaffold/commit/36f8b87))
* bump version number ([5ab2637](https://github.com/chenasraf/simple-scaffold/commit/5ab2637))
* bump version number ([53e8bc4](https://github.com/chenasraf/simple-scaffold/commit/53e8bc4))
* bump version: v1.0.0 ([d06c0d6](https://github.com/chenasraf/simple-scaffold/commit/d06c0d6))
* code splitting ([208ee30](https://github.com/chenasraf/simple-scaffold/commit/208ee30))
* Create FUNDING.yml ([40b5920](https://github.com/chenasraf/simple-scaffold/commit/40b5920))
* fail handlebars parse silently ([0af6392](https://github.com/chenasraf/simple-scaffold/commit/0af6392))
* fix basename in some cases ([b1b1aca](https://github.com/chenasraf/simple-scaffold/commit/b1b1aca))
* fix build output files ([99c9055](https://github.com/chenasraf/simple-scaffold/commit/99c9055))
* fix build/publish cmd ([a59f29d](https://github.com/chenasraf/simple-scaffold/commit/a59f29d))
* fix cmd ([cd34930](https://github.com/chenasraf/simple-scaffold/commit/cd34930))
* fix copyright ([54b9023](https://github.com/chenasraf/simple-scaffold/commit/54b9023))
* fix errors, fix nested output ([8413225](https://github.com/chenasraf/simple-scaffold/commit/8413225))
* fix log level 0 ([84e6207](https://github.com/chenasraf/simple-scaffold/commit/84e6207))
* fix main field in package.json ([7273538](https://github.com/chenasraf/simple-scaffold/commit/7273538))
* fix readme [skip publish] ([ad30ee0](https://github.com/chenasraf/simple-scaffold/commit/ad30ee0))
* fix yarn.lock ([a21a35f](https://github.com/chenasraf/simple-scaffold/commit/a21a35f))
* fixed release tarball file location ([3f2945e](https://github.com/chenasraf/simple-scaffold/commit/3f2945e))
* fixed release tarball file location ([9303446](https://github.com/chenasraf/simple-scaffold/commit/9303446))
* fixes + add log level [skip publish] ([2623b78](https://github.com/chenasraf/simple-scaffold/commit/2623b78))
* helpers fix ([f4cc44c](https://github.com/chenasraf/simple-scaffold/commit/f4cc44c))
* improve tests ([2d5626c](https://github.com/chenasraf/simple-scaffold/commit/2d5626c))
* maintain directory structure ([564e821](https://github.com/chenasraf/simple-scaffold/commit/564e821))
* major refactor ([5483490](https://github.com/chenasraf/simple-scaffold/commit/5483490))
* migrate cmd to massarg + update tests ([a52f9a0](https://github.com/chenasraf/simple-scaffold/commit/a52f9a0))
* refactoring - code cleanup ([c3835a7](https://github.com/chenasraf/simple-scaffold/commit/c3835a7))
* remove excess files ([d0c0152](https://github.com/chenasraf/simple-scaffold/commit/d0c0152))
* remove types from package.json ([559b5ad](https://github.com/chenasraf/simple-scaffold/commit/559b5ad))
* run tests on ci [skip publish] ([2305083](https://github.com/chenasraf/simple-scaffold/commit/2305083))
* support node 12 for fs package ([bc224d9](https://github.com/chenasraf/simple-scaffold/commit/bc224d9))
* support node 12 for fs package ([cf923d8](https://github.com/chenasraf/simple-scaffold/commit/cf923d8))
* try fix release upload ([8575b1e](https://github.com/chenasraf/simple-scaffold/commit/8575b1e))
* try fix workflow ([6f03ed9](https://github.com/chenasraf/simple-scaffold/commit/6f03ed9))
* try fix workflow ([474a3dc](https://github.com/chenasraf/simple-scaffold/commit/474a3dc))
* try new release version ([d8aba21](https://github.com/chenasraf/simple-scaffold/commit/d8aba21))
* try to fix workflow ([c17e630](https://github.com/chenasraf/simple-scaffold/commit/c17e630))
* update alpha workflow ([91116bb](https://github.com/chenasraf/simple-scaffold/commit/91116bb))
* update deps + add MIGRATION.md ([5b72b6c](https://github.com/chenasraf/simple-scaffold/commit/5b72b6c))
* update deps + update cmd requirements ([d96992c](https://github.com/chenasraf/simple-scaffold/commit/d96992c))
* update docs [skip publish] ([c2bc8b7](https://github.com/chenasraf/simple-scaffold/commit/c2bc8b7))
* update jest config ([01e458e](https://github.com/chenasraf/simple-scaffold/commit/01e458e))
* Update MIGRATION.md [skip ci] ([d0a0db0](https://github.com/chenasraf/simple-scaffold/commit/d0a0db0))
* update readme [skip ci] ([9259939](https://github.com/chenasraf/simple-scaffold/commit/9259939))
* update readme [skip ci] ([09403e1](https://github.com/chenasraf/simple-scaffold/commit/09403e1))
* Update README.md ([edcf1ac](https://github.com/chenasraf/simple-scaffold/commit/edcf1ac))
* update README.md, default output fix ([391a08a](https://github.com/chenasraf/simple-scaffold/commit/391a08a))
* update workflow ([27e84d1](https://github.com/chenasraf/simple-scaffold/commit/27e84d1))
* update workflows ([c7749a8](https://github.com/chenasraf/simple-scaffold/commit/c7749a8))
* update workflows ([a6f25fa](https://github.com/chenasraf/simple-scaffold/commit/a6f25fa))
* update workflows [skip publish] ([956b007](https://github.com/chenasraf/simple-scaffold/commit/956b007))
* update workflows [skip publish] ([54848f9](https://github.com/chenasraf/simple-scaffold/commit/54848f9))
* use node 12 ([9385371](https://github.com/chenasraf/simple-scaffold/commit/9385371))
* v0.7.4 ([43b6496](https://github.com/chenasraf/simple-scaffold/commit/43b6496))
* publish: debug mode off, try to fix workflow ([535260a](https://github.com/chenasraf/simple-scaffold/commit/535260a))
* publish: debug mode on ([1498857](https://github.com/chenasraf/simple-scaffold/commit/1498857))
* build: add workflow ([0ce19a7](https://github.com/chenasraf/simple-scaffold/commit/0ce19a7))
* build: update workflow ([3ee66b2](https://github.com/chenasraf/simple-scaffold/commit/3ee66b2))
* chore: cleanup ([8fcc7a6](https://github.com/chenasraf/simple-scaffold/commit/8fcc7a6))
* docs: update README ([b4b0de6](https://github.com/chenasraf/simple-scaffold/commit/b4b0de6))
## <small>0.7.5 (2021-09-26)</small>
* fix main field in package.json ([3cb9a6f](https://github.com/chenasraf/simple-scaffold/commit/3cb9a6f))
## <small>0.7.4 (2021-09-26)</small>
* v0.7.4 ([12974b5](https://github.com/chenasraf/simple-scaffold/commit/12974b5))
## <small>0.7.3 (2021-09-26)</small>
* added --quiet flag ([7f98d46](https://github.com/chenasraf/simple-scaffold/commit/7f98d46))
* Bump handlebars from 4.7.6 to 4.7.7 ([552614c](https://github.com/chenasraf/simple-scaffold/commit/552614c))
* Bump hosted-git-info from 2.8.8 to 2.8.9 ([2e12907](https://github.com/chenasraf/simple-scaffold/commit/2e12907))
* Bump lodash from 4.17.20 to 4.17.21 ([5b7e0e3](https://github.com/chenasraf/simple-scaffold/commit/5b7e0e3))
* Bump url-parse from 1.4.7 to 1.5.1 ([0923830](https://github.com/chenasraf/simple-scaffold/commit/0923830))
* update readme ([813f706](https://github.com/chenasraf/simple-scaffold/commit/813f706))
* update readme ([1bc2221](https://github.com/chenasraf/simple-scaffold/commit/1bc2221))
* Update README.md ([cd25b04](https://github.com/chenasraf/simple-scaffold/commit/cd25b04))
## <small>0.7.2 (2021-04-19)</small>
* add basename to output config function (fixes #3) ([f07affa](https://github.com/chenasraf/simple-scaffold/commit/f07affa)), closes [#3](https://github.com/chenasraf/simple-scaffold/issues/3)
* disable overwriting files + parse JSON for locals ([ce22a2c](https://github.com/chenasraf/simple-scaffold/commit/ce22a2c))
## <small>0.6.1 (2021-02-01)</small>
* fix: binary files ([7c0c347](https://github.com/chenasraf/simple-scaffold/commit/7c0c347))
## 0.6.0 (2021-02-01)
* build: upgrade packages ([977288a](https://github.com/chenasraf/simple-scaffold/commit/977288a))
* fix: support deeper file structure ([4afafa5](https://github.com/chenasraf/simple-scaffold/commit/4afafa5))
* 0.5.0 ([7bee2a5](https://github.com/chenasraf/simple-scaffold/commit/7bee2a5))
## 0.5.0 (2019-02-27)
* Fixed output argument + updated README ([06590c4](https://github.com/chenasraf/simple-scaffold/commit/06590c4))
* v0.5.0 ([d4c049b](https://github.com/chenasraf/simple-scaffold/commit/d4c049b))
## <small>0.4.5 (2019-02-27)</small>
* Improved docs ([a410b79](https://github.com/chenasraf/simple-scaffold/commit/a410b79))
* v0.4.5 ([c4f2dfb](https://github.com/chenasraf/simple-scaffold/commit/c4f2dfb))
## <small>0.4.4 (2019-02-27)</small>
* v0.4.4 ([71d544a](https://github.com/chenasraf/simple-scaffold/commit/71d544a))
## <small>0.4.3 (2019-02-27)</small>
* mapfile ([d7a4362](https://github.com/chenasraf/simple-scaffold/commit/d7a4362))
* v0.4.3 ([20389d7](https://github.com/chenasraf/simple-scaffold/commit/20389d7))
## <small>0.4.2 (2019-02-25)</small>
* bugfixes ([a92c471](https://github.com/chenasraf/simple-scaffold/commit/a92c471))
* mapfile ([07b1c4b](https://github.com/chenasraf/simple-scaffold/commit/07b1c4b))
* v0.4.2 ([0a2d7c0](https://github.com/chenasraf/simple-scaffold/commit/0a2d7c0))
## <small>0.4.1 (2019-02-25)</small>
* added 'createSubFolder' option, cleaned up CMD file ([d6195c6](https://github.com/chenasraf/simple-scaffold/commit/d6195c6))
* Update README.md ([b14e3d2](https://github.com/chenasraf/simple-scaffold/commit/b14e3d2))
* v0.4.1 ([ec91fbf](https://github.com/chenasraf/simple-scaffold/commit/ec91fbf))
* Bugfix: dotfiles ([85aa9f9](https://github.com/chenasraf/simple-scaffold/commit/85aa9f9))
## <small>0.3.1 (2018-01-15)</small>
* Update README.md ([686b0bf](https://github.com/chenasraf/simple-scaffold/commit/686b0bf))
* v0.3.1 ([fa2ddca](https://github.com/chenasraf/simple-scaffold/commit/fa2ddca))
## 0.3.0 (2018-01-15)
* cleanups ([4f29a61](https://github.com/chenasraf/simple-scaffold/commit/4f29a61))
* output is optional ([14b60ff](https://github.com/chenasraf/simple-scaffold/commit/14b60ff))
* Rename ([45e8de3](https://github.com/chenasraf/simple-scaffold/commit/45e8de3))
* Uodate README.md ([b09299b](https://github.com/chenasraf/simple-scaffold/commit/b09299b))
* Update README.md ([1275743](https://github.com/chenasraf/simple-scaffold/commit/1275743))
* Update README.md ([a3a77e2](https://github.com/chenasraf/simple-scaffold/commit/a3a77e2))
* v0.3.0 ([0be29dd](https://github.com/chenasraf/simple-scaffold/commit/0be29dd))
## 0.2.0 (2018-01-05)
* Fixed cmd ([4ca7c6a](https://github.com/chenasraf/simple-scaffold/commit/4ca7c6a))
* Improve build ([0fd9964](https://github.com/chenasraf/simple-scaffold/commit/0fd9964))
* Improve cmd script, add readme ([e391f8f](https://github.com/chenasraf/simple-scaffold/commit/e391f8f))
* Move all scripts to webpack, add wip cmd script for bin ([3e42ac5](https://github.com/chenasraf/simple-scaffold/commit/3e42ac5))
* Use handlebars, add cmd script in package.json ([e64c0e4](https://github.com/chenasraf/simple-scaffold/commit/e64c0e4))
* v0.2.0 ([f360159](https://github.com/chenasraf/simple-scaffold/commit/f360159))
## <small>0.1.5 (2018-01-01)</small>
* v0.1.5 ([eecec82](https://github.com/chenasraf/simple-scaffold/commit/eecec82))
## <small>0.1.4 (2018-01-01)</small>
* v0.1.4 ([6ec19fc](https://github.com/chenasraf/simple-scaffold/commit/6ec19fc))
## <small>0.1.3 (2018-01-01)</small>
* v0.1.3 ([a5776d6](https://github.com/chenasraf/simple-scaffold/commit/a5776d6))
## <small>0.1.2 (2018-01-01)</small>
* Get comp name from argv ([c341fe7](https://github.com/chenasraf/simple-scaffold/commit/c341fe7))
* Initial commit ([4896c10](https://github.com/chenasraf/simple-scaffold/commit/4896c10))
* Published + renamed, bugfixes ([1e0abf9](https://github.com/chenasraf/simple-scaffold/commit/1e0abf9))
* Remove dist from gitignore ([7f9a385](https://github.com/chenasraf/simple-scaffold/commit/7f9a385))
* Scaffold basically works, file path resolving sucky atm ([652621f](https://github.com/chenasraf/simple-scaffold/commit/652621f))
* v0.1.2 ([0fed899](https://github.com/chenasraf/simple-scaffold/commit/0fed899))

266
README.md
View File

@@ -1,4 +1,6 @@
<h1 align="center">Simple Scaffold</h1>
<p align="center">
<img src="https://chenasraf.github.io//simple-scaffold/img/logo-lg.png" alt="Logo" />
</p>
<h2 align="center">
@@ -6,79 +8,142 @@
[Documentation](https://chenasraf.github.io/simple-scaffold) |
[NPM](https://npmjs.com/package/simple-scaffold) | [casraf.dev](https://casraf.dev)
![version](https://img.shields.io/github/package-json/v/chenasraf/simple-scaffold/master?label=version)
![master](https://img.shields.io/github/package-json/v/chenasraf/simple-scaffold/master?label=master)
![build](https://img.shields.io/github/actions/workflow/status/chenasraf/simple-scaffold/release.yml?branch=master)
</h2>
Looking to streamline your workflow and get your projects up and running quickly? Look no further
than Simple Scaffold - the easy-to-use NPM package that simplifies the process of organizing and
copying your commonly-created files.
Simple Scaffold is a file scaffolding tool. You define templates once, then generate files from them
whenever you need — whether it's a single component or an entire app boilerplate.
With its agnostic and un-opinionated approach, Simple Scaffold can handle anything from a few simple
files to an entire app boilerplate setup. Plus, with the power of **Handlebars.js** syntax, you can
easily replace custom data and personalize your files to fit your exact needs. But that's not all -
you can also use it to loop through data, use conditions, and write custom functions using helpers.
Don't waste any more time manually copying and pasting files - let Simple Scaffold do the heavy
lifting for you and start building your projects faster and more efficiently today!
Templates use **Handlebars.js** syntax, so you can inject data, loop over lists, use conditionals,
and write custom helpers. It works as a CLI or as a Node.js library, and it doesn't care what kind
of files you're generating.
<div align="center">
![sample animation](media/intro.gif)
![Intro](https://chenasraf.github.io/simple-scaffold/img/intro.gif)
</div>
---
## Quick Start
## Documentation
### Local Templates
See full documentation [here](https://chenasraf.github.io/simple-scaffold).
The fastest way to get started is to use `npx` to immediately start a scaffold process.
- [Command Line Interface (CLI) usage](https://chenasraf.github.io/simple-scaffold/docs/usage/cli)
- [Node.js usage](https://chenasraf.github.io/simple-scaffold/docs/usage/node)
- [Templates](https://chenasraf.github.io/simple-scaffold/docs/usage/templates)
- [Configuration Files](https://chenasraf.github.io/simple-scaffold/docs/usage/configuration_files)
- [Migration](https://chenasraf.github.io/simple-scaffold/docs/usage/migration)
Prepare any templates you want to use - for example, in the directory `templates/component`; and use
that in the CLI args. Here is a simple example file:
## Getting Started
Simple Scaffold will maintain any file and directory structure you try to generate.
### Quick Start
`templates/component/{{ pascalName name }}.tsx`
The fastest way to get started is to run `init` in your project directory:
```tsx
// Created: {{ now 'yyyy-MM-dd' }}
import React from 'react'
```sh
npx simple-scaffold init
```
export default {{pascalCase name}}: React.FC = (props) => {
return (
<div className="{{camelCase name}}">{{pascalCase name}} Component</div>
)
This creates a `scaffold.config.js` and an example template in `templates/default/`. Then generate
files with:
```sh
npx simple-scaffold MyProject
```
### Cheat Sheet
A quick rundown of common usage scenarios:
- Remote template config file on GitHub:
```sh
npx simple-scaffold -g username/repository -c scaffold.js -k component NewComponentName
```
- Local template config file:
```sh
npx simple-scaffold -c scaffold.js -k component NewComponentName
```
- Local one-time usage:
```sh
npx simple-scaffold -t templates/component -o src/components NewComponentName
```
### Remote Configurations
The fastest way to get started is to is to re-use someone else's (or your own) work using a template
repository.
A remote config can be loaded in one of these ways:
- For templates hosted on GitHub, the syntax is `-g user/repository_name`
- For other Git platforms like GitLab, use `-g https://example.com/user/repository_name.git`
These remote configurations support multiple scaffold groups, which can be specified using the
`--key` or `-k` argument:
```sh
$ npx simple-scaffold \
-g chenasraf/simple-scaffold \
-k component \
PageWrapper
# equivalent to:
$ npx simple-scaffold \
-g https://github.com/chenasraf/simple-scaffold.git \
-c scaffold.config.js \
-k component \
PageWrapper
```
By default, the template name is set to `default` when the `--key` option is not provided.
See information about each option and flag using the `--help` flag, or read the
[CLI documentation](https://chenasraf.github.io/simple-scaffold/docs/usage/cli). For information
about how configuration files work, [see below](#configuration-files).
### Interactive Mode
When running in a terminal, Simple Scaffold will interactively prompt for any missing required
values — name, output directory, template paths, and template key (if multiple are available).
Config files can also define **inputs** — custom fields that are prompted interactively and become
template data:
```js
module.exports = {
component: {
templates: ["templates/component"],
output: "src/components",
inputs: {
author: { message: "Author name", required: true },
license: { message: "License", default: "MIT" },
},
},
}
```
To generate the template output, run:
Inputs can be pre-provided via `--data` or `-D` to skip the prompt:
```shell
# generate single component
$ npx simple-scaffold@latest \
-t templates/component -o src/components PageWrapper
```
This will immediately create the following file: `src/components/PageWrapper.tsx`
```tsx
// Created: 2077/01/01
import React from 'react'
export default PageWrapper: React.FC = (props) => {
return (
<div className="pageWrapper">PageWrapper Component</div>
)
}
```sh
npx simple-scaffold -c scaffold.config.js -k component -D author=John MyComponent
```
### Configuration Files
You can also use a config file to more easily maintain all your scaffold definitions.
You can use a config file to more easily maintain all your scaffold definitions. Simple Scaffold
**auto-detects** config files in the current directory — no `--config` flag needed.
It searches for these files in order: `scaffold.config.{mjs,cjs,js,json}`,
`scaffold.{mjs,cjs,js,json}`, `.scaffold.{mjs,cjs,js,json}`.
`scaffold.config.js`
@@ -96,63 +161,65 @@ module.exports = {
}
```
Then call your scaffold like this:
Then just run from the same directory:
```shell
$ npx simple-scaffold@latest -c scaffold.config.js PageWrapper
```sh
$ npx simple-scaffold PageWrapper
# or explicitly: npx simple-scaffold -c scaffold.config.js PageWrapper
```
This will allow you to avoid needing to remember which configs are needed or to store them in a
1-liner in `packqge.json` which can get pretty long and messy, which is harder to maintain.
one-liner in `package.json` which can get pretty long and messy, and harder to maintain.
Also, this allows you to define more complex scaffolds with logic without having to use the Node.js
API directly. (Of course you always have the option to still do so if you wish)
See more at the [CLI documentation](https://chenasraf.github.io/simple-scaffold/pages/cli.html) and
[Configuration Files](https://chenasraf.github.io/simple-scaffold/pages/configuration_files.html).
More information can be found at the
[Configuration Files documentation](https://chenasraf.github.io/simple-scaffold/docs/usage/configuration_files).
### Remote Configurations
### Templates Structure
Another quick way to start is to re-use someone else's (or your own) work using a template
repository.
Templates are **any file** in the a directory given to `--templates`.
A remote config can be loaded in one of these ways:
Simple Scaffold will maintain any file and directory structure you try to generate, while replacing
any tokens such as `{{ name }}` or other custom-data using
[Handlebars.js](https://handlebarsjs.com/).
- If it's on GitHub, you can use `-gh user/repository_name`
- If it's on another git server (such as GitLab), you can use
`-c https://example.com/user/repository_name.git`
`templates/component/{{ pascalName name }}.tsx`
Configurations can hold multiple scaffold groups. Each group can be accessed using its key by
supplying the `--key` or `-k` argument, or by appending a hash and then the key name, like so:
`-gh user/repository_name#key_name` - this also works for the `-c` flag.
```tsx
// Created: {{ now 'yyyy-MM-dd' }}
import React from 'react'
Here is an example for loading the example component templates in this very repository:
export default {{pascalCase name}}: React.FC = (props) => {
return (
<div className="{{camelCase name}}">{{pascalCase name}} Component</div>
)
}
```
```shell
$ npx simple-scaffold@latest \
-gh chenasraf/simple-scaffold#scaffold.config.js:component \
PageWrapper
To generate the template output once without saving a configuration file, run:
# equivalent to:
$ npx simple-scaffold@latest \
-c https://github.com/chenasraf/simple-scaffold.git#scaffold.config.js:component \
```sh
# generate single component
$ npx simple-scaffold \
-t templates/component \
-o src/components \
PageWrapper
```
When template name (`:component`) is omitted, `default` is used.
This will immediately create the following file: `src/components/PageWrapper.tsx`
See more at the [CLI documentation](https://chenasraf.github.io/simple-scaffold/pages/cli.html) and
[Configuration Files](https://chenasraf.github.io/simple-scaffold/pages/configuration_files.html).
```tsx
// Created: 2077-01-01
import React from 'react'
## Documentation
See full documentation [here](https://chenasraf.github.io/simple-scaffold).
- [Command Line Interface (CLI) usage](https://chenasraf.github.io/simple-scaffold/pages/cli.html)
- [Node.js usage](https://chenasraf.github.io/simple-scaffold/pages/node.html)
- [Templates](https://chenasraf.github.io/simple-scaffold/pages/templates.html)
- [Configuration Files](https://chenasraf.github.io/simple-scaffold/pages/configuration_files.html)
- [Migrating v0.x to v1.x](https://chenasraf.github.io/simple-scaffold/pages/migration.html)
export default PageWrapper: React.FC = (props) => {
return (
<div className="pageWrapper">PageWrapper Component</div>
)
}
```
## Contributing
@@ -161,9 +228,11 @@ very helpful to sustaining its life. If you are feeling incredibly generous and
just a small amount to help sustain this project, I would be very very thankful!
<a href='https://ko-fi.com/casraf' target='_blank'>
<img height='36' style='border:0px;height:36px;'
<img
height='36'
src='https://cdn.ko-fi.com/cdn/kofi1.png?v=3'
alt='Buy Me a Coffee at ko-fi.com' />
alt='Buy Me a Coffee at ko-fi.com'
/>
</a>
I welcome any issues or pull requests on GitHub. If you find a bug, or would like a new feature,
@@ -172,8 +241,8 @@ don't hesitate to open an appropriate issue and I will do my best to reply promp
If you are a developer and want to contribute code, here are some starting tips:
1. Fork this repository
2. Run `yarn install`
3. Run `yarn dev` to start file watch mode
2. Run `pnpm install`
3. Run `pnpm dev` to start file watch mode
4. Make any changes you would like
5. Create tests for your changes
6. Update the relevant documentation (readme, code comments, type comments)
@@ -181,22 +250,9 @@ If you are a developer and want to contribute code, here are some starting tips:
Some tips on getting around the code:
- Use `yarn dev` for development - it runs TypeScript compile in watch mode, allowing you to make
changes and immediately be able to try them using `yarn cmd`.
- Use `yarn build` to build the output once
- Use `yarn test` to run tests
- Use `yarn cmd` to use the CLI feature of Simple Scaffold from within the root directory, enabling
you to test different behaviors. See `yarn cmd -h` for more information.
> This requires an updated build, and does not trigger one itself. From here you have several
> options:
>
> - Run `yarn dev` to watch for file changes and build automatically
> - Run `yarn build` before running this to trigger a one-time build
> - Run `yarn build-cmd` which triggers a build right before running `yarn cmd` automatically with
> the rest of the given arguments.
- Use `yarn build-docs` to build the documentation once
- Use `yarn watch-docs` to start docs in watch mode
- To see the documentation, currently you have to serve the directory yourself with a static web
server (like node's built in serve, VS code's "Go Live" mode, etc)
- Use `pnpm cmd` to use the CLI feature of Simple Scaffold from within the root directory, enabling
you to test different behaviors. See `pnpm cmd -h` for more information.
- Use `pnpm test` to run tests
- Use `pnpm docs:build` to build the documentation once
- Use `pnpm docs:watch` to start docs in watch mode
- Use `pnpm build` to build the output

22
docs/.gitignore vendored Normal file
View File

@@ -0,0 +1,22 @@
# Dependencies
/node_modules
# Production
/build
# Generated files
.docusaurus
.cache-loader
# Misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
docs/api

44
docs/README.md Normal file
View File

@@ -0,0 +1,44 @@
# Website
This website is built using [Docusaurus](https://docusaurus.io/), a modern static website generator.
### Installation
```
$ yarn
```
### Local Development
```
$ yarn start
```
This command starts a local development server and opens up a browser window. Most changes are
reflected live without having to restart the server.
### Build
```
$ yarn build
```
This command generates static content into the `build` directory and can be served using any static
contents hosting service.
### Deployment
Using SSH:
```
$ USE_SSH=true yarn deploy
```
Not using SSH:
```
$ GIT_USER=<Your GitHub username> yarn deploy
```
If you are using GitHub pages for hosting, this command is a convenient way to build the website and
push to the `gh-pages` branch.

3
docs/babel.config.js Normal file
View File

@@ -0,0 +1,3 @@
module.exports = {
presets: [require.resolve("@docusaurus/core/lib/babel/preset")],
}

View File

@@ -1,3 +1,7 @@
---
title: Template Files
---
# Preparing template files
Put your template files anywhere, and fill them with tokens for replacement.
@@ -18,6 +22,25 @@ Examples:
| `./templates/directory` | `outer/{{name}}.txt`,<br />`outer2/inner/{{name}}.txt` | `src/outer/AppName.txt`,<br />`src/outer2/inner/AppName.txt` |
| `./templates/others/**/*.txt` | `outer/{{name}}.jpg`,<br />`outer2/inner/{{name}}.txt` | `src/outer2/inner/AppName.txt` |
## Ignoring files
You can create a `.scaffoldignore` file in your template directory to exclude files from being
copied to the output. It works like `.gitignore` — one pattern per line, comments with `#`.
```text
# .scaffoldignore
*.log
node_modules
README.md
dist/**
```
The `.scaffoldignore` file itself is never copied to the output.
Patterns are matched against both the file's basename and its path relative to the template
directory, so `README.md` will match at any depth, while `dist/**` only matches a `dist` directory
at the template root.
## Variable/token replacement
Scaffolding will replace `{{ varName }}` in both the file name and its contents and put the
@@ -46,7 +69,6 @@ The contents of the file will be transformed in a similar fashion.
Your `data` will be pre-populated with the following:
- `{{Name}}`: PascalCase of the component name
- `{{name}}`: raw name of the component as you entered it
> Simple-Scaffold uses [Handlebars.js](https://handlebarsjs.com/) for outputting the file contents.
@@ -106,7 +128,15 @@ Further details:
```
- **The now helper** (for current time) takes the same arguments, minus the first one (`date`) as it
is implicitly the current date.
is implicitly the current date:
```typescript
(
format: string,
offsetAmount?: number,
offsetType?: "years" | "months" | "weeks" | "days" | "hours" | "minutes" | "seconds"
)
```
### Custom Helpers
@@ -121,104 +151,7 @@ config.helpers = {
```
All of the above helpers (built in and custom) will also be available to you when using
`subFolderNameHelper` (`--sub-folder-name-helper`/`-sh`) as a possible value.
`subdirHelper` (`--sub-dir-helper`/`-H`) as a possible value.
> To see more information on how helpers work and more features, see
> [Handlebars.js docs](https://handlebarsjs.com/guide/#custom-helpers).
# Examples
## Run
### Command Example
```bash
simple-scaffold MyComponent \
-t project/scaffold/**/* \
-o src/components \
-d '{"className": "myClassName","author": "Chen Asraf"}'
MyComponent
```
### Equivalent Node Module Example
```typescript
import Scaffold from "simple-scaffold"
async function main() {
await Scaffold({
name: "MyComponent",
templates: ["project/scaffold/**/*"],
output: ["src/components"],
data: {
className: "myClassName",
author: "Chen Asraf",
},
})
console.log("Done.")
}
```
## Files
### Input
- Input file path:
```text
project → scaffold → {{Name}}.js → src → components
```
- Input file contents:
```typescript
/**
* Author: {{ author }}
* Date: {{ now "yyyy-MM-dd" }}
*/
import React from 'react'
export default {{camelCase name}}: React.FC = (props) => {
return (
<div className="{{className}}">{{camelCase name}} Component</div>
)
}
```
### Output
- Output file path:
- With `createSubFolder = false` (default):
```text
project → src → components → MyComponent.js
```
- With `createSubFolder = true`:
```text
project → src → components → MyComponent → MyComponent.js
```
- With `createSubFolder = true` and `subFolderNameHelper = 'upperCase'`:
```text
project → src → components → MYCOMPONENT → MyComponent.js
```
- Output file contents:
```typescript
/**
* Author: Chen Asraf
* Date: 2077-01-01
*/
import React from 'react'
export default MyComponent: React.FC = (props) => {
return (
<div className="myClassName">MyComponent Component</div>
)
}
```

View File

@@ -0,0 +1,215 @@
---
title: Configuration Files
---
If you want to have reusable configurations which are complex and don't fit into command lines
easily, or just want to manage your templates easier, you can use configuration files to load your
scaffolding configurations.
## Creating config files
Configuration files should be valid `.js`/`.mjs`/`.cjs`/`.json` files that contain valid Scaffold
configurations.
Each file hold multiple scaffolds. Each scaffold is a key, and its value is the configuration. For
example:
```js
module.exports = {
component: {
templates: ["templates/component"],
output: "src/components",
},
}
```
For the full configuration options, see
[ScaffoldConfigFile](../api/type-aliases/ScaffoldConfigFile).
If you want to supply functions inside the configurations, you must use a `.js`/`.cjs`/`.mjs` file
as JSON does not support non-primitives.
Another feature of using a JS file is you can export a function which will be loaded with the CMD
config provided to Simple Scaffold. The `extra` key contains any values not consumed by built-in
flags, so you can pre-process your args before outputting a config:
```js
/** @type {import('simple-scaffold').ScaffoldConfigFile} */
module.exports = (config) => {
console.log("Config:", config)
return {
component: {
templates: ["templates/component"],
output: "src/components",
},
}
}
```
### Template Inputs
You can define **inputs** in your config to prompt users for custom values when scaffolding. Each
input becomes a template data variable:
```js
module.exports = {
component: {
templates: ["templates/component"],
output: "src/components",
inputs: {
author: { message: "Author name", required: true },
license: {
type: "select",
message: "License",
options: ["MIT", "Apache-2.0", "GPL-3.0"],
},
private: { type: "confirm", message: "Private?", default: false },
port: { type: "number", message: "Port", default: 3000 },
},
},
}
```
In your templates, use these as `{{ author }}`, `{{ license }}`, `{{ private }}`, `{{ port }}`.
Supported input types: `text` (default), `select`, `confirm`, `number`. See
[Template Inputs](cli#template-inputs) for the full reference.
- **Required** inputs are prompted interactively if not provided via `--data` or `-D`
- **Select and confirm** inputs are always prompted unless pre-provided
- **Optional** inputs with a `default` use that value silently if not provided
- In non-interactive environments, only defaults are applied
If you want to provide templates that need no name (such as common config files which are easily
portable between projects), you may provide the `name` property in the config object.
You will always be able to override it using `--name NewName`, but it will be given a value by
default and therefore it will no longer be required in the CLI arguments.
## Using a config file
### Auto-detection
By default, Simple Scaffold automatically searches the current working directory for a config file.
No `--config` flag is needed if your config file uses one of the standard names.
The following files are tried in order:
1. `scaffold.config.mjs`
2. `scaffold.config.cjs`
3. `scaffold.config.js`
4. `scaffold.config.json`
5. `scaffold.mjs`
6. `scaffold.cjs`
7. `scaffold.js`
8. `scaffold.json`
9. `.scaffold.mjs`
10. `.scaffold.cjs`
11. `.scaffold.js`
12. `.scaffold.json`
If a config file is found, it is loaded automatically. If multiple templates are defined and no
`--key` is provided, you'll be prompted to select one interactively.
```sh
# Just run from a directory containing scaffold.config.js — no flags needed
simple-scaffold MyComponentName
```
### Explicit config path
You can also provide a specific file or directory path using `--config` (or `-c`), optionally
alongside `--key` or `-k`:
```sh
simple-scaffold -c <file> -k <template_key>
```
For example:
```sh
simple-scaffold -c scaffold.json -k component MyComponentName
```
When a directory is given, the same auto-detection order listed above is used to find a config file
within that directory.
### Default template key
If you don't supply a template key (e.g. `component`), `default` will be used:
```js
/** @type {import('simple-scaffold').ScaffoldConfigFile} */
module.exports = {
default: {
// ...
},
}
```
```sh
# will use 'default' template
simple-scaffold -c scaffold.json MyComponentName
```
If multiple keys exist and no key is specified, you'll be prompted to choose one interactively.
### Supported file types
Any importable file is supported, depending on your build process.
Common extensions:
- `.mjs`
- `.cjs`
- `.js`
- `.json`
Note that you might need to find the correct extension of `.js`, `.cjs` or `.mjs` depending on your
build process and your package type (for example, packages with `"type": "module"` in their
`package.json` might be required to use `.mjs`.)
### Git/GitHub Templates
You may specify a git or GitHub url to use remote templates.
The command line option is `--git` or `-g`.
- You may specify a full git or HTTPS git URL, which will be tried
- You may specify a git username and project if the project is on GitHub
```sh
# GitHub shorthand
simple-scaffold -g <username>/<project_name> [-c <filename>] [-k <template_key>]
# Any git URL, git:// and https:// are supported
simple-scaffold -g git://gitlab.com/<username>/<project_name> [-c <filename>] [-k <template_key>]
simple-scaffold -g https://gitlab.com/<username>/<project_name>.git [-c <filename>] [-k <template_key>]
```
When a config file path is omitted, the files given in the list above will be tried on the root
directory of the git repository.
**Note:** The repository will be cloned to a temporary directory and removed after the scaffolding
has been done.
## Use In Node.js
You can also start a scaffold from Node.js with a remote file or URL config.
Just use the `Scaffold.fromConfig` function:
```ts
Scaffold.fromConfig(
"scaffold.config.js", // file or HTTPS git URL
{
// name of the generated component
name: "My Component",
// key to load from the config
key: "component",
},
{
// other config overrides
},
)
```

243
docs/docs/usage/03-cli.md Normal file
View File

@@ -0,0 +1,243 @@
---
title: CLI Usage
---
## Available flags
```text
Usage: simple-scaffold [options]
```
To see this and more information anytime, add the `-h` or `--help` flag to your call, e.g.
`npx simple-scaffold@latest -h`.
Options:
| Option/flag \| Alias | Description |
| ------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--name` \| `-n` | Name to be passed to the generated files. `{{name}}` and other data parameters inside contents and file names will be replaced accordingly. If omitted in an interactive terminal, you will be prompted. |
| `--config` \| `-c` | Filename or directory to load config from. If omitted, the current directory is searched automatically for a config file (see [Auto-detection](configuration_files#auto-detection)). |
| `--git` \| `-g` | Git URL or GitHub path to load a template from. |
| `--key` \| `-k` | Key to load inside the config file. If omitted and multiple templates are available, you will be prompted to select one. |
| `--output` \| `-o` | Path to output to. If `--subdir` is enabled, the subdir will be created inside this path. If omitted in an interactive terminal, you will be prompted. |
| `--templates` \| `-t` | Template files to use as input. You may provide multiple files, each of which can be a relative or absolute path, or a glob pattern for multiple file matching easily. If omitted in an interactive terminal, you will be prompted for a comma-separated list. |
| `--overwrite` \| `-w` \| `--no-overwrite` \| `-W` | Enable to override output files, even if they already exist. (default: false) |
| `--data` \| `-d` | Add custom data to the templates. By default, only your app name is included. |
| `--append-data` \| `-D` | Append additional custom data to the templates, which will overwrite `--data`, using an alternate syntax, which is easier to use with CLI: `-D key1=string -D key2:=raw` |
| `--subdir` \| `-s` \| `--no-subdir` \| `-S` | Create a parent directory with the input name (and possibly `--subdir-helper` (default: false) |
| `--subdir-helper` \| `-H` | Default helper to apply to subdir name when using `--subdir`. |
| `--quiet` \| `-q` | Suppress output logs (Same as `--log-level none`)(default: false) |
| `--log-level` \| `-l` | Determine amount of logs to display. The values are: `none, debug, info, warn, error`. The provided level will display messages of the same level or higher. (default: info) |
| `--before-write` \| `-B` | Run a script before writing the files. This can be a command or a path to a file. A temporary file path will be passed to the given command and the command should return a string for the final output. |
| `--after-scaffold` \| `-A` | Run a shell command after all files have been written. The command is executed in the output directory (e.g. `--after-scaffold 'npm install'`). |
| `--dry-run` \| `-dr` | Don't emit files. This is good for testing your scaffolds and making sure they don't fail, without having to write actual file contents or create directories. (default: false) |
| `--version` \| `-v` | Display version. |
### Interactive Mode
When running in a terminal (TTY), Simple Scaffold will prompt for any missing required values:
- **Name** — text input if `--name` is not provided
- **Template key** — selectable list if `--key` is not provided and the config file has multiple
templates
- **Output directory** — text input if `--output` is not provided
- **Template paths** — comma-separated text input if `--templates` is not provided
In non-interactive environments (CI, piped input), missing values will cause an error instead of
prompting.
### Template Inputs
Config files can define **inputs** — custom fields that are prompted interactively and injected as
template data. This is useful for templates that need user-specific values like author name,
license, or description.
```js
module.exports = {
component: {
templates: ["templates/component"],
output: "src/components",
inputs: {
author: { message: "Author name", required: true },
license: {
type: "select",
message: "License",
options: ["MIT", "Apache-2.0", "GPL-3.0"],
},
private: { type: "confirm", message: "Private package?", default: false },
port: { type: "number", message: "Dev server port", default: 3000 },
description: { message: "Description" },
},
},
}
```
Each input becomes available as a Handlebars variable in your templates (e.g., `{{ author }}`,
`{{ license }}`).
**Input types:**
| Type | Description | Value type |
| --------- | ------------------------------ | ---------- |
| `text` | Free-form text input (default) | `string` |
| `select` | Choose from a list of options | `string` |
| `confirm` | Yes/no prompt | `boolean` |
| `number` | Numeric input | `number` |
- **Required inputs** without a value will be prompted interactively
- **Select and confirm** inputs are always prompted (unless pre-provided)
- **Optional text/number inputs** with a `default` will use that value silently
- All inputs can be pre-provided via `--data` or `-D` to skip the prompt:
```shell
simple-scaffold -c scaffold.config.js -k component -D author=John -D license=Apache-2.0 MyComponent
```
### Before Write option
This option allows you to preprocess a file before it is being written, such as running a formatter,
linter or other commands.
To use this option, pass it the command you would like to run. The following tokens will be replaced
in your string:
- `{{path}}` - the temporary file path for you to read from
- `{{rawpath}}` - a different file path containing the raw file contents **before** they were
handled by Handlebars.js.
If none of these tokens are found, the regular (non-raw) path will be appended to the end of the
command.
```shell
simple-scaffold -c . --before-write prettier
# command: prettier /tmp/somefile
simple-scaffold -c . --before-write 'cat {{path}} | my-linter'
# command: cat /tmp/somefile | my-linter
```
The command should return the string to write to the file through standard output (stdout), and not
re-write the tmp file as it is not used for writing. Returning an empty string (after trimming) will
discard the result and write the original file contents.
See
[beforeWrite](https://chenasraf.github.io/simple-scaffold/docs/api/interfaces/ScaffoldConfig#beforewrite)
Node.js API for more details. Instead of returning `undefined` to keep the default behavior, you can
output `''` for the same effect.
### After Scaffold option
This option runs a shell command after all files have been written. The command is executed in the
output directory, making it useful for post-scaffolding tasks like installing dependencies or
initializing a git repo.
```shell
simple-scaffold -c . --after-scaffold 'npm install'
simple-scaffold -c . --after-scaffold 'git init && git add .'
```
In a config file, you can use a function for more control:
```js
module.exports = {
default: {
templates: ["templates/app"],
output: ".",
afterScaffold: async ({ config, files }) => {
console.log(`Created ${files.length} files in ${config.output}`)
// run any post-processing here
},
},
}
```
The function receives a context with the resolved `config` and an array of `files` (absolute paths)
that were written.
## Available Commands:
| Command \| Alias | Description |
| ---------------- | ------------------------------------------------------------------------------------ |
| `init` | Initialize a new scaffold config file and example template in the current directory. |
| `list` \| `ls` | List all available templates for a given config. See `list -h` for more information. |
### `init`
Creates a scaffold config file and an example template directory to get you started quickly.
```shell
simple-scaffold init
```
Options:
| Option | Description |
| ------------------ | -------------------------------------------------------------------- |
| `--dir` \| `-d` | Directory to create the config in. Defaults to current directory. |
| `--format` \| `-f` | Config format: `js`, `mjs`, or `json`. If omitted, you are prompted. |
The command creates:
- A config file (`scaffold.config.js` by default) with a `default` template key
- A `templates/default/` directory with an example `{{name}}.md` template
Existing files are never overwritten.
## Examples:
> See
> [Configuration Files](https://chenasraf.github.io/simple-scaffold/docs/usage/configuration_files)
> for organizing multiple scaffold types into easy-to-maintain files
Usage with config file
```shell
$ simple-scaffold -c scaffold.cmd.js -k component MyComponent
```
Usage with GitHub config file
```shell
$ simple-scaffold -g chenasraf/simple-scaffold -k component MyComponent
```
Usage with https git URL (for non-GitHub)
```shell
$ simple-scaffold \
-g https://example.com/user/template.git \
-c scaffold.cmd.js \
-k component \
MyComponent
```
Full syntax with config path and template key (applicable to all above methods)
```shell
$ simple-scaffold -c scaffold.cmd.js -k component MyComponent
```
Excluded template key, assumes 'default' key
```shell
$ simple-scaffold -c scaffold.cmd.js MyComponent
```
Shortest syntax for GitHub, assumes file 'scaffold.cmd.js' and template key 'default'
```shell
$ simple-scaffold -g chenasraf/simple-scaffold MyComponent
```
You can also add this as a script in your `package.json`:
```json
{
"scripts": {
"scaffold-cfg": "npx simple-scaffold -c scaffold.cmd.js -k component",
"scaffold-gh": "npx simple-scaffold -g chenasraf/simple-scaffold -k component",
"scaffold": "npx simple-scaffold@latest -t scaffolds/component/**/* -o src/components -d '{\"myProp\": \"propName\", \"myVal\": 123}'"
"scaffold-component": "npx simple-scaffold -c scaffold.cmd.js -k"
}
}
```

152
docs/docs/usage/04-node.md Normal file
View File

@@ -0,0 +1,152 @@
---
title: Node.js Usage
---
## Overview
You can build the scaffold yourself, if you want to create more complex arguments, scaffold groups,
etc - simply pass a config object to the Scaffold function when you are ready to start.
The config takes similar arguments to the command line. See the full
[API documentation](https://chenasraf.github.io/simple-scaffold/docs/api/interfaces/ScaffoldConfig)
for all configuration options and their behavior.
```ts
interface ScaffoldConfig {
name: string
templates: string[]
output: FileResponse<string>
subdir?: boolean
data?: Record<string, unknown>
overwrite?: FileResponse<boolean>
logLevel?: LogLevel
dryRun?: boolean
helpers?: Record<string, Helper>
subdirHelper?: DefaultHelpers | string
inputs?: Record<string, ScaffoldInput>
beforeWrite?(
content: Buffer,
rawContent: Buffer,
outputPath: string,
): string | Buffer | undefined | Promise<string | Buffer | undefined>
afterScaffold?: AfterScaffoldHook
}
interface ScaffoldInput {
type?: "text" | "select" | "confirm" | "number"
message?: string
required?: boolean
default?: string | boolean | number
options?: (string | { name: string; value: string })[] // for type: "select"
}
interface AfterScaffoldContext {
config: ScaffoldConfig
files: string[] // absolute paths of written files
}
type AfterScaffoldHook = ((context: AfterScaffoldContext) => void | Promise<void>) | string
```
### Before Write option
This option allows you to preprocess a file before it is being written, such as running a formatter,
linter or other commands.
To use this option, you can run any async/blocking command, and return a string as the final output
to be used as the file contents.
Returning `undefined` will keep the file contents as-is, after normal Handlebars.js procesing by
Simple Scaffold.
### After Scaffold hook
The `afterScaffold` option runs after all files have been written. It receives a context object with
the resolved config and the list of files that were created.
```typescript
import Scaffold from "simple-scaffold"
await Scaffold({
name: "my-app",
templates: ["templates/app"],
output: ".",
afterScaffold: async ({ config, files }) => {
console.log(`Created ${files.length} files`)
// e.g. run npm install, git init, open editor, etc.
},
})
```
You can also pass a shell command string, which will be executed in the output directory:
```typescript
await Scaffold({
name: "my-app",
templates: ["templates/app"],
output: "my-app",
afterScaffold: "npm install && git init",
})
```
In dry-run mode, the hook is still called but the `files` array will be empty.
### Inputs
The `inputs` option lets you define fields that will be prompted interactively (when running in a
TTY) and merged into the template data. This is useful when your templates need user-specific
values.
```typescript
import Scaffold from "simple-scaffold"
await Scaffold({
name: "component",
templates: ["templates/component"],
output: "src/components",
inputs: {
author: { message: "Author name", required: true },
license: {
type: "select",
message: "License",
options: ["MIT", "Apache-2.0", "GPL-3.0"],
},
private: { type: "confirm", message: "Private package?", default: false },
port: { type: "number", message: "Dev server port", default: 3000 },
},
})
// In templates: {{ author }}, {{ license }}
```
- **Required** inputs are prompted if not already in `data`
- **Optional** inputs with a `default` are applied silently
- Pre-providing values in `data` skips the prompt for that input
## Example
This is an example of loading a complete scaffold via Node.js:
```typescript
import Scaffold from "simple-scaffold"
await Scaffold({
name: "component",
templates: [path.join(__dirname, "scaffolds", "component")],
output: path.join(__dirname, "src", "components"),
subdir: true,
subdirHelper: "upperCase",
data: {
property: "value",
},
helpers: {
twice: (text) => [text, text].join(" "),
},
inputs: {
author: { message: "Author name", required: true },
license: { message: "License", default: "MIT" },
},
// return a string to replace the final file contents after pre-processing, or `undefined`
// to keep it as-is
beforeWrite: (content, rawContent, outputPath) => content.toString().toUpperCase(),
})
```

View File

@@ -0,0 +1,140 @@
---
title: Examples
---
## Example files
### Input
- Input file path:
```text
project → scaffold → {{Name}}.js → src → components
```
- Input file contents:
```typescript
/**
* Author: {{ author }}
* Date: {{ now "yyyy-MM-dd" }}
*/
import React from 'react'
export default {{camelCase name}}: React.FC = (props) => {
return (
<div className="{{className}}">{{camelCase name}} Component</div>
)
}
```
### Output
- Output file path:
- With `subdir = false` (default):
```text
project → src → components → MyComponent.js
```
- With `subdir = true`:
```text
project → src → components → MyComponent → MyComponent.js
```
- With `subdir = true` and `subdirHelper = 'upperCase'`:
```text
project → src → components → MYCOMPONENT → MyComponent.js
```
- Output file contents:
```typescript
/**
* Author: My Name
* Date: 2077-01-01
*/
import React from 'react'
export default MyComponent: React.FC = (props) => {
return (
<div className="myClassName">MyComponent Component</div>
)
}
```
## Example run commands
### Command Example
```bash
simple-scaffold \
-t project/scaffold/**/* \
-o src/components \
-d '{"className": "myClassName","author": "My Name"}'
MyComponent
```
### Equivalent Node Module Example
```typescript
import Scaffold from "simple-scaffold"
async function main() {
await Scaffold({
name: "MyComponent",
templates: ["project/scaffold/**/*"],
output: ["src/components"],
data: {
className: "myClassName",
author: "My Name",
},
})
console.log("Done.")
}
```
### Re-usable config
#### Shell
```bash
# cjs
simple-scaffold -c scaffold.cjs MyComponent \
-d '{"className": "myClassName","author": "My Name"}'
# mjs
simple-scaffold -c scaffold.mjs MyComponent \
-d '{"className": "myClassName","author": "My Name"}'
```
#### scaffold.cjs
```js
module.exports = (config) => ({
default: {
templates: ["project/scaffold/**/*"],
output: ["src/components"],
data: {
className: "myClassName",
author: "My Name",
},
},
})
```
#### scaffold.mjs
```js
export default (config) => ({
default: {
templates: ["project/scaffold/**/*"],
output: ["src/components"],
data: {
className: "myClassName",
author: "My Name",
},
},
})
```

View File

@@ -0,0 +1,61 @@
---
title: Migration
---
## v1.x to v2.x
### CLI option changes
- Several changes to how remote configs are loaded via CLI:
- The `:template_key` syntax has been removed. You can still use `-k template_key` to achieve the
same result.
- The `--github` (`-gh`) flag has been replaced by a generic `--git` (`-g`) one, which handles any
git URL. Providing a partial GitHub path will default to trying to find the project on GitHub,
e.g. `-g username/project`
- The `#template_file` syntax has been removed, you may use `--config` or `-c` to tell Simple
Scaffold which file to look for inside the git project. There is a default file priority list
which can find the file for you if it is in one of the supported filenames.
- `verbose` can now take the names `debug`, `info`, `warn`, `error` or `none` (case insensitive).
- `--create-sub-folder` (`-s`) has been renamed to `--subdir` (`-s`) in the CLI. The Node.js names
have been changed as well.
- `--sub-folder-name-helper` (`-sh`) has been renamed to `--subdir-helper` (`-sh`). The Node.js
names have been changed as well.
- All boolean flags no longer take a value. `-q` instead of `-q 1` or `-q true`, `-s` instead of
`-s 1`, `-w` instead of `-w 1`, etc.
### Behavior changes
- Data is no longer auto-populated with `Name` (PascalCase) by default. You can just use the helper
in your templates contents and file names, simply use `{{ pascalCase name }}` instead of
`{{ Name }}`. `Name` was arbitrary and it is confusing (is it `Title Case`? `PascalCase`? only
reading the docs can tell). Alternatively, you can inject the transformed name into your `data`
manually using a scaffold config file, by using the Node API or by appending the data to the CLI
invocation.
## v0.x to v1.x
In Simple Scaffold v1.0, the entire codebase was overhauled, yet usage remains mostly the same
between versions. With these notable exceptions:
- Some of the argument names have changed
- Template syntax has been improved
- The command to run Scaffold has been simplified from `new SimpleScaffold(opts).run()` to
`SimpleScaffold(opts)`, which now returns a promise that you can await to know when the process
has been completed.
### Argument changes
- `locals` has been renamed to `data`. The appropriate command line args have been updated as well
to `--data` | `-d`.
- Additional options have been added to both CLI and Node interfaces. See
[Command Line Interface (CLI) usage](https://chenasraf.github.io/simple-scaffold/docs/usage/cli)
and [Node.js usage](https://chenasraf.github.io/simple-scaffold/docs/usage/node) for more
information.
### Template syntax changes
Simple Scaffold still uses Handlebars.js to handle template content and file names. However, helpers
have been added to remove the need for you to pre-process the template data on simple use-cases such
as case type manipulation (converting to camel case, snake case, etc)
See the readme for the full information on how to use these helpers and which are available.

View File

@@ -0,0 +1 @@
label: "Usage"

11
docs/docs/usage/index.md Normal file
View File

@@ -0,0 +1,11 @@
---
title: Usage
sidebar_position: 0
---
- [Template Files](templates)
- [Configuration Files](configuration_files)
- [CLI Usage](cli)
- [Node.js Usage](node)
- [Examples](examples)
- [Migration](migration)

191
docs/docusaurus.config.ts Normal file
View File

@@ -0,0 +1,191 @@
import { themes as prismThemes } from "prism-react-renderer"
import type { Config } from "@docusaurus/types"
import type * as Preset from "@docusaurus/preset-classic"
const config: Config = {
title: "Simple Scaffold",
tagline: "Generate any file structure - from single components to entire app boilerplates, with a single command.",
favicon: "img/favicon.svg",
// Set the production url of your site here
url: "https://chenasraf.github.io",
// Set the /<baseUrl>/ pathname under which your site is served
// For GitHub pages deployment, it is often '/<projectName>/'
baseUrl: "/simple-scaffold",
// GitHub pages deployment config.
// If you aren't using GitHub pages, you don't need these.
organizationName: "chenasraf", // Usually your GitHub org/user name.
projectName: "simple-scaffold", // Usually your repo name.
onBrokenLinks: "warn",
markdown: {
hooks: {
onBrokenMarkdownLinks: "warn",
},
},
// Even if you don't use internationalization, you can use this field to set
// useful metadata like html lang. For example, if your site is Chinese, you
// may want to replace "en" with "zh-Hans".
i18n: {
defaultLocale: "en",
locales: ["en"],
},
plugins: [
[
"docusaurus-plugin-typedoc",
// Plugin / TypeDoc options
{
entryPoints: ["../src/index.ts"],
tsconfig: "../tsconfig.json",
// typedoc options
watch: process.env.NODE_ENV === "development",
excludePrivate: true,
excludeProtected: true,
excludeInternal: true,
// includeVersion: true,
categorizeByGroup: false,
sort: ["visibility"],
categoryOrder: ["Main", "*"],
entryPointStrategy: "expand",
pageTitleTemplates: {
index: "{projectName}",
member: "`{rawName}`",
module: "{name}",
},
validation: {
invalidLink: true,
},
},
],
],
presets: [
[
"classic",
{
docs: {
sidebarPath: "./sidebars.ts",
// Please change this to your repo.
// Remove this to remove the "edit this page" links.
editUrl: "https://github.com/chenasraf/simple-scaffold/blob/master/docs",
},
theme: {
customCss: "./src/css/custom.css",
},
googleTagManager: {
containerId: "GTM-KHQS9TQ",
},
} satisfies Preset.Options,
],
],
themeConfig: {
// Replace with your project's social card
image: "img/docusaurus-social-card.jpg",
navbar: {
title: "Simple Scaffold",
logo: {
alt: "Simple Scaffold",
src: "img/favicon.svg",
},
items: [
{
position: "left",
type: "docSidebar",
sidebarId: "api",
label: "API",
to: "docs/api",
},
{
position: "left",
type: "docSidebar",
sidebarId: "usage",
label: "Usage",
to: "docs/usage",
},
// {
// position: "left",
// type: "docSidebar",
// sidebarId: "docs",
// },
// {
// label: "API",
// href: "/docs/api",
// position: "left",
// },
// {
// label: "Usage",
// href: "/docs/usage",
// position: "left",
// },
{
href: "https://npmjs.com/package/simple-scaffold",
label: "NPM",
position: "right",
},
{
href: "https://github.com/chenasraf/simple-scaffold",
label: "GitHub",
position: "right",
},
],
},
footer: {
style: "dark",
links: [
{
title: "Docs",
items: [
{
label: "Usage",
to: "/docs/usage",
},
{
label: "API",
to: "/docs/api",
},
],
},
{
title: "More from @casraf",
items: [
{
label: "Massarg - CLI Argument Parser",
href: "https://chenasraf.github.io/massarg",
},
{
label: "Website",
href: "https://casraf.dev",
},
],
},
{
title: "More",
items: [
{
label: "npm",
href: "https://npmjs.com/package/simple-scaffold",
},
{
label: "GitHub",
href: "https://github.com/chenasraf/simple-scaffold",
},
],
},
],
copyright: `Copyright © ${new Date().getFullYear()} Chen Asraf. Built with Docusaurus.`,
},
prism: {
theme: prismThemes.github,
darkTheme: prismThemes.dracula,
},
} satisfies Preset.ThemeConfig,
}
export default config

51
docs/package.json Normal file
View File

@@ -0,0 +1,51 @@
{
"name": "simple-scaffold-docs",
"version": "0.0.0",
"private": true,
"scripts": {
"docusaurus": "docusaurus",
"start": "docusaurus start --port 3001",
"build": "docusaurus build",
"swizzle": "docusaurus swizzle",
"deploy": "docusaurus deploy",
"clear": "docusaurus clear",
"serve": "docusaurus serve",
"write-translations": "docusaurus write-translations",
"write-heading-ids": "docusaurus write-heading-ids",
"typecheck": "tsc"
},
"dependencies": {
"@docusaurus/core": "^3.9.2",
"@docusaurus/plugin-google-tag-manager": "^3.9.2",
"@docusaurus/preset-classic": "^3.9.2",
"@mdx-js/react": "^3.1.1",
"clsx": "^2.1.1",
"prism-react-renderer": "^2.4.1",
"react": "^19.2.4",
"react-dom": "^19.2.4"
},
"devDependencies": {
"@docusaurus/module-type-aliases": "^3.9.2",
"@docusaurus/tsconfig": "^3.9.2",
"@docusaurus/types": "^3.9.2",
"docusaurus-plugin-typedoc": "^1.4.2",
"typedoc": "^0.28.18",
"typedoc-plugin-markdown": "^4.11.0",
"typescript": "~5.9.3"
},
"browserslist": {
"production": [
">0.5%",
"not dead",
"not op_mini all"
],
"development": [
"last 3 chrome version",
"last 3 firefox version",
"last 5 safari version"
]
},
"engines": {
"node": ">=18.0"
}
}

11776
docs/pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

28
docs/sidebars.ts Normal file
View File

@@ -0,0 +1,28 @@
import type { SidebarsConfig } from "@docusaurus/plugin-content-docs"
const sidebars: SidebarsConfig = {
usage: [{ type: "autogenerated", dirName: "usage" }],
api: [
{ type: "doc", id: "api/index", label: "Overview" },
{
type: "category",
label: "Functions",
items: [{ type: "autogenerated", dirName: "api/functions" }],
},
{
type: "category",
label: "Types",
items: [
{ type: "autogenerated", dirName: "api/interfaces" },
{ type: "autogenerated", dirName: "api/type-aliases" },
],
},
{
type: "category",
label: "Variables",
items: [{ type: "autogenerated", dirName: "api/variables" }],
},
],
}
export default sidebars

View File

@@ -0,0 +1,72 @@
import clsx from "clsx"
import Heading from "@theme/Heading"
import styles from "./styles.module.css"
type FeatureItem = {
title: string
Svg: React.ComponentType<React.ComponentProps<"svg">>
description: JSX.Element
}
const FeatureList: FeatureItem[] = [
{
title: "Easy to Use",
Svg: require("@site/static/img/undraw_docusaurus_mountain.svg").default,
description: (
<>
Generate anything from a simple component to an entire app boilerplate - you decide! Put dynamic data in your
templates to quickly generate skeletons, formatted data dumps, or repetitive code - and immediately get to
coding!
</>
),
},
{
title: "Use It Anywhere, For Anything",
Svg: require("@site/static/img/undraw_docusaurus_tree.svg").default,
description: (
<>
Whether you need files specific to your project or commonly used templates - you can use them both locally or
use Git to share them with your team. Spackle on some one-time-use data, and run one command.
</>
),
},
{
title: "Handlebars Support",
Svg: require("@site/static/img/undraw_docusaurus_react.svg").default,
description: (
<>
Did you think you stop at some static data? Generate entire mapped lists of items, pre-parse information, fake
data, and more - you can attach any function or any data to your templates. Handlebars will parse it all and
generate the files you need.
</>
),
},
]
function Feature({ title, Svg, description }: FeatureItem) {
return (
<div className={clsx("col col--4")}>
<div className="text--center">
<Svg className={styles.featureSvg} role="img" />
</div>
<div className="text--center padding-horiz--md">
<Heading as="h3">{title}</Heading>
<p>{description}</p>
</div>
</div>
)
}
export default function HomepageFeatures(): JSX.Element {
return (
<section className={styles.features}>
<div className="container">
<div className="row">
{FeatureList.map((props, idx) => (
<Feature key={idx} {...props} />
))}
</div>
</div>
</section>
)
}

View File

@@ -0,0 +1,11 @@
.features {
display: flex;
align-items: center;
padding: 2rem 0;
width: 100%;
}
.featureSvg {
height: 200px;
width: 200px;
}

30
docs/src/css/custom.css Normal file
View File

@@ -0,0 +1,30 @@
/**
* Any CSS included here will be global. The classic template
* bundles Infima by default. Infima is a CSS framework designed to
* work well for content-centric websites.
*/
/* You can override the default Infima variables here. */
:root {
--ifm-color-primary: #2e8555;
--ifm-color-primary-dark: #29784c;
--ifm-color-primary-darker: #277148;
--ifm-color-primary-darkest: #205d3b;
--ifm-color-primary-light: #33925d;
--ifm-color-primary-lighter: #359962;
--ifm-color-primary-lightest: #3cad6e;
--ifm-code-font-size: 95%;
--docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1);
}
/* For readability concerns, you should choose a lighter palette in dark mode. */
[data-theme="dark"] {
--ifm-color-primary: #25c2a0;
--ifm-color-primary-dark: #21af90;
--ifm-color-primary-darker: #1fa588;
--ifm-color-primary-darkest: #1a8870;
--ifm-color-primary-light: #29d5b0;
--ifm-color-primary-lighter: #32d8b4;
--ifm-color-primary-lightest: #4fddbf;
--docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3);
}

View File

@@ -0,0 +1,34 @@
/**
* CSS files with the .module.css suffix will be treated as CSS modules
* and scoped locally.
*/
.heroBanner {
padding: 4rem 0;
text-align: center;
position: relative;
overflow: hidden;
}
@media screen and (max-width: 996px) {
.heroBanner {
padding: 2rem;
}
}
.buttons {
display: flex;
align-items: center;
justify-content: center;
gap: 2rem;
}
.heroImage {
margin-bottom: 1.5rem;
}
.logo {
width: 100%;
max-width: 300px;
margin: 0 auto;
}

44
docs/src/pages/index.tsx Normal file
View File

@@ -0,0 +1,44 @@
import clsx from "clsx"
import Link from "@docusaurus/Link"
import useDocusaurusContext from "@docusaurus/useDocusaurusContext"
import Layout from "@theme/Layout"
import HomepageFeatures from "@site/src/components/HomepageFeatures"
import Heading from "@theme/Heading"
import styles from "./index.module.css"
function HomepageHeader() {
const { siteConfig } = useDocusaurusContext()
return (
<header className={clsx("hero hero--primary", styles.heroBanner)}>
<div className="container">
<img className={styles.logo} src="img/logo-lg.svg" alt="Simple Scaffold" />
<Heading as="h1" className="hero__title">
{siteConfig.title}
</Heading>
<p className="hero__subtitle">{siteConfig.tagline}</p>
<img className={styles.heroImage} src="img/intro.gif" alt="Simple-Scaffold doing its thing" />
<div className={styles.buttons}>
<Link className="button button--secondary button--lg" to="/docs/api">
API
</Link>
<Link className="button button--secondary button--lg" to="/docs/usage">
Usage
</Link>
</div>
</div>
</header>
)
}
export default function Home(): JSX.Element {
const { siteConfig } = useDocusaurusContext()
return (
<Layout title={siteConfig.title} description="Description will go into a meta tag in <head />">
<HomepageHeader />
<main>
<HomepageFeatures />
</main>
</Layout>
)
}

View File

@@ -0,0 +1,7 @@
---
title: Markdown page example
---
# Markdown page example
You don't need React to write simple standalone pages.

0
docs/static/.nojekyll vendored Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

BIN
docs/static/img/docusaurus.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

BIN
docs/static/img/favicon.ico vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

BIN
docs/static/img/favicon.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

20
docs/static/img/favicon.svg vendored Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 265 KiB

View File

Before

Width:  |  Height:  |  Size: 385 KiB

After

Width:  |  Height:  |  Size: 385 KiB

BIN
docs/static/img/logo-lg.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

20
docs/static/img/logo-lg.svg vendored Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 281 KiB

BIN
docs/static/img/logo.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

20
docs/static/img/logo.svg vendored Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 432 KiB

View File

@@ -0,0 +1,171 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1088" height="687.962" viewBox="0 0 1088 687.962">
<title>Easy to Use</title>
<g id="Group_12" data-name="Group 12" transform="translate(-57 -56)">
<g id="Group_11" data-name="Group 11" transform="translate(57 56)">
<path id="Path_83" data-name="Path 83" d="M1017.81,560.461c-5.27,45.15-16.22,81.4-31.25,110.31-20,38.52-54.21,54.04-84.77,70.28a193.275,193.275,0,0,1-27.46,11.94c-55.61,19.3-117.85,14.18-166.74,3.99a657.282,657.282,0,0,0-104.09-13.16q-14.97-.675-29.97-.67c-15.42.02-293.07,5.29-360.67-131.57-16.69-33.76-28.13-75-32.24-125.27-11.63-142.12,52.29-235.46,134.74-296.47,155.97-115.41,369.76-110.57,523.43,7.88C941.15,276.621,1036.99,396.031,1017.81,560.461Z" transform="translate(-56 -106.019)" fill="#3f3d56"/>
<path id="Path_84" data-name="Path 84" d="M986.56,670.771c-20,38.52-47.21,64.04-77.77,80.28a193.272,193.272,0,0,1-27.46,11.94c-55.61,19.3-117.85,14.18-166.74,3.99a657.3,657.3,0,0,0-104.09-13.16q-14.97-.675-29.97-.67-23.13.03-46.25,1.72c-100.17,7.36-253.82-6.43-321.42-143.29L382,283.981,444.95,445.6l20.09,51.59,55.37-75.98L549,381.981l130.2,149.27,36.8-81.27L970.78,657.9l14.21,11.59Z" transform="translate(-56 -106.019)" fill="#f2f2f2"/>
<path id="Path_85" data-name="Path 85" d="M302,282.962l26-57,36,83-31-60Z" opacity="0.1"/>
<path id="Path_86" data-name="Path 86" d="M610.5,753.821q-14.97-.675-29.97-.67L465.04,497.191Z" transform="translate(-56 -106.019)" opacity="0.1"/>
<path id="Path_87" data-name="Path 87" d="M464.411,315.191,493,292.962l130,150-132-128Z" opacity="0.1"/>
<path id="Path_88" data-name="Path 88" d="M908.79,751.051a193.265,193.265,0,0,1-27.46,11.94L679.2,531.251Z" transform="translate(-56 -106.019)" opacity="0.1"/>
<circle id="Ellipse_11" data-name="Ellipse 11" cx="3" cy="3" r="3" transform="translate(479 98.962)" fill="#f2f2f2"/>
<circle id="Ellipse_12" data-name="Ellipse 12" cx="3" cy="3" r="3" transform="translate(396 201.962)" fill="#f2f2f2"/>
<circle id="Ellipse_13" data-name="Ellipse 13" cx="2" cy="2" r="2" transform="translate(600 220.962)" fill="#f2f2f2"/>
<circle id="Ellipse_14" data-name="Ellipse 14" cx="2" cy="2" r="2" transform="translate(180 265.962)" fill="#f2f2f2"/>
<circle id="Ellipse_15" data-name="Ellipse 15" cx="2" cy="2" r="2" transform="translate(612 96.962)" fill="#f2f2f2"/>
<circle id="Ellipse_16" data-name="Ellipse 16" cx="2" cy="2" r="2" transform="translate(736 192.962)" fill="#f2f2f2"/>
<circle id="Ellipse_17" data-name="Ellipse 17" cx="2" cy="2" r="2" transform="translate(858 344.962)" fill="#f2f2f2"/>
<path id="Path_89" data-name="Path 89" d="M306,121.222h-2.76v-2.76h-1.48v2.76H299V122.7h2.76v2.759h1.48V122.7H306Z" fill="#f2f2f2"/>
<path id="Path_90" data-name="Path 90" d="M848,424.222h-2.76v-2.76h-1.48v2.76H841V425.7h2.76v2.759h1.48V425.7H848Z" fill="#f2f2f2"/>
<path id="Path_91" data-name="Path 91" d="M1144,719.981c0,16.569-243.557,74-544,74s-544-57.431-544-74,243.557,14,544,14S1144,703.413,1144,719.981Z" transform="translate(-56 -106.019)" fill="#3f3d56"/>
<path id="Path_92" data-name="Path 92" d="M1144,719.981c0,16.569-243.557,74-544,74s-544-57.431-544-74,243.557,14,544,14S1144,703.413,1144,719.981Z" transform="translate(-56 -106.019)" opacity="0.1"/>
<ellipse id="Ellipse_18" data-name="Ellipse 18" cx="544" cy="30" rx="544" ry="30" transform="translate(0 583.962)" fill="#3f3d56"/>
<path id="Path_93" data-name="Path 93" d="M624,677.981c0,33.137-14.775,24-33,24s-33,9.137-33-24,33-96,33-96S624,644.844,624,677.981Z" transform="translate(-56 -106.019)" fill="#ff6584"/>
<path id="Path_94" data-name="Path 94" d="M606,690.66c0,15.062-6.716,10.909-15,10.909s-15,4.153-15-10.909,15-43.636,15-43.636S606,675.6,606,690.66Z" transform="translate(-56 -106.019)" opacity="0.1"/>
<rect id="Rectangle_97" data-name="Rectangle 97" width="92" height="18" rx="9" transform="translate(489 604.962)" fill="#2f2e41"/>
<rect id="Rectangle_98" data-name="Rectangle 98" width="92" height="18" rx="9" transform="translate(489 586.962)" fill="#2f2e41"/>
<path id="Path_95" data-name="Path 95" d="M193,596.547c0,55.343,34.719,100.126,77.626,100.126" transform="translate(-56 -106.019)" fill="#3f3d56"/>
<path id="Path_96" data-name="Path 96" d="M270.626,696.673c0-55.965,38.745-101.251,86.626-101.251" transform="translate(-56 -106.019)" fill="#6c63ff"/>
<path id="Path_97" data-name="Path 97" d="M221.125,601.564c0,52.57,22.14,95.109,49.5,95.109" transform="translate(-56 -106.019)" fill="#6c63ff"/>
<path id="Path_98" data-name="Path 98" d="M270.626,696.673c0-71.511,44.783-129.377,100.126-129.377" transform="translate(-56 -106.019)" fill="#3f3d56"/>
<path id="Path_99" data-name="Path 99" d="M254.3,697.379s11.009-.339,14.326-2.7,16.934-5.183,17.757-1.395,16.544,18.844,4.115,18.945-28.879-1.936-32.19-3.953S254.3,697.379,254.3,697.379Z" transform="translate(-56 -106.019)" fill="#a8a8a8"/>
<path id="Path_100" data-name="Path 100" d="M290.716,710.909c-12.429.1-28.879-1.936-32.19-3.953-2.522-1.536-3.527-7.048-3.863-9.591l-.368.014s.7,8.879,4.009,10.9,19.761,4.053,32.19,3.953c3.588-.029,4.827-1.305,4.759-3.2C294.755,710.174,293.386,710.887,290.716,710.909Z" transform="translate(-56 -106.019)" opacity="0.2"/>
<path id="Path_101" data-name="Path 101" d="M777.429,633.081c0,38.029,23.857,68.8,53.341,68.8" transform="translate(-56 -106.019)" fill="#3f3d56"/>
<path id="Path_102" data-name="Path 102" d="M830.769,701.882c0-38.456,26.623-69.575,59.525-69.575" transform="translate(-56 -106.019)" fill="#6c63ff"/>
<path id="Path_103" data-name="Path 103" d="M796.755,636.528c0,36.124,15.213,65.354,34.014,65.354" transform="translate(-56 -106.019)" fill="#6c63ff"/>
<path id="Path_104" data-name="Path 104" d="M830.769,701.882c0-49.139,30.773-88.9,68.8-88.9" transform="translate(-56 -106.019)" fill="#3f3d56"/>
<path id="Path_105" data-name="Path 105" d="M819.548,702.367s7.565-.233,9.844-1.856,11.636-3.562,12.2-.958,11.368,12.949,2.828,13.018-19.844-1.33-22.119-2.716S819.548,702.367,819.548,702.367Z" transform="translate(-56 -106.019)" fill="#a8a8a8"/>
<path id="Path_106" data-name="Path 106" d="M844.574,711.664c-8.54.069-19.844-1.33-22.119-2.716-1.733-1.056-2.423-4.843-2.654-6.59l-.253.01s.479,6.1,2.755,7.487,13.579,2.785,22.119,2.716c2.465-.02,3.317-.9,3.27-2.2C847.349,711.159,846.409,711.649,844.574,711.664Z" transform="translate(-56 -106.019)" opacity="0.2"/>
<path id="Path_107" data-name="Path 107" d="M949.813,724.718s11.36-1.729,14.5-4.591,16.89-7.488,18.217-3.667,19.494,17.447,6.633,19.107-30.153,1.609-33.835-.065S949.813,724.718,949.813,724.718Z" transform="translate(-56 -106.019)" fill="#a8a8a8"/>
<path id="Path_108" data-name="Path 108" d="M989.228,734.173c-12.86,1.659-30.153,1.609-33.835-.065-2.8-1.275-4.535-6.858-5.2-9.45l-.379.061s1.833,9.109,5.516,10.783,20.975,1.725,33.835.065c3.712-.479,4.836-1.956,4.529-3.906C993.319,732.907,991.991,733.817,989.228,734.173Z" transform="translate(-56 -106.019)" opacity="0.2"/>
<path id="Path_109" data-name="Path 109" d="M670.26,723.9s9.587-1.459,12.237-3.875,14.255-6.32,15.374-3.095,16.452,14.725,5.6,16.125-25.448,1.358-28.555-.055S670.26,723.9,670.26,723.9Z" transform="translate(-56 -106.019)" fill="#a8a8a8"/>
<path id="Path_110" data-name="Path 110" d="M703.524,731.875c-10.853,1.4-25.448,1.358-28.555-.055-2.367-1.076-3.827-5.788-4.39-7.976l-.32.051s1.547,7.687,4.655,9.1,17.7,1.456,28.555.055c3.133-.4,4.081-1.651,3.822-3.3C706.977,730.807,705.856,731.575,703.524,731.875Z" transform="translate(-56 -106.019)" opacity="0.2"/>
<path id="Path_111" data-name="Path 111" d="M178.389,719.109s7.463-1.136,9.527-3.016,11.1-4.92,11.969-2.409,12.808,11.463,4.358,12.553-19.811,1.057-22.23-.043S178.389,719.109,178.389,719.109Z" transform="translate(-56 -106.019)" fill="#a8a8a8"/>
<path id="Path_112" data-name="Path 112" d="M204.285,725.321c-8.449,1.09-19.811,1.057-22.23-.043-1.842-.838-2.979-4.506-3.417-6.209l-.249.04s1.2,5.984,3.624,7.085,13.781,1.133,22.23.043c2.439-.315,3.177-1.285,2.976-2.566C206.973,724.489,206.1,725.087,204.285,725.321Z" transform="translate(-56 -106.019)" opacity="0.2"/>
<path id="Path_113" data-name="Path 113" d="M439.7,707.337c0,30.22-42.124,20.873-93.7,20.873s-93.074,9.347-93.074-20.873,42.118-36.793,93.694-36.793S439.7,677.117,439.7,707.337Z" transform="translate(-56 -106.019)" opacity="0.1"/>
<path id="Path_114" data-name="Path 114" d="M439.7,699.9c0,30.22-42.124,20.873-93.7,20.873s-93.074,9.347-93.074-20.873S295.04,663.1,346.616,663.1,439.7,669.676,439.7,699.9Z" transform="translate(-56 -106.019)" fill="#3f3d56"/>
</g>
<g id="docusaurus_keytar" transform="translate(312.271 493.733)">
<path id="Path_40" data-name="Path 40" d="M99,52h91.791V89.153H99Z" transform="translate(5.904 -14.001)" fill="#fff" fill-rule="evenodd"/>
<path id="Path_41" data-name="Path 41" d="M24.855,163.927A21.828,21.828,0,0,1,5.947,153a21.829,21.829,0,0,0,18.908,32.782H46.71V163.927Z" transform="translate(-3 -4.634)" fill="#3ecc5f" fill-rule="evenodd"/>
<path id="Path_42" data-name="Path 42" d="M121.861,61.1l76.514-4.782V45.39A21.854,21.854,0,0,0,176.52,23.535H78.173L75.441,18.8a3.154,3.154,0,0,0-5.464,0l-2.732,4.732L64.513,18.8a3.154,3.154,0,0,0-5.464,0l-2.732,4.732L53.586,18.8a3.154,3.154,0,0,0-5.464,0L45.39,23.535c-.024,0-.046,0-.071,0l-4.526-4.525a3.153,3.153,0,0,0-5.276,1.414l-1.5,5.577-5.674-1.521a3.154,3.154,0,0,0-3.863,3.864L26,34.023l-5.575,1.494a3.155,3.155,0,0,0-1.416,5.278l4.526,4.526c0,.023,0,.046,0,.07L18.8,48.122a3.154,3.154,0,0,0,0,5.464l4.732,2.732L18.8,59.05a3.154,3.154,0,0,0,0,5.464l4.732,2.732L18.8,69.977a3.154,3.154,0,0,0,0,5.464l4.732,2.732L18.8,80.9a3.154,3.154,0,0,0,0,5.464L23.535,89.1,18.8,91.832a3.154,3.154,0,0,0,0,5.464l4.732,2.732L18.8,102.76a3.154,3.154,0,0,0,0,5.464l4.732,2.732L18.8,113.687a3.154,3.154,0,0,0,0,5.464l4.732,2.732L18.8,124.615a3.154,3.154,0,0,0,0,5.464l4.732,2.732L18.8,135.542a3.154,3.154,0,0,0,0,5.464l4.732,2.732L18.8,146.469a3.154,3.154,0,0,0,0,5.464l4.732,2.732L18.8,157.4a3.154,3.154,0,0,0,0,5.464l4.732,2.732L18.8,168.324a3.154,3.154,0,0,0,0,5.464l4.732,2.732A21.854,21.854,0,0,0,45.39,198.375H176.52a21.854,21.854,0,0,0,21.855-21.855V89.1l-76.514-4.782a11.632,11.632,0,0,1,0-23.219" transform="translate(-1.681 -17.226)" fill="#3ecc5f" fill-rule="evenodd"/>
<path id="Path_43" data-name="Path 43" d="M143,186.71h32.782V143H143Z" transform="translate(9.984 -5.561)" fill="#3ecc5f" fill-rule="evenodd"/>
<path id="Path_44" data-name="Path 44" d="M196.71,159.855a5.438,5.438,0,0,0-.7.07c-.042-.164-.081-.329-.127-.493a5.457,5.457,0,1,0-5.4-9.372q-.181-.185-.366-.367a5.454,5.454,0,1,0-9.384-5.4c-.162-.046-.325-.084-.486-.126a5.467,5.467,0,1,0-10.788,0c-.162.042-.325.08-.486.126a5.457,5.457,0,1,0-9.384,5.4,21.843,21.843,0,1,0,36.421,21.02,5.452,5.452,0,1,0,.7-10.858" transform="translate(10.912 -6.025)" fill="#44d860" fill-rule="evenodd"/>
<path id="Path_45" data-name="Path 45" d="M153,124.855h32.782V103H153Z" transform="translate(10.912 -9.271)" fill="#3ecc5f" fill-rule="evenodd"/>
<path id="Path_46" data-name="Path 46" d="M194.855,116.765a2.732,2.732,0,1,0,0-5.464,2.811,2.811,0,0,0-.349.035c-.022-.082-.04-.164-.063-.246a2.733,2.733,0,0,0-1.052-5.253,2.7,2.7,0,0,0-1.648.566q-.09-.093-.184-.184a2.7,2.7,0,0,0,.553-1.633,2.732,2.732,0,0,0-5.245-1.07,10.928,10.928,0,1,0,0,21.031,2.732,2.732,0,0,0,5.245-1.07,2.7,2.7,0,0,0-.553-1.633q.093-.09.184-.184a2.7,2.7,0,0,0,1.648.566,2.732,2.732,0,0,0,1.052-5.253c.023-.081.042-.164.063-.246a2.814,2.814,0,0,0,.349.035" transform="translate(12.767 -9.377)" fill="#44d860" fill-rule="evenodd"/>
<path id="Path_47" data-name="Path 47" d="M65.087,56.891a2.732,2.732,0,0,1-2.732-2.732,8.2,8.2,0,0,0-16.391,0,2.732,2.732,0,0,1-5.464,0,13.659,13.659,0,0,1,27.319,0,2.732,2.732,0,0,1-2.732,2.732" transform="translate(0.478 -15.068)" fill-rule="evenodd"/>
<path id="Path_48" data-name="Path 48" d="M103,191.347h65.565a21.854,21.854,0,0,0,21.855-21.855V93H124.855A21.854,21.854,0,0,0,103,114.855Z" transform="translate(6.275 -10.199)" fill="#ffff50" fill-rule="evenodd"/>
<path id="Path_49" data-name="Path 49" d="M173.216,129.787H118.535a1.093,1.093,0,1,1,0-2.185h54.681a1.093,1.093,0,0,1,0,2.185m0,21.855H118.535a1.093,1.093,0,1,1,0-2.186h54.681a1.093,1.093,0,0,1,0,2.186m0,21.855H118.535a1.093,1.093,0,1,1,0-2.185h54.681a1.093,1.093,0,0,1,0,2.185m0-54.434H118.535a1.093,1.093,0,1,1,0-2.185h54.681a1.093,1.093,0,0,1,0,2.185m0,21.652H118.535a1.093,1.093,0,1,1,0-2.186h54.681a1.093,1.093,0,0,1,0,2.186m0,21.855H118.535a1.093,1.093,0,1,1,0-2.186h54.681a1.093,1.093,0,0,1,0,2.186M189.585,61.611c-.013,0-.024-.007-.037-.005-3.377.115-4.974,3.492-6.384,6.472-1.471,3.114-2.608,5.139-4.473,5.078-2.064-.074-3.244-2.406-4.494-4.874-1.436-2.835-3.075-6.049-6.516-5.929-3.329.114-4.932,3.053-6.346,5.646-1.5,2.762-2.529,4.442-4.5,4.364-2.106-.076-3.225-1.972-4.52-4.167-1.444-2.443-3.112-5.191-6.487-5.1-3.272.113-4.879,2.606-6.3,4.808-1.5,2.328-2.552,3.746-4.551,3.662-2.156-.076-3.27-1.65-4.558-3.472-1.447-2.047-3.077-4.363-6.442-4.251-3.2.109-4.807,2.153-6.224,3.954-1.346,1.709-2.4,3.062-4.621,2.977a1.093,1.093,0,0,0-.079,2.186c3.3.11,4.967-1.967,6.417-3.81,1.286-1.635,2.4-3.045,4.582-3.12,2.1-.09,3.091,1.218,4.584,3.327,1.417,2,3.026,4.277,6.263,4.394,3.391.114,5.022-2.42,6.467-4.663,1.292-2,2.406-3.734,4.535-3.807,1.959-.073,3.026,1.475,4.529,4.022,1.417,2.4,3.023,5.121,6.324,5.241,3.415.118,5.064-2.863,6.5-5.5,1.245-2.282,2.419-4.437,4.5-4.509,1.959-.046,2.981,1.743,4.492,4.732,1.412,2.79,3.013,5.95,6.365,6.071l.185,0c3.348,0,4.937-3.36,6.343-6.331,1.245-2.634,2.423-5.114,4.444-5.216Z" transform="translate(7.109 -13.11)" fill-rule="evenodd"/>
<path id="Path_50" data-name="Path 50" d="M83,186.71h43.71V143H83Z" transform="translate(4.42 -5.561)" fill="#3ecc5f" fill-rule="evenodd"/>
<g id="Group_8" data-name="Group 8" transform="matrix(0.966, -0.259, 0.259, 0.966, 109.327, 91.085)">
<rect id="Rectangle_3" data-name="Rectangle 3" width="92.361" height="36.462" rx="2" transform="translate(0 0)" fill="#d8d8d8"/>
<g id="Group_2" data-name="Group 2" transform="translate(1.531 23.03)">
<rect id="Rectangle_4" data-name="Rectangle 4" width="5.336" height="5.336" rx="1" transform="translate(16.797 0)" fill="#4a4a4a"/>
<rect id="Rectangle_5" data-name="Rectangle 5" width="5.336" height="5.336" rx="1" transform="translate(23.12 0)" fill="#4a4a4a"/>
<rect id="Rectangle_6" data-name="Rectangle 6" width="5.336" height="5.336" rx="1" transform="translate(29.444 0)" fill="#4a4a4a"/>
<rect id="Rectangle_7" data-name="Rectangle 7" width="5.336" height="5.336" rx="1" transform="translate(35.768 0)" fill="#4a4a4a"/>
<rect id="Rectangle_8" data-name="Rectangle 8" width="5.336" height="5.336" rx="1" transform="translate(42.091 0)" fill="#4a4a4a"/>
<rect id="Rectangle_9" data-name="Rectangle 9" width="5.336" height="5.336" rx="1" transform="translate(48.415 0)" fill="#4a4a4a"/>
<rect id="Rectangle_10" data-name="Rectangle 10" width="5.336" height="5.336" rx="1" transform="translate(54.739 0)" fill="#4a4a4a"/>
<rect id="Rectangle_11" data-name="Rectangle 11" width="5.336" height="5.336" rx="1" transform="translate(61.063 0)" fill="#4a4a4a"/>
<rect id="Rectangle_12" data-name="Rectangle 12" width="5.336" height="5.336" rx="1" transform="translate(67.386 0)" fill="#4a4a4a"/>
<path id="Path_51" data-name="Path 51" d="M1.093,0H14.518a1.093,1.093,0,0,1,1.093,1.093V4.243a1.093,1.093,0,0,1-1.093,1.093H1.093A1.093,1.093,0,0,1,0,4.243V1.093A1.093,1.093,0,0,1,1.093,0ZM75,0H88.426a1.093,1.093,0,0,1,1.093,1.093V4.243a1.093,1.093,0,0,1-1.093,1.093H75a1.093,1.093,0,0,1-1.093-1.093V1.093A1.093,1.093,0,0,1,75,0Z" transform="translate(0 0)" fill="#4a4a4a" fill-rule="evenodd"/>
</g>
<g id="Group_3" data-name="Group 3" transform="translate(1.531 10.261)">
<path id="Path_52" data-name="Path 52" d="M1.093,0H6.218A1.093,1.093,0,0,1,7.31,1.093V4.242A1.093,1.093,0,0,1,6.218,5.335H1.093A1.093,1.093,0,0,1,0,4.242V1.093A1.093,1.093,0,0,1,1.093,0Z" transform="translate(0 0)" fill="#4a4a4a" fill-rule="evenodd"/>
<rect id="Rectangle_13" data-name="Rectangle 13" width="5.336" height="5.336" rx="1" transform="translate(8.299 0)" fill="#4a4a4a"/>
<rect id="Rectangle_14" data-name="Rectangle 14" width="5.336" height="5.336" rx="1" transform="translate(14.623 0)" fill="#4a4a4a"/>
<rect id="Rectangle_15" data-name="Rectangle 15" width="5.336" height="5.336" rx="1" transform="translate(20.947 0)" fill="#4a4a4a"/>
<rect id="Rectangle_16" data-name="Rectangle 16" width="5.336" height="5.336" rx="1" transform="translate(27.271 0)" fill="#4a4a4a"/>
<rect id="Rectangle_17" data-name="Rectangle 17" width="5.336" height="5.336" rx="1" transform="translate(33.594 0)" fill="#4a4a4a"/>
<rect id="Rectangle_18" data-name="Rectangle 18" width="5.336" height="5.336" rx="1" transform="translate(39.918 0)" fill="#4a4a4a"/>
<rect id="Rectangle_19" data-name="Rectangle 19" width="5.336" height="5.336" rx="1" transform="translate(46.242 0)" fill="#4a4a4a"/>
<rect id="Rectangle_20" data-name="Rectangle 20" width="5.336" height="5.336" rx="1" transform="translate(52.565 0)" fill="#4a4a4a"/>
<rect id="Rectangle_21" data-name="Rectangle 21" width="5.336" height="5.336" rx="1" transform="translate(58.888 0)" fill="#4a4a4a"/>
<rect id="Rectangle_22" data-name="Rectangle 22" width="5.336" height="5.336" rx="1" transform="translate(65.212 0)" fill="#4a4a4a"/>
<rect id="Rectangle_23" data-name="Rectangle 23" width="5.336" height="5.336" rx="1" transform="translate(71.536 0)" fill="#4a4a4a"/>
<rect id="Rectangle_24" data-name="Rectangle 24" width="5.336" height="5.336" rx="1" transform="translate(77.859 0)" fill="#4a4a4a"/>
<rect id="Rectangle_25" data-name="Rectangle 25" width="5.336" height="5.336" rx="1" transform="translate(84.183 0)" fill="#4a4a4a"/>
</g>
<g id="Group_4" data-name="Group 4" transform="translate(91.05 9.546) rotate(180)">
<path id="Path_53" data-name="Path 53" d="M1.093,0H6.219A1.093,1.093,0,0,1,7.312,1.093v3.15A1.093,1.093,0,0,1,6.219,5.336H1.093A1.093,1.093,0,0,1,0,4.243V1.093A1.093,1.093,0,0,1,1.093,0Z" transform="translate(0 0)" fill="#4a4a4a" fill-rule="evenodd"/>
<rect id="Rectangle_26" data-name="Rectangle 26" width="5.336" height="5.336" rx="1" transform="translate(8.299 0)" fill="#4a4a4a"/>
<rect id="Rectangle_27" data-name="Rectangle 27" width="5.336" height="5.336" rx="1" transform="translate(14.623 0)" fill="#4a4a4a"/>
<rect id="Rectangle_28" data-name="Rectangle 28" width="5.336" height="5.336" rx="1" transform="translate(20.947 0)" fill="#4a4a4a"/>
<rect id="Rectangle_29" data-name="Rectangle 29" width="5.336" height="5.336" rx="1" transform="translate(27.271 0)" fill="#4a4a4a"/>
<rect id="Rectangle_30" data-name="Rectangle 30" width="5.336" height="5.336" rx="1" transform="translate(33.594 0)" fill="#4a4a4a"/>
<rect id="Rectangle_31" data-name="Rectangle 31" width="5.336" height="5.336" rx="1" transform="translate(39.918 0)" fill="#4a4a4a"/>
<rect id="Rectangle_32" data-name="Rectangle 32" width="5.336" height="5.336" rx="1" transform="translate(46.242 0)" fill="#4a4a4a"/>
<rect id="Rectangle_33" data-name="Rectangle 33" width="5.336" height="5.336" rx="1" transform="translate(52.565 0)" fill="#4a4a4a"/>
<rect id="Rectangle_34" data-name="Rectangle 34" width="5.336" height="5.336" rx="1" transform="translate(58.889 0)" fill="#4a4a4a"/>
<rect id="Rectangle_35" data-name="Rectangle 35" width="5.336" height="5.336" rx="1" transform="translate(65.213 0)" fill="#4a4a4a"/>
<rect id="Rectangle_36" data-name="Rectangle 36" width="5.336" height="5.336" rx="1" transform="translate(71.537 0)" fill="#4a4a4a"/>
<rect id="Rectangle_37" data-name="Rectangle 37" width="5.336" height="5.336" rx="1" transform="translate(77.86 0)" fill="#4a4a4a"/>
<rect id="Rectangle_38" data-name="Rectangle 38" width="5.336" height="5.336" rx="1" transform="translate(84.183 0)" fill="#4a4a4a"/>
<rect id="Rectangle_39" data-name="Rectangle 39" width="5.336" height="5.336" rx="1" transform="translate(8.299 0)" fill="#4a4a4a"/>
<rect id="Rectangle_40" data-name="Rectangle 40" width="5.336" height="5.336" rx="1" transform="translate(14.623 0)" fill="#4a4a4a"/>
<rect id="Rectangle_41" data-name="Rectangle 41" width="5.336" height="5.336" rx="1" transform="translate(20.947 0)" fill="#4a4a4a"/>
<rect id="Rectangle_42" data-name="Rectangle 42" width="5.336" height="5.336" rx="1" transform="translate(27.271 0)" fill="#4a4a4a"/>
<rect id="Rectangle_43" data-name="Rectangle 43" width="5.336" height="5.336" rx="1" transform="translate(33.594 0)" fill="#4a4a4a"/>
<rect id="Rectangle_44" data-name="Rectangle 44" width="5.336" height="5.336" rx="1" transform="translate(39.918 0)" fill="#4a4a4a"/>
<rect id="Rectangle_45" data-name="Rectangle 45" width="5.336" height="5.336" rx="1" transform="translate(46.242 0)" fill="#4a4a4a"/>
<rect id="Rectangle_46" data-name="Rectangle 46" width="5.336" height="5.336" rx="1" transform="translate(52.565 0)" fill="#4a4a4a"/>
<rect id="Rectangle_47" data-name="Rectangle 47" width="5.336" height="5.336" rx="1" transform="translate(58.889 0)" fill="#4a4a4a"/>
<rect id="Rectangle_48" data-name="Rectangle 48" width="5.336" height="5.336" rx="1" transform="translate(65.213 0)" fill="#4a4a4a"/>
<rect id="Rectangle_49" data-name="Rectangle 49" width="5.336" height="5.336" rx="1" transform="translate(71.537 0)" fill="#4a4a4a"/>
<rect id="Rectangle_50" data-name="Rectangle 50" width="5.336" height="5.336" rx="1" transform="translate(77.86 0)" fill="#4a4a4a"/>
<rect id="Rectangle_51" data-name="Rectangle 51" width="5.336" height="5.336" rx="1" transform="translate(84.183 0)" fill="#4a4a4a"/>
</g>
<g id="Group_6" data-name="Group 6" transform="translate(1.531 16.584)">
<path id="Path_54" data-name="Path 54" d="M1.093,0h7.3A1.093,1.093,0,0,1,9.485,1.093v3.15A1.093,1.093,0,0,1,8.392,5.336h-7.3A1.093,1.093,0,0,1,0,4.243V1.094A1.093,1.093,0,0,1,1.093,0Z" transform="translate(0 0)" fill="#4a4a4a" fill-rule="evenodd"/>
<g id="Group_5" data-name="Group 5" transform="translate(10.671 0)">
<rect id="Rectangle_52" data-name="Rectangle 52" width="5.336" height="5.336" rx="1" fill="#4a4a4a"/>
<rect id="Rectangle_53" data-name="Rectangle 53" width="5.336" height="5.336" rx="1" transform="translate(6.324 0)" fill="#4a4a4a"/>
<rect id="Rectangle_54" data-name="Rectangle 54" width="5.336" height="5.336" rx="1" transform="translate(12.647 0)" fill="#4a4a4a"/>
<rect id="Rectangle_55" data-name="Rectangle 55" width="5.336" height="5.336" rx="1" transform="translate(18.971 0)" fill="#4a4a4a"/>
<rect id="Rectangle_56" data-name="Rectangle 56" width="5.336" height="5.336" rx="1" transform="translate(25.295 0)" fill="#4a4a4a"/>
<rect id="Rectangle_57" data-name="Rectangle 57" width="5.336" height="5.336" rx="1" transform="translate(31.619 0)" fill="#4a4a4a"/>
<rect id="Rectangle_58" data-name="Rectangle 58" width="5.336" height="5.336" rx="1" transform="translate(37.942 0)" fill="#4a4a4a"/>
<rect id="Rectangle_59" data-name="Rectangle 59" width="5.336" height="5.336" rx="1" transform="translate(44.265 0)" fill="#4a4a4a"/>
<rect id="Rectangle_60" data-name="Rectangle 60" width="5.336" height="5.336" rx="1" transform="translate(50.589 0)" fill="#4a4a4a"/>
<rect id="Rectangle_61" data-name="Rectangle 61" width="5.336" height="5.336" rx="1" transform="translate(56.912 0)" fill="#4a4a4a"/>
<rect id="Rectangle_62" data-name="Rectangle 62" width="5.336" height="5.336" rx="1" transform="translate(63.236 0)" fill="#4a4a4a"/>
</g>
<path id="Path_55" data-name="Path 55" d="M1.094,0H8A1.093,1.093,0,0,1,9.091,1.093v3.15A1.093,1.093,0,0,1,8,5.336H1.093A1.093,1.093,0,0,1,0,4.243V1.094A1.093,1.093,0,0,1,1.093,0Z" transform="translate(80.428 0)" fill="#4a4a4a" fill-rule="evenodd"/>
</g>
<g id="Group_7" data-name="Group 7" transform="translate(1.531 29.627)">
<rect id="Rectangle_63" data-name="Rectangle 63" width="5.336" height="5.336" rx="1" transform="translate(0 0)" fill="#4a4a4a"/>
<rect id="Rectangle_64" data-name="Rectangle 64" width="5.336" height="5.336" rx="1" transform="translate(6.324 0)" fill="#4a4a4a"/>
<rect id="Rectangle_65" data-name="Rectangle 65" width="5.336" height="5.336" rx="1" transform="translate(12.647 0)" fill="#4a4a4a"/>
<rect id="Rectangle_66" data-name="Rectangle 66" width="5.336" height="5.336" rx="1" transform="translate(18.971 0)" fill="#4a4a4a"/>
<path id="Path_56" data-name="Path 56" d="M1.093,0H31.515a1.093,1.093,0,0,1,1.093,1.093V4.244a1.093,1.093,0,0,1-1.093,1.093H1.093A1.093,1.093,0,0,1,0,4.244V1.093A1.093,1.093,0,0,1,1.093,0ZM34.687,0h3.942a1.093,1.093,0,0,1,1.093,1.093V4.244a1.093,1.093,0,0,1-1.093,1.093H34.687a1.093,1.093,0,0,1-1.093-1.093V1.093A1.093,1.093,0,0,1,34.687,0Z" transform="translate(25.294 0)" fill="#4a4a4a" fill-rule="evenodd"/>
<rect id="Rectangle_67" data-name="Rectangle 67" width="5.336" height="5.336" rx="1" transform="translate(66.003 0)" fill="#4a4a4a"/>
<rect id="Rectangle_68" data-name="Rectangle 68" width="5.336" height="5.336" rx="1" transform="translate(72.327 0)" fill="#4a4a4a"/>
<rect id="Rectangle_69" data-name="Rectangle 69" width="5.336" height="5.336" rx="1" transform="translate(84.183 0)" fill="#4a4a4a"/>
<path id="Path_57" data-name="Path 57" d="M5.336,0V1.18A1.093,1.093,0,0,1,4.243,2.273H1.093A1.093,1.093,0,0,1,0,1.18V0Z" transform="translate(83.59 2.273) rotate(180)" fill="#4a4a4a"/>
<path id="Path_58" data-name="Path 58" d="M5.336,0V1.18A1.093,1.093,0,0,1,4.243,2.273H1.093A1.093,1.093,0,0,1,0,1.18V0Z" transform="translate(78.255 3.063)" fill="#4a4a4a"/>
</g>
<rect id="Rectangle_70" data-name="Rectangle 70" width="88.927" height="2.371" rx="1.085" transform="translate(1.925 1.17)" fill="#4a4a4a"/>
<rect id="Rectangle_71" data-name="Rectangle 71" width="4.986" height="1.581" rx="0.723" transform="translate(4.1 1.566)" fill="#d8d8d8" opacity="0.136"/>
<rect id="Rectangle_72" data-name="Rectangle 72" width="4.986" height="1.581" rx="0.723" transform="translate(10.923 1.566)" fill="#d8d8d8" opacity="0.136"/>
<rect id="Rectangle_73" data-name="Rectangle 73" width="4.986" height="1.581" rx="0.723" transform="translate(16.173 1.566)" fill="#d8d8d8" opacity="0.136"/>
<rect id="Rectangle_74" data-name="Rectangle 74" width="4.986" height="1.581" rx="0.723" transform="translate(21.421 1.566)" fill="#d8d8d8" opacity="0.136"/>
<rect id="Rectangle_75" data-name="Rectangle 75" width="4.986" height="1.581" rx="0.723" transform="translate(26.671 1.566)" fill="#d8d8d8" opacity="0.136"/>
<rect id="Rectangle_76" data-name="Rectangle 76" width="4.986" height="1.581" rx="0.723" transform="translate(33.232 1.566)" fill="#d8d8d8" opacity="0.136"/>
<rect id="Rectangle_77" data-name="Rectangle 77" width="4.986" height="1.581" rx="0.723" transform="translate(38.48 1.566)" fill="#d8d8d8" opacity="0.136"/>
<rect id="Rectangle_78" data-name="Rectangle 78" width="4.986" height="1.581" rx="0.723" transform="translate(43.73 1.566)" fill="#d8d8d8" opacity="0.136"/>
<rect id="Rectangle_79" data-name="Rectangle 79" width="4.986" height="1.581" rx="0.723" transform="translate(48.978 1.566)" fill="#d8d8d8" opacity="0.136"/>
<rect id="Rectangle_80" data-name="Rectangle 80" width="4.986" height="1.581" rx="0.723" transform="translate(55.54 1.566)" fill="#d8d8d8" opacity="0.136"/>
<rect id="Rectangle_81" data-name="Rectangle 81" width="4.986" height="1.581" rx="0.723" transform="translate(60.788 1.566)" fill="#d8d8d8" opacity="0.136"/>
<rect id="Rectangle_82" data-name="Rectangle 82" width="4.986" height="1.581" rx="0.723" transform="translate(66.038 1.566)" fill="#d8d8d8" opacity="0.136"/>
<rect id="Rectangle_83" data-name="Rectangle 83" width="4.986" height="1.581" rx="0.723" transform="translate(72.599 1.566)" fill="#d8d8d8" opacity="0.136"/>
<rect id="Rectangle_84" data-name="Rectangle 84" width="4.986" height="1.581" rx="0.723" transform="translate(77.847 1.566)" fill="#d8d8d8" opacity="0.136"/>
<rect id="Rectangle_85" data-name="Rectangle 85" width="4.986" height="1.581" rx="0.723" transform="translate(83.097 1.566)" fill="#d8d8d8" opacity="0.136"/>
</g>
<path id="Path_59" data-name="Path 59" d="M146.71,159.855a5.439,5.439,0,0,0-.7.07c-.042-.164-.081-.329-.127-.493a5.457,5.457,0,1,0-5.4-9.372q-.181-.185-.366-.367a5.454,5.454,0,1,0-9.384-5.4c-.162-.046-.325-.084-.486-.126a5.467,5.467,0,1,0-10.788,0c-.162.042-.325.08-.486.126a5.457,5.457,0,1,0-9.384,5.4,21.843,21.843,0,1,0,36.421,21.02,5.452,5.452,0,1,0,.7-10.858" transform="translate(6.275 -6.025)" fill="#44d860" fill-rule="evenodd"/>
<path id="Path_60" data-name="Path 60" d="M83,124.855h43.71V103H83Z" transform="translate(4.42 -9.271)" fill="#3ecc5f" fill-rule="evenodd"/>
<path id="Path_61" data-name="Path 61" d="M134.855,116.765a2.732,2.732,0,1,0,0-5.464,2.811,2.811,0,0,0-.349.035c-.022-.082-.04-.164-.063-.246a2.733,2.733,0,0,0-1.052-5.253,2.7,2.7,0,0,0-1.648.566q-.09-.093-.184-.184a2.7,2.7,0,0,0,.553-1.633,2.732,2.732,0,0,0-5.245-1.07,10.928,10.928,0,1,0,0,21.031,2.732,2.732,0,0,0,5.245-1.07,2.7,2.7,0,0,0-.553-1.633q.093-.09.184-.184a2.7,2.7,0,0,0,1.648.566,2.732,2.732,0,0,0,1.052-5.253c.023-.081.042-.164.063-.246a2.811,2.811,0,0,0,.349.035" transform="translate(7.202 -9.377)" fill="#44d860" fill-rule="evenodd"/>
<path id="Path_62" data-name="Path 62" d="M143.232,42.33a2.967,2.967,0,0,1-.535-.055,2.754,2.754,0,0,1-.514-.153,2.838,2.838,0,0,1-.471-.251,4.139,4.139,0,0,1-.415-.339,3.2,3.2,0,0,1-.338-.415A2.7,2.7,0,0,1,140.5,39.6a2.968,2.968,0,0,1,.055-.535,3.152,3.152,0,0,1,.152-.514,2.874,2.874,0,0,1,.252-.47,2.633,2.633,0,0,1,.753-.754,2.837,2.837,0,0,1,.471-.251,2.753,2.753,0,0,1,.514-.153,2.527,2.527,0,0,1,1.071,0,2.654,2.654,0,0,1,.983.4,4.139,4.139,0,0,1,.415.339,4.019,4.019,0,0,1,.339.415,2.786,2.786,0,0,1,.251.47,2.864,2.864,0,0,1,.208,1.049,2.77,2.77,0,0,1-.8,1.934,4.139,4.139,0,0,1-.415.339,2.722,2.722,0,0,1-1.519.459m21.855-1.366a2.789,2.789,0,0,1-1.935-.8,4.162,4.162,0,0,1-.338-.415,2.7,2.7,0,0,1-.459-1.519,2.789,2.789,0,0,1,.8-1.934,4.139,4.139,0,0,1,.415-.339,2.838,2.838,0,0,1,.471-.251,2.752,2.752,0,0,1,.514-.153,2.527,2.527,0,0,1,1.071,0,2.654,2.654,0,0,1,.983.4,4.139,4.139,0,0,1,.415.339,2.79,2.79,0,0,1,.8,1.934,3.069,3.069,0,0,1-.055.535,2.779,2.779,0,0,1-.153.514,3.885,3.885,0,0,1-.251.47,4.02,4.02,0,0,1-.339.415,4.138,4.138,0,0,1-.415.339,2.722,2.722,0,0,1-1.519.459" transform="translate(9.753 -15.532)" fill-rule="evenodd"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 31 KiB

View File

@@ -0,0 +1,170 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1041.277" height="554.141" viewBox="0 0 1041.277 554.141">
<title>Powered by React</title>
<g id="Group_24" data-name="Group 24" transform="translate(-440 -263)">
<g id="Group_23" data-name="Group 23" transform="translate(439.989 262.965)">
<path id="Path_299" data-name="Path 299" d="M1040.82,611.12q-1.74,3.75-3.47,7.4-2.7,5.67-5.33,11.12c-.78,1.61-1.56,3.19-2.32,4.77-8.6,17.57-16.63,33.11-23.45,45.89A73.21,73.21,0,0,1,942.44,719l-151.65,1.65h-1.6l-13,.14-11.12.12-34.1.37h-1.38l-17.36.19h-.53l-107,1.16-95.51,1-11.11.12-69,.75H429l-44.75.48h-.48l-141.5,1.53-42.33.46a87.991,87.991,0,0,1-10.79-.54h0c-1.22-.14-2.44-.3-3.65-.49a87.38,87.38,0,0,1-51.29-27.54C116,678.37,102.75,655,93.85,629.64q-1.93-5.49-3.6-11.12C59.44,514.37,97,380,164.6,290.08q4.25-5.64,8.64-11l.07-.08c20.79-25.52,44.1-46.84,68.93-62,44-26.91,92.75-34.49,140.7-11.9,40.57,19.12,78.45,28.11,115.17,30.55,3.71.24,7.42.42,11.11.53,84.23,2.65,163.17-27.7,255.87-47.29,3.69-.78,7.39-1.55,11.12-2.28,66.13-13.16,139.49-20.1,226.73-5.51a189.089,189.089,0,0,1,26.76,6.4q5.77,1.86,11.12,4c41.64,16.94,64.35,48.24,74,87.46q1.37,5.46,2.37,11.11C1134.3,384.41,1084.19,518.23,1040.82,611.12Z" transform="translate(-79.34 -172.91)" fill="#f2f2f2"/>
<path id="Path_300" data-name="Path 300" d="M576.36,618.52a95.21,95.21,0,0,1-1.87,11.12h93.7V618.52Zm-78.25,62.81,11.11-.09V653.77c-3.81-.17-7.52-.34-11.11-.52ZM265.19,618.52v11.12h198.5V618.52ZM1114.87,279h-74V191.51q-5.35-2.17-11.12-4V279H776.21V186.58c-3.73.73-7.43,1.5-11.12,2.28V279H509.22V236.15c-3.69-.11-7.4-.29-11.11-.53V279H242.24V217c-24.83,15.16-48.14,36.48-68.93,62h-.07v.08q-4.4,5.4-8.64,11h8.64V618.52h-83q1.66,5.63,3.6,11.12h79.39v93.62a87,87,0,0,0,12.2,2.79c1.21.19,2.43.35,3.65.49h0a87.991,87.991,0,0,0,10.79.54l42.33-.46v-97H498.11v94.21l11.11-.12V629.64H765.09V721l11.12-.12V629.64H1029.7v4.77c.76-1.58,1.54-3.16,2.32-4.77q2.63-5.45,5.33-11.12,1.73-3.64,3.47-7.4v-321h76.42Q1116.23,284.43,1114.87,279ZM242.24,618.52V290.08H498.11V618.52Zm267,0V290.08H765.09V618.52Zm520.48,0H776.21V290.08H1029.7Z" transform="translate(-79.34 -172.91)" opacity="0.1"/>
<path id="Path_301" data-name="Path 301" d="M863.09,533.65v13l-151.92,1.4-1.62.03-57.74.53-1.38.02-17.55.15h-.52l-106.98.99L349.77,551.4h-.15l-44.65.42-.48.01-198.4,1.82v-15l46.65-28,93.6-.78,2-.01.66-.01,2-.03,44.94-.37,2.01-.01.64-.01,2-.01L315,509.3l.38-.01,35.55-.3h.29l277.4-2.34,6.79-.05h.68l5.18-.05,37.65-.31,2-.03,1.85-.02h.96l11.71-.09,2.32-.03,3.11-.02,9.75-.09,15.47-.13,2-.02,3.48-.02h.65l74.71-.64Z" fill="#65617d"/>
<path id="Path_302" data-name="Path 302" d="M863.09,533.65v13l-151.92,1.4-1.62.03-57.74.53-1.38.02-17.55.15h-.52l-106.98.99L349.77,551.4h-.15l-44.65.42-.48.01-198.4,1.82v-15l46.65-28,93.6-.78,2-.01.66-.01,2-.03,44.94-.37,2.01-.01.64-.01,2-.01L315,509.3l.38-.01,35.55-.3h.29l277.4-2.34,6.79-.05h.68l5.18-.05,37.65-.31,2-.03,1.85-.02h.96l11.71-.09,2.32-.03,3.11-.02,9.75-.09,15.47-.13,2-.02,3.48-.02h.65l74.71-.64Z" opacity="0.2"/>
<path id="Path_303" data-name="Path 303" d="M375.44,656.57v24.49a6.13,6.13,0,0,1-3.5,5.54,6,6,0,0,1-2.5.6l-34.9.74a6,6,0,0,1-2.7-.57,6.12,6.12,0,0,1-3.57-5.57V656.57Z" transform="translate(-79.34 -172.91)" fill="#3f3d56"/>
<path id="Path_304" data-name="Path 304" d="M375.44,656.57v24.49a6.13,6.13,0,0,1-3.5,5.54,6,6,0,0,1-2.5.6l-34.9.74a6,6,0,0,1-2.7-.57,6.12,6.12,0,0,1-3.57-5.57V656.57Z" transform="translate(-79.34 -172.91)" opacity="0.1"/>
<path id="Path_305" data-name="Path 305" d="M377.44,656.57v24.49a6.13,6.13,0,0,1-3.5,5.54,6,6,0,0,1-2.5.6l-34.9.74a6,6,0,0,1-2.7-.57,6.12,6.12,0,0,1-3.57-5.57V656.57Z" transform="translate(-79.34 -172.91)" fill="#3f3d56"/>
<rect id="Rectangle_137" data-name="Rectangle 137" width="47.17" height="31.5" transform="translate(680.92 483.65)" fill="#3f3d56"/>
<rect id="Rectangle_138" data-name="Rectangle 138" width="47.17" height="31.5" transform="translate(680.92 483.65)" opacity="0.1"/>
<rect id="Rectangle_139" data-name="Rectangle 139" width="47.17" height="31.5" transform="translate(678.92 483.65)" fill="#3f3d56"/>
<path id="Path_306" data-name="Path 306" d="M298.09,483.65v4.97l-47.17,1.26v-6.23Z" opacity="0.1"/>
<path id="Path_307" data-name="Path 307" d="M460.69,485.27v168.2a4,4,0,0,1-3.85,3.95l-191.65,5.1h-.05a4,4,0,0,1-3.95-3.95V485.27a4,4,0,0,1,3.95-3.95h191.6a4,4,0,0,1,3.95,3.95Z" transform="translate(-79.34 -172.91)" fill="#65617d"/>
<path id="Path_308" data-name="Path 308" d="M265.19,481.32v181.2h-.05a4,4,0,0,1-3.95-3.95V485.27a4,4,0,0,1,3.95-3.95Z" transform="translate(-79.34 -172.91)" opacity="0.1"/>
<path id="Path_309" data-name="Path 309" d="M194.59,319.15h177.5V467.4l-177.5,4Z" fill="#39374d"/>
<path id="Path_310" data-name="Path 310" d="M726.09,483.65v6.41l-47.17-1.26v-5.15Z" opacity="0.1"/>
<path id="Path_311" data-name="Path 311" d="M867.69,485.27v173.3a4,4,0,0,1-4,3.95h0L672,657.42a4,4,0,0,1-3.85-3.95V485.27a4,4,0,0,1,3.95-3.95H863.7a4,4,0,0,1,3.99,3.95Z" transform="translate(-79.34 -172.91)" fill="#65617d"/>
<path id="Path_312" data-name="Path 312" d="M867.69,485.27v173.3a4,4,0,0,1-4,3.95h0V481.32h0a4,4,0,0,1,4,3.95Z" transform="translate(-79.34 -172.91)" opacity="0.1"/>
<path id="Path_313" data-name="Path 313" d="M775.59,319.15H598.09V467.4l177.5,4Z" fill="#39374d"/>
<path id="Path_314" data-name="Path 314" d="M663.19,485.27v168.2a4,4,0,0,1-3.85,3.95l-191.65,5.1h0a4,4,0,0,1-4-3.95V485.27a4,4,0,0,1,3.95-3.95h191.6A4,4,0,0,1,663.19,485.27Z" transform="translate(-79.34 -172.91)" fill="#65617d"/>
<path id="Path_315" data-name="Path 315" d="M397.09,319.15h177.5V467.4l-177.5,4Z" fill="#4267b2"/>
<path id="Path_316" data-name="Path 316" d="M863.09,533.65v13l-151.92,1.4-1.62.03-57.74.53-1.38.02-17.55.15h-.52l-106.98.99L349.77,551.4h-.15l-44.65.42-.48.01-198.4,1.82v-15l202.51-1.33h.48l40.99-.28h.19l283.08-1.87h.29l.17-.01h.47l4.79-.03h1.46l74.49-.5,4.4-.02.98-.01Z" opacity="0.1"/>
<circle id="Ellipse_111" data-name="Ellipse 111" cx="51.33" cy="51.33" r="51.33" transform="translate(435.93 246.82)" fill="#fbbebe"/>
<path id="Path_317" data-name="Path 317" d="M617.94,550.07s-99.5,12-90,0c3.44-4.34,4.39-17.2,4.2-31.85-.06-4.45-.22-9.06-.45-13.65-1.1-22-3.75-43.5-3.75-43.5s87-41,77-8.5c-4,13.13-2.69,31.57.35,48.88.89,5.05,1.92,10,3,14.7a344.66,344.66,0,0,0,9.65,33.92Z" transform="translate(-79.34 -172.91)" fill="#fbbebe"/>
<path id="Path_318" data-name="Path 318" d="M585.47,546c11.51-2.13,23.7-6,34.53-1.54,2.85,1.17,5.47,2.88,8.39,3.86s6.12,1.22,9.16,1.91c10.68,2.42,19.34,10.55,24.9,20s8.44,20.14,11.26,30.72l6.9,25.83c6,22.45,12,45.09,13.39,68.3a2437.506,2437.506,0,0,1-250.84,1.43c5.44-10.34,11-21.31,10.54-33s-7.19-23.22-4.76-34.74c1.55-7.34,6.57-13.39,9.64-20.22,8.75-19.52,1.94-45.79,17.32-60.65,6.92-6.68,17-9.21,26.63-8.89,12.28.41,24.85,4.24,37,6.11C555.09,547.48,569.79,548.88,585.47,546Z" transform="translate(-79.34 -172.91)" fill="#ff6584"/>
<path id="Path_319" data-name="Path 319" d="M716.37,657.17l-.1,1.43v.1l-.17,2.3-1.33,18.51-1.61,22.3-.46,6.28-1,13.44v.17l-107,1-175.59,1.9v.84h-.14v-1.12l.45-14.36.86-28.06.74-23.79.07-2.37a10.53,10.53,0,0,1,11.42-10.17c4.72.4,10.85.89,18.18,1.41l3,.22c42.33,2.94,120.56,6.74,199.5,2,1.66-.09,3.33-.19,5-.31,12.24-.77,24.47-1.76,36.58-3a10.53,10.53,0,0,1,11.6,11.23Z" transform="translate(-79.34 -172.91)" opacity="0.1"/>
<path id="Path_320" data-name="Path 320" d="M429.08,725.44v-.84l175.62-1.91,107-1h.3v-.17l1-13.44.43-6,1.64-22.61,1.29-17.9v-.44a10.617,10.617,0,0,0-.11-2.47.3.3,0,0,0,0-.1,10.391,10.391,0,0,0-2-4.64,10.54,10.54,0,0,0-9.42-4c-12.11,1.24-24.34,2.23-36.58,3-1.67.12-3.34.22-5,.31-78.94,4.69-157.17.89-199.5-2l-3-.22c-7.33-.52-13.46-1-18.18-1.41a10.54,10.54,0,0,0-11.24,8.53,11,11,0,0,0-.18,1.64l-.68,22.16L429.54,710l-.44,14.36v1.12Z" transform="translate(-79.34 -172.91)" fill="#3f3d56"/>
<path id="Path_321" data-name="Path 321" d="M716.67,664.18l-1.23,15.33-1.83,22.85-.46,5.72-1,12.81-.06.64v.17h0l-.15,1.48.11-1.48h-.29l-107,1-175.65,1.9v-.28l.49-14.36,1-28.06.64-18.65A6.36,6.36,0,0,1,434.3,658a6.25,6.25,0,0,1,3.78-.9c2.1.17,4.68.37,7.69.59,4.89.36,10.92.78,17.94,1.22,13,.82,29.31,1.7,48,2.42,52,2,122.2,2.67,188.88-3.17,3-.26,6.1-.55,9.13-.84a6.26,6.26,0,0,1,3.48.66,5.159,5.159,0,0,1,.86.54,6.14,6.14,0,0,1,2,2.46,3.564,3.564,0,0,1,.25.61A6.279,6.279,0,0,1,716.67,664.18Z" transform="translate(-79.34 -172.91)" opacity="0.1"/>
<path id="Path_322" data-name="Path 322" d="M377.44,677.87v3.19a6.13,6.13,0,0,1-3.5,5.54l-40.1.77a6.12,6.12,0,0,1-3.57-5.57v-3Z" transform="translate(-79.34 -172.91)" opacity="0.1"/>
<path id="Path_323" data-name="Path 323" d="M298.59,515.57l-52.25,1V507.9l52.25-1Z" fill="#3f3d56"/>
<path id="Path_324" data-name="Path 324" d="M298.59,515.57l-52.25,1V507.9l52.25-1Z" opacity="0.1"/>
<path id="Path_325" data-name="Path 325" d="M300.59,515.57l-52.25,1V507.9l52.25-1Z" fill="#3f3d56"/>
<path id="Path_326" data-name="Path 326" d="M758.56,679.87v3.19a6.13,6.13,0,0,0,3.5,5.54l40.1.77a6.12,6.12,0,0,0,3.57-5.57v-3Z" transform="translate(-79.34 -172.91)" opacity="0.1"/>
<path id="Path_327" data-name="Path 327" d="M678.72,517.57l52.25,1V509.9l-52.25-1Z" opacity="0.1"/>
<path id="Path_328" data-name="Path 328" d="M676.72,517.57l52.25,1V509.9l-52.25-1Z" fill="#3f3d56"/>
<path id="Path_329" data-name="Path 329" d="M534.13,486.79c.08,7-3.16,13.6-5.91,20.07a163.491,163.491,0,0,0-12.66,74.71c.73,11,2.58,22,.73,32.9s-8.43,21.77-19,24.9c17.53,10.45,41.26,9.35,57.76-2.66,8.79-6.4,15.34-15.33,21.75-24.11a97.86,97.86,0,0,1-13.31,44.75A103.43,103.43,0,0,0,637,616.53c4.31-5.81,8.06-12.19,9.72-19.23,3.09-13-1.22-26.51-4.51-39.5a266.055,266.055,0,0,1-6.17-33c-.43-3.56-.78-7.22.1-10.7,1-4.07,3.67-7.51,5.64-11.22,5.6-10.54,5.73-23.3,2.86-34.88s-8.49-22.26-14.06-32.81c-4.46-8.46-9.3-17.31-17.46-22.28-5.1-3.1-11-4.39-16.88-5.64l-25.37-5.43c-5.55-1.19-11.26-2.38-16.87-1.51-9.47,1.48-16.14,8.32-22,15.34-4.59,5.46-15.81,15.71-16.6,22.86-.72,6.59,5.1,17.63,6.09,24.58,1.3,9,2.22,6,7.3,11.52C532,478.05,534.07,482,534.13,486.79Z" transform="translate(-79.34 -172.91)" fill="#3f3d56"/>
</g>
<g id="docusaurus_keytar" transform="translate(670.271 615.768)">
<path id="Path_40" data-name="Path 40" d="M99,52h43.635V69.662H99Z" transform="translate(-49.132 -33.936)" fill="#fff" fill-rule="evenodd"/>
<path id="Path_41" data-name="Path 41" d="M13.389,158.195A10.377,10.377,0,0,1,4.4,153a10.377,10.377,0,0,0,8.988,15.584H23.779V158.195Z" transform="translate(-3 -82.47)" fill="#3ecc5f" fill-rule="evenodd"/>
<path id="Path_42" data-name="Path 42" d="M66.967,38.083l36.373-2.273V30.615A10.389,10.389,0,0,0,92.95,20.226H46.2l-1.3-2.249a1.5,1.5,0,0,0-2.6,0L41,20.226l-1.3-2.249a1.5,1.5,0,0,0-2.6,0l-1.3,2.249-1.3-2.249a1.5,1.5,0,0,0-2.6,0l-1.3,2.249-.034,0-2.152-2.151a1.5,1.5,0,0,0-2.508.672L25.21,21.4l-2.7-.723a1.5,1.5,0,0,0-1.836,1.837l.722,2.7-2.65.71a1.5,1.5,0,0,0-.673,2.509l2.152,2.152c0,.011,0,.022,0,.033l-2.249,1.3a1.5,1.5,0,0,0,0,2.6l2.249,1.3-2.249,1.3a1.5,1.5,0,0,0,0,2.6L20.226,41l-2.249,1.3a1.5,1.5,0,0,0,0,2.6l2.249,1.3-2.249,1.3a1.5,1.5,0,0,0,0,2.6l2.249,1.3-2.249,1.3a1.5,1.5,0,0,0,0,2.6l2.249,1.3-2.249,1.3a1.5,1.5,0,0,0,0,2.6l2.249,1.3-2.249,1.3a1.5,1.5,0,0,0,0,2.6l2.249,1.3-2.249,1.3a1.5,1.5,0,0,0,0,2.6l2.249,1.3-2.249,1.3a1.5,1.5,0,0,0,0,2.6l2.249,1.3-2.249,1.3a1.5,1.5,0,0,0,0,2.6l2.249,1.3-2.249,1.3a1.5,1.5,0,0,0,0,2.6l2.249,1.3-2.249,1.3a1.5,1.5,0,0,0,0,2.6l2.249,1.3A10.389,10.389,0,0,0,30.615,103.34H92.95A10.389,10.389,0,0,0,103.34,92.95V51.393L66.967,49.12a5.53,5.53,0,0,1,0-11.038" transform="translate(-9.836 -17.226)" fill="#3ecc5f" fill-rule="evenodd"/>
<path id="Path_43" data-name="Path 43" d="M143,163.779h15.584V143H143Z" transform="translate(-70.275 -77.665)" fill="#3ecc5f" fill-rule="evenodd"/>
<path id="Path_44" data-name="Path 44" d="M173.779,148.389a2.582,2.582,0,0,0-.332.033c-.02-.078-.038-.156-.06-.234a2.594,2.594,0,1,0-2.567-4.455q-.086-.088-.174-.175a2.593,2.593,0,1,0-4.461-2.569c-.077-.022-.154-.04-.231-.06a2.6,2.6,0,1,0-5.128,0c-.077.02-.154.038-.231.06a2.594,2.594,0,1,0-4.461,2.569,10.384,10.384,0,1,0,17.314,9.992,2.592,2.592,0,1,0,.332-5.161" transform="translate(-75.08 -75.262)" fill="#44d860" fill-rule="evenodd"/>
<path id="Path_45" data-name="Path 45" d="M153,113.389h15.584V103H153Z" transform="translate(-75.08 -58.444)" fill="#3ecc5f" fill-rule="evenodd"/>
<path id="Path_46" data-name="Path 46" d="M183.389,108.944a1.3,1.3,0,1,0,0-2.6,1.336,1.336,0,0,0-.166.017c-.01-.039-.019-.078-.03-.117a1.3,1.3,0,0,0-.5-2.5,1.285,1.285,0,0,0-.783.269q-.043-.044-.087-.087a1.285,1.285,0,0,0,.263-.776,1.3,1.3,0,0,0-2.493-.509,5.195,5.195,0,1,0,0,10,1.3,1.3,0,0,0,2.493-.509,1.285,1.285,0,0,0-.263-.776q.044-.043.087-.087a1.285,1.285,0,0,0,.783.269,1.3,1.3,0,0,0,.5-2.5c.011-.038.02-.078.03-.117a1.337,1.337,0,0,0,.166.017" transform="translate(-84.691 -57.894)" fill="#44d860" fill-rule="evenodd"/>
<path id="Path_47" data-name="Path 47" d="M52.188,48.292a1.3,1.3,0,0,1-1.3-1.3,3.9,3.9,0,0,0-7.792,0,1.3,1.3,0,1,1-2.6,0,6.493,6.493,0,0,1,12.987,0,1.3,1.3,0,0,1-1.3,1.3" transform="translate(-21.02 -28.41)" fill-rule="evenodd"/>
<path id="Path_48" data-name="Path 48" d="M103,139.752h31.168a10.389,10.389,0,0,0,10.389-10.389V93H113.389A10.389,10.389,0,0,0,103,103.389Z" transform="translate(-51.054 -53.638)" fill="#ffff50" fill-rule="evenodd"/>
<path id="Path_49" data-name="Path 49" d="M141.1,94.017H115.106a.519.519,0,1,1,0-1.039H141.1a.519.519,0,0,1,0,1.039m0,10.389H115.106a.519.519,0,1,1,0-1.039H141.1a.519.519,0,0,1,0,1.039m0,10.389H115.106a.519.519,0,1,1,0-1.039H141.1a.519.519,0,0,1,0,1.039m0-25.877H115.106a.519.519,0,1,1,0-1.039H141.1a.519.519,0,0,1,0,1.039m0,10.293H115.106a.519.519,0,1,1,0-1.039H141.1a.519.519,0,0,1,0,1.039m0,10.389H115.106a.519.519,0,1,1,0-1.039H141.1a.519.519,0,0,1,0,1.039m7.782-47.993c-.006,0-.011,0-.018,0-1.605.055-2.365,1.66-3.035,3.077-.7,1.48-1.24,2.443-2.126,2.414-.981-.035-1.542-1.144-2.137-2.317-.683-1.347-1.462-2.876-3.1-2.819-1.582.054-2.344,1.451-3.017,2.684-.715,1.313-1.2,2.112-2.141,2.075-1-.036-1.533-.938-2.149-1.981-.686-1.162-1.479-2.467-3.084-2.423-1.555.053-2.319,1.239-2.994,2.286-.713,1.106-1.213,1.781-2.164,1.741-1.025-.036-1.554-.784-2.167-1.65-.688-.973-1.463-2.074-3.062-2.021a3.815,3.815,0,0,0-2.959,1.879c-.64.812-1.14,1.456-2.2,1.415a.52.52,0,0,0-.037,1.039,3.588,3.588,0,0,0,3.05-1.811c.611-.777,1.139-1.448,2.178-1.483,1-.043,1.47.579,2.179,1.582.674.953,1.438,2.033,2.977,2.089,1.612.054,2.387-1.151,3.074-2.217.614-.953,1.144-1.775,2.156-1.81.931-.035,1.438.7,2.153,1.912.674,1.141,1.437,2.434,3.006,2.491,1.623.056,2.407-1.361,3.09-2.616.592-1.085,1.15-2.109,2.14-2.143.931-.022,1.417.829,2.135,2.249.671,1.326,1.432,2.828,3.026,2.886l.088,0c1.592,0,2.347-1.6,3.015-3.01.592-1.252,1.152-2.431,2.113-2.479Z" transform="translate(-55.378 -38.552)" fill-rule="evenodd"/>
<path id="Path_50" data-name="Path 50" d="M83,163.779h20.779V143H83Z" transform="translate(-41.443 -77.665)" fill="#3ecc5f" fill-rule="evenodd"/>
<g id="Group_8" data-name="Group 8" transform="matrix(0.966, -0.259, 0.259, 0.966, 51.971, 43.3)">
<rect id="Rectangle_3" data-name="Rectangle 3" width="43.906" height="17.333" rx="2" transform="translate(0 0)" fill="#d8d8d8"/>
<g id="Group_2" data-name="Group 2" transform="translate(0.728 10.948)">
<rect id="Rectangle_4" data-name="Rectangle 4" width="2.537" height="2.537" rx="1" transform="translate(7.985 0)" fill="#4a4a4a"/>
<rect id="Rectangle_5" data-name="Rectangle 5" width="2.537" height="2.537" rx="1" transform="translate(10.991 0)" fill="#4a4a4a"/>
<rect id="Rectangle_6" data-name="Rectangle 6" width="2.537" height="2.537" rx="1" transform="translate(13.997 0)" fill="#4a4a4a"/>
<rect id="Rectangle_7" data-name="Rectangle 7" width="2.537" height="2.537" rx="1" transform="translate(17.003 0)" fill="#4a4a4a"/>
<rect id="Rectangle_8" data-name="Rectangle 8" width="2.537" height="2.537" rx="1" transform="translate(20.009 0)" fill="#4a4a4a"/>
<rect id="Rectangle_9" data-name="Rectangle 9" width="2.537" height="2.537" rx="1" transform="translate(23.015 0)" fill="#4a4a4a"/>
<rect id="Rectangle_10" data-name="Rectangle 10" width="2.537" height="2.537" rx="1" transform="translate(26.021 0)" fill="#4a4a4a"/>
<rect id="Rectangle_11" data-name="Rectangle 11" width="2.537" height="2.537" rx="1" transform="translate(29.028 0)" fill="#4a4a4a"/>
<rect id="Rectangle_12" data-name="Rectangle 12" width="2.537" height="2.537" rx="1" transform="translate(32.034 0)" fill="#4a4a4a"/>
<path id="Path_51" data-name="Path 51" d="M.519,0H6.9A.519.519,0,0,1,7.421.52v1.5a.519.519,0,0,1-.519.519H.519A.519.519,0,0,1,0,2.017V.519A.519.519,0,0,1,.519,0ZM35.653,0h6.383a.519.519,0,0,1,.519.519v1.5a.519.519,0,0,1-.519.519H35.652a.519.519,0,0,1-.519-.519V.519A.519.519,0,0,1,35.652,0Z" transform="translate(0 0)" fill="#4a4a4a" fill-rule="evenodd"/>
</g>
<g id="Group_3" data-name="Group 3" transform="translate(0.728 4.878)">
<path id="Path_52" data-name="Path 52" d="M.519,0H2.956a.519.519,0,0,1,.519.519v1.5a.519.519,0,0,1-.519.519H.519A.519.519,0,0,1,0,2.017V.519A.519.519,0,0,1,.519,0Z" transform="translate(0 0)" fill="#4a4a4a" fill-rule="evenodd"/>
<rect id="Rectangle_13" data-name="Rectangle 13" width="2.537" height="2.537" rx="1" transform="translate(3.945 0)" fill="#4a4a4a"/>
<rect id="Rectangle_14" data-name="Rectangle 14" width="2.537" height="2.537" rx="1" transform="translate(6.951 0)" fill="#4a4a4a"/>
<rect id="Rectangle_15" data-name="Rectangle 15" width="2.537" height="2.537" rx="1" transform="translate(9.958 0)" fill="#4a4a4a"/>
<rect id="Rectangle_16" data-name="Rectangle 16" width="2.537" height="2.537" rx="1" transform="translate(12.964 0)" fill="#4a4a4a"/>
<rect id="Rectangle_17" data-name="Rectangle 17" width="2.537" height="2.537" rx="1" transform="translate(15.97 0)" fill="#4a4a4a"/>
<rect id="Rectangle_18" data-name="Rectangle 18" width="2.537" height="2.537" rx="1" transform="translate(18.976 0)" fill="#4a4a4a"/>
<rect id="Rectangle_19" data-name="Rectangle 19" width="2.537" height="2.537" rx="1" transform="translate(21.982 0)" fill="#4a4a4a"/>
<rect id="Rectangle_20" data-name="Rectangle 20" width="2.537" height="2.537" rx="1" transform="translate(24.988 0)" fill="#4a4a4a"/>
<rect id="Rectangle_21" data-name="Rectangle 21" width="2.537" height="2.537" rx="1" transform="translate(27.994 0)" fill="#4a4a4a"/>
<rect id="Rectangle_22" data-name="Rectangle 22" width="2.537" height="2.537" rx="1" transform="translate(31 0)" fill="#4a4a4a"/>
<rect id="Rectangle_23" data-name="Rectangle 23" width="2.537" height="2.537" rx="1" transform="translate(34.006 0)" fill="#4a4a4a"/>
<rect id="Rectangle_24" data-name="Rectangle 24" width="2.537" height="2.537" rx="1" transform="translate(37.012 0)" fill="#4a4a4a"/>
<rect id="Rectangle_25" data-name="Rectangle 25" width="2.537" height="2.537" rx="1" transform="translate(40.018 0)" fill="#4a4a4a"/>
</g>
<g id="Group_4" data-name="Group 4" transform="translate(43.283 4.538) rotate(180)">
<path id="Path_53" data-name="Path 53" d="M.519,0H2.956a.519.519,0,0,1,.519.519v1.5a.519.519,0,0,1-.519.519H.519A.519.519,0,0,1,0,2.017V.519A.519.519,0,0,1,.519,0Z" transform="translate(0 0)" fill="#4a4a4a" fill-rule="evenodd"/>
<rect id="Rectangle_26" data-name="Rectangle 26" width="2.537" height="2.537" rx="1" transform="translate(3.945 0)" fill="#4a4a4a"/>
<rect id="Rectangle_27" data-name="Rectangle 27" width="2.537" height="2.537" rx="1" transform="translate(6.951 0)" fill="#4a4a4a"/>
<rect id="Rectangle_28" data-name="Rectangle 28" width="2.537" height="2.537" rx="1" transform="translate(9.958 0)" fill="#4a4a4a"/>
<rect id="Rectangle_29" data-name="Rectangle 29" width="2.537" height="2.537" rx="1" transform="translate(12.964 0)" fill="#4a4a4a"/>
<rect id="Rectangle_30" data-name="Rectangle 30" width="2.537" height="2.537" rx="1" transform="translate(15.97 0)" fill="#4a4a4a"/>
<rect id="Rectangle_31" data-name="Rectangle 31" width="2.537" height="2.537" rx="1" transform="translate(18.976 0)" fill="#4a4a4a"/>
<rect id="Rectangle_32" data-name="Rectangle 32" width="2.537" height="2.537" rx="1" transform="translate(21.982 0)" fill="#4a4a4a"/>
<rect id="Rectangle_33" data-name="Rectangle 33" width="2.537" height="2.537" rx="1" transform="translate(24.988 0)" fill="#4a4a4a"/>
<rect id="Rectangle_34" data-name="Rectangle 34" width="2.537" height="2.537" rx="1" transform="translate(27.994 0)" fill="#4a4a4a"/>
<rect id="Rectangle_35" data-name="Rectangle 35" width="2.537" height="2.537" rx="1" transform="translate(31.001 0)" fill="#4a4a4a"/>
<rect id="Rectangle_36" data-name="Rectangle 36" width="2.537" height="2.537" rx="1" transform="translate(34.007 0)" fill="#4a4a4a"/>
<rect id="Rectangle_37" data-name="Rectangle 37" width="2.537" height="2.537" rx="1" transform="translate(37.013 0)" fill="#4a4a4a"/>
<rect id="Rectangle_38" data-name="Rectangle 38" width="2.537" height="2.537" rx="1" transform="translate(40.018 0)" fill="#4a4a4a"/>
<rect id="Rectangle_39" data-name="Rectangle 39" width="2.537" height="2.537" rx="1" transform="translate(3.945 0)" fill="#4a4a4a"/>
<rect id="Rectangle_40" data-name="Rectangle 40" width="2.537" height="2.537" rx="1" transform="translate(6.951 0)" fill="#4a4a4a"/>
<rect id="Rectangle_41" data-name="Rectangle 41" width="2.537" height="2.537" rx="1" transform="translate(9.958 0)" fill="#4a4a4a"/>
<rect id="Rectangle_42" data-name="Rectangle 42" width="2.537" height="2.537" rx="1" transform="translate(12.964 0)" fill="#4a4a4a"/>
<rect id="Rectangle_43" data-name="Rectangle 43" width="2.537" height="2.537" rx="1" transform="translate(15.97 0)" fill="#4a4a4a"/>
<rect id="Rectangle_44" data-name="Rectangle 44" width="2.537" height="2.537" rx="1" transform="translate(18.976 0)" fill="#4a4a4a"/>
<rect id="Rectangle_45" data-name="Rectangle 45" width="2.537" height="2.537" rx="1" transform="translate(21.982 0)" fill="#4a4a4a"/>
<rect id="Rectangle_46" data-name="Rectangle 46" width="2.537" height="2.537" rx="1" transform="translate(24.988 0)" fill="#4a4a4a"/>
<rect id="Rectangle_47" data-name="Rectangle 47" width="2.537" height="2.537" rx="1" transform="translate(27.994 0)" fill="#4a4a4a"/>
<rect id="Rectangle_48" data-name="Rectangle 48" width="2.537" height="2.537" rx="1" transform="translate(31.001 0)" fill="#4a4a4a"/>
<rect id="Rectangle_49" data-name="Rectangle 49" width="2.537" height="2.537" rx="1" transform="translate(34.007 0)" fill="#4a4a4a"/>
<rect id="Rectangle_50" data-name="Rectangle 50" width="2.537" height="2.537" rx="1" transform="translate(37.013 0)" fill="#4a4a4a"/>
<rect id="Rectangle_51" data-name="Rectangle 51" width="2.537" height="2.537" rx="1" transform="translate(40.018 0)" fill="#4a4a4a"/>
</g>
<g id="Group_6" data-name="Group 6" transform="translate(0.728 7.883)">
<path id="Path_54" data-name="Path 54" d="M.519,0h3.47a.519.519,0,0,1,.519.519v1.5a.519.519,0,0,1-.519.519H.519A.519.519,0,0,1,0,2.017V.52A.519.519,0,0,1,.519,0Z" transform="translate(0 0)" fill="#4a4a4a" fill-rule="evenodd"/>
<g id="Group_5" data-name="Group 5" transform="translate(5.073 0)">
<rect id="Rectangle_52" data-name="Rectangle 52" width="2.537" height="2.537" rx="1" transform="translate(0 0)" fill="#4a4a4a"/>
<rect id="Rectangle_53" data-name="Rectangle 53" width="2.537" height="2.537" rx="1" transform="translate(3.006 0)" fill="#4a4a4a"/>
<rect id="Rectangle_54" data-name="Rectangle 54" width="2.537" height="2.537" rx="1" transform="translate(6.012 0)" fill="#4a4a4a"/>
<rect id="Rectangle_55" data-name="Rectangle 55" width="2.537" height="2.537" rx="1" transform="translate(9.018 0)" fill="#4a4a4a"/>
<rect id="Rectangle_56" data-name="Rectangle 56" width="2.537" height="2.537" rx="1" transform="translate(12.025 0)" fill="#4a4a4a"/>
<rect id="Rectangle_57" data-name="Rectangle 57" width="2.537" height="2.537" rx="1" transform="translate(15.031 0)" fill="#4a4a4a"/>
<rect id="Rectangle_58" data-name="Rectangle 58" width="2.537" height="2.537" rx="1" transform="translate(18.037 0)" fill="#4a4a4a"/>
<rect id="Rectangle_59" data-name="Rectangle 59" width="2.537" height="2.537" rx="1" transform="translate(21.042 0)" fill="#4a4a4a"/>
<rect id="Rectangle_60" data-name="Rectangle 60" width="2.537" height="2.537" rx="1" transform="translate(24.049 0)" fill="#4a4a4a"/>
<rect id="Rectangle_61" data-name="Rectangle 61" width="2.537" height="2.537" rx="1" transform="translate(27.055 0)" fill="#4a4a4a"/>
<rect id="Rectangle_62" data-name="Rectangle 62" width="2.537" height="2.537" rx="1" transform="translate(30.061 0)" fill="#4a4a4a"/>
</g>
<path id="Path_55" data-name="Path 55" d="M.52,0H3.8a.519.519,0,0,1,.519.519v1.5a.519.519,0,0,1-.519.519H.519A.519.519,0,0,1,0,2.017V.52A.519.519,0,0,1,.519,0Z" transform="translate(38.234 0)" fill="#4a4a4a" fill-rule="evenodd"/>
</g>
<g id="Group_7" data-name="Group 7" transform="translate(0.728 14.084)">
<rect id="Rectangle_63" data-name="Rectangle 63" width="2.537" height="2.537" rx="1" transform="translate(0 0)" fill="#4a4a4a"/>
<rect id="Rectangle_64" data-name="Rectangle 64" width="2.537" height="2.537" rx="1" transform="translate(3.006 0)" fill="#4a4a4a"/>
<rect id="Rectangle_65" data-name="Rectangle 65" width="2.537" height="2.537" rx="1" transform="translate(6.012 0)" fill="#4a4a4a"/>
<rect id="Rectangle_66" data-name="Rectangle 66" width="2.537" height="2.537" rx="1" transform="translate(9.018 0)" fill="#4a4a4a"/>
<path id="Path_56" data-name="Path 56" d="M.519,0H14.981A.519.519,0,0,1,15.5.519v1.5a.519.519,0,0,1-.519.519H.519A.519.519,0,0,1,0,2.018V.519A.519.519,0,0,1,.519,0Zm15.97,0h1.874a.519.519,0,0,1,.519.519v1.5a.519.519,0,0,1-.519.519H16.489a.519.519,0,0,1-.519-.519V.519A.519.519,0,0,1,16.489,0Z" transform="translate(12.024 0)" fill="#4a4a4a" fill-rule="evenodd"/>
<rect id="Rectangle_67" data-name="Rectangle 67" width="2.537" height="2.537" rx="1" transform="translate(31.376 0)" fill="#4a4a4a"/>
<rect id="Rectangle_68" data-name="Rectangle 68" width="2.537" height="2.537" rx="1" transform="translate(34.382 0)" fill="#4a4a4a"/>
<rect id="Rectangle_69" data-name="Rectangle 69" width="2.537" height="2.537" rx="1" transform="translate(40.018 0)" fill="#4a4a4a"/>
<path id="Path_57" data-name="Path 57" d="M2.537,0V.561a.519.519,0,0,1-.519.519H.519A.519.519,0,0,1,0,.561V0Z" transform="translate(39.736 1.08) rotate(180)" fill="#4a4a4a"/>
<path id="Path_58" data-name="Path 58" d="M2.537,0V.561a.519.519,0,0,1-.519.519H.519A.519.519,0,0,1,0,.561V0Z" transform="translate(37.2 1.456)" fill="#4a4a4a"/>
</g>
<rect id="Rectangle_70" data-name="Rectangle 70" width="42.273" height="1.127" rx="0.564" transform="translate(0.915 0.556)" fill="#4a4a4a"/>
<rect id="Rectangle_71" data-name="Rectangle 71" width="2.37" height="0.752" rx="0.376" transform="translate(1.949 0.744)" fill="#d8d8d8" opacity="0.136"/>
<rect id="Rectangle_72" data-name="Rectangle 72" width="2.37" height="0.752" rx="0.376" transform="translate(5.193 0.744)" fill="#d8d8d8" opacity="0.136"/>
<rect id="Rectangle_73" data-name="Rectangle 73" width="2.37" height="0.752" rx="0.376" transform="translate(7.688 0.744)" fill="#d8d8d8" opacity="0.136"/>
<rect id="Rectangle_74" data-name="Rectangle 74" width="2.37" height="0.752" rx="0.376" transform="translate(10.183 0.744)" fill="#d8d8d8" opacity="0.136"/>
<rect id="Rectangle_75" data-name="Rectangle 75" width="2.37" height="0.752" rx="0.376" transform="translate(12.679 0.744)" fill="#d8d8d8" opacity="0.136"/>
<rect id="Rectangle_76" data-name="Rectangle 76" width="2.37" height="0.752" rx="0.376" transform="translate(15.797 0.744)" fill="#d8d8d8" opacity="0.136"/>
<rect id="Rectangle_77" data-name="Rectangle 77" width="2.37" height="0.752" rx="0.376" transform="translate(18.292 0.744)" fill="#d8d8d8" opacity="0.136"/>
<rect id="Rectangle_78" data-name="Rectangle 78" width="2.37" height="0.752" rx="0.376" transform="translate(20.788 0.744)" fill="#d8d8d8" opacity="0.136"/>
<rect id="Rectangle_79" data-name="Rectangle 79" width="2.37" height="0.752" rx="0.376" transform="translate(23.283 0.744)" fill="#d8d8d8" opacity="0.136"/>
<rect id="Rectangle_80" data-name="Rectangle 80" width="2.37" height="0.752" rx="0.376" transform="translate(26.402 0.744)" fill="#d8d8d8" opacity="0.136"/>
<rect id="Rectangle_81" data-name="Rectangle 81" width="2.37" height="0.752" rx="0.376" transform="translate(28.897 0.744)" fill="#d8d8d8" opacity="0.136"/>
<rect id="Rectangle_82" data-name="Rectangle 82" width="2.37" height="0.752" rx="0.376" transform="translate(31.393 0.744)" fill="#d8d8d8" opacity="0.136"/>
<rect id="Rectangle_83" data-name="Rectangle 83" width="2.37" height="0.752" rx="0.376" transform="translate(34.512 0.744)" fill="#d8d8d8" opacity="0.136"/>
<rect id="Rectangle_84" data-name="Rectangle 84" width="2.37" height="0.752" rx="0.376" transform="translate(37.007 0.744)" fill="#d8d8d8" opacity="0.136"/>
<rect id="Rectangle_85" data-name="Rectangle 85" width="2.37" height="0.752" rx="0.376" transform="translate(39.502 0.744)" fill="#d8d8d8" opacity="0.136"/>
</g>
<path id="Path_59" data-name="Path 59" d="M123.779,148.389a2.583,2.583,0,0,0-.332.033c-.02-.078-.038-.156-.06-.234a2.594,2.594,0,1,0-2.567-4.455q-.086-.088-.174-.175a2.593,2.593,0,1,0-4.461-2.569c-.077-.022-.154-.04-.231-.06a2.6,2.6,0,1,0-5.128,0c-.077.02-.154.038-.231.06a2.594,2.594,0,1,0-4.461,2.569,10.384,10.384,0,1,0,17.314,9.992,2.592,2.592,0,1,0,.332-5.161" transform="translate(-51.054 -75.262)" fill="#44d860" fill-rule="evenodd"/>
<path id="Path_60" data-name="Path 60" d="M83,113.389h20.779V103H83Z" transform="translate(-41.443 -58.444)" fill="#3ecc5f" fill-rule="evenodd"/>
<path id="Path_61" data-name="Path 61" d="M123.389,108.944a1.3,1.3,0,1,0,0-2.6,1.338,1.338,0,0,0-.166.017c-.01-.039-.019-.078-.03-.117a1.3,1.3,0,0,0-.5-2.5,1.285,1.285,0,0,0-.783.269q-.043-.044-.087-.087a1.285,1.285,0,0,0,.263-.776,1.3,1.3,0,0,0-2.493-.509,5.195,5.195,0,1,0,0,10,1.3,1.3,0,0,0,2.493-.509,1.285,1.285,0,0,0-.263-.776q.044-.043.087-.087a1.285,1.285,0,0,0,.783.269,1.3,1.3,0,0,0,.5-2.5c.011-.038.02-.078.03-.117a1.335,1.335,0,0,0,.166.017" transform="translate(-55.859 -57.894)" fill="#44d860" fill-rule="evenodd"/>
<path id="Path_62" data-name="Path 62" d="M141.8,38.745a1.41,1.41,0,0,1-.255-.026,1.309,1.309,0,0,1-.244-.073,1.349,1.349,0,0,1-.224-.119,1.967,1.967,0,0,1-.2-.161,1.52,1.52,0,0,1-.161-.2,1.282,1.282,0,0,1-.218-.722,1.41,1.41,0,0,1,.026-.255,1.5,1.5,0,0,1,.072-.244,1.364,1.364,0,0,1,.12-.223,1.252,1.252,0,0,1,.358-.358,1.349,1.349,0,0,1,.224-.119,1.309,1.309,0,0,1,.244-.073,1.2,1.2,0,0,1,.509,0,1.262,1.262,0,0,1,.468.192,1.968,1.968,0,0,1,.2.161,1.908,1.908,0,0,1,.161.2,1.322,1.322,0,0,1,.12.223,1.361,1.361,0,0,1,.1.5,1.317,1.317,0,0,1-.379.919,1.968,1.968,0,0,1-.2.161,1.346,1.346,0,0,1-.223.119,1.332,1.332,0,0,1-.5.1m10.389-.649a1.326,1.326,0,0,1-.92-.379,1.979,1.979,0,0,1-.161-.2,1.282,1.282,0,0,1-.218-.722,1.326,1.326,0,0,1,.379-.919,1.967,1.967,0,0,1,.2-.161,1.351,1.351,0,0,1,.224-.119,1.308,1.308,0,0,1,.244-.073,1.2,1.2,0,0,1,.509,0,1.262,1.262,0,0,1,.468.192,1.967,1.967,0,0,1,.2.161,1.326,1.326,0,0,1,.379.919,1.461,1.461,0,0,1-.026.255,1.323,1.323,0,0,1-.073.244,1.847,1.847,0,0,1-.119.223,1.911,1.911,0,0,1-.161.2,1.967,1.967,0,0,1-.2.161,1.294,1.294,0,0,1-.722.218" transform="translate(-69.074 -26.006)" fill-rule="evenodd"/>
</g>
<g id="React-icon" transform="translate(906.3 541.56)">
<path id="Path_330" data-name="Path 330" d="M263.668,117.179c0-5.827-7.3-11.35-18.487-14.775,2.582-11.4,1.434-20.477-3.622-23.382a7.861,7.861,0,0,0-4.016-1v4a4.152,4.152,0,0,1,2.044.466c2.439,1.4,3.5,6.724,2.672,13.574-.2,1.685-.52,3.461-.914,5.272a86.9,86.9,0,0,0-11.386-1.954,87.469,87.469,0,0,0-7.459-8.965c5.845-5.433,11.332-8.41,15.062-8.41V78h0c-4.931,0-11.386,3.514-17.913,9.611-6.527-6.061-12.982-9.539-17.913-9.539v4c3.712,0,9.216,2.959,15.062,8.356a84.687,84.687,0,0,0-7.405,8.947,83.732,83.732,0,0,0-11.4,1.972c-.412-1.793-.717-3.532-.932-5.2-.843-6.85.2-12.175,2.618-13.592a3.991,3.991,0,0,1,2.062-.466v-4h0a8,8,0,0,0-4.052,1c-5.039,2.9-6.168,11.96-3.568,23.328-11.153,3.443-18.415,8.947-18.415,14.757,0,5.828,7.3,11.35,18.487,14.775-2.582,11.4-1.434,20.477,3.622,23.382a7.882,7.882,0,0,0,4.034,1c4.931,0,11.386-3.514,17.913-9.611,6.527,6.061,12.982,9.539,17.913,9.539a8,8,0,0,0,4.052-1c5.039-2.9,6.168-11.96,3.568-23.328C256.406,128.511,263.668,122.988,263.668,117.179Zm-23.346-11.96c-.663,2.313-1.488,4.7-2.421,7.083-.735-1.434-1.506-2.869-2.349-4.3-.825-1.434-1.7-2.833-2.582-4.2C235.517,104.179,237.974,104.645,240.323,105.219Zm-8.212,19.1c-1.4,2.421-2.833,4.716-4.321,6.85-2.672.233-5.379.359-8.1.359-2.708,0-5.415-.126-8.069-.341q-2.232-3.2-4.339-6.814-2.044-3.523-3.73-7.136c1.112-2.4,2.367-4.805,3.712-7.154,1.4-2.421,2.833-4.716,4.321-6.85,2.672-.233,5.379-.359,8.1-.359,2.708,0,5.415.126,8.069.341q2.232,3.2,4.339,6.814,2.044,3.523,3.73,7.136C234.692,119.564,233.455,121.966,232.11,124.315Zm5.792-2.331c.968,2.4,1.793,4.805,2.474,7.136-2.349.574-4.823,1.058-7.387,1.434.879-1.381,1.757-2.8,2.582-4.25C236.4,124.871,237.167,123.419,237.9,121.984ZM219.72,141.116a73.921,73.921,0,0,1-4.985-5.738c1.614.072,3.263.126,4.931.126,1.685,0,3.353-.036,4.985-.126A69.993,69.993,0,0,1,219.72,141.116ZM206.38,130.555c-2.546-.377-5-.843-7.352-1.417.663-2.313,1.488-4.7,2.421-7.083.735,1.434,1.506,2.869,2.349,4.3S205.5,129.192,206.38,130.555ZM219.63,93.241a73.924,73.924,0,0,1,4.985,5.738c-1.614-.072-3.263-.126-4.931-.126-1.686,0-3.353.036-4.985.126A69.993,69.993,0,0,1,219.63,93.241ZM206.362,103.8c-.879,1.381-1.757,2.8-2.582,4.25-.825,1.434-1.6,2.869-2.331,4.3-.968-2.4-1.793-4.805-2.474-7.136C201.323,104.663,203.8,104.179,206.362,103.8Zm-16.227,22.449c-6.348-2.708-10.454-6.258-10.454-9.073s4.106-6.383,10.454-9.073c1.542-.663,3.228-1.255,4.967-1.811a86.122,86.122,0,0,0,4.034,10.92,84.9,84.9,0,0,0-3.981,10.866C193.38,127.525,191.694,126.915,190.134,126.252Zm9.647,25.623c-2.439-1.4-3.5-6.724-2.672-13.574.2-1.686.52-3.461.914-5.272a86.9,86.9,0,0,0,11.386,1.954,87.465,87.465,0,0,0,7.459,8.965c-5.845,5.433-11.332,8.41-15.062,8.41A4.279,4.279,0,0,1,199.781,151.875Zm42.532-13.663c.843,6.85-.2,12.175-2.618,13.592a3.99,3.99,0,0,1-2.062.466c-3.712,0-9.216-2.959-15.062-8.356a84.689,84.689,0,0,0,7.405-8.947,83.731,83.731,0,0,0,11.4-1.972A50.194,50.194,0,0,1,242.313,138.212Zm6.9-11.96c-1.542.663-3.228,1.255-4.967,1.811a86.12,86.12,0,0,0-4.034-10.92,84.9,84.9,0,0,0,3.981-10.866c1.775.556,3.461,1.165,5.039,1.829,6.348,2.708,10.454,6.258,10.454,9.073C259.67,119.994,255.564,123.562,249.216,126.252Z" fill="#61dafb"/>
<path id="Path_331" data-name="Path 331" d="M320.8,78.4Z" transform="translate(-119.082 -0.328)" fill="#61dafb"/>
<circle id="Ellipse_112" data-name="Ellipse 112" cx="8.194" cy="8.194" r="8.194" transform="translate(211.472 108.984)" fill="#61dafb"/>
<path id="Path_332" data-name="Path 332" d="M520.5,78.1Z" transform="translate(-282.975 -0.082)" fill="#61dafb"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 35 KiB

View File

@@ -0,0 +1,40 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1129" height="663" viewBox="0 0 1129 663">
<title>Focus on What Matters</title>
<circle cx="321" cy="321" r="321" fill="#f2f2f2" />
<ellipse cx="559" cy="635.49998" rx="514" ry="27.50002" fill="#3f3d56" />
<ellipse cx="558" cy="627" rx="460" ry="22" opacity="0.2" />
<rect x="131" y="152.5" width="840" height="50" fill="#3f3d56" />
<path d="M166.5,727.3299A21.67009,21.67009,0,0,0,188.1701,749H984.8299A21.67009,21.67009,0,0,0,1006.5,727.3299V296h-840Z" transform="translate(-35.5 -118.5)" fill="#3f3d56" />
<path d="M984.8299,236H188.1701A21.67009,21.67009,0,0,0,166.5,257.6701V296h840V257.6701A21.67009,21.67009,0,0,0,984.8299,236Z" transform="translate(-35.5 -118.5)" fill="#3f3d56" />
<path d="M984.8299,236H188.1701A21.67009,21.67009,0,0,0,166.5,257.6701V296h840V257.6701A21.67009,21.67009,0,0,0,984.8299,236Z" transform="translate(-35.5 -118.5)" opacity="0.2" />
<circle cx="181" cy="147.5" r="13" fill="#3f3d56" />
<circle cx="217" cy="147.5" r="13" fill="#3f3d56" />
<circle cx="253" cy="147.5" r="13" fill="#3f3d56" />
<rect x="168" y="213.5" width="337" height="386" rx="5.33505" fill="#606060" />
<rect x="603" y="272.5" width="284" height="22" rx="5.47638" fill="#2e8555" />
<rect x="537" y="352.5" width="416" height="15" rx="5.47638" fill="#2e8555" />
<rect x="537" y="396.5" width="416" height="15" rx="5.47638" fill="#2e8555" />
<rect x="537" y="440.5" width="416" height="15" rx="5.47638" fill="#2e8555" />
<rect x="537" y="484.5" width="416" height="15" rx="5.47638" fill="#2e8555" />
<rect x="865" y="552.5" width="88" height="26" rx="7.02756" fill="#3ecc5f" />
<path d="M1088.60287,624.61594a30.11371,30.11371,0,0,0,3.98291-15.266c0-13.79652-8.54358-24.98081-19.08256-24.98081s-19.08256,11.18429-19.08256,24.98081a30.11411,30.11411,0,0,0,3.98291,15.266,31.248,31.248,0,0,0,0,30.53213,31.248,31.248,0,0,0,0,30.53208,31.248,31.248,0,0,0,0,30.53208,30.11408,30.11408,0,0,0-3.98291,15.266c0,13.79652,8.54353,24.98081,19.08256,24.98081s19.08256-11.18429,19.08256-24.98081a30.11368,30.11368,0,0,0-3.98291-15.266,31.248,31.248,0,0,0,0-30.53208,31.248,31.248,0,0,0,0-30.53208,31.248,31.248,0,0,0,0-30.53213Z" transform="translate(-35.5 -118.5)" fill="#3f3d56" />
<ellipse cx="1038.00321" cy="460.31783" rx="19.08256" ry="24.9808" fill="#3f3d56" />
<ellipse cx="1038.00321" cy="429.78574" rx="19.08256" ry="24.9808" fill="#3f3d56" />
<path d="M1144.93871,339.34489a91.61081,91.61081,0,0,0,7.10658-10.46092l-50.141-8.23491,54.22885.4033a91.566,91.566,0,0,0,1.74556-72.42605l-72.75449,37.74139,67.09658-49.32086a91.41255,91.41255,0,1,0-150.971,102.29805,91.45842,91.45842,0,0,0-10.42451,16.66946l65.0866,33.81447-69.40046-23.292a91.46011,91.46011,0,0,0,14.73837,85.83669,91.40575,91.40575,0,1,0,143.68892,0,91.41808,91.41808,0,0,0,0-113.02862Z" transform="translate(-35.5 -118.5)" fill="#3ecc5f" fill-rule="evenodd" />
<path d="M981.6885,395.8592a91.01343,91.01343,0,0,0,19.56129,56.51431,91.40575,91.40575,0,1,0,143.68892,0C1157.18982,436.82067,981.6885,385.60008,981.6885,395.8592Z" transform="translate(-35.5 -118.5)" opacity="0.1" />
<path d="M365.62,461.43628H477.094v45.12043H365.62Z" transform="translate(-35.5 -118.5)" fill="#fff" fill-rule="evenodd" />
<path d="M264.76252,608.74122a26.50931,26.50931,0,0,1-22.96231-13.27072,26.50976,26.50976,0,0,0,22.96231,39.81215H291.304V608.74122Z" transform="translate(-35.5 -118.5)" fill="#3ecc5f" fill-rule="evenodd" />
<path d="M384.17242,468.57061l92.92155-5.80726V449.49263a26.54091,26.54091,0,0,0-26.54143-26.54143H331.1161l-3.31768-5.74622a3.83043,3.83043,0,0,0-6.63536,0l-3.31768,5.74622-3.31767-5.74622a3.83043,3.83043,0,0,0-6.63536,0l-3.31768,5.74622L301.257,417.205a3.83043,3.83043,0,0,0-6.63536,0L291.304,422.9512c-.02919,0-.05573.004-.08625.004l-5.49674-5.49541a3.8293,3.8293,0,0,0-6.4071,1.71723l-1.81676,6.77338L270.607,424.1031a3.82993,3.82993,0,0,0-4.6912,4.69253l1.84463,6.89148-6.77072,1.81411a3.8315,3.8315,0,0,0-1.71988,6.40975l5.49673,5.49673c0,.02787-.004.05574-.004.08493l-5.74622,3.31768a3.83043,3.83043,0,0,0,0,6.63536l5.74621,3.31768L259.0163,466.081a3.83043,3.83043,0,0,0,0,6.63536l5.74622,3.31768-5.74622,3.31767a3.83043,3.83043,0,0,0,0,6.63536l5.74622,3.31768-5.74622,3.31768a3.83043,3.83043,0,0,0,0,6.63536l5.74622,3.31768-5.74622,3.31767a3.83043,3.83043,0,0,0,0,6.63536l5.74622,3.31768-5.74622,3.31768a3.83043,3.83043,0,0,0,0,6.63536l5.74622,3.31768-5.74622,3.31768a3.83042,3.83042,0,0,0,0,6.63535l5.74622,3.31768-5.74622,3.31768a3.83043,3.83043,0,0,0,0,6.63536l5.74622,3.31768L259.0163,558.976a3.83042,3.83042,0,0,0,0,6.63535l5.74622,3.31768-5.74622,3.31768a3.83043,3.83043,0,0,0,0,6.63536l5.74622,3.31768-5.74622,3.31768a3.83042,3.83042,0,0,0,0,6.63535l5.74622,3.31768-5.74622,3.31768a3.83043,3.83043,0,0,0,0,6.63536l5.74622,3.31768A26.54091,26.54091,0,0,0,291.304,635.28265H450.55254A26.5409,26.5409,0,0,0,477.094,608.74122V502.5755l-92.92155-5.80727a14.12639,14.12639,0,0,1,0-28.19762" transform="translate(-35.5 -118.5)" fill="#3ecc5f" fill-rule="evenodd" />
<path d="M424.01111,635.28265h39.81214V582.19979H424.01111Z" transform="translate(-35.5 -118.5)" fill="#3ecc5f" fill-rule="evenodd" />
<path d="M490.36468,602.10586a6.60242,6.60242,0,0,0-.848.08493c-.05042-.19906-.09821-.39945-.15393-.59852A6.62668,6.62668,0,1,0,482.80568,590.21q-.2203-.22491-.44457-.44589a6.62391,6.62391,0,1,0-11.39689-6.56369c-.1964-.05575-.39414-.10218-.59056-.15262a6.63957,6.63957,0,1,0-13.10086,0c-.1964.05042-.39414.09687-.59056.15262a6.62767,6.62767,0,1,0-11.39688,6.56369,26.52754,26.52754,0,1,0,44.23127,25.52756,6.6211,6.6211,0,1,0,.848-13.18579" transform="translate(-35.5 -118.5)" fill="#44d860" fill-rule="evenodd" />
<path d="M437.28182,555.65836H477.094V529.11693H437.28182Z" transform="translate(-35.5 -118.5)" fill="#3ecc5f" fill-rule="evenodd" />
<path d="M490.36468,545.70532a3.31768,3.31768,0,0,0,0-6.63536,3.41133,3.41133,0,0,0-.42333.04247c-.02655-.09953-.04911-.19907-.077-.29859a3.319,3.319,0,0,0-1.278-6.37923,3.28174,3.28174,0,0,0-2.00122.68742q-.10947-.11346-.22294-.22295a3.282,3.282,0,0,0,.67149-1.98265,3.31768,3.31768,0,0,0-6.37-1.2992,13.27078,13.27078,0,1,0,0,25.54082,3.31768,3.31768,0,0,0,6.37-1.2992,3.282,3.282,0,0,0-.67149-1.98265q.11347-.10947.22294-.22294a3.28174,3.28174,0,0,0,2.00122.68742,3.31768,3.31768,0,0,0,1.278-6.37923c.02786-.0982.05042-.19907.077-.29859a3.41325,3.41325,0,0,0,.42333.04246" transform="translate(-35.5 -118.5)" fill="#44d860" fill-rule="evenodd" />
<path d="M317.84538,466.081a3.31768,3.31768,0,0,1-3.31767-3.31768,9.953,9.953,0,1,0-19.90608,0,3.31768,3.31768,0,1,1-6.63535,0,16.58839,16.58839,0,1,1,33.17678,0,3.31768,3.31768,0,0,1-3.31768,3.31768" transform="translate(-35.5 -118.5)" fill-rule="evenodd" />
<path d="M370.92825,635.28265h79.62429A26.5409,26.5409,0,0,0,477.094,608.74122v-92.895H397.46968a26.54091,26.54091,0,0,0-26.54143,26.54143Z" transform="translate(-35.5 -118.5)" fill="#ffff50" fill-rule="evenodd" />
<path d="M457.21444,556.98543H390.80778a1.32707,1.32707,0,0,1,0-2.65414h66.40666a1.32707,1.32707,0,0,1,0,2.65414m0,26.54143H390.80778a1.32707,1.32707,0,1,1,0-2.65414h66.40666a1.32707,1.32707,0,0,1,0,2.65414m0,26.54143H390.80778a1.32707,1.32707,0,1,1,0-2.65414h66.40666a1.32707,1.32707,0,0,1,0,2.65414m0-66.10674H390.80778a1.32707,1.32707,0,0,1,0-2.65414h66.40666a1.32707,1.32707,0,0,1,0,2.65414m0,26.29459H390.80778a1.32707,1.32707,0,0,1,0-2.65414h66.40666a1.32707,1.32707,0,0,1,0,2.65414m0,26.54143H390.80778a1.32707,1.32707,0,0,1,0-2.65414h66.40666a1.32707,1.32707,0,0,1,0,2.65414M477.094,474.19076c-.01592,0-.0292-.008-.04512-.00663-4.10064.13934-6.04083,4.24132-7.75274,7.86024-1.78623,3.78215-3.16771,6.24122-5.43171,6.16691-2.50685-.09024-3.94007-2.92222-5.45825-5.91874-1.74377-3.44243-3.73438-7.34667-7.91333-7.20069-4.04227.138-5.98907,3.70784-7.70631,6.857-1.82738,3.35484-3.07084,5.39455-5.46887,5.30033-2.55727-.09289-3.91619-2.39536-5.48877-5.06013-1.75306-2.96733-3.77951-6.30359-7.8775-6.18946-3.97326.13669-5.92537,3.16507-7.64791,5.83912-1.82207,2.82666-3.09872,4.5492-5.52725,4.447-2.61832-.09289-3.9706-2.00388-5.53522-4.21611-1.757-2.4856-3.737-5.299-7.82308-5.16231-3.88567.13271-5.83779,2.61434-7.559,4.80135-1.635,2.07555-2.9116,3.71846-5.61218,3.615a1.32793,1.32793,0,1,0-.09555,2.65414c4.00377.134,6.03154-2.38873,7.79257-4.6275,1.562-1.9853,2.91027-3.69855,5.56441-3.78879,2.55594-.10882,3.75429,1.47968,5.56707,4.04093,1.7212,2.43385,3.67465,5.19416,7.60545,5.33616,4.11789.138,6.09921-2.93946,7.8536-5.66261,1.56861-2.43385,2.92221-4.53461,5.50734-4.62352,2.37944-.08892,3.67466,1.79154,5.50072,4.885,1.72121,2.91557,3.67069,6.21865,7.67977,6.36463,4.14709.14332,6.14965-3.47693,7.89475-6.68181,1.51155-2.77092,2.93814-5.38791,5.46621-5.4755,2.37944-.05573,3.62025,2.11668,5.45558,5.74622,1.71459,3.388,3.65875,7.22591,7.73019,7.37321l.22429.004c4.06614,0,5.99571-4.08074,7.70364-7.68905,1.51154-3.19825,2.94211-6.21069,5.3972-6.33411Z" transform="translate(-35.5 -118.5)" fill-rule="evenodd" />
<path d="M344.38682,635.28265h53.08286V582.19979H344.38682Z" transform="translate(-35.5 -118.5)" fill="#3ecc5f" fill-rule="evenodd" />
<path d="M424.01111,602.10586a6.60242,6.60242,0,0,0-.848.08493c-.05042-.19906-.09821-.39945-.15394-.59852A6.62667,6.62667,0,1,0,416.45211,590.21q-.2203-.22491-.44458-.44589a6.62391,6.62391,0,1,0-11.39689-6.56369c-.1964-.05575-.39413-.10218-.59054-.15262a6.63957,6.63957,0,1,0-13.10084,0c-.19641.05042-.39414.09687-.59055.15262a6.62767,6.62767,0,1,0-11.39689,6.56369,26.52755,26.52755,0,1,0,44.2313,25.52756,6.6211,6.6211,0,1,0,.848-13.18579" transform="translate(-35.5 -118.5)" fill="#44d860" fill-rule="evenodd" />
<path d="M344.38682,555.65836h53.08286V529.11693H344.38682Z" transform="translate(-35.5 -118.5)" fill="#3ecc5f" fill-rule="evenodd" />
<path d="M410.74039,545.70532a3.31768,3.31768,0,1,0,0-6.63536,3.41133,3.41133,0,0,0-.42333.04247c-.02655-.09953-.04911-.19907-.077-.29859a3.319,3.319,0,0,0-1.278-6.37923,3.28174,3.28174,0,0,0-2.00122.68742q-.10947-.11346-.22294-.22295a3.282,3.282,0,0,0,.67149-1.98265,3.31768,3.31768,0,0,0-6.37-1.2992,13.27078,13.27078,0,1,0,0,25.54082,3.31768,3.31768,0,0,0,6.37-1.2992,3.282,3.282,0,0,0-.67149-1.98265q.11347-.10947.22294-.22294a3.28174,3.28174,0,0,0,2.00122.68742,3.31768,3.31768,0,0,0,1.278-6.37923c.02786-.0982.05042-.19907.077-.29859a3.41325,3.41325,0,0,0,.42333.04246" transform="translate(-35.5 -118.5)" fill="#44d860" fill-rule="evenodd" />
<path d="M424.01111,447.8338a3.60349,3.60349,0,0,1-.65028-.06636,3.34415,3.34415,0,0,1-.62372-.18579,3.44679,3.44679,0,0,1-.572-.30522,5.02708,5.02708,0,0,1-.50429-.4114,3.88726,3.88726,0,0,1-.41007-.50428,3.27532,3.27532,0,0,1-.55737-1.84463,3.60248,3.60248,0,0,1,.06636-.65027,3.82638,3.82638,0,0,1,.18447-.62373,3.48858,3.48858,0,0,1,.30656-.57064,3.197,3.197,0,0,1,.91436-.91568,3.44685,3.44685,0,0,1,.572-.30523,3.344,3.344,0,0,1,.62372-.18578,3.06907,3.06907,0,0,1,1.30053,0,3.22332,3.22332,0,0,1,1.19436.491,5.02835,5.02835,0,0,1,.50429.41139,4.8801,4.8801,0,0,1,.41139.50429,3.38246,3.38246,0,0,1,.30522.57064,3.47806,3.47806,0,0,1,.25215,1.274A3.36394,3.36394,0,0,1,426.36,446.865a5.02708,5.02708,0,0,1-.50429.4114,3.3057,3.3057,0,0,1-1.84463.55737m26.54143-1.65884a3.38754,3.38754,0,0,1-2.35024-.96877,5.04185,5.04185,0,0,1-.41007-.50428,3.27532,3.27532,0,0,1-.55737-1.84463,3.38659,3.38659,0,0,1,.96744-2.34892,5.02559,5.02559,0,0,1,.50429-.41139,3.44685,3.44685,0,0,1,.572-.30523,3.3432,3.3432,0,0,1,.62373-.18579,3.06952,3.06952,0,0,1,1.30052,0,3.22356,3.22356,0,0,1,1.19436.491,5.02559,5.02559,0,0,1,.50429.41139,3.38792,3.38792,0,0,1,.96876,2.34892,3.72635,3.72635,0,0,1-.06636.65026,3.37387,3.37387,0,0,1-.18579.62373,4.71469,4.71469,0,0,1-.30522.57064,4.8801,4.8801,0,0,1-.41139.50429,5.02559,5.02559,0,0,1-.50429.41139,3.30547,3.30547,0,0,1-1.84463.55737" transform="translate(-35.5 -118.5)" fill-rule="evenodd" />
</svg>

After

Width:  |  Height:  |  Size: 12 KiB

7
docs/tsconfig.json Normal file
View File

@@ -0,0 +1,7 @@
{
// This file is not used in compilation. It is here just for a nice editor experience.
"extends": "@docusaurus/tsconfig",
"compilerOptions": {
"baseUrl": ".",
},
}

18
eslint.config.mjs Normal file
View File

@@ -0,0 +1,18 @@
import eslint from '@eslint/js'
import tseslint from 'typescript-eslint'
export default [
...tseslint.config(eslint.configs.recommended, ...tseslint.configs.recommended),
{
rules: {
'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': [
'warn',
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_' },
],
},
},
{
ignores: ['node_modules/', 'build/', 'dist/', 'gen/'],
},
]

5
examples/.dotdir/README.md Executable file
View File

@@ -0,0 +1,5 @@
# {{ name }} Readme
TO DO:
- [ ] ...

View File

@@ -1,17 +0,0 @@
import * as React from "react"
import * as css from "./{{Name}}.css"
class {{Name}} extends React.Component<any> {
private {{ property }}
constructor(props: any) {
super(props)
this.{{ property }} = {{ value }}
}
public render() {
return <div className={ css.{{Name}} } />
}
}
export default {{Name}}

View File

@@ -0,0 +1,17 @@
import * as React from "react"
import * as css from "./{{pascalCase name}}.css"
class {{pascalCase name}} extends React.Component<any> {
private {{ property }}
constructor(props: any) {
super(props)
this.{{ property }} = {{ value }}
}
public render() {
return <div className={ css.{{pascalCase name}} } />
}
}
export default {{pascalCase name}}

View File

@@ -1,205 +0,0 @@
/*
* For a detailed explanation regarding each configuration property and type check, visit:
* https://jestjs.io/docs/configuration
*/
export default {
// All imported modules in your tests should be mocked automatically
// automock: false,
// Stop running tests after `n` failures
// bail: 0,
// The directory where Jest should store its cached dependency information
// cacheDirectory: "/private/var/folders/q9/0mns8fgd00b4t5j5lq2wh2yh0000gn/T/jest_dx",
// Automatically clear mock calls, instances, contexts and results before every test
clearMocks: true,
// Indicates whether the coverage information should be collected while executing the test
collectCoverage: true,
// An array of glob patterns indicating a set of files for which coverage information should be collected
// collectCoverageFrom: undefined,
// The directory where Jest should output its coverage files
coverageDirectory: "coverage",
// An array of regexp pattern strings used to skip coverage collection
// coveragePathIgnorePatterns: [
// "/node_modules/"
// ],
// Indicates which provider should be used to instrument code for coverage
coverageProvider: "v8",
// A list of reporter names that Jest uses when writing coverage reports
// coverageReporters: [
// "json",
// "text",
// "lcov",
// "clover"
// ],
// An object that configures minimum threshold enforcement for coverage results
// coverageThreshold: undefined,
// A path to a custom dependency extractor
// dependencyExtractor: undefined,
// Make calling deprecated APIs throw helpful error messages
// errorOnDeprecated: false,
// The default configuration for fake timers
// fakeTimers: {
// "enableGlobally": false
// },
// Force coverage collection from ignored files using an array of glob patterns
// forceCoverageMatch: [],
// A path to a module which exports an async function that is triggered once before all test suites
// globalSetup: undefined,
// A path to a module which exports an async function that is triggered once after all test suites
// globalTeardown: undefined,
// A set of global variables that need to be available in all test environments
// globals: {},
// The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers.
// maxWorkers: "50%",
// An array of directory names to be searched recursively up from the requiring module's location
// moduleDirectories: [
// "node_modules"
// ],
// An array of file extensions your modules use
// moduleFileExtensions: [
// "js",
// "mjs",
// "cjs",
// "jsx",
// "ts",
// "tsx",
// "json",
// "node"
// ],
// A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
// moduleNameMapper: {},
// moduleNameMapper: {
// chalk: "<rootDir>/node_modules/chalk/source/index.js",
// "#ansi-styles": "<rootDir>/node_modules/chalk/source/vendor/ansi-styles/index.js",
// "#supports-color": "<rootDir>/node_modules/chalk/source/vendor/supports-color/index.js",
// },
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
modulePathIgnorePatterns: ["<rootDir>/dist"],
// Activates notifications for test results
// notify: false,
// An enum that specifies notification mode. Requires { notify: true }
// notifyMode: "failure-change",
// A preset that is used as a base for Jest's configuration
preset: "ts-jest",
// Run tests from one or more projects
// projects: undefined,
// Use this configuration option to add custom reporters to Jest
// reporters: undefined,
// Automatically reset mock state before every test
// resetMocks: false,
// Reset the module registry before running each individual test
// resetModules: false,
// A path to a custom resolver
// resolver: undefined,
// Automatically restore mock state and implementation before every test
// restoreMocks: false,
// The root directory that Jest should scan for tests and modules within
// rootDir: undefined,
// A list of paths to directories that Jest should use to search for files in
// roots: [
// "<rootDir>"
// ],
// Allows you to use a custom runner instead of Jest's default test runner
// runner: "jest-runner",
// The paths to modules that run some code to configure or set up the testing environment before each test
// setupFiles: [],
// A list of paths to modules that run some code to configure or set up the testing framework before each test
// setupFilesAfterEnv: [],
// The number of seconds after which a test is considered as slow and reported as such in the results.
// slowTestThreshold: 5,
// A list of paths to snapshot serializer modules Jest should use for snapshot testing
// snapshotSerializers: [],
// The test environment that will be used for testing
// testEnvironment: "jest-environment-node",
// Options that will be passed to the testEnvironment
// testEnvironmentOptions: {},
// Adds a location field to test results
// testLocationInResults: false,
// The glob patterns Jest uses to detect test files
// testMatch: [
// "**/__tests__/**/*.[jt]s?(x)",
// "**/?(*.)+(spec|test).[tj]s?(x)"
// ],
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
// testPathIgnorePatterns: [
// "/node_modules/"
// ],
// The regexp pattern or array of patterns that Jest uses to detect test files
// testRegex: [],
// This option allows the use of a custom results processor
// testResultsProcessor: undefined,
// This option allows use of a custom test runner
// testRunner: "jest-circus/runner",
// A map from regular expressions to paths to transformers
// transform: undefined,
// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
// transformIgnorePatterns: [
// "/node_modules/",
// "\\.pnp\\.[^\\/]+$"
// ],
// transform: {
// "^.+\\.ts?$": "ts-jest",
// },
// transformIgnorePatterns: ["<rootDir>/node_modules/"],
// An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
// unmockedModulePathPatterns: undefined,
// Indicates whether each individual test should be reported during the run
// verbose: undefined,
// An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
// watchPathIgnorePatterns: [],
// Whether to use watchman for file crawling
// watchman: true,
}

6
nodemon.json Normal file
View File

@@ -0,0 +1,6 @@
{
"ignore": ["**/*.test.ts", "**/*.spec.ts", ".git", "node_modules"],
"watch": ["src"],
"exec": "node -r tsconfig-paths/register -r ts-node/register ./src/index.ts",
"ext": "ts, js"
}

View File

@@ -1,14 +1,19 @@
{
"name": "simple-scaffold",
"version": "1.7.0-develop.3",
"version": "3.1.0",
"description": "Generate any file structure - from single components to entire app boilerplates, with a single command.",
"homepage": "https://chenasraf.github.io/simple-scaffold",
"repository": "https://github.com/chenasraf/simple-scaffold.git",
"author": "Chen Asraf <contact@casraf.dev>",
"repository": {
"type": "git",
"url": "https://github.com/chenasraf/simple-scaffold.git"
},
"author": "Chen Asraf <contact@casraf.dev> (https://casraf.dev)",
"license": "MIT",
"main": "index.js",
"bin": "cmd.js",
"packageManager": "pnpm@8.5.0",
"bin": {
"simple-scaffold": "cmd.js"
},
"packageManager": "pnpm@9.9.0",
"keywords": [
"javascript",
"cli",
@@ -21,47 +26,57 @@
"scaffolding"
],
"scripts": {
"clean": "rimraf dist/",
"build": "pnpm clean && tsc && chmod -R +x ./dist && cp ./package.json ./README.md ./dist/",
"dev": "tsc --watch",
"start": "node dist/scaffold.js",
"test": "jest",
"cmd": "node --trace-warnings dist/cmd.js",
"build-test": "pnpm build && pnpm test",
"build-cmd": "pnpm build && pnpm cmd",
"build-docs": "typedoc",
"watch-docs": "pnpm typedoc --watch",
"clean": "rimraf dist",
"build": "pnpm clean && vite build && tsc --emitDeclarationOnly && chmod -R +x ./dist && cp ./package.json ./README.md ./dist/",
"dev": "vite build --watch",
"start": "vite-node src/scaffold.ts",
"test": "vitest run",
"test:watch": "vitest",
"coverage": "vitest run --coverage && open coverage/lcov-report/index.html",
"cmd": "vite-node src/cmd.ts",
"docs:build": "cd docs && pnpm build",
"docs:watch": "cd docs && pnpm start",
"audit-fix": "pnpm audit --fix",
"changelog": "conventional-changelog -p conventionalcommits -i CHANGELOG.md -s -r 0; echo \"# Change Log\n\n$(cat CHANGELOG.md)\" > CHANGELOG.md"
"ci": "pnpm install --frozen-lockfile",
"lint": "eslint src/ tests/",
"format": "prettier --write .",
"lint-staged": "lint-staged",
"prepare": "husky"
},
"lint-staged": {
"*.{ts,mts,js,mjs}": [
"eslint --fix",
"prettier --write"
],
"*.{json,md,yml,yaml,css}": [
"prettier --write"
]
},
"dependencies": {
"chalk": "^4.1.2",
"date-fns": "^2.29.3",
"glob": "^9.2.1",
"handlebars": "^4.7.7",
"lodash": "^4.17.21",
"massarg": "^1.0.7-pre.1"
"@inquirer/confirm": "^6.0.10",
"@inquirer/input": "^5.0.10",
"@inquirer/number": "^4.0.10",
"@inquirer/select": "^5.1.2",
"date-fns": "^4.1.0",
"glob": "^13.0.6",
"handlebars": "^4.7.8",
"massarg": "2.1.1",
"minimatch": "^10.2.4",
"zod": "^4.3.6"
},
"devDependencies": {
"@knodes/typedoc-plugin-pages": "^0.23.4",
"@semantic-release/changelog": "^6.0.3",
"@semantic-release/git": "^10.0.1",
"@semantic-release/release-notes-generator": "^10.0.3",
"@types/jest": "^29.5.1",
"@types/lodash": "^4.14.171",
"@types/mock-fs": "^4.13.1",
"@types/node": "^18.16.0",
"conventional-changelog": "^3.1.25",
"conventional-changelog-cli": "^2.2.2",
"conventional-changelog-conventionalcommits": "^5.0.0",
"jest": "^29.5.0",
"mock-fs": "^5.2.0",
"rimraf": "^5.0.0",
"semantic-release": "^21.0.1",
"semantic-release-conventional-commits": "^3.0.0",
"ts-jest": "^29.1.0",
"ts-node": "^10.9.1",
"typedoc": "^0.24.6",
"typescript": "^5.0.4"
"@eslint/js": "^10.0.1",
"@types/node": "^25.5.0",
"@vitest/coverage-v8": "^4.1.0",
"husky": "^9.1.7",
"lint-staged": "^16.4.0",
"mock-fs": "^5.5.0",
"prettier": "^3.8.1",
"rimraf": "^6.1.3",
"typescript": "^5.9.3",
"typescript-eslint": "^8.57.1",
"vite": "^8.0.1",
"vite-node": "^6.0.0",
"vitest": "^4.1.0"
}
}

View File

@@ -1,7 +0,0 @@
See full documentation [here](https://chenasraf.github.io/simple-scaffold).
- [Command Line Interface (CLI) usage](https://chenasraf.github.io/simple-scaffold/pages/cli.html)
- [Node.js usage](https://chenasraf.github.io/simple-scaffold/pages/node.html)
- [Templates](https://chenasraf.github.io/simple-scaffold/pages/templates.html)
- [Configuration Files](https://chenasraf.github.io/simple-scaffold/pages/configuration_files.html)
- [Migrating v0.x to v1.x](https://chenasraf.github.io/simple-scaffold/pages/migration.html)

View File

@@ -1,79 +0,0 @@
## Available flags
```text
Usage: simple-scaffold [options]
```
To see this and more information anytime, add the `-h` or `--help` flag to your call, e.g.
`npx simple-scaffold@latest -h`.
| Command \| alias | |
| --------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--help`\|`-h` | Display help information |
| `--name`\|`-n` | Name to be passed to the generated files. {{name}} and {{Name}} inside contents and file names will be replaced accordingly. |
| `--config`\|`-c` | Filename or HTTPS git URL to load config from instead of passing arguments to CLI or using a Node.js script. |
| `--github`\|`-gh` | GitHub path to load config from instead of passing arguments to CLI or using a Node.js script. |
| `--key`\|`-k` | Key to load inside the config file. This overwrites the config key provided after the colon in --config (e.g. --config scaffold.cmd.js:component) |
| `--output`\|`-o` | Path to output to. If --create-sub-folder is enabled, the subfolder will be created inside this path. (default: current dir) |
| `--templates`\|`-t` | Template files to use as input. You may provide multiple files, each of which can be a relative or absolute path, or a glob pattern for multiple file matching easily. |
| `--overwrite`\|`-w` | Enable to override output files, even if they already exist. (default: false) |
| `--data`\|`-d` | Add custom data to the templates. By default, only your app name is included. |
| `--append-data`\|`-D` | Append additional custom data to the templates, which will overwrite --data, using an alternate syntax, which is easier to use with CLI: -D key1=string -D key2:=raw |
| `--create-sub-folder`\|`-s` | Create subfolder with the input name (default: false) |
| `--sub-folder-name-helper`\|`-sh` | Default helper to apply to subfolder name when using `--create-sub-folder true`. |
| `--quiet`\|`-q` | Suppress output logs (Same as --verbose 0) (default: false) |
| `--verbose`\|`-v` | Determine amount of logs to display. The values are: 0 (none) \| 1 (debug) \| 2 (info) \| 3 (warn) \| 4 (error). The provided level will display messages of the same level or higher. (default: 2) |
| `--dry-run`\|`-dr` | Don't emit files. This is good for testing your scaffolds and making sure they don't fail, without having to write actual file contents or create directories. (default: false) |
## Examples:
> See
> [Configuration Files](https://chenasraf.github.io/simple-scaffold/pages/docs/configuration_files.md)
> for organizing multiple scaffold types into easy-to-maintain files
Usage with config file
```shell
$ simple-scaffold -c scaffold.cmd.js --key component
```
Usage with GitHub config file
```shell
$ simple-scaffold -gh chenasraf/simple-scaffold --key component
```
Usage with https git URL (for non-GitHub)
```shell
$ simple-scaffold -c \
https://example.com/user/template.git#scaffold.cmd.js --key component
```
Full syntax with config path and template key (applicable to all above methods)
```shell
$ simple-scaffold -c scaffold.cmd.js:component MyComponent
```
Excluded template key, assumes 'default' key
```shell
$ simple-scaffold -c scaffold.cmd.js MyComponent
```
Shortest syntax for GitHub, assumes file 'scaffold.cmd.js' and template key 'default'
```shell
$ simple-scaffold -gh chenasraf/simple-scaffold MyComponent
```
You can also add this as a script in your `package.json`:
```json
{
"scripts": {
"scaffold": "npx simple-scaffold@latest -t scaffolds/component/**/* -o src/components -d '{\"myProp\": \"propName\", \"myVal\": 123}'"
}
}
```

View File

@@ -1,155 +0,0 @@
If you want to have reusable configurations which are complex and don't fit into command lines
easily, or just want to manage your templates easier, you can use configuration files to load your
scaffolding configurations.
## Creating config files
Configuration files should be valid `.js`/`.json` files that contain valid Scaffold configurations.
Each file hold multiple scaffolds. Each scaffold is a key, and its value is the configuration. For
example:
```json
{
"component": {
"templates": ["templates/component"],
"output": "src/components"
}
}
```
The configuration contents are identical to the
[Node.js configuration structure](https://chenasraf.github.io/simple-scaffold/pages/node.md):
```ts
interface ScaffoldConfig {
name: string
templates: string[]
output: FileResponse<string>
createSubFolder?: boolean
data?: Record<string, any>
overwrite?: FileResponse<boolean>
quiet?: boolean
verbose?: LogLevel
dryRun?: boolean
helpers?: Record<string, Helper>
subFolderNameHelper?: DefaultHelpers | string
beforeWrite?(
content: Buffer,
rawContent: Buffer,
outputPath: string,
): string | Buffer | undefined | Promise<string | Buffer | undefined>
}
```
If you want to supply functions inside the configurations, you must use a `.js` file as JSON does
not support non-primitives.
A `.js` file is just like a `.json` file, make sure to export the final configuration:
```js
/** @type {import('simple-scaffold').ScaffoldConfigFile} */
module.exports = {
component: {
templates: ["templates/component"],
output: "src/components",
},
}
```
## Using a config file
Once your config is created, you can use it by providing the file name to the `--config` (or `-c`
for brevity), optionally followed by a colon, then your scaffold config name.
```shell
simple-scaffold -c <file>[:<template_key>]
```
For example:
```shell
simple-scaffold -c scaffold.json:component MyComponentName
```
If you don't want to supply a template/config name (e.g. `component`), you can omit the colon and
the name, and it will use the configuration named `default`:
```js
/** @type {import('simple-scaffold').ScaffoldConfigFile} */
module.exports = {
default: {
// ...
},
}
```
And then:
```shell
# will use 'default' template
simple-scaffold -c scaffold.json MyComponentName
```
## Remote Templates
You can load template groups remotely, similar to how you would pass a config normally.
The main difference is the templates will be hosted on a remote location such as a git server, and
not locally in your project. This can be done to easily share & reuse templates.
When passing a git URL to `--config`, you will clone that repo and use the files there as template.
The syntax is as follows:
```shell
simple-scaffold -c <git_url>[#<git_file>][:<template_key>]
```
For example, to use this repository's example as base:
```shell
simple-scaffold -c https://github.com/chenasraf/simple-scaffold.git#examples/test-input/scaffold.config.js:component
```
When the filename is omitted, `/scaffold.config.js` will be used as default.
When the template_key is ommitted, `default` will be used as default.
### GitHub Templates
As a shorter alternative to the above example, you can use `--github` or `-gh` to reference a GitHub
URL without specifying the whole path.
The syntax is as follows:
```shell
simple-scaffold -gh <username>/<project_name>[#<git_file>][:<template_key>]
```
This example is equivalent to the above, just shorter to write:
```shell
simple-scaffold -c chenasraf/simple-scaffold#examples/test-input/scaffold.config.js:component
```
## Use In Node.js
You can also start a scaffold from Node.js with a remote file or URL config.
Just use the `Scaffold.fromConfig` function:
```ts
Scaffold.fromConfig(
"scaffold.config.js", // file or HTTPS git URL
{
// name of the generated component
name: "My Component",
// key to load from the config
key: "component",
},
{
// other config overrides
},
)
```

View File

@@ -1,25 +0,0 @@
In Simple Scaffold v1.0, the entire codebase was overhauled, yet usage remains mostly the same
between versions. With these notable exceptions:
- Some of the argument names have changed
- Template syntax has been improved
- The command to run Scaffold has been simplified from `new SimpleScaffold(opts).run()` to
`SimpleScaffold(opts)`, which now returns a promise that you can await to know when the process
has been completed.
## Argument changes
- `locals` has been renamed to `data`. The appropriate command line args have been updated as well
to `--data` | `-d`.
- Additional options have been added to both CLI and Node interfaces. See
[Command Line Interface (CLI) usage](https://chenasraf.github.io/simple-scaffold/pages/cli.html)
and [Node.js usage](https://chenasraf.github.io/simple-scaffold/pages/node.html) for more
information.
## Template syntax changes
Simple Scaffold still uses Handlebars.js to handle template content and file names. However, helpers
have been added to remove the need for you to pre-process the template data on simple use-cases such
as case type manipulation (converting to camel case, snake case, etc)
See the readme for the full information on how to use these helpers and which are available.

View File

@@ -1,53 +0,0 @@
You can build the scaffold yourself, if you want to create more complex arguments, scaffold groups,
etc - simply pass a config object to the Scaffold function when you are ready to start.
The config takes similar arguments to the command line. The full type definitions can be found in
[src/types.ts](https://github.com/chenasraf/simple-scaffold/blob/develop/src/types.ts#L13).
See the full
[documentation](https://chenasraf.github.io/simple-scaffold/interfaces/ScaffoldConfig.html) for the
configuration options and their behavior.
```ts
interface ScaffoldConfig {
name: string
templates: string[]
output: FileResponse<string>
createSubFolder?: boolean
data?: Record<string, any>
overwrite?: FileResponse<boolean>
quiet?: boolean
verbose?: LogLevel
dryRun?: boolean
helpers?: Record<string, Helper>
subFolderNameHelper?: DefaultHelpers | string
beforeWrite?(
content: Buffer,
rawContent: Buffer,
outputPath: string,
): string | Buffer | undefined | Promise<string | Buffer | undefined>
}
```
This is an example of loading a complete scaffold via Node.js:
```typescript
import Scaffold from "simple-scaffold"
const config = {
name: "component",
templates: [path.join(__dirname, "scaffolds", "component")],
output: path.join(__dirname, "src", "components"),
createSubFolder: true,
subFolderNameHelper: "upperCase"
data: {
property: "value",
},
helpers: {
twice: (text) => [text, text].join(" ")
},
beforeWrite: (content, rawContent, outputPath) => content.toString().toUpperCase()
}
const scaffold = Scaffold(config)
```

6646
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,84 +0,0 @@
const releaseRules = [
{ type: "feat", section: "Features", release: "minor" },
{ type: "revert", section: "Features", release: "minor" },
{ type: "fix", section: "Bug Fixes", release: "patch" },
{ type: "chore", section: "Misc", release: "patch" },
{ type: "refactor", section: "Misc", release: "patch" },
{ type: "perf", section: "Misc", release: "patch" },
{ type: "build", section: "Build", release: "patch" },
{ type: "docs", section: "Build", release: false },
{ type: "test", section: "Tests", release: "patch" },
]
/** @type {import('semantic-release').Options} */
module.exports = {
branches: [
"+([0-9])?(.{+([0-9]),x}).x",
"master",
"next",
"next-major",
{ name: "develop", prerelease: true },
{ name: "beta", prerelease: true },
{ name: "alpha", prerelease: true },
],
analyzeCommits: {
path: "semantic-release-conventional-commits",
majorTypes: releaseRules.filter((x) => x.release === "major").map((x) => x.type),
minorTypes: releaseRules.filter((x) => x.release === "minor").map((x) => x.type),
patchTypes: releaseRules.filter((x) => x.release === "patch").map((x) => x.type),
},
plugins: [
[
"@semantic-release/commit-analyzer",
{
preset: "conventionalcommits",
parserOpts: {
noteKeywords: ["breaking:", "breaking-fix:", "breaking-feat:"],
},
releaseRules: releaseRules,
},
],
[
"@semantic-release/release-notes-generator",
{
preset: "conventionalcommits",
parserOpts: {
noteKeywords: ["breaking", "major"],
types: releaseRules,
},
},
],
[
"@semantic-release/changelog",
{
changelogFile: "CHANGELOG.md",
changelogTitle: "# Change Log",
},
],
[
"@semantic-release/npm",
{
npmPublish: false,
},
],
[
"@semantic-release/npm",
{
npmPublish: true,
pkgRoot: "dist",
},
],
[
"@semantic-release/git",
{
assets: ["CHANGELOG.md", "package.json"],
},
],
[
"@semantic-release/github",
{
assets: ["*.tgz"],
},
],
],
}

22
scaffold.config.cjs Normal file
View File

@@ -0,0 +1,22 @@
// @ts-check
/** @type {import('./dist').ScaffoldConfigFile} */
module.exports = () => {
// console.log("Config:", conf)
return {
default: {
templates: ["examples/test-input/Component"],
output: "examples/test-output",
data: { property: "myProp", value: "10" },
},
component: {
templates: ["examples/test-input/Component"],
output: "examples/test-output/component",
data: { property: "myProp", value: "10" },
},
configs: {
templates: ["examples/test-input/**/.*"],
output: "examples/test-output/configs",
name: "---",
},
}
}

View File

@@ -1,13 +0,0 @@
/** @type {import('simple-scaffold').ScaffoldConfigFile} */
module.exports = {
default: {
templates: ["examples/test-input/Component"],
output: "examples/test-output",
data: { property: "myProp", value: "10" },
},
component: {
templates: ["examples/test-input/Component"],
output: "examples/test-output/component",
data: { property: "myProp", value: "10" },
},
}

86
src/before-write.ts Normal file
View File

@@ -0,0 +1,86 @@
import path from "node:path"
import fs from "node:fs/promises"
import { exec } from "node:child_process"
import { LogConfig, LogLevel, ScaffoldConfig } from "./types"
import { log } from "./logger"
import { createDirIfNotExists, getUniqueTmpPath } from "./fs-utils"
/**
* Wraps a CLI beforeWrite command string into a beforeWrite callback function.
* The command receives the processed content via a temp file and can return modified content via stdout.
* @internal
*/
export function wrapBeforeWrite(
config: LogConfig & Pick<ScaffoldConfig, "dryRun">,
beforeWrite: string,
): ScaffoldConfig["beforeWrite"] {
return async (content, rawContent, outputFile) => {
const tmpDir = path.join(getUniqueTmpPath(), path.basename(outputFile))
await createDirIfNotExists(path.dirname(tmpDir), config)
const ext = path.extname(outputFile)
const rawTmpPath = tmpDir.replace(ext, ".raw" + ext)
try {
log(config, LogLevel.debug, "Parsing beforeWrite command", beforeWrite)
const cmd = await prepareBeforeWriteCmd({
beforeWrite,
tmpDir,
content,
rawTmpPath,
rawContent,
})
const result = await new Promise<string | undefined>((resolve, reject) => {
log(config, LogLevel.debug, "Running parsed beforeWrite command:", cmd)
const proc = exec(cmd)
proc.stdout!.on("data", (data) => {
if (data.trim()) {
resolve(data.toString())
} else {
resolve(undefined)
}
})
proc.stderr!.on("data", (data) => {
reject(data.toString())
})
})
return result
} catch (e) {
log(config, LogLevel.debug, e)
log(config, LogLevel.warning, "Error running beforeWrite command, returning original content")
return undefined
} finally {
await fs.rm(tmpDir, { force: true })
await fs.rm(rawTmpPath, { force: true })
}
}
}
async function prepareBeforeWriteCmd({
beforeWrite,
tmpDir,
content,
rawTmpPath,
rawContent,
}: {
beforeWrite: string
tmpDir: string
content: Buffer
rawTmpPath: string
rawContent: Buffer
}): Promise<string> {
let cmd: string = ""
const pathReg = /\{\{\s*path\s*\}\}/gi
const rawPathReg = /\{\{\s*rawpath\s*\}\}/gi
if (pathReg.test(beforeWrite)) {
await fs.writeFile(tmpDir, content)
cmd = beforeWrite.replaceAll(pathReg, tmpDir)
}
if (rawPathReg.test(beforeWrite)) {
await fs.writeFile(rawTmpPath, rawContent)
cmd = beforeWrite.replaceAll(rawPathReg, rawTmpPath)
}
if (!cmd) {
await fs.writeFile(tmpDir, content)
cmd = [beforeWrite, tmpDir].join(" ")
}
return cmd
}

View File

@@ -1,169 +1,349 @@
#!/usr/bin/env node
import massarg from "massarg"
import chalk from "chalk"
import { LogLevel, ScaffoldCmdConfig } from "./types"
import path from "node:path"
import fs from "node:fs/promises"
import { massarg } from "massarg"
import { ListCommandCliOptions, LogLevel, ScaffoldCmdConfig, ScaffoldConfigMap } from "./types"
import { Scaffold } from "./scaffold"
import path from "path"
import fs from "fs/promises"
import { parseAppendData, parseConfig } from "./config"
import { findConfigFile, getConfigFile, parseAppendData, parseConfigFile } from "./config"
import { log } from "./logger"
import { MassargCommand } from "massarg/command"
import { getUniqueTmpPath as generateUniqueTmpPath } from "./file"
import { colorize } from "./colors"
import { promptBeforeConfig, promptAfterConfig, resolveInputs } from "./prompts"
import { initScaffold } from "./init"
export async function parseCliArgs(args = process.argv.slice(2)) {
const pkgFile = await fs.readFile(path.join(__dirname, "package.json"))
const pkg = JSON.parse(pkgFile.toString())
const isConfigProvided =
args.includes("--config") || args.includes("-c") || args.includes("--github") || args.includes("-gh")
return (
massarg<ScaffoldCmdConfig>()
.main(async (config) => {
const _config = await parseConfig(config)
return Scaffold(_config)
})
.option({
name: "name",
aliases: ["n"],
description:
"Name to be passed to the generated files. {{name}} and {{Name}} inside contents and file names will be replaced accordingly.",
isDefault: true,
required: true,
})
.option({
name: "config",
aliases: ["c"],
description:
"Filename or https git URL to load config from instead of passing arguments to CLI or using a Node.js script. See examples for syntax.",
})
.option({
name: "github",
aliases: ["gh"],
description:
"GitHub path to load config from instead of passing arguments to CLI or using a Node.js script. See examples for syntax.",
})
.option({
name: "key",
aliases: ["k"],
description:
"Key to load inside the config file. This overwrites the config key provided after the colon in --config (e.g. --config scaffold.cmd.js:component)",
})
.option({
name: "output",
aliases: ["o"],
description: `Path to output to. If --create-sub-folder is enabled, the subfolder will be created inside this path. ${chalk.reset`${chalk.white`(default: current dir)`}`}`,
required: !isConfigProvided,
})
.option({
name: "templates",
aliases: ["t"],
array: true,
description:
"Template files to use as input. You may provide multiple files, each of which can be a relative or absolute path, " +
"or a glob pattern for multiple file matching easily.",
required: !isConfigProvided,
})
.option({
name: "overwrite",
aliases: ["w"],
boolean: true,
defaultValue: false,
description: "Enable to override output files, even if they already exist.",
})
.option({
name: "data",
aliases: ["d"],
description: "Add custom data to the templates. By default, only your app name is included.",
parse: (v) => JSON.parse(v),
})
.option({
name: "append-data",
aliases: ["D"],
description:
"Append additional custom data to the templates, which will overwrite --data, using an alternate syntax, which is easier to use with CLI: -D key1=string -D key2:=raw",
parse: parseAppendData,
})
.option({
name: "create-sub-folder",
aliases: ["s"],
boolean: true,
defaultValue: false,
description: "Create subfolder with the input name",
})
.option({
name: "sub-folder-name-helper",
aliases: ["sh"],
description: "Default helper to apply to subfolder name when using `--create-sub-folder true`.",
})
.option({
name: "quiet",
aliases: ["q"],
boolean: true,
defaultValue: false,
description: "Suppress output logs (Same as --verbose 0)",
})
.option({
name: "verbose",
aliases: ["v"],
defaultValue: LogLevel.Info,
description:
"Determine amount of logs to display. The values are: " +
`${chalk.bold`0 (none) | 1 (debug) | 2 (info) | 3 (warn) | 4 (error)`}. ` +
"The provided level will display messages of the same level or higher.",
parse: Number,
})
.option({
name: "dry-run",
aliases: ["dr"],
boolean: true,
defaultValue: false,
description:
"Don't emit files. This is good for testing your scaffolds and making sure they " +
"don't fail, without having to write actual file contents or create directories.",
})
// .example({
// input: `yarn cmd -t examples/test-input/Component -o examples/test-output -d '{"property":"myProp","value":"10"}'`,
// description: "Usage",
// output: "",
// })
.example({
description: "Usage with config file",
input: "simple-scaffold -c scaffold.cmd.js --key component",
})
.example({
description: "Usage with GitHub config file",
input: "simple-scaffold -gh chenasraf/simple-scaffold --key component",
})
.example({
description: "Usage with https git URL (for non-GitHub)",
input: "simple-scaffold -c https://example.com/user/template.git#scaffold.cmd.js --key component",
})
.example({
description: "Full syntax with config path and template key (applicable to all above methods)",
input: "simple-scaffold -c scaffold.cmd.js:component MyComponent",
})
.example({
description: "Excluded template key, assumes 'default' key",
input: "simple-scaffold -c scaffold.cmd.js MyComponent",
})
.example({
description: "Shortest syntax for GitHub, assumes file 'scaffold.cmd.js' and template key 'default'",
input: "simple-scaffold -gh chenasraf/simple-scaffold MyComponent",
})
.help({
binName: "simple-scaffold",
useGlobalColumns: true,
usageExample: "[options]",
printWidth: 100,
header: [`Create structured files based on templates.`].join("\n"),
footer: [
`Version: ${pkg.version}`,
`Copyright © Chen Asraf 2017-${new Date().getFullYear()}`,
``,
`Documentation: ${chalk.underline`https://chenasraf.github.io/simple-scaffold`}`,
`NPM: ${chalk.underline`https://npmjs.com/package/simple-scaffold`}`,
`GitHub: ${chalk.underline`https://github.com/chenasraf/simple-scaffold`}`,
].join("\n"),
})
.parse(args)
const isProjectRoot = Boolean(
await fs.stat(path.join(__dirname, "package.json")).catch(() => false),
)
const pkgFile = await fs.readFile(
path.resolve(__dirname, isProjectRoot ? "." : "..", "package.json"),
)
const pkg = JSON.parse(pkgFile.toString())
return massarg<ScaffoldCmdConfig>({
name: pkg.name,
description: pkg.description,
})
.main(async (config) => {
if (config.version) {
console.log(pkg.version)
return
}
log(config, LogLevel.debug, `Simple Scaffold v${pkg.version}`)
config.tmpDir = generateUniqueTmpPath()
try {
// Auto-detect config file in cwd — but only if the user didn't
// explicitly provide templates/output (which signals a one-time run)
const isOneTimeRun = config.templates?.length > 0 || config.output
if (!config.config && !config.git && !isOneTimeRun) {
try {
config.config = await findConfigFile(process.cwd())
log(config, LogLevel.debug, `Auto-detected config file: ${config.config}`)
} catch {
// No config file found — that's fine, continue without one
}
}
// Load config map early so we can prompt for name and template key
const hasConfigSource = Boolean(config.config || config.git)
let configMap: ScaffoldConfigMap | undefined
if (hasConfigSource) {
configMap = await getConfigFile(config)
}
// Phase 1: prompt for name + key (needed before parseConfigFile)
config = await promptBeforeConfig(config, configMap)
// Parse and merge the config file
log(config, LogLevel.debug, "Parsing config file...", config)
const parsed = await parseConfigFile(config)
// Phase 2: prompt for anything still missing after config merge
const prompted = await promptAfterConfig(parsed)
const resolved = await resolveInputs(prompted)
await Scaffold(resolved)
} catch (e) {
const message = "message" in (e as object) ? (e as Error).message : e?.toString()
log(config, LogLevel.error, message)
} finally {
log(config, LogLevel.debug, "Cleaning up temporary files...", config.tmpDir)
if (config.tmpDir) await fs.rm(config.tmpDir, { recursive: true, force: true })
}
})
.option({
name: "name",
aliases: ["n"],
description:
"Name to be passed to the generated files. `{{name}}` and other data parameters inside " +
"contents and file names will be replaced accordingly. You may omit the `--name` or `-n` " +
"for this specific option. If omitted in an interactive terminal, you will be prompted.",
isDefault: true,
})
.option({
name: "config",
aliases: ["c"],
description: "Filename or directory to load config from",
})
.option({
name: "git",
aliases: ["g"],
description: "Git URL or GitHub path to load a template from.",
})
.option({
name: "key",
aliases: ["k"],
description:
"Key to load inside the config file. This overwrites the config key provided after the colon in `--config` " +
"(e.g. `--config scaffold.cmd.js:component)`. If omitted and multiple templates are available, " +
"you will be prompted to select one.",
})
.option({
name: "output",
aliases: ["o"],
description:
"Path to output to. If `--subdir` is enabled, the subdir will be created inside " +
"this path. If omitted in an interactive terminal, you will be prompted.",
})
.option({
name: "templates",
aliases: ["t"],
array: true,
description:
"Template files to use as input. You may provide multiple files, each of which can be a relative or " +
"absolute path, " +
"or a glob pattern for multiple file matching easily. If omitted in an interactive terminal, " +
"you will be prompted for a comma-separated list.",
})
.flag({
name: "overwrite",
aliases: ["w"],
defaultValue: false,
description: "Enable to override output files, even if they already exist.",
negatable: true,
})
.option({
name: "data",
aliases: ["d"],
description: "Add custom data to the templates. By default, only your app name is included.",
parse: (v) => JSON.parse(v),
})
.option({
name: "append-data",
aliases: ["D"],
description:
"Append additional custom data to the templates, which will overwrite `--data`, using an alternate syntax, " +
"which is easier to use with CLI: `-D key1=string -D key2:=raw`",
parse: parseAppendData,
})
.flag({
name: "subdir",
aliases: ["s"],
defaultValue: false,
description: "Create a parent directory with the input name (and possibly `--subdir-helper`",
negatable: true,
negationName: "no-subdir",
})
.option({
name: "subdir-helper",
aliases: ["H"],
description: "Default helper to apply to subdir name when using `--subdir`.",
})
.flag({
name: "quiet",
aliases: ["q"],
defaultValue: false,
description: "Suppress output logs (Same as `--log-level none`)",
})
.option({
name: "log-level",
aliases: ["l"],
defaultValue: LogLevel.info,
description:
"Determine amount of logs to display. The values are: " +
`${colorize.bold`\`none | debug | info | warn | error\``}. ` +
"The provided level will display messages of the same level or higher.",
parse: (v) => {
const val = v.toLowerCase()
if (!(val in LogLevel)) {
throw new Error(
`Invalid log level: ${val}, must be one of: ${Object.keys(LogLevel).join(", ")}`,
)
}
return val
},
})
.option({
name: "before-write",
aliases: ["B"],
description:
"Run a script before writing the files. This can be a command or a path to a" +
" file. A temporary file path will be passed to the given command and the command should " +
"return a string for the final output.",
})
.option({
name: "after-scaffold",
aliases: ["A"],
description:
"Run a shell command after all files have been written. " +
"The command is executed in the output directory. " +
"For example: `--after-scaffold 'npm install'`",
})
.flag({
name: "dry-run",
aliases: ["dr"],
defaultValue: false,
description:
"Don't emit files. This is good for testing your scaffolds and making sure they " +
"don't fail, without having to write actual file contents or create directories.",
})
.flag({
name: "version",
aliases: ["v"],
description: "Display version.",
})
.command(
new MassargCommand<ListCommandCliOptions>({
name: "list",
aliases: ["ls"],
description:
"List all available templates for a given config. See `list -h` for more information.",
run: async (_config) => {
const config = {
templates: [],
name: "",
version: false,
output: "",
subdir: false,
overwrite: false,
dryRun: false,
tmpDir: generateUniqueTmpPath(),
..._config,
config: _config.config ?? (!_config.git ? process.cwd() : undefined),
}
try {
const file = await getConfigFile(config)
console.log(colorize.underline`Available templates:\n`)
console.log(Object.keys(file).join("\n"))
} catch (e) {
const message = "message" in (e as object) ? (e as Error).message : e?.toString()
log(config, LogLevel.error, message)
} finally {
log(config, LogLevel.debug, "Cleaning up temporary files...", config.tmpDir)
if (config.tmpDir) await fs.rm(config.tmpDir, { recursive: true, force: true })
}
},
})
.option({
name: "config",
aliases: ["c"],
description:
"Filename or directory to load config from. Defaults to current working directory.",
})
.option({
name: "git",
aliases: ["g"],
description: "Git URL or GitHub path to load a template from.",
})
.option({
name: "log-level",
aliases: ["l"],
defaultValue: LogLevel.none,
description:
"Determine amount of logs to display. The values are: " +
`${colorize.bold`\`none | debug | info | warn | error\``}. ` +
"The provided level will display messages of the same level or higher.",
parse: (v) => {
const val = v.toLowerCase()
if (!(val in LogLevel)) {
throw new Error(
`Invalid log level: ${val}, must be one of: ${Object.keys(LogLevel).join(", ")}`,
)
}
return val
},
})
.help({
bindOption: true,
}),
)
.command(
new MassargCommand<{ dir?: string; format?: string }>({
name: "init",
aliases: [],
description:
"Initialize a new scaffold config file and example template in the current directory.",
run: async (config) => {
try {
await initScaffold({
dir: config.dir,
format: config.format as "js" | "mjs" | "json" | undefined,
})
} catch (e) {
const message = "message" in (e as object) ? (e as Error).message : e?.toString()
console.error(colorize.red(message ?? "Unknown error"))
}
},
})
.option({
name: "dir",
aliases: ["d"],
description: "Directory to create the config in. Defaults to current working directory.",
})
.option({
name: "format",
aliases: ["f"],
description: "Config file format: js, mjs, or json. If omitted, you will be prompted.",
})
.help({
bindOption: true,
}),
)
.example({
description: "Usage with config file",
input: "simple-scaffold -c scaffold.cmd.js --key component",
})
.example({
description: "Usage with GitHub config file",
input: "simple-scaffold -g chenasraf/simple-scaffold --key component",
})
.example({
description: "Usage with https git URL (for non-GitHub)",
input:
"simple-scaffold -g https://example.com/user/template.git -c scaffold.cmd.js --key component",
})
.example({
description: "Excluded template key, assumes 'default' key",
input: "simple-scaffold -c scaffold.cmd.js MyComponent",
})
.example({
description:
"Shortest syntax for GitHub, searches for config file automaticlly, assumes and template key 'default'",
input: "simple-scaffold -g chenasraf/simple-scaffold MyComponent",
})
.help({
bindOption: true,
lineLength: 100,
useGlobalTableColumns: true,
usageText: [
colorize.yellow`simple-scaffold`,
colorize.gray`[options]`,
colorize.cyan`<name>`,
].join(" "),
optionOptions: {
displayNegations: true,
},
footerText: [
`Version: ${pkg.version}`,
`Copyright © Chen Asraf 2017-${new Date().getFullYear()}`,
``,
`Documentation: ${colorize.underline`https://chenasraf.github.io/simple-scaffold`}`,
`NPM: ${colorize.underline`https://npmjs.com/package/simple-scaffold`}`,
`GitHub: ${colorize.underline`https://github.com/chenasraf/simple-scaffold`}`,
].join("\n"),
})
.parse(args)
}
parseCliArgs()

74
src/colors.ts Normal file
View File

@@ -0,0 +1,74 @@
/** ANSI color code mapping for terminal output. */
const colorMap = {
reset: 0,
dim: 2,
bold: 1,
italic: 3,
underline: 4,
red: 31,
green: 32,
yellow: 33,
blue: 34,
magenta: 35,
cyan: 36,
white: 37,
gray: 90,
} as const
/** Available terminal color names. */
export type TermColor = keyof typeof colorMap
function _colorize(text: string, color: TermColor): string {
const c = colorMap[color]!
let r: number
if (c > 1 && c < 30) {
r = c + 20
} else if (c === 1) {
r = 23
} else {
r = 0
}
return `\x1b[${c}m${text}\x1b[${r}m`
}
function isTemplateStringArray(
template: TemplateStringsArray | unknown,
): template is TemplateStringsArray {
return Array.isArray(template) && typeof template[0] === "string"
}
const createColorize =
(color: TermColor) =>
(template: TemplateStringsArray | unknown, ...params: unknown[]): string => {
return isTemplateStringArray(template)
? _colorize(
(template as TemplateStringsArray).reduce(
(acc, str, i) => acc + str + (params[i] ?? ""),
"",
),
color,
)
: _colorize(String(template), color)
}
type TemplateStringsFn = ReturnType<typeof createColorize> & ((text: string) => string)
type TemplateStringsFns = { [key in TermColor]: TemplateStringsFn }
/**
* Colorize text for terminal output.
*
* Can be used as a function: `colorize("text", "red")`
* Or via named helpers: `colorize.red("text")` / `colorize.red\`template\``
*/
export const colorize: typeof _colorize & TemplateStringsFns = Object.assign(
_colorize,
Object.entries(colorMap).reduce(
(acc, [key]) => {
acc[key as TermColor] = createColorize(key as TermColor)
return acc
},
{} as Record<TermColor, TemplateStringsFn>,
),
)

View File

@@ -1,44 +1,27 @@
import path from "path"
import path from "node:path"
import {
AsyncResolver,
ConfigLoadConfig,
FileResponse,
FileResponseHandler,
LogConfig,
LogLevel,
Resolver,
RemoteConfigLoadConfig,
ScaffoldCmdConfig,
ScaffoldConfig,
ScaffoldConfigFile,
ScaffoldConfigMap,
} from "./types"
import { OptionsBase } from "massarg/types"
import { spawn } from "node:child_process"
import os from "node:os"
import { handlebarsParse } from "./parser"
import { log } from "./logger"
import { resolve } from "./utils"
import { resolve, wrapNoopResolver } from "./utils"
import { getGitConfig } from "./git"
import { isDir, pathExists } from "./fs-utils"
import { wrapBeforeWrite } from "./before-write"
export function getOptionValueForFile<T>(
config: ScaffoldConfig,
filePath: string,
fn: FileResponse<T>,
defaultValue?: T,
): T {
if (typeof fn !== "function") {
return defaultValue ?? (fn as T)
}
return (fn as FileResponseHandler<T>)(
filePath,
path.dirname(handlebarsParse(config, filePath, { isPath: true }).toString()),
path.basename(handlebarsParse(config, filePath, { isPath: true }).toString()),
)
}
// Re-export for backward compatibility (tests import from here)
export { getOptionValueForFile } from "./file"
export function parseAppendData(value: string, options: ScaffoldCmdConfig & OptionsBase): unknown {
/** Parses CLI append-data syntax (`key=value` or `key:=jsonValue`) into a data object. @internal */
export function parseAppendData(value: string, options: ScaffoldCmdConfig): unknown {
const data = options.data ?? {}
const [key, val] = value.split(/\:?=/)
// raw
const [key, val] = value.split(/:?=/)
if (value.includes(":=") && !val.includes(":=")) {
return { ...data, [key]: JSON.parse(val) }
}
@@ -46,123 +29,178 @@ export function parseAppendData(value: string, options: ScaffoldCmdConfig & Opti
}
function isWrappedWithQuotes(string: string): boolean {
return (string.startsWith('"') && string.endsWith('"')) || (string.startsWith("'") && string.endsWith("'"))
return (
(string.startsWith('"') && string.endsWith('"')) ||
(string.startsWith("'") && string.endsWith("'"))
)
}
/** @internal */
export async function parseConfig(config: ScaffoldCmdConfig & OptionsBase): Promise<ScaffoldConfig> {
let c: ScaffoldConfig = config
if (config.github) {
log(config, LogLevel.Info, `Loading config from github ${config.github}`)
const gitUrl = new URL(`https://github.com/${config.github}`)
if (!gitUrl.pathname.endsWith(".git")) {
gitUrl.pathname += ".git"
}
config.config = gitUrl.toString()
/** Loads and resolves a config file (local or remote). @internal */
export async function getConfigFile(config: ScaffoldCmdConfig): Promise<ScaffoldConfigMap> {
if (config.git && !config.git.includes("://")) {
log(config, LogLevel.debug, `Loading config from GitHub ${config.git}`)
config.git = githubPartToUrl(config.git)
}
if (config.config) {
const isUrl = config.config.includes("://")
const isGit = Boolean(config.git)
const configFilename = config.config
const configPath = isGit ? config.git : configFilename
const hasColonToken = (!isUrl && config.config.includes(":")) || (isUrl && count(config.config, ":") > 1)
const colonIndex = config.config.lastIndexOf(":")
const [configFile, templateKey = "default"] = hasColonToken
? [config.config.substring(0, colonIndex), config.config.substring(colonIndex + 1)]
: [config.config, undefined]
const key = (config.key ?? templateKey) || "default"
log(config, LogLevel.Info, `Loading config from ${configFile} with key ${key}`)
const configPromise = await getConfig({ config: configFile, quiet: config.quiet, verbose: config.verbose })
const configImport = await resolve(configPromise, config)
log(config, LogLevel.debug, `Loading config from file ${configFilename}`)
const configPromise = await (isGit
? getRemoteConfig({
git: configPath,
config: configFilename,
logLevel: config.logLevel,
tmpDir: config.tmpDir!,
})
: getLocalConfig({ config: configFilename, logLevel: config.logLevel }))
let configImport = await resolve(configPromise, config)
if (typeof configImport.default === "function" || configImport.default instanceof Promise) {
log(config, LogLevel.debug, "Config is a function or promise, resolving...")
configImport = await resolve(configImport.default, config)
}
return configImport
}
/**
* Parses a CLI config into a full ScaffoldConfig by merging CLI args, config file values,
* and append-data overrides. @internal
*/
export async function parseConfigFile(config: ScaffoldCmdConfig): Promise<ScaffoldConfig> {
let output: ScaffoldConfig = {
name: config.name,
templates: config.templates ?? [],
output: config.output,
logLevel: config.logLevel,
dryRun: config.dryRun,
data: config.data,
subdir: config.subdir,
overwrite: config.overwrite,
subdirHelper: config.subdirHelper,
beforeWrite: undefined,
tmpDir: config.tmpDir!,
}
if (config.quiet) {
config.logLevel = LogLevel.none
}
const shouldLoadConfig = Boolean(config.config || config.git)
if (shouldLoadConfig) {
const key = config.key ?? "default"
const configImport = await getConfigFile(config)
if (!configImport[key]) {
throw new Error(`Template "${key}" not found in ${configFile}`)
throw new Error(`Template "${key}" not found in ${config.config}`)
}
c = {
...config,
...configImport[key],
const imported = configImport[key]
log(config, LogLevel.debug, "Imported result", imported)
output = {
...output,
...imported,
beforeWrite: undefined,
templates: config.templates || imported.templates,
output: config.output || imported.output,
data: {
...configImport[key].data,
...imported.data,
...config.data,
},
}
}
c.data = { ...c.data, ...config.appendData }
delete config.appendData
return c
}
function wrapNoopResolver<T, R = T>(value: Resolver<T, R>): Resolver<T, R> {
if (typeof value === "function") {
return value
output.data = { ...output.data, ...config.appendData }
const cmdBeforeWrite = config.beforeWrite
? wrapBeforeWrite(config, config.beforeWrite)
: undefined
output.beforeWrite = cmdBeforeWrite ?? output.beforeWrite
if (config.afterScaffold) {
output.afterScaffold = config.afterScaffold
}
return (_) => value
if (!output.name) {
throw new Error("simple-scaffold: Missing required option: name")
}
log(output, LogLevel.debug, "Parsed config", output)
return output
}
/** @internal */
export async function getConfig(config: ConfigLoadConfig): Promise<ScaffoldConfigFile> {
/** Converts a GitHub shorthand (user/repo) to a full HTTPS git URL. @internal */
export function githubPartToUrl(part: string): string {
const gitUrl = new URL(`https://github.com/${part}`)
if (!gitUrl.pathname.endsWith(".git")) {
gitUrl.pathname += ".git"
}
return gitUrl.toString()
}
/** Loads a scaffold config from a local file or directory. @internal */
export async function getLocalConfig(
config: ConfigLoadConfig & Partial<LogConfig>,
): Promise<ScaffoldConfigFile> {
const { config: configFile, ...logConfig } = config as Required<typeof config>
const url = new URL(configFile)
if (url.protocol === "file:") {
log(logConfig, LogLevel.Info, `Loading config from file ${configFile}`)
const absolutePath = path.resolve(process.cwd(), configFile)
return wrapNoopResolver(import(absolutePath))
const absolutePath = path.resolve(process.cwd(), configFile)
const _isDir = await isDir(absolutePath)
if (_isDir) {
log(logConfig, LogLevel.debug, `Resolving config file from directory ${absolutePath}`)
const file = await findConfigFile(absolutePath)
const exists = await pathExists(file)
if (!exists) {
throw new Error(`Could not find config file in directory ${absolutePath}`)
}
log(logConfig, LogLevel.debug, `Loading config from: ${path.resolve(absolutePath, file)}`)
return wrapNoopResolver(import(path.resolve(absolutePath, file)))
}
log(logConfig, LogLevel.debug, `Loading config from: ${absolutePath}`)
return wrapNoopResolver(import(absolutePath))
}
/** Loads a scaffold config from a remote git repository. @internal */
export async function getRemoteConfig(
config: RemoteConfigLoadConfig & Partial<LogConfig>,
): Promise<ScaffoldConfigFile> {
const { config: configFile, git, tmpDir, ...logConfig } = config as Required<typeof config>
log(
logConfig,
LogLevel.debug,
`Loading config from remote ${git}, config file ${configFile || "<auto-detect>"}`,
)
const url = new URL(git!)
const isHttp = url.protocol === "http:" || url.protocol === "https:"
const isGit = url.protocol === "git:" || (isHttp && url.pathname.endsWith(".git"))
if (isHttp || isGit) {
if (isGit) {
return getGitConfig(url, logConfig)
}
if (!isGit) {
throw new Error(`Unsupported protocol ${url.protocol}`)
}
return wrapNoopResolver(import(path.resolve(process.cwd(), configFile)))
return getGitConfig(url, configFile, tmpDir, logConfig)
}
async function getGitConfig(
url: URL,
logConfig: LogConfig,
): Promise<AsyncResolver<ScaffoldCmdConfig, ScaffoldConfigMap>> {
const repoUrl = `${url.protocol}//${url.host}${url.pathname}`
log(logConfig, LogLevel.Info, `Cloning git repo ${repoUrl}`)
const tmpPath = path.resolve(os.tmpdir(), `scaffold-config-${Date.now()}`)
return new Promise((resolve, reject) => {
const clone = spawn("git", ["clone", "--depth", "1", repoUrl, tmpPath])
clone.on("error", reject)
clone.on("close", async (code) => {
if (code === 0) {
log(logConfig, LogLevel.Info, `Loading config from git repo: ${repoUrl}`)
const hashPath = url.hash?.replace("#", "") || "scaffold.config.js"
const absolutePath = path.resolve(tmpPath, hashPath)
const loadedConfig = (await import(absolutePath)).default as ScaffoldConfigMap
log(logConfig, LogLevel.Info, `Loaded config from git`)
log(logConfig, LogLevel.Debug, `Raw config:`, loadedConfig)
const fixedConfig: ScaffoldConfigMap = Object.fromEntries(
Object.entries(loadedConfig).map(([k, v]) => [
k,
// use absolute paths for template as config is necessarily in another directory
{ ...v, templates: v.templates.map((t) => path.resolve(tmpPath, t)) },
]),
)
resolve(wrapNoopResolver(fixedConfig))
} else {
reject(new Error(`Git clone failed with code ${code}`))
}
})
})
}
function count(string: string, substring: string): number {
return string.split(substring).length - 1
/** Searches for a scaffold config file in the given directory, trying known filenames in order. @internal */
export async function findConfigFile(root: string): Promise<string> {
const allowed = ["mjs", "cjs", "js", "json"].reduce((acc, ext) => {
acc.push(`scaffold.config.${ext}`)
acc.push(`scaffold.${ext}`)
acc.push(`.scaffold.${ext}`)
return acc
}, [] as string[])
for (const file of allowed) {
const exists = await pathExists(path.resolve(root, file))
if (exists) {
return file
}
}
throw new Error(`Could not find config file in git repo`)
}

View File

@@ -1,103 +1,93 @@
import path from "path"
import { F_OK } from "constants"
import { LogLevel, ScaffoldConfig } from "./types"
import { promises as fsPromises } from "fs"
const { stat, access, mkdir } = fsPromises
import path from "node:path"
import fs from "node:fs/promises"
import { FileResponse, FileResponseHandler, LogLevel, ScaffoldConfig } from "./types"
import { glob, hasMagic } from "glob"
import { log } from "./logger"
import { getOptionValueForFile } from "./config"
import { handlebarsParse } from "./parser"
import { handleErr } from "./utils"
import { createDirIfNotExists, pathExists, isDir } from "./fs-utils"
import { removeGlob } from "./path-utils"
const { readFile, writeFile } = fsPromises
const { readFile, writeFile } = fs
export async function createDirIfNotExists(dir: string, config: ScaffoldConfig): Promise<void> {
const parentDir = path.dirname(dir)
// Re-export extracted utilities for backward compatibility (tests import from here)
export { createDirIfNotExists, pathExists, isDir, getUniqueTmpPath } from "./fs-utils"
export { removeGlob, makeRelativePath, getBasePath } from "./path-utils"
if (!(await pathExists(parentDir))) {
await createDirIfNotExists(parentDir, config)
}
if (!(await pathExists(dir))) {
try {
log(config, LogLevel.Debug, `Creating dir ${dir}`)
await mkdir(dir)
return
} catch (e: any) {
if (e.code !== "EEXIST") {
throw e
}
return
}
/**
* Resolves a config option that may be either a static value or a per-file function.
* For function values, the file path is parsed through Handlebars before being passed.
* @internal
*/
export function getOptionValueForFile<T>(
config: ScaffoldConfig,
filePath: string,
fn: FileResponse<T>,
defaultValue?: T,
): T {
if (typeof fn !== "function") {
return defaultValue ?? (fn as T)
}
return (fn as FileResponseHandler<T>)(
filePath,
path.dirname(handlebarsParse(config, filePath, { asPath: true }).toString()),
path.basename(handlebarsParse(config, filePath, { asPath: true }).toString()),
)
}
export async function pathExists(filePath: string): Promise<boolean> {
try {
await access(filePath, F_OK)
return true
} catch (e: any) {
if (e.code === "ENOENT") {
return false
}
throw e
}
/** Information about a template glob pattern and how it was resolved. */
export interface GlobInfo {
/** The template path with glob wildcards stripped. */
baseTemplatePath: string
/** The original template string as provided by the user. */
origTemplate: string
/** Whether the template is a directory or contains glob patterns. */
isDirOrGlob: boolean
/** Whether the template contains glob wildcard characters. */
isGlob: boolean
/** The final resolved template path (with `**\/*` appended for directories). */
template: string
}
export async function isDir(path: string): Promise<boolean> {
const tplStat = await stat(path)
return tplStat.isDirectory()
}
export function removeGlob(template: string): string {
return template.replace(/\*/g, "").replace(/(\/\/|\\\\)/g, path.sep)
}
export function makeRelativePath(str: string): string {
return str.startsWith(path.sep) ? str.slice(1) : str
}
export function getBasePath(relPath: string): string {
return path
.resolve(process.cwd(), relPath)
.replace(process.cwd() + path.sep, "")
.replace(process.cwd(), "")
}
export async function getFileList(_config: ScaffoldConfig, template: string): Promise<string[]> {
/** Expands a list of glob patterns into a flat list of matching file paths. */
export async function getFileList(config: ScaffoldConfig, templates: string[]): Promise<string[]> {
log(config, LogLevel.debug, `Getting file list for glob list: ${templates}`)
return (
await glob(template, {
await glob(templates, {
dot: true,
nodir: true,
// debug: config.verbose === LogLevel.Debug,
})
).map(path.normalize)
}
export interface GlobInfo {
nonGlobTemplate: string
origTemplate: string
isDirOrGlob: boolean
isGlob: boolean
template: string
}
/** Analyzes a template path to determine if it's a glob, directory, or single file. */
export async function getTemplateGlobInfo(
config: ScaffoldConfig,
template: string,
): Promise<GlobInfo> {
const _isGlob = hasMagic(template)
log(config, LogLevel.debug, "before isDir", "isGlob:", _isGlob, template)
export async function getTemplateGlobInfo(config: ScaffoldConfig, template: string): Promise<GlobInfo> {
const isGlob = hasMagic(template)
log(config, LogLevel.Debug, "before isDir", "isGlob:", isGlob, template)
let _template = template
let nonGlobTemplate = isGlob ? removeGlob(template) : template
nonGlobTemplate = path.normalize(nonGlobTemplate)
const isDirOrGlob = isGlob ? true : await isDir(template)
log(config, LogLevel.Debug, "after isDir", isDirOrGlob)
const _shouldAddGlob = !isGlob && isDirOrGlob
const origTemplate = template
if (_shouldAddGlob) {
_template = path.join(template, "**", "*")
let resolvedTemplate = template
let baseTemplatePath = _isGlob ? removeGlob(template) : template
baseTemplatePath = path.normalize(baseTemplatePath)
const isDirOrGlob = _isGlob ? true : await isDir(template)
const shouldAddGlob = !_isGlob && isDirOrGlob
log(config, LogLevel.debug, "after", { isDirOrGlob, shouldAddGlob })
if (shouldAddGlob) {
resolvedTemplate = path.join(template, "**", "*")
}
return {
baseTemplatePath,
origTemplate: template,
isDirOrGlob,
isGlob: _isGlob,
template: resolvedTemplate,
}
return { nonGlobTemplate, origTemplate, isDirOrGlob, isGlob, template: _template }
}
/** Complete information about a template file's output destination. */
export interface OutputFileInfo {
inputPath: string
outputPathOpt: string
@@ -106,20 +96,24 @@ export interface OutputFileInfo {
exists: boolean
}
/** Computes the full output path and metadata for a single template file. */
export async function getTemplateFileInfo(
config: ScaffoldConfig,
{ templatePath, basePath }: { templatePath: string; basePath: string },
): Promise<OutputFileInfo> {
const inputPath = path.resolve(process.cwd(), templatePath)
const outputPathOpt = getOptionValueForFile(config, inputPath, config.output)
const outputDir = getOutputDir(config, outputPathOpt, basePath)
const outputPath = handlebarsParse(config, path.join(outputDir, path.basename(inputPath)), {
isPath: true,
}).toString()
const outputDir = getOutputDir(config, outputPathOpt, basePath.replace(config.tmpDir!, "./"))
const rawOutputPath = path.join(outputDir, path.basename(inputPath))
const outputPath = handlebarsParse(config, rawOutputPath, { asPath: true }).toString()
const exists = await pathExists(outputPath)
return { inputPath, outputPathOpt, outputDir, outputPath, exists }
}
/**
* Reads a template file, applies Handlebars parsing, runs the beforeWrite hook,
* and writes the result to the output path.
*/
export async function copyFileTransformed(
config: ScaffoldConfig,
{
@@ -136,72 +130,85 @@ export async function copyFileTransformed(
): Promise<void> {
if (!exists || overwrite) {
if (exists && overwrite) {
log(config, LogLevel.Info, `File ${outputPath} exists, overwriting`)
log(config, LogLevel.debug, `Overwriting ${outputPath}`)
}
log(config, LogLevel.debug, `Processing file ${inputPath}`)
const templateBuffer = await readFile(inputPath)
const unprocessedOutputContents = handlebarsParse(config, templateBuffer)
const finalOutputContents =
(await config.beforeWrite?.(unprocessedOutputContents, templateBuffer, outputPath)) ?? unprocessedOutputContents
(await config.beforeWrite?.(unprocessedOutputContents, templateBuffer, outputPath)) ??
unprocessedOutputContents
if (!config.dryRun) {
await writeFile(outputPath, finalOutputContents)
log(config, LogLevel.Info, "Done.")
} else {
log(config, LogLevel.Info, "Dry Run. Output should be:")
log(config, LogLevel.Info, finalOutputContents)
log(config, LogLevel.debug, "Dry run — output would be:")
log(config, LogLevel.debug, finalOutputContents.toString())
}
} else if (exists) {
log(config, LogLevel.Info, `File ${outputPath} already exists, skipping`)
log(config, LogLevel.debug, `Skipped ${outputPath} (already exists)`)
}
}
export function getOutputDir(config: ScaffoldConfig, outputPathOpt: string, basePath: string): string {
/** Computes the output directory for a file, combining the output path, base path, and optional subdir. */
export function getOutputDir(
config: ScaffoldConfig,
outputPathOpt: string,
basePath: string,
): string {
return path.resolve(
process.cwd(),
...([
outputPathOpt,
basePath,
config.createSubFolder
? config.subFolderNameHelper
? handlebarsParse(config, `{{ ${config.subFolderNameHelper} name }}`).toString()
config.subdir
? config.subdirHelper
? handlebarsParse(config, `{{ ${config.subdirHelper} name }}`).toString()
: config.name
: undefined,
].filter(Boolean) as string[]),
)
}
/**
* Processes a single template file: resolves output paths, creates directories,
* and writes the transformed output.
* Returns the output path if the file was written, or null if skipped.
*/
export async function handleTemplateFile(
config: ScaffoldConfig,
{ templatePath, basePath }: { templatePath: string; basePath: string },
): Promise<void> {
return new Promise(async (resolve, reject) => {
try {
const { inputPath, outputPathOpt, outputDir, outputPath, exists } = await getTemplateFileInfo(config, {
): Promise<string | null> {
try {
const { inputPath, outputPathOpt, outputDir, outputPath, exists } = await getTemplateFileInfo(
config,
{
templatePath,
basePath,
})
const overwrite = getOptionValueForFile(config, inputPath, config.overwrite ?? false)
},
)
const overwrite = getOptionValueForFile(config, inputPath, config.overwrite ?? false)
log(
config,
LogLevel.Debug,
`\nParsing ${templatePath}`,
`\nBase path: ${basePath}`,
`\nFull input path: ${inputPath}`,
`\nOutput Path Opt: ${outputPathOpt}`,
`\nFull output dir: ${outputDir}`,
`\nFull output path: ${outputPath}`,
`\n`,
)
log(
config,
LogLevel.debug,
`\nParsing ${templatePath}`,
`\nBase path: ${basePath}`,
`\nFull input path: ${inputPath}`,
`\nOutput Path Opt: ${outputPathOpt}`,
`\nFull output dir: ${outputDir}`,
`\nFull output path: ${outputPath}`,
`\n`,
)
await createDirIfNotExists(path.dirname(outputPath), config)
await createDirIfNotExists(path.dirname(outputPath), config)
log(config, LogLevel.Info, `Writing to ${outputPath}`)
await copyFileTransformed(config, { exists, overwrite, outputPath, inputPath })
resolve()
} catch (e: any) {
handleErr(e)
reject(e)
}
})
const shouldWrite = (!exists || overwrite) && !config.dryRun
log(config, LogLevel.debug, `Writing to ${outputPath}`)
await copyFileTransformed(config, { exists, overwrite, outputPath, inputPath })
return shouldWrite ? outputPath : null
} catch (e: unknown) {
handleErr(e as NodeJS.ErrnoException)
throw e
}
}

64
src/fs-utils.ts Normal file
View File

@@ -0,0 +1,64 @@
import os from "node:os"
import path from "node:path"
import fs from "node:fs/promises"
import { F_OK } from "node:constants"
import { LogConfig, LogLevel, ScaffoldConfig } from "./types"
import { log } from "./logger"
const { stat, access, mkdir } = fs
/** Recursively creates a directory and its parents if they don't exist. */
export async function createDirIfNotExists(
dir: string,
config: LogConfig & Pick<ScaffoldConfig, "dryRun">,
): Promise<void> {
if (config.dryRun) {
log(config, LogLevel.info, `Dry Run. Not creating dir ${dir}`)
return
}
const parentDir = path.dirname(dir)
if (!(await pathExists(parentDir))) {
await createDirIfNotExists(parentDir, config)
}
if (!(await pathExists(dir))) {
try {
log(config, LogLevel.debug, `Creating dir ${dir}`)
await mkdir(dir)
return
} catch (e: unknown) {
if (e && (e as NodeJS.ErrnoException).code !== "EEXIST") {
throw e
}
return
}
}
}
/** Checks whether a file or directory exists at the given path. */
export async function pathExists(filePath: string): Promise<boolean> {
try {
await access(filePath, F_OK)
return true
} catch (e: unknown) {
if (e && (e as NodeJS.ErrnoException).code === "ENOENT") {
return false
}
throw e
}
}
/** Returns true if the given path is a directory. */
export async function isDir(dirPath: string): Promise<boolean> {
const tplStat = await stat(dirPath)
return tplStat.isDirectory()
}
/** Generates a unique temporary directory path for scaffold operations. @internal */
export function getUniqueTmpPath(): string {
return path.resolve(
os.tmpdir(),
`scaffold-config-${Date.now()}-${Math.random().toString(36).slice(2)}`,
)
}

65
src/git.ts Normal file
View File

@@ -0,0 +1,65 @@
import path from "node:path"
import { log } from "./logger"
import { AsyncResolver, LogConfig, LogLevel, ScaffoldCmdConfig, ScaffoldConfigMap } from "./types"
import { spawn } from "node:child_process"
import { resolve, wrapNoopResolver } from "./utils"
import { findConfigFile } from "./config"
export async function getGitConfig(
url: URL,
file: string,
tmpPath: string,
logConfig: LogConfig,
): Promise<AsyncResolver<ScaffoldCmdConfig, ScaffoldConfigMap>> {
const repoUrl = `${url.protocol}//${url.host}${url.pathname}`
log(logConfig, LogLevel.debug, `Cloning git repo ${repoUrl}`)
return new Promise((res, reject) => {
log(logConfig, LogLevel.debug, `Cloning git repo to ${tmpPath}`)
const clone = spawn("git", ["clone", "--recurse-submodules", "--depth", "1", repoUrl, tmpPath])
clone.on("error", reject)
clone.on("close", async (code) => {
if (code === 0) {
res(await loadGitConfig({ logConfig, url: repoUrl, file, tmpPath }))
return
}
reject(new Error(`Git clone failed with code ${code}`))
})
})
}
/** @internal */
export async function loadGitConfig({
logConfig,
url: repoUrl,
file,
tmpPath,
}: {
logConfig: LogConfig
url: string
file: string
tmpPath: string
}): Promise<AsyncResolver<ScaffoldCmdConfig, ScaffoldConfigMap>> {
log(logConfig, LogLevel.debug, `Loading config from git repo: ${repoUrl}`)
const filename = file || (await findConfigFile(tmpPath))
const absolutePath = path.resolve(tmpPath, filename)
log(logConfig, LogLevel.debug, `Resolving config file: ${absolutePath}`)
const loadedConfig = await resolve(
async () => (await import(absolutePath)).default as ScaffoldConfigMap,
logConfig,
)
log(logConfig, LogLevel.debug, `Loaded config from git`)
log(logConfig, LogLevel.debug, `Raw config:`, loadedConfig)
const fixedConfig: ScaffoldConfigMap = {}
for (const [k, v] of Object.entries(loadedConfig)) {
fixedConfig[k] = {
...v,
templates: v.templates.map((t) => path.resolve(tmpPath, t)),
}
}
return wrapNoopResolver(fixedConfig)
}

67
src/ignore.ts Normal file
View File

@@ -0,0 +1,67 @@
import path from "node:path"
import fs from "node:fs/promises"
import { minimatch } from "minimatch"
import { pathExists } from "./fs-utils"
const IGNORE_FILENAME = ".scaffoldignore"
/**
* Reads a `.scaffoldignore` file from the given directory and returns
* the parsed patterns for filtering.
*
* Lines starting with `#` are comments. Empty lines are skipped.
*
* @param dir The directory to search for `.scaffoldignore`
* @returns Array of glob patterns to ignore
*/
export async function loadIgnorePatterns(dir: string): Promise<string[]> {
const ignorePath = path.resolve(dir, IGNORE_FILENAME)
if (!(await pathExists(ignorePath))) {
return []
}
const content = await fs.readFile(ignorePath, "utf-8")
return parseIgnoreFile(content)
}
/**
* Parses the contents of a `.scaffoldignore` file into glob patterns.
* @internal
*/
export function parseIgnoreFile(content: string): string[] {
return content
.split("\n")
.map((line) => line.trim())
.filter((line) => line.length > 0 && !line.startsWith("#"))
}
/**
* Filters a list of file paths, removing any that match the ignore patterns.
* Patterns are matched against the relative path from baseDir.
* Also always excludes `.scaffoldignore` itself.
*/
export function filterIgnoredFiles(
files: string[],
ignorePatterns: string[],
baseDir: string,
): string[] {
return files.filter((file) => {
const basename = path.basename(file)
if (basename === IGNORE_FILENAME) {
return false
}
const relPath = path.relative(baseDir, file)
for (const pattern of ignorePatterns) {
if (
minimatch(relPath, pattern, { dot: true }) ||
minimatch(basename, pattern, { dot: true })
) {
return false
}
}
return true
})
}

View File

@@ -1,4 +1,6 @@
export * from "./scaffold"
export * from "./types"
export { validateConfig, assertConfigValid, scaffoldConfigSchema } from "./validate"
import Scaffold from "./scaffold"
export default Scaffold

111
src/init.ts Normal file
View File

@@ -0,0 +1,111 @@
import path from "node:path"
import fs from "node:fs/promises"
import select from "@inquirer/select"
import { colorize } from "./colors"
import { pathExists } from "./fs-utils"
const CONFIG_EXTENSIONS = {
js: "scaffold.config.js",
mjs: "scaffold.config.mjs",
json: "scaffold.config.json",
} as const
type ConfigFormat = keyof typeof CONFIG_EXTENSIONS
const CONFIG_TEMPLATES: Record<ConfigFormat, string> = {
js: `/** @type {import('simple-scaffold').ScaffoldConfigMap} */
module.exports = {
default: {
templates: ["templates/default"],
output: ".",
// inputs: {
// author: { message: "Author name", required: true },
// license: { message: "License", default: "MIT" },
// },
},
}
`,
mjs: `/** @type {import('simple-scaffold').ScaffoldConfigMap} */
export default {
default: {
templates: ["templates/default"],
output: ".",
// inputs: {
// author: { message: "Author name", required: true },
// license: { message: "License", default: "MIT" },
// },
},
}
`,
json: `{
"default": {
"templates": ["templates/default"],
"output": "."
}
}
`,
}
const EXAMPLE_TEMPLATE_CONTENT = `# {{ name }}
Created by Simple Scaffold.
{{#if description}}{{ description }}{{/if}}
`
export interface InitOptions {
/** Working directory to create the config in. Defaults to cwd. */
dir?: string
/** Config format to use. If not provided, the user is prompted. */
format?: ConfigFormat
/** Whether to create an example template directory. Defaults to true. */
createExample?: boolean
}
/**
* Initializes a new Simple Scaffold project by creating a config file
* and an optional example template directory.
*/
export async function initScaffold(options: InitOptions = {}): Promise<void> {
const dir = options.dir ?? process.cwd()
const format =
options.format ??
(await select<ConfigFormat>({
message: colorize.cyan("Config file format:"),
choices: [
{ name: "JavaScript (CommonJS)", value: "js" },
{ name: "JavaScript (ESM)", value: "mjs" },
{ name: "JSON", value: "json" },
],
}))
const filename = CONFIG_EXTENSIONS[format]
const configPath = path.resolve(dir, filename)
if (await pathExists(configPath)) {
console.log(colorize.yellow(`${filename} already exists, skipping config creation.`))
} else {
await fs.writeFile(configPath, CONFIG_TEMPLATES[format])
console.log(colorize.green(`Created ${filename}`))
}
const createExample = options.createExample ?? true
if (createExample) {
const templateDir = path.resolve(dir, "templates", "default")
const templateFile = path.join(templateDir, "{{name}}.md")
if (await pathExists(templateDir)) {
console.log(colorize.yellow("templates/default/ already exists, skipping example template."))
} else {
await fs.mkdir(templateDir, { recursive: true })
await fs.writeFile(templateFile, EXAMPLE_TEMPLATE_CONTENT)
console.log(colorize.green("Created templates/default/{{name}}.md"))
}
}
console.log()
console.log(colorize.dim("Get started:"))
console.log(colorize.dim(` npx simple-scaffold MyProject`))
console.log()
}

View File

@@ -1,33 +1,54 @@
import util from "util"
import path from "node:path"
import { LogConfig, LogLevel, ScaffoldConfig } from "./types"
import chalk from "chalk"
import { colorize, TermColor } from "./colors"
export function log(config: LogConfig, level: LogLevel, ...obj: any[]): void {
if (config.quiet || config.verbose === LogLevel.None || level < (config.verbose ?? LogLevel.Info)) {
/** Priority ordering for log levels (higher = more severe). */
const LOG_PRIORITY: Record<LogLevel, number> = {
[LogLevel.none]: 0,
[LogLevel.debug]: 1,
[LogLevel.info]: 2,
[LogLevel.warning]: 3,
[LogLevel.error]: 4,
}
/** Maps each log level to a terminal color. */
const LOG_LEVEL_COLOR: Record<LogLevel, TermColor> = {
[LogLevel.none]: "reset",
[LogLevel.debug]: "dim",
[LogLevel.info]: "reset",
[LogLevel.warning]: "yellow",
[LogLevel.error]: "red",
}
/** Logs a message at the given level, respecting the configured log level filter. */
export function log(config: LogConfig, level: LogLevel, ...obj: unknown[]): void {
if (
config.logLevel === LogLevel.none ||
LOG_PRIORITY[level] < LOG_PRIORITY[config.logLevel ?? LogLevel.info]
) {
return
}
const levelColor: Record<LogLevel, keyof typeof chalk> = {
[LogLevel.None]: "reset",
[LogLevel.Debug]: "blue",
[LogLevel.Info]: "dim",
[LogLevel.Warning]: "yellow",
[LogLevel.Error]: "red",
}
const chalkFn: any = chalk[levelColor[level]]
const key: "log" | "warn" | "error" = level === LogLevel.Error ? "error" : level === LogLevel.Warning ? "warn" : "log"
const logFn: any = console[key]
const colorFn = colorize[LOG_LEVEL_COLOR[level]]
const key: "log" | "warn" | "error" =
level === LogLevel.error ? "error" : level === LogLevel.warning ? "warn" : "log"
const logFn: (..._args: unknown[]) => void = console[key]
logFn(
...obj.map((i) =>
i instanceof Error
? chalkFn(i, JSON.stringify(i, undefined, 1), i.stack)
? colorFn(i, JSON.stringify(i, undefined, 1), i.stack)
: typeof i === "object"
? chalkFn(JSON.stringify(i, undefined, 1))
: chalkFn(i),
? util.inspect(i, { depth: null, colors: true })
: colorFn(i),
),
)
}
/**
* Logs detailed file processing information at debug level.
* @deprecated Use `log(config, LogLevel.debug, data)` directly instead.
*/
export function logInputFile(
config: ScaffoldConfig,
data: {
@@ -41,25 +62,74 @@ export function logInputFile(
isGlob: boolean
},
): void {
log(config, LogLevel.Debug, data)
log(config, LogLevel.debug, data)
}
/** Logs the full scaffold configuration at debug level. */
export function logInitStep(config: ScaffoldConfig): void {
log(config, LogLevel.Debug, "Full config:", {
log(config, LogLevel.debug, "Full config:", {
name: config.name,
templates: config.templates,
output: config.output,
createSubFolder: config.createSubFolder,
subdir: config.subdir,
data: config.data,
overwrite: config.overwrite,
quiet: config.quiet,
subFolderNameHelper: config.subFolderNameHelper,
subdirHelper: config.subdirHelper,
helpers: Object.keys(config.helpers ?? {}),
verbose: `${config.verbose} (${Object.keys(LogLevel).find(
(k) => (LogLevel[k as any] as unknown as number) === config.verbose!,
)})`,
logLevel: config.logLevel,
dryRun: config.dryRun,
beforeWrite: config.beforeWrite,
} as Record<keyof ScaffoldConfig, unknown>)
log(config, LogLevel.Info, "Data:", config.data)
}
/**
* Logs a tree of created files, grouped by directory.
*/
export function logFileTree(config: LogConfig, files: string[]): void {
if (files.length === 0) return
// Find common prefix to make paths relative
const commonDir = files.reduce((prefix, file) => {
while (!file.startsWith(prefix)) {
prefix = path.dirname(prefix)
}
return prefix
}, path.dirname(files[0]))
log(config, LogLevel.info, "")
log(config, LogLevel.info, colorize.bold(`📁 ${commonDir}`))
const relPaths = files.map((f) => path.relative(commonDir, f)).sort()
for (let i = 0; i < relPaths.length; i++) {
const isLast = i === relPaths.length - 1
const prefix = isLast ? "└── " : "├── "
log(config, LogLevel.info, colorize.dim(prefix) + relPaths[i])
}
}
/**
* Logs a final summary line with file count and elapsed time.
*/
export function logSummary(
config: LogConfig,
fileCount: number,
elapsedMs: number,
dryRun?: boolean,
): void {
const timeStr =
elapsedMs < 1000 ? `${Math.round(elapsedMs)}ms` : `${(elapsedMs / 1000).toFixed(2)}s`
log(config, LogLevel.info, "")
if (dryRun) {
log(
config,
LogLevel.info,
colorize.yellow(`🏜️ Dry run complete — ${fileCount} file(s) would be created (${timeStr})`),
)
} else if (fileCount === 0) {
log(config, LogLevel.info, colorize.yellow(`⚠️ No files created (${timeStr})`))
} else {
log(config, LogLevel.info, colorize.green(`✅ Created ${fileCount} file(s) in ${timeStr}`))
}
}

View File

@@ -1,20 +1,10 @@
import path from "path"
import path from "node:path"
import { DefaultHelpers, Helper, LogLevel, ScaffoldConfig } from "./types"
import camelCase from "lodash/camelCase"
import snakeCase from "lodash/snakeCase"
import kebabCase from "lodash/kebabCase"
import startCase from "lodash/startCase"
import Handlebars from "handlebars"
import dtAdd from "date-fns/add"
import dtFormat from "date-fns/format"
import dtParseISO from "date-fns/parseISO"
import { add, format, parseISO, type Duration } from "date-fns"
import { log } from "./logger"
const dateFns = {
add: dtAdd,
format: dtFormat,
parseISO: dtParseISO,
}
const dateFns = { add, format, parseISO }
export const defaultHelpers: Record<DefaultHelpers, Helper> = {
camelCase,
@@ -29,14 +19,14 @@ export const defaultHelpers: Record<DefaultHelpers, Helper> = {
date: dateHelper,
}
export function _dateHelper(date: Date, formatString: string): string
export function _dateHelper(
function _dateHelper(date: Date, formatString: string): string
function _dateHelper(
date: Date,
formatString: string,
durationDifference: number,
durationType: keyof Duration,
): string
export function _dateHelper(
function _dateHelper(
date: Date,
formatString: string,
durationDifference?: number,
@@ -49,8 +39,16 @@ export function _dateHelper(
}
export function nowHelper(formatString: string): string
export function nowHelper(formatString: string, durationDifference: number, durationType: keyof Duration): string
export function nowHelper(formatString: string, durationDifference?: number, durationType?: keyof Duration): string {
export function nowHelper(
formatString: string,
durationDifference: number,
durationType: keyof Duration,
): string
export function nowHelper(
formatString: string,
durationDifference?: number,
durationType?: keyof Duration,
): string {
return _dateHelper(new Date(), formatString, durationDifference!, durationType!)
}
@@ -61,7 +59,6 @@ export function dateHelper(
durationDifference: number,
durationType: keyof Duration,
): string
export function dateHelper(
date: string,
formatString: string,
@@ -71,14 +68,49 @@ export function dateHelper(
return _dateHelper(dateFns.parseISO(date), formatString, durationDifference!, durationType!)
}
export function pascalCase(s: string): string {
// splits by either non-alphanumeric character or capital letter boundaries
function toWordParts(string: string): string[] {
// First split on non-alphanumeric characters
return string
.split(/[^a-zA-Z0-9]/)
.flatMap((segment) =>
// Then split camelCase/PascalCase boundaries, handling consecutive uppercase (e.g. "HTMLParser" -> "HTML", "Parser")
segment.split(/(?<=[a-z0-9])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])/),
)
.filter((s) => s.length > 0)
}
function camelCase(s: string): string {
return toWordParts(s).reduce((acc, part, i) => {
if (i === 0) {
return part.toLowerCase()
}
return acc + part[0].toUpperCase() + part.slice(1).toLowerCase()
}, "")
}
function snakeCase(s: string): string {
return toWordParts(s).join("_").toLowerCase()
}
function kebabCase(s: string): string {
return toWordParts(s).join("-").toLowerCase()
}
function startCase(s: string): string {
return toWordParts(s)
.map((part) => part[0].toUpperCase() + part.slice(1).toLowerCase())
.join(" ")
}
function pascalCase(s: string): string {
return startCase(s).replace(/\s+/g, "")
}
export function registerHelpers(config: ScaffoldConfig): void {
const _helpers = { ...defaultHelpers, ...config.helpers }
for (const helperName in _helpers) {
log(config, LogLevel.Debug, `Registering helper: ${helperName}`)
log(config, LogLevel.debug, `Registering helper: ${helperName}`)
Handlebars.registerHelper(helperName, _helpers[helperName as keyof typeof _helpers])
}
}
@@ -86,23 +118,23 @@ export function registerHelpers(config: ScaffoldConfig): void {
export function handlebarsParse(
config: ScaffoldConfig,
templateBuffer: Buffer | string,
{ isPath = false }: { isPath?: boolean } = {},
{ asPath = false }: { asPath?: boolean } = {},
): Buffer {
const { data } = config
try {
let str = templateBuffer.toString()
if (isPath) {
if (asPath) {
str = str.replace(/\\/g, "/")
}
const parser = Handlebars.compile(str, { noEscape: true })
let outputContents = parser(data)
if (isPath && path.sep !== "/") {
if (asPath && path.sep !== "/") {
outputContents = outputContents.replace(/\//g, "\\")
}
return Buffer.from(outputContents)
} catch (e) {
log(config, LogLevel.Debug, e)
log(config, LogLevel.Warning, "Couldn't parse file with handlebars, returning original content")
log(config, LogLevel.debug, e)
log(config, LogLevel.debug, "Couldn't parse file with handlebars, returning original content")
return Buffer.from(templateBuffer)
}
}

19
src/path-utils.ts Normal file
View File

@@ -0,0 +1,19 @@
import path from "node:path"
/** Strips glob wildcard characters from a template path. */
export function removeGlob(template: string): string {
return path.normalize(template.replace(/\*/g, ""))
}
/** Removes a leading path separator, making the path relative. */
export function makeRelativePath(str: string): string {
return str.startsWith(path.sep) ? str.slice(1) : str
}
/** Computes a base path relative to the current working directory. */
export function getBasePath(relPath: string): string {
return path
.resolve(process.cwd(), relPath)
.replace(process.cwd() + path.sep, "")
.replace(process.cwd(), "")
}

253
src/prompts.ts Normal file
View File

@@ -0,0 +1,253 @@
import input from "@inquirer/input"
import select from "@inquirer/select"
import confirm from "@inquirer/confirm"
import number from "@inquirer/number"
import { colorize } from "./colors"
import {
ScaffoldCmdConfig,
ScaffoldConfig,
ScaffoldConfigMap,
ScaffoldInput,
ScaffoldInputType,
} from "./types"
/** Prompts the user for a scaffold name. */
export async function promptForName(): Promise<string> {
return input({
message: colorize.cyan("Scaffold name:"),
required: true,
validate: (value) => {
if (!value.trim()) return "Name cannot be empty"
return true
},
})
}
/** Prompts the user to select a template key from the available config keys. */
export async function promptForTemplateKey(configMap: ScaffoldConfigMap): Promise<string> {
const keys = Object.keys(configMap)
if (keys.length === 0) {
throw new Error("No templates found in config file")
}
if (keys.length === 1) {
return keys[0]
}
return select({
message: colorize.cyan("Select a template:"),
choices: keys.map((key) => ({
name: key,
value: key,
})),
})
}
/** Prompts the user for an output directory path. */
export async function promptForOutput(): Promise<string> {
return input({
message: colorize.cyan("Output directory:"),
required: true,
default: ".",
validate: (value) => {
if (!value.trim()) return "Output directory cannot be empty"
return true
},
})
}
/** Prompts the user for template paths (comma-separated). */
export async function promptForTemplates(): Promise<string[]> {
const value = await input({
message: colorize.cyan("Template paths (comma-separated):"),
required: true,
validate: (value) => {
if (!value.trim()) return "At least one template path is required"
return true
},
})
return value
.split(",")
.map((t) => t.trim())
.filter(Boolean)
}
/** Prompts for a single input based on its type. */
async function promptSingleInput(
key: string,
def: ScaffoldInput,
): Promise<string | boolean | number | undefined> {
const type: ScaffoldInputType = def.type ?? "text"
const message = colorize.cyan(def.message ?? `${key}:`)
switch (type) {
case "text":
return input({
message,
required: def.required,
default: def.default as string | undefined,
validate: def.required
? (value) => {
if (!value.trim()) return `${key} is required`
return true
}
: undefined,
})
case "select": {
const choices = (def.options ?? []).map((opt) =>
typeof opt === "string" ? { name: opt, value: opt } : opt,
)
if (choices.length === 0) {
throw new Error(`Input "${key}" has type "select" but no options defined`)
}
return select({
message,
choices,
default: def.default as string | undefined,
})
}
case "confirm":
return confirm({
message,
default: (def.default as boolean | undefined) ?? false,
})
case "number":
return (
(await number({
message,
required: def.required,
default: def.default as number | undefined,
})) ?? def.default
)
}
}
/**
* Prompts the user for any required scaffold inputs that are not already provided in data.
* Also applies default values for optional inputs that have one.
* Returns the merged data object.
*/
export async function promptForInputs(
inputs: Record<string, ScaffoldInput>,
existingData: Record<string, unknown> = {},
): Promise<Record<string, unknown>> {
const data = { ...existingData }
for (const [key, def] of Object.entries(inputs)) {
// Skip if already provided via data/CLI
if (key in data && data[key] !== undefined && data[key] !== "") {
continue
}
if (def.required || def.type === "select" || def.type === "confirm") {
data[key] = await promptSingleInput(key, def)
} else if (def.default !== undefined && !(key in data)) {
data[key] = def.default
}
}
return data
}
/** Returns true if the process is running in an interactive terminal. */
export function isInteractive(): boolean {
return Boolean(process.stdin.isTTY)
}
/**
* Prompts for name and template key before the config file is parsed.
* These are needed by parseConfigFile to know which template to load.
*/
export async function promptBeforeConfig(
config: ScaffoldCmdConfig,
configMap?: ScaffoldConfigMap,
): Promise<ScaffoldCmdConfig> {
if (!isInteractive()) {
return config
}
if (!config.name) {
config.name = await promptForName()
}
if (configMap && !config.key) {
const keys = Object.keys(configMap)
if (keys.length > 1) {
config.key = await promptForTemplateKey(configMap)
}
}
return config
}
/**
* Prompts for any values still missing after the config file has been parsed.
* Only prompts in interactive mode.
*/
export async function promptAfterConfig(config: ScaffoldConfig): Promise<ScaffoldConfig> {
if (!isInteractive()) {
return config
}
if (!config.output || (typeof config.output === "string" && !config.output)) {
config.output = await promptForOutput()
}
if (!config.templates || config.templates.length === 0) {
config.templates = await promptForTemplates()
}
return config
}
/**
* @deprecated Use {@link promptBeforeConfig} and {@link promptAfterConfig} instead.
*/
export async function promptForMissingConfig(
config: ScaffoldCmdConfig,
configMap?: ScaffoldConfigMap,
): Promise<ScaffoldCmdConfig> {
const afterPre = await promptBeforeConfig(config, configMap)
if (!isInteractive()) {
return afterPre
}
if (!afterPre.output) {
afterPre.output = await promptForOutput()
}
if (!afterPre.templates || afterPre.templates.length === 0) {
afterPre.templates = await promptForTemplates()
}
return afterPre
}
/**
* Prompts for any required inputs defined in the scaffold config and merges them into data.
* Only prompts in interactive mode; in non-interactive mode, only applies defaults.
*/
export async function resolveInputs(config: ScaffoldConfig): Promise<ScaffoldConfig> {
if (!config.inputs) {
return config
}
const interactive = isInteractive()
if (interactive) {
config.data = await promptForInputs(config.inputs, config.data)
} else {
// Non-interactive: only apply defaults
const data = { ...config.data }
for (const [key, def] of Object.entries(config.inputs)) {
if (def.default !== undefined && !(key in data)) {
data[key] = def.default
}
}
config.data = data
}
return config
}

View File

@@ -4,22 +4,20 @@
*
* See [readme](README.md)
*/
import path from "path"
import path from "node:path"
import os from "node:os"
import { exec } from "node:child_process"
import { handleErr, resolve } from "./utils"
import {
isDir,
removeGlob,
makeRelativePath,
getTemplateGlobInfo,
getFileList,
getBasePath,
handleTemplateFile,
} from "./file"
import { isDir, getTemplateGlobInfo, getFileList, handleTemplateFile, GlobInfo } from "./file"
import { removeGlob, makeRelativePath, getBasePath } from "./path-utils"
import { LogLevel, MinimalConfig, Resolver, ScaffoldCmdConfig, ScaffoldConfig } from "./types"
import { OptionsBase } from "massarg/types"
import { pascalCase, registerHelpers } from "./parser"
import { log, logInitStep, logInputFile } from "./logger"
import { parseConfig } from "./config"
import { registerHelpers } from "./parser"
import { log, logInitStep, logFileTree, logSummary } from "./logger"
import { parseConfigFile } from "./config"
import { resolveInputs } from "./prompts"
import { loadIgnorePatterns, filterIgnoredFiles } from "./ignore"
import { assertConfigValid } from "./validate"
/**
* Create a scaffold using given `options`.
@@ -56,46 +54,135 @@ import { parseConfig } from "./config"
export async function Scaffold(config: ScaffoldConfig): Promise<void> {
config.output ??= process.cwd()
await assertConfigValid(config)
config = await resolveInputs(config)
registerHelpers(config)
const startTime = performance.now()
const writtenFiles: string[] = []
try {
config.data = { name: config.name, Name: pascalCase(config.name), ...config.data }
config.data = { name: config.name, ...config.data }
logInitStep(config)
for (let _template of config.templates) {
try {
const { nonGlobTemplate, origTemplate, isDirOrGlob, isGlob, template } = await getTemplateGlobInfo(
config,
_template,
)
const files = await getFileList(config, template)
for (const inputFilePath of files) {
if (await isDir(inputFilePath)) {
continue
}
const relPath = makeRelativePath(path.dirname(removeGlob(inputFilePath).replace(nonGlobTemplate, "")))
const basePath = getBasePath(relPath)
logInputFile(config, {
originalTemplate: origTemplate,
relativePath: relPath,
parsedTemplate: template,
inputFilePath,
nonGlobTemplate,
basePath,
isDirOrGlob,
isGlob,
})
await handleTemplateFile(config, {
templatePath: inputFilePath,
basePath,
})
}
} catch (e: any) {
handleErr(e)
}
log(config, LogLevel.info, `Scaffolding "${config.name}"...`)
const excludes = config.templates.filter((t) => t.startsWith("!"))
const includes = config.templates.filter((t) => !t.startsWith("!"))
const templates = await resolveTemplateGlobs(config, includes)
for (const tpl of templates) {
const files = await processTemplateGlob(config, tpl, excludes)
writtenFiles.push(...files)
}
} catch (e: any) {
log(config, LogLevel.Error, e)
} catch (e: unknown) {
log(config, LogLevel.error, e)
throw e
}
const elapsed = performance.now() - startTime
logFileTree(config, writtenFiles)
logSummary(config, writtenFiles.length, elapsed, config.dryRun)
if (config.afterScaffold) {
await runAfterScaffoldHook(config, writtenFiles)
}
}
/** Resolves included template paths into GlobInfo objects. */
async function resolveTemplateGlobs(
config: ScaffoldConfig,
includes: string[],
): Promise<GlobInfo[]> {
const templates: GlobInfo[] = []
for (const includedTemplate of includes) {
try {
templates.push(await getTemplateGlobInfo(config, includedTemplate))
} catch (e: unknown) {
handleErr(e as NodeJS.ErrnoException)
}
}
return templates
}
/** Processes all files matching a single template glob pattern. Returns paths of written files. */
async function processTemplateGlob(
config: ScaffoldConfig,
tpl: GlobInfo,
excludes: string[],
): Promise<string[]> {
const written: string[] = []
// Load .scaffoldignore from the template base directory
const ignorePatterns = await loadIgnorePatterns(tpl.baseTemplatePath)
if (ignorePatterns.length > 0) {
log(config, LogLevel.debug, `Loaded .scaffoldignore patterns:`, ignorePatterns)
}
const allFiles = await getFileList(config, [tpl.template, ...excludes])
const files = filterIgnoredFiles(allFiles, ignorePatterns, tpl.baseTemplatePath)
for (const file of files) {
if (await isDir(file)) {
continue
}
log(config, LogLevel.debug, "Iterating files", { files, file })
const relPath = makeRelativePath(
path.dirname(removeGlob(file).replace(tpl.baseTemplatePath, "")),
)
const basePath = getBasePath(relPath)
log(config, LogLevel.debug, {
originalTemplate: tpl.origTemplate,
relativePath: relPath,
parsedTemplate: tpl.template,
inputFilePath: file,
baseTemplatePath: tpl.baseTemplatePath,
basePath,
isDirOrGlob: tpl.isDirOrGlob,
isGlob: tpl.isGlob,
})
const outputPath = await handleTemplateFile(config, { templatePath: file, basePath })
if (outputPath) {
written.push(outputPath)
}
}
return written
}
/** Executes the afterScaffold hook — either a function or a shell command string. */
async function runAfterScaffoldHook(config: ScaffoldConfig, files: string[]): Promise<void> {
const hook = config.afterScaffold!
if (typeof hook === "function") {
log(config, LogLevel.debug, "Running afterScaffold function hook")
await hook({ config, files })
return
}
// Shell command string
const outputDir = typeof config.output === "string" ? config.output : process.cwd()
const cwd = path.resolve(process.cwd(), outputDir)
log(config, LogLevel.info, `Running afterScaffold command: ${hook}`)
await new Promise<void>((resolve, reject) => {
const proc = exec(hook, { cwd })
proc.stdout?.on("data", (data: string) => {
log(config, LogLevel.info, data.toString().trimEnd())
})
proc.stderr?.on("data", (data: string) => {
log(config, LogLevel.warning, data.toString().trimEnd())
})
proc.on("close", (code) => {
if (code === 0) {
resolve()
} else {
reject(new Error(`afterScaffold command exited with code ${code}`))
}
})
proc.on("error", reject)
})
}
/**
@@ -110,28 +197,26 @@ export async function Scaffold(config: ScaffoldConfig): Promise<void> {
* @return {Promise<void>} A promise that resolves when the scaffold is complete
*/
Scaffold.fromConfig = async function (
/** The path or URL to the config file */
pathOrUrl: string,
/** Information needed before loading the config */
config: MinimalConfig,
/** Any overrides to the loaded config */
overrides?: Resolver<ScaffoldCmdConfig, Partial<Omit<ScaffoldConfig, "name">>>,
): Promise<void> {
const _cmdConfig: ScaffoldCmdConfig & OptionsBase = {
const tmpPath = path.resolve(os.tmpdir(), `scaffold-config-${Date.now()}`)
const _cmdConfig: ScaffoldCmdConfig = {
dryRun: false,
output: process.cwd(),
verbose: LogLevel.Info,
logLevel: LogLevel.info,
overwrite: false,
templates: [],
createSubFolder: false,
subdir: false,
quiet: false,
help: false,
extras: [],
config: pathOrUrl,
version: false,
tmpDir: tmpPath,
...config,
}
const _overrides = resolve(overrides, _cmdConfig)
const _config = await parseConfig(_cmdConfig)
const _config = await parseConfigFile(_cmdConfig)
return Scaffold({ ..._config, ..._overrides })
}

View File

@@ -3,8 +3,8 @@ import { HelperDelegate } from "handlebars/runtime"
/**
* The config object for defining a scaffolding group.
*
* @see {@link https://chenasraf.github.io/simple-scaffold/pages/node.html | Node.js usage}
* @see {@link https://chenasraf.github.io/simple-scaffold/pages/cli.html | CLI usage}
* @see {@link https://chenasraf.github.io/simple-scaffold/docs/usage/node| Node.js usage}
* @see {@link https://chenasraf.github.io/simple-scaffold/docs/usage/cli| CLI usage}
* @see {@link DefaultHelpers}
* @see {@link CaseHelpers}
* @see {@link DateHelpers}
@@ -22,12 +22,17 @@ export interface ScaffoldConfig {
* Template files to use as input. You may provide multiple files, each of which can be a relative or absolute path,
* or a glob pattern for multiple file matching easily.
*
* You may omit files from output by prepending a `!` to their glob pattern.
*
* For example, `["components/**", "!components/README.md"]` will include everything in the directory `components`
* except the `README.md` file inside.
*
* @default Current working directory
*/
templates: string[]
/**
* Path to output to. If `createSubFolder` is `true`, the subfolder will be created inside this path.
* Path to output to. If `subdir` is `true`, the subdir will be created inside this path.
*
* May also be a {@link FileResponseHandler} which returns a new output path to override the default one.
*
@@ -37,21 +42,21 @@ export interface ScaffoldConfig {
output: FileResponse<string>
/**
* Whether to create subfolder with the input name.
* Whether to create subdir with the input name.
*
* When `true`, you may also use {@link subFolderNameHelper} to determine a pre-process helper on
* When `true`, you may also use {@link subdirHelper} to determine a pre-process helper on
* the directory name.
*
* @default `false`
*/
createSubFolder?: boolean
subdir?: boolean
/**
* Add custom data to the templates. By default, only your app name is included as `{{name}}` and `{{Name}}`.
*
* This can be any object that will be usable by Handlebars.
*/
data?: Record<string, any>
data?: Record<string, unknown>
/**
* Enable to override output files, even if they already exist.
@@ -68,12 +73,6 @@ export interface ScaffoldConfig {
*/
overwrite?: FileResponse<boolean>
/**
* Suppress output logs (Same as `verbose: 0` or `verbose: LogLevel.None`)
* @see {@link verbose}
*/
quiet?: boolean
/**
* Determine amount of logs to display.
*
@@ -84,7 +83,7 @@ export interface ScaffoldConfig {
*
* @default `2 (info)`
*/
verbose?: LogLevel
logLevel?: LogLevel
/**
* Don't emit files. This is good for testing your scaffolds and making sure they don't fail, without having to write
@@ -132,20 +131,20 @@ export interface ScaffoldConfig {
* @see {@link DefaultHelpers}
* @see {@link CaseHelpers}
* @see {@link DateHelpers}
* @see {@link https://chenasraf.github.io/simple-scaffold/pages/templates.html | Templates}
* @see {@link https://chenasraf.github.io/simple-scaffold/docs/usage/templates| Templates}
* */
helpers?: Record<string, Helper>
/**
* Default transformer to apply to subfolder name when using `createSubFolder: true`. Can be one of the default
* Default transformer to apply to subdir name when using `subdir: true`. Can be one of the default
* capitalization helpers, or a custom one you provide to `helpers`. Defaults to `undefined`, which means no
* transformation is done.
*
* @see {@link createSubFolder}
* @see {@link subdir}
* @see {@link CaseHelpers}
* @see {@link DefaultHelpers}
*/
subFolderNameHelper?: DefaultHelpers | string
subdirHelper?: DefaultHelpers | string
/**
* This callback runs right before content is being written to the disk. If you supply this function, you may return
@@ -166,12 +165,121 @@ export interface ScaffoldConfig {
rawContent: Buffer,
outputPath: string,
): string | Buffer | undefined | Promise<string | Buffer | undefined>
/**
* Defines interactive inputs for the template. Each input becomes a template data variable.
*
* When running interactively, required inputs that are not already provided via `data` or CLI args
* will be prompted for. Optional inputs without a value will use their `default` if defined.
*
* @example
* ```typescript
* Scaffold({
* // ...
* inputs: {
* author: { message: "Author name", required: true },
* license: { message: "License", default: "MIT" },
* },
* })
* ```
*
* In templates: `{{ author }}`, `{{ license }}`
*
* @see {@link ScaffoldInput}
*/
inputs?: Record<string, ScaffoldInput>
/**
* A callback or shell command that runs after all files have been written.
*
* When provided as a **function** (Node.js API), it receives a context object with the scaffold
* config and the list of files that were written:
*
* ```typescript
* Scaffold({
* // ...
* afterScaffold: async ({ config, files }) => {
* console.log(`Created ${files.length} files`)
* execSync("npm install", { cwd: config.output })
* },
* })
* ```
*
* When provided as a **string** (CLI `--after` flag), it is executed as a shell command
* in the output directory after scaffolding completes.
*
* @see {@link AfterScaffoldContext}
*/
afterScaffold?: AfterScaffoldHook
/** @internal */
tmpDir?: string
}
/**
* Context passed to the {@link ScaffoldConfig.afterScaffold} hook.
*
* @category Config
*/
export interface AfterScaffoldContext {
/** The resolved scaffold config that was used. */
config: ScaffoldConfig
/** List of absolute paths to files that were written. */
files: string[]
}
/**
* A hook that runs after scaffolding completes.
* Can be a function receiving context, or a shell command string.
*
* @category Config
*/
export type AfterScaffoldHook = ((context: AfterScaffoldContext) => void | Promise<void>) | string
/**
* The type of an interactive input prompt.
*
* - `"text"` — free-form text input (default)
* - `"select"` — choose from a list of options
* - `"confirm"` — yes/no boolean prompt
* - `"number"` — numeric input
*
* @category Config
*/
export type ScaffoldInputType = "text" | "select" | "confirm" | "number"
/**
* Defines a single interactive input for a scaffold template.
*
* @example
* ```typescript
* inputs: {
* author: { message: "Author name", required: true },
* license: { type: "select", message: "License", options: ["MIT", "Apache-2.0", "GPL-3.0"] },
* private: { type: "confirm", message: "Private package?", default: false },
* port: { type: "number", message: "Dev server port", default: 3000 },
* }
* ```
*
* @category Config
*/
export interface ScaffoldInput {
/** The type of prompt. Defaults to `"text"`. */
type?: ScaffoldInputType
/** The prompt message shown to the user. Defaults to the input key name if omitted. */
message?: string
/** Whether this input must be provided. If true and missing, the user will be prompted interactively. */
required?: boolean
/** Default value. Type depends on the input type: string for text/select, boolean for confirm, number for number. */
default?: string | boolean | number
/** List of options for `type: "select"`. Each can be a string or `{ name, value }`. */
options?: (string | { name: string; value: string })[]
}
/**
* The names of the available helper functions that relate to text capitalization.
*
* These are available for `subfolderNameHelper`.
* These are available for `subdirHelper`.
*
* | Helper name | Example code | Example output |
* | ------------ | ----------------------- | -------------- |
@@ -188,7 +296,7 @@ export interface ScaffoldConfig {
* @see {@link DefaultHelpers}
* @see {@link DateHelpers}
* @see {@link ScaffoldConfig}
* @see {@link ScaffoldConfig.subFolderNameHelper}
* @see {@link ScaffoldConfig.subdirHelper}
*
* @category Helpers
*/
@@ -265,31 +373,45 @@ export type Helper = HelperDelegate
/**
* The amount of information to log when generating scaffold.
* When not `None`, the selected level will be the lowest level included.
* When not `none`, the selected level will be the lowest level included.
*
* For example, level `Info` (2) will include `Info`, `Warning` and `Error`, but not `Debug`; and `Warning` will only
* show `Warning` and `Error`.
* For example, level `info` will include `info`, `warning` and `error`, but not `debug`; and `warning` will only
* show `warning` and `error`, but not `info` or `debug`.
*
* @default `2 (info)`
* @default `info`
*
* @category Logging
* @category Logging (const)
*/
export enum LogLevel {
export const LogLevel = {
/** Silent output */
None = 0,
none: "none",
/** Debugging information. Very verbose and only recommended for troubleshooting. */
Debug = 1,
debug: "debug",
/**
* The regular level of logging. Major actions are logged to show the scaffold progress.
*
* @default
*/
Info = 2,
info: "info",
/** Warnings such as when file fails to replace token values properly in template. */
Warning = 3,
warning: "warning",
/** Errors, such as missing files, bad replacement token syntax, or un-writable directories. */
Error = 4,
}
error: "error",
} as const
/**
* The amount of information to log when generating scaffold.
* When not `none`, the selected level will be the lowest level included.
*
* For example, level `info` will include `info`, `warning` and `error`, but not `debug`; and `warning` will only
* show `warning` and `error`, but not `info` or `debug`.
*
* @default `info`
*
* @category Logging (type)
*/
export type LogLevel = (typeof LogLevel)[keyof typeof LogLevel]
/**
* A function that takes path information about file, and returns a value of type `T`
@@ -315,34 +437,61 @@ export type FileResponseHandler<T> = (fullPath: string, basedir: string, basenam
* (fullPath: string, basedir: string, basename: string) => T
* ```
*
* @typedef T The return type
*
* @see {@link FileResponseHandler}
*
* @category Config
* */
export type FileResponse<T> = T | FileResponseHandler<T>
/** @internal */
export interface ScaffoldCmdConfig {
/**
* Name to be passed to the generated files. `{{name}}` and `{{Name}}` inside contents and file names will be replaced
* accordingly.
*/
/**
* The Scaffold config for CLI
* Contains less and more specific options than {@link ScaffoldConfig}.
*
* For more information about each option, see {@link ScaffoldConfig}.
*
* @internal
*/
export type ScaffoldCmdConfig = {
/** The name of the scaffold template to use. */
name: string
/** The templates to use for generation */
templates: string[]
/** The output path to write to */
output: string
createSubFolder: boolean
/** Whether to create subdir with the input name */
subdir: boolean
/** Default transformer to apply to subdir name when using `subdir: true` */
subdirHelper?: string
/** Add custom data to the templates */
data?: Record<string, string>
/** Add custom data to the template in a CLI-friendly syntax (and not JSON) */
appendData?: Record<string, string>
/** Enable to override output files, even if they already exist */
overwrite: boolean
/** Silence logs, same as `logLevel: "none"` */
quiet: boolean
verbose: LogLevel
/**
* Determine amount of logs to display.
*
* @see {@link LogLevel}
*/
logLevel: LogLevel
/** Don't emit files. This is good for testing your scaffolds and making sure they don't fail, without having to write actual file contents or create directories. */
dryRun: boolean
/** Config file path to use */
config?: string
/** The key to use for the file which contains the template configurations. */
/** The key of the template to use */
key?: string
github?: string
/** The git repository to use to fetch the config file */
git?: string
/** Display version */
version: boolean
/** Run a script before writing the files. This can be a command or a path to a file. The file contents will be passed to the given command. */
beforeWrite?: string
/** Run a shell command after all files have been written. Executed in the output directory. */
afterScaffold?: string
/** @internal */
tmpDir?: string
}
/**
@@ -355,28 +504,42 @@ export interface ScaffoldCmdConfig {
* When no template key is provided to the scaffold command, the "default" template is used.
*
* @see {@link ScaffoldConfig}
*
* @internal
* @category Config
*/
export type ScaffoldConfigMap = Record<string, ScaffoldConfig>
/** The scaffold config file is either:
/**
* The scaffold config file is either:
* - A {@link ScaffoldConfigMap} object
* - A function that returns a {@link ScaffoldConfigMap} object
* - A promise that resolves to a {@link ScaffoldConfigMap} object
* - A function that returns a promise that resolves to a {@link ScaffoldConfigMap} object
*
* @internal
* @category Config
*/
export type ScaffoldConfigFile = AsyncResolver<ScaffoldCmdConfig, ScaffoldConfigMap>
/** @internal */
export type Resolver<T, R = T> = R | ((value: T) => R)
export type Resolver<T, R = T> = R | ((_value: T) => R)
/** @internal */
export type AsyncResolver<T, R = T> = Resolver<T, Promise<R> | R>
/** @internal */
export type LogConfig = Pick<ScaffoldConfig, "quiet" | "verbose">
export type LogConfig = Pick<ScaffoldConfig, "logLevel">
/** @internal */
export type ConfigLoadConfig = LogConfig & Pick<ScaffoldCmdConfig, "config">
/** @internal */
export type RemoteConfigLoadConfig = LogConfig &
Pick<ScaffoldCmdConfig, "config" | "git" | "tmpDir">
/** @internal */
export type MinimalConfig = Pick<ScaffoldCmdConfig, "name" | "key">
/** @internal */
export type ListCommandCliOptions = Pick<ScaffoldCmdConfig, "config" | "git" | "logLevel" | "quiet">

View File

@@ -1,9 +1,23 @@
import { Resolver } from "./types"
// Re-export colors for backward compatibility
export { colorize, type TermColor } from "./colors"
/** Throws the error if non-null, no-ops otherwise. */
export function handleErr(err: NodeJS.ErrnoException | null): void {
if (err) throw err
}
/** Resolves a value that may be either a static value or a function that produces one. */
export function resolve<T, R = T>(resolver: Resolver<T, R>, arg: T): R {
return typeof resolver === "function" ? (resolver as (value: T) => R)(arg) : (resolver as R)
}
/** Wraps a static value in a resolver function. If already a function, returns as-is. */
export function wrapNoopResolver<T, R = T>(value: Resolver<T, R>): Resolver<T, R> {
if (typeof value === "function") {
return value
}
return (_) => value
}

171
src/validate.ts Normal file
View File

@@ -0,0 +1,171 @@
import { z } from "zod/v4"
import { pathExists } from "./fs-utils"
// --- Reusable schemas ---
/** Schema for a JavaScript function value. */
const functionSchema = z
.any()
.refine((v) => typeof v === "function", { message: "Expected a function" })
/** Schema for a value that can be either a string or a function. */
const stringOrFunctionSchema = z.union([z.string(), functionSchema])
/** Schema for a value that can be either a boolean or a function. */
const booleanOrFunctionSchema = z.union([z.boolean(), functionSchema])
/** Schema for a select input option — either a plain string or a `{ name, value }` object. */
const selectOptionSchema = z.union([z.string(), z.object({ name: z.string(), value: z.string() })])
/** Schema for the input type enum. */
const inputTypeSchema = z.enum(["text", "select", "confirm", "number"])
/** Schema for the log level enum. */
const logLevelSchema = z.enum(["none", "debug", "info", "warning", "error"])
// --- Input schema ---
/** Zod schema for a single scaffold input definition. */
const scaffoldInputSchema = z.object({
type: inputTypeSchema.optional(),
message: z.string().optional(),
required: z.boolean().optional(),
default: z.union([z.string(), z.boolean(), z.number()]).optional(),
options: z.array(selectOptionSchema).optional(),
})
type InputDef = z.infer<typeof scaffoldInputSchema>
function validateInputSemantics(
key: string,
input: InputDef,
): { path: (string | number)[]; message: string }[] {
const issues: { path: (string | number)[]; message: string }[] = []
if (input.type === "select" && (!input.options || input.options.length === 0)) {
issues.push({
path: ["inputs", key, "options"],
message: "select input must have a non-empty options array",
})
}
if (
input.type === "confirm" &&
input.default !== undefined &&
typeof input.default !== "boolean"
) {
issues.push({
path: ["inputs", key, "default"],
message: "confirm input default must be a boolean",
})
}
if (input.type === "number" && input.default !== undefined && typeof input.default !== "number") {
issues.push({
path: ["inputs", key, "default"],
message: "number input default must be a number",
})
}
return issues
}
// --- Config schema ---
/** Zod schema for ScaffoldConfig. */
const scaffoldConfigSchema = z
.object({
name: z.string().min(1, "name is required"),
templates: z.array(z.string()).min(1, "templates must contain at least one entry"),
output: stringOrFunctionSchema,
subdir: z.boolean().optional(),
data: z.record(z.string(), z.unknown()).optional(),
overwrite: booleanOrFunctionSchema.optional(),
logLevel: logLevelSchema.optional(),
dryRun: z.boolean().optional(),
helpers: z.record(z.string(), functionSchema).optional(),
subdirHelper: z.string().optional(),
inputs: z.record(z.string(), scaffoldInputSchema).optional(),
beforeWrite: functionSchema.optional(),
afterScaffold: stringOrFunctionSchema.optional(),
tmpDir: z.string().optional(),
})
.check((ctx) => {
const config = ctx.value
if (config.subdirHelper && !config.subdir) {
ctx.issues.push({
code: "custom",
message: "subdirHelper is set but subdir is not enabled",
path: ["subdirHelper"],
input: config,
})
}
if (config.inputs) {
for (const [key, val] of Object.entries(config.inputs)) {
for (const issue of validateInputSemantics(key, val)) {
ctx.issues.push({ code: "custom", ...issue, input: config })
}
}
}
})
export {
scaffoldConfigSchema,
scaffoldInputSchema,
functionSchema,
stringOrFunctionSchema,
booleanOrFunctionSchema,
selectOptionSchema,
inputTypeSchema,
logLevelSchema,
}
/**
* Validates a scaffold config and returns a list of human-readable errors.
* Returns an empty array if the config is valid.
*/
export function validateConfig(config: unknown): string[] {
const result = scaffoldConfigSchema.safeParse(config)
if (result.success) {
return []
}
return result.error.issues.map((issue) => {
const path = issue.path.length > 0 ? issue.path.join(".") : "(root)"
return `${path}: ${issue.message}`
})
}
/**
* Validates template paths exist on disk.
* Only checks non-glob, non-negation paths.
*/
export async function validateTemplatePaths(templates: string[]): Promise<string[]> {
const errors: string[] = []
for (const tpl of templates) {
if (tpl.startsWith("!") || tpl.includes("*")) continue
if (!(await pathExists(tpl))) {
errors.push(`templates: path does not exist: ${tpl}`)
}
}
return errors
}
/**
* Validates the config and throws a formatted error if any issues are found.
* Checks both schema validity and template path existence.
*/
export async function assertConfigValid(config: unknown): Promise<void> {
const schemaErrors = validateConfig(config)
const pathErrors =
config &&
typeof config === "object" &&
"templates" in config &&
Array.isArray((config as { templates: unknown }).templates)
? await validateTemplatePaths((config as { templates: string[] }).templates)
: []
const allErrors = [...schemaErrors, ...pathErrors]
if (allErrors.length > 0) {
const lines = allErrors.map((e) => ` - ${e}`)
throw new Error(`Invalid scaffold config:\n${lines.join("\n")}`)
}
}

539
tests/config.test.ts Normal file
View File

@@ -0,0 +1,539 @@
import { describe, test, expect, beforeEach, afterEach, beforeAll, vi } from "vitest"
import mockFs from "mock-fs"
import FileSystem from "mock-fs/lib/filesystem"
import { Console } from "console"
import { LogLevel, ScaffoldCmdConfig, ScaffoldConfig } from "../src/types"
import * as config from "../src/config"
import { resolve } from "../src/utils"
import configFile from "./test-config"
import { findConfigFile, getOptionValueForFile } from "../src/config"
import { registerHelpers } from "../src/parser"
import path from "path"
vi.mock("../src/git", async () => {
const actual = await vi.importActual<typeof import("../src/git")>("../src/git")
return {
...actual,
getGitConfig: () => {
return Promise.resolve(blankCliConf)
},
}
})
const { githubPartToUrl, parseAppendData, parseConfigFile } = config
const blankCliConf: ScaffoldCmdConfig = {
logLevel: LogLevel.none,
name: "",
output: "",
templates: [],
data: { name: "test" },
overwrite: false,
subdir: false,
dryRun: false,
quiet: false,
version: false,
}
const blankConfig: ScaffoldCmdConfig = {
...blankCliConf,
data: {},
}
describe("config", () => {
describe("parseAppendData", () => {
test('works for "key=value"', () => {
expect(parseAppendData("key=value", blankCliConf)).toEqual({ key: "value", name: "test" })
})
test('works for "key:=value"', () => {
expect(parseAppendData("key:=123", blankCliConf)).toEqual({ key: 123, name: "test" })
})
test("overwrites existing value", () => {
expect(parseAppendData("name:=123", blankCliConf)).toEqual({ name: 123 })
})
test("works with quotes", () => {
expect(parseAppendData('key="value test"', blankCliConf)).toEqual({
key: "value test",
name: "test",
})
})
test("handles JSON array values with :=", () => {
expect(parseAppendData('items:=["a","b"]', blankCliConf)).toEqual({
items: ["a", "b"],
name: "test",
})
})
test("handles JSON boolean with :=", () => {
expect(parseAppendData("flag:=true", blankCliConf)).toEqual({ flag: true, name: "test" })
expect(parseAppendData("flag:=false", blankCliConf)).toEqual({ flag: false, name: "test" })
})
test("handles JSON null with :=", () => {
expect(parseAppendData("val:=null", blankCliConf)).toEqual({ val: null, name: "test" })
})
test("handles JSON object with :=", () => {
expect(parseAppendData('obj:={"a":1}', blankCliConf)).toEqual({ obj: { a: 1 }, name: "test" })
})
test("handles single quoted values", () => {
expect(parseAppendData("key='value test'", blankCliConf)).toEqual({
key: "value test",
name: "test",
})
})
test("handles empty string value", () => {
expect(parseAppendData("key=", blankCliConf)).toEqual({ key: "", name: "test" })
})
test("handles negative number with :=", () => {
expect(parseAppendData("num:=-42", blankCliConf)).toEqual({ num: -42, name: "test" })
})
test("handles float with :=", () => {
expect(parseAppendData("num:=3.14", blankCliConf)).toEqual({ num: 3.14, name: "test" })
})
})
describe("githubPartToUrl", () => {
test("works", () => {
expect(githubPartToUrl("chenasraf/simple-scaffold")).toEqual(
"https://github.com/chenasraf/simple-scaffold.git",
)
expect(githubPartToUrl("chenasraf/simple-scaffold.git")).toEqual(
"https://github.com/chenasraf/simple-scaffold.git",
)
})
test("handles organization repos", () => {
expect(githubPartToUrl("org/sub-repo")).toEqual("https://github.com/org/sub-repo.git")
})
test("handles repos with dots in name", () => {
expect(githubPartToUrl("user/my.repo")).toEqual("https://github.com/user/my.repo.git")
})
})
describe("parseConfigFile", () => {
test("normal config does not change", async () => {
const tmpDir = `/tmp/scaffold-config-${Date.now()}`
const { quiet: _, tmpDir: _tmpDir, version: __, ...conf } = blankCliConf
expect(
await parseConfigFile({
...blankCliConf,
name: "-",
tmpDir,
}),
).toEqual({ ...conf, name: "-", tmpDir, subdirHelper: undefined, beforeWrite: undefined })
})
describe("appendData", () => {
test("appends", async () => {
const result = await parseConfigFile({
...blankCliConf,
name: "-",
appendData: { key: "value" },
tmpDir: `/tmp/scaffold-config-${Date.now()}`,
})
expect(result?.data?.key).toEqual("value")
})
test("overwrites existing value", async () => {
const result = await parseConfigFile({
...blankCliConf,
name: "-",
data: { num: "123" },
appendData: { num: "1234" },
tmpDir: `/tmp/scaffold-config-${Date.now()}`,
})
expect(result?.data?.num).toEqual("1234")
})
test("CLI output overrides config file output", async () => {
const tmpDir = `/tmp/scaffold-config-${Date.now()}`
const result = await parseConfigFile({
...blankCliConf,
config: path.resolve(__dirname, "test-config.js"),
key: "component",
output: "examples/test-output/override",
name: "Component",
tmpDir,
})
expect(result.output).toEqual("examples/test-output/override")
})
})
test("throws when name is missing", async () => {
await expect(
parseConfigFile({
...blankCliConf,
name: "",
tmpDir: `/tmp/scaffold-config-${Date.now()}`,
}),
).rejects.toThrow("Missing required option: name")
})
test("preserves dryRun setting", async () => {
const result = await parseConfigFile({
...blankCliConf,
name: "test",
dryRun: true,
tmpDir: `/tmp/scaffold-config-${Date.now()}`,
})
expect(result.dryRun).toBe(true)
})
test("preserves subdir setting", async () => {
const result = await parseConfigFile({
...blankCliConf,
name: "test",
subdir: true,
tmpDir: `/tmp/scaffold-config-${Date.now()}`,
})
expect(result.subdir).toBe(true)
})
test("preserves overwrite setting", async () => {
const result = await parseConfigFile({
...blankCliConf,
name: "test",
overwrite: true,
tmpDir: `/tmp/scaffold-config-${Date.now()}`,
})
expect(result.overwrite).toBe(true)
})
test("merges data from config and appendData", async () => {
const result = await parseConfigFile({
...blankCliConf,
name: "test",
data: { key1: "val1" },
appendData: { key2: "val2" },
tmpDir: `/tmp/scaffold-config-${Date.now()}`,
})
expect(result.data).toEqual({ key1: "val1", key2: "val2" })
})
test("appendData overrides data", async () => {
const result = await parseConfigFile({
...blankCliConf,
name: "test",
data: { key: "original" },
appendData: { key: "overridden" },
tmpDir: `/tmp/scaffold-config-${Date.now()}`,
})
expect(result.data?.key).toEqual("overridden")
})
test("sets subdirHelper from config", async () => {
const result = await parseConfigFile({
...blankCliConf,
name: "test",
subdirHelper: "pascalCase",
tmpDir: `/tmp/scaffold-config-${Date.now()}`,
})
expect(result.subdirHelper).toEqual("pascalCase")
})
test("handles empty templates array", async () => {
const result = await parseConfigFile({
...blankCliConf,
name: "test",
templates: [],
tmpDir: `/tmp/scaffold-config-${Date.now()}`,
})
expect(result.templates).toEqual([])
})
test("throws when config key not found", async () => {
await expect(
parseConfigFile({
...blankCliConf,
name: "test",
config: path.resolve(__dirname, "test-config.js"),
key: "nonexistent",
tmpDir: `/tmp/scaffold-config-${Date.now()}`,
}),
).rejects.toThrow('Template "nonexistent" not found')
})
test("uses default key when key not specified", async () => {
const result = await parseConfigFile({
...blankCliConf,
name: "MyComponent",
templates: undefined as unknown as string[],
config: path.resolve(__dirname, "test-config.js"),
tmpDir: `/tmp/scaffold-config-${Date.now()}`,
})
expect(result.templates.length).toBeGreaterThan(0)
})
})
describe("getConfig", () => {
test("gets git config", async () => {
const resultFn = await config.getRemoteConfig({
git: "https://github.com/chenasraf/simple-scaffold.git",
logLevel: LogLevel.none,
tmpDir: `/tmp/scaffold-config-${Date.now()}`,
})
const result = await resolve(resultFn, blankCliConf)
expect(result).toEqual(blankCliConf)
})
test("gets local file config", async () => {
const resultFn = await config.getLocalConfig({
config: path.join(__dirname, "test-config.js"),
logLevel: LogLevel.none,
})
const result = (await resolve(resultFn, {} as ScaffoldCmdConfig)).default
expect(result).toEqual(configFile)
})
})
describe("getRemoteConfig", () => {
test("throws for unsupported protocol", async () => {
await expect(
config.getRemoteConfig({
git: "ftp://example.com/repo.git",
logLevel: LogLevel.none,
tmpDir: `/tmp/scaffold-config-${Date.now()}`,
}),
).rejects.toThrow("Unsupported protocol")
})
})
describe("getOptionValueForFile", () => {
const conf: ScaffoldConfig = {
name: "test",
output: "output",
templates: [],
logLevel: LogLevel.none,
data: { name: "test" },
}
beforeAll(() => {
registerHelpers(conf)
})
test("returns static string value", () => {
expect(getOptionValueForFile(conf, "/some/path", "static-value")).toEqual("static-value")
})
test("returns static boolean value", () => {
expect(getOptionValueForFile(conf, "/some/path", true)).toBe(true)
expect(getOptionValueForFile(conf, "/some/path", false)).toBe(false)
})
test("calls function with file path info", () => {
const fn = vi.fn().mockReturnValue("custom-output")
const result = getOptionValueForFile(conf, "/home/user/file.txt", fn)
expect(result).toEqual("custom-output")
expect(fn).toHaveBeenCalledWith("/home/user/file.txt", expect.any(String), expect.any(String))
})
test("returns default value when fn is not a function and no value", () => {
expect(
getOptionValueForFile(conf, "/some/path", undefined as unknown as string, "default"),
).toEqual("default")
})
test("function receives parsed basename", () => {
const fn = (_fullPath: string, _basedir: string, basename: string) => basename
const result = getOptionValueForFile(conf, "/home/user/{{name}}.txt", fn)
expect(result).toEqual("test.txt")
})
})
describe("findConfigFile", () => {
const struct1 = {
"scaffold.config.js": `module.exports = '${JSON.stringify(blankConfig)}'`,
}
const struct2 = {
"scaffold.js": `module.exports = '${JSON.stringify(blankConfig)}'`,
}
const struct3 = {
"scaffold.cjs": `module.exports = '${JSON.stringify(blankConfig)}'`,
}
const struct4 = {
"scaffold.json": JSON.stringify(blankConfig),
}
function withMock(fileStruct: FileSystem.DirectoryItems, testFn: () => void): () => void {
return () => {
beforeEach(() => {
// console.log("Mocking:", fileStruct)
console = new Console(process.stdout, process.stderr)
mockFs(fileStruct)
// logMock = jest.spyOn(console, 'log').mockImplementation((...args) => {
// logsTemp.push(args)
// })
})
testFn()
afterEach(() => {
// console.log("Restoring mock")
mockFs.restore()
})
}
}
for (const struct of [struct1, struct2, struct3, struct4]) {
const [k] = Object.keys(struct)
describe(
`finds config file ${k}`,
withMock(struct, () => {
test(`finds ${k}`, async () => {
const result = await findConfigFile(process.cwd())
expect(result).toEqual(k)
})
}),
)
}
describe(
"finds .mjs config file",
withMock({ "scaffold.config.mjs": "export default {}" }, () => {
test("finds scaffold.config.mjs", async () => {
const result = await findConfigFile(process.cwd())
expect(result).toEqual("scaffold.config.mjs")
})
}),
)
describe(
"priority order",
withMock(
{
"scaffold.config.js": "module.exports = {}",
"scaffold.js": "module.exports = {}",
},
() => {
test("prefers scaffold.config.js over scaffold.js", async () => {
const result = await findConfigFile(process.cwd())
expect(result).toEqual("scaffold.config.js")
})
},
),
)
describe(
"throws when no config found",
withMock({ "unrelated-file.txt": "content" }, () => {
test("throws error when no config file exists", async () => {
await expect(findConfigFile(process.cwd())).rejects.toThrow("Could not find config file")
})
}),
)
describe(
"finds scaffold.config.cjs",
withMock({ "scaffold.config.cjs": "module.exports = {}" }, () => {
test("finds .cjs config file", async () => {
const result = await findConfigFile(process.cwd())
expect(result).toEqual("scaffold.config.cjs")
})
}),
)
describe(
"finds scaffold.config.json",
withMock({ "scaffold.config.json": "{}" }, () => {
test("finds .json config file", async () => {
const result = await findConfigFile(process.cwd())
expect(result).toEqual("scaffold.config.json")
})
}),
)
describe(
"finds scaffold.mjs",
withMock({ "scaffold.mjs": "export default {}" }, () => {
test("finds scaffold.mjs", async () => {
const result = await findConfigFile(process.cwd())
expect(result).toEqual("scaffold.mjs")
})
}),
)
describe(
"finds scaffold.cjs",
withMock({ "scaffold.cjs": "module.exports = {}" }, () => {
test("finds scaffold.cjs", async () => {
const result = await findConfigFile(process.cwd())
expect(result).toEqual("scaffold.cjs")
})
}),
)
describe(
"finds scaffold.json",
withMock({ "scaffold.json": "{}" }, () => {
test("finds scaffold.json", async () => {
const result = await findConfigFile(process.cwd())
expect(result).toEqual("scaffold.json")
})
}),
)
describe(
"finds .scaffold.js",
withMock({ ".scaffold.js": "module.exports = {}" }, () => {
test("finds dotfile config", async () => {
const result = await findConfigFile(process.cwd())
expect(result).toEqual(".scaffold.js")
})
}),
)
describe(
"finds .scaffold.json",
withMock({ ".scaffold.json": "{}" }, () => {
test("finds dotfile json config", async () => {
const result = await findConfigFile(process.cwd())
expect(result).toEqual(".scaffold.json")
})
}),
)
describe(
"prefers scaffold.config over .scaffold",
withMock(
{
"scaffold.config.js": "module.exports = {}",
".scaffold.js": "module.exports = {}",
},
() => {
test("prefers scaffold.config.js over .scaffold.js", async () => {
const result = await findConfigFile(process.cwd())
expect(result).toEqual("scaffold.config.js")
})
},
),
)
describe(
"prefers scaffold over .scaffold",
withMock(
{
"scaffold.js": "module.exports = {}",
".scaffold.js": "module.exports = {}",
},
() => {
test("prefers scaffold.js over .scaffold.js", async () => {
const result = await findConfigFile(process.cwd())
expect(result).toEqual("scaffold.js")
})
},
),
)
})
})

456
tests/file.test.ts Normal file
View File

@@ -0,0 +1,456 @@
import { describe, test, expect, beforeEach, afterEach } from "vitest"
import mockFs from "mock-fs"
import FileSystem from "mock-fs/lib/filesystem"
import { Console } from "console"
import path from "node:path"
import os from "node:os"
import {
removeGlob,
makeRelativePath,
getBasePath,
getOutputDir,
getUniqueTmpPath,
pathExists,
createDirIfNotExists,
getTemplateGlobInfo,
getTemplateFileInfo,
copyFileTransformed,
handleTemplateFile,
getFileList,
} from "../src/file"
import { ScaffoldConfig, LogLevel } from "../src/types"
import { registerHelpers } from "../src/parser"
import { readFileSync } from "fs"
function withMock(fileStruct: FileSystem.DirectoryItems, testFn: () => void): () => void {
return () => {
beforeEach(() => {
console = new Console(process.stdout, process.stderr)
mockFs(fileStruct)
})
testFn()
afterEach(() => {
mockFs.restore()
})
}
}
const baseConfig: ScaffoldConfig = {
name: "test_app",
output: "output",
templates: ["input"],
logLevel: LogLevel.none,
data: { name: "test_app" },
tmpDir: ".",
}
describe("file utilities", () => {
describe("removeGlob", () => {
test("removes single wildcard", () => {
expect(removeGlob("input/*")).toEqual(path.normalize("input/"))
})
test("removes double wildcard", () => {
expect(removeGlob("input/**/*")).toEqual(path.normalize("input///"))
})
test("returns path unchanged when no glob", () => {
expect(removeGlob("input/file.txt")).toEqual(path.normalize("input/file.txt"))
})
test("removes wildcards from nested path", () => {
expect(removeGlob("a/b/*/c/**")).toEqual(path.normalize("a/b//c//"))
})
test("handles empty string", () => {
expect(removeGlob("")).toEqual(".")
})
})
describe("makeRelativePath", () => {
test("removes leading separator", () => {
expect(makeRelativePath(path.sep + "some/path")).toEqual("some/path")
})
test("returns path unchanged if no leading separator", () => {
expect(makeRelativePath("some/path")).toEqual("some/path")
})
test("handles empty string", () => {
expect(makeRelativePath("")).toEqual("")
})
test("removes only the first separator", () => {
expect(makeRelativePath(path.sep + "a" + path.sep + "b")).toEqual("a" + path.sep + "b")
})
})
describe("getBasePath", () => {
test("resolves relative path against cwd", () => {
const result = getBasePath("some/path")
expect(result).toEqual("some/path")
})
test("handles empty string", () => {
const result = getBasePath("")
expect(result).toEqual("")
})
test("handles current directory", () => {
const result = getBasePath(".")
expect(result).toEqual("")
})
})
describe("getOutputDir", () => {
test("returns output path without subdir", () => {
const config: ScaffoldConfig = { ...baseConfig, subdir: false }
registerHelpers(config)
const result = getOutputDir(config, "output", "")
expect(result).toEqual(path.resolve(process.cwd(), "output"))
})
test("returns output path with subdir", () => {
const config: ScaffoldConfig = { ...baseConfig, subdir: true }
registerHelpers(config)
const result = getOutputDir(config, "output", "")
expect(result).toEqual(path.resolve(process.cwd(), "output", "test_app"))
})
test("applies subdirHelper to subdir name", () => {
const config: ScaffoldConfig = {
...baseConfig,
subdir: true,
subdirHelper: "pascalCase",
}
registerHelpers(config)
const result = getOutputDir(config, "output", "")
expect(result).toEqual(path.resolve(process.cwd(), "output", "TestApp"))
})
test("includes basePath in output", () => {
const config: ScaffoldConfig = { ...baseConfig, subdir: false }
registerHelpers(config)
const result = getOutputDir(config, "output", "nested/dir")
expect(result).toEqual(path.resolve(process.cwd(), "output", "nested/dir"))
})
test("combines output, basePath, and subdir", () => {
const config: ScaffoldConfig = { ...baseConfig, subdir: true }
registerHelpers(config)
const result = getOutputDir(config, "output", "nested")
expect(result).toEqual(path.resolve(process.cwd(), "output", "nested", "test_app"))
})
})
describe("getUniqueTmpPath", () => {
test("returns a path in os temp directory", () => {
const result = getUniqueTmpPath()
expect(result.startsWith(os.tmpdir())).toBe(true)
})
test("includes scaffold-config prefix", () => {
const result = getUniqueTmpPath()
expect(path.basename(result)).toMatch(/^scaffold-config-/)
})
test("generates unique paths", () => {
const a = getUniqueTmpPath()
const b = getUniqueTmpPath()
expect(a).not.toEqual(b)
})
})
describe(
"pathExists",
withMock(
{
"existing-file.txt": "content",
"existing-dir": {},
},
() => {
test("returns true for existing file", async () => {
expect(await pathExists("existing-file.txt")).toBe(true)
})
test("returns true for existing directory", async () => {
expect(await pathExists("existing-dir")).toBe(true)
})
test("returns false for non-existing path", async () => {
expect(await pathExists("non-existing")).toBe(false)
})
},
),
)
describe(
"createDirIfNotExists",
withMock({}, () => {
test("creates directory", async () => {
await createDirIfNotExists("new-dir", { logLevel: LogLevel.none, dryRun: false })
expect(await pathExists("new-dir")).toBe(true)
})
test("creates nested directories recursively", async () => {
await createDirIfNotExists("a/b/c", { logLevel: LogLevel.none, dryRun: false })
expect(await pathExists("a/b/c")).toBe(true)
})
test("does not create directory in dry run mode", async () => {
await createDirIfNotExists("dry-dir", { logLevel: LogLevel.none, dryRun: true })
expect(await pathExists("dry-dir")).toBe(false)
})
test("does not throw if directory already exists", async () => {
await createDirIfNotExists("existing", { logLevel: LogLevel.none, dryRun: false })
await expect(
createDirIfNotExists("existing", { logLevel: LogLevel.none, dryRun: false }),
).resolves.toBeUndefined()
})
}),
)
describe(
"getTemplateGlobInfo",
withMock(
{
"template-dir": {
"file1.txt": "content1",
"file2.txt": "content2",
},
"single-file.txt": "content",
},
() => {
test("detects directory template", async () => {
const result = await getTemplateGlobInfo(baseConfig, "template-dir")
expect(result.isDirOrGlob).toBe(true)
expect(result.isGlob).toBe(false)
expect(result.template).toEqual(path.join("template-dir", "**", "*"))
})
test("detects glob template", async () => {
const result = await getTemplateGlobInfo(baseConfig, "template-dir/**/*.txt")
expect(result.isDirOrGlob).toBe(true)
expect(result.isGlob).toBe(true)
})
test("preserves non-glob single file", async () => {
const result = await getTemplateGlobInfo(baseConfig, "single-file.txt")
expect(result.isDirOrGlob).toBe(false)
expect(result.isGlob).toBe(false)
expect(result.template).toEqual("single-file.txt")
})
test("stores original template", async () => {
const result = await getTemplateGlobInfo(baseConfig, "template-dir")
expect(result.origTemplate).toEqual("template-dir")
})
},
),
)
describe(
"getFileList",
withMock(
{
templates: {
"file1.txt": "content1",
"file2.js": "content2",
".hidden": "hidden content",
nested: {
"file3.txt": "content3",
},
},
},
() => {
test("lists all files with glob", async () => {
const files = await getFileList(baseConfig, ["templates/**/*"])
expect(files.length).toBe(4)
})
test("includes dotfiles", async () => {
const files = await getFileList(baseConfig, ["templates/**/*"])
expect(files.some((f) => f.includes(".hidden"))).toBe(true)
})
test("filters by extension", async () => {
const files = await getFileList(baseConfig, ["templates/**/*.txt"])
expect(files.length).toBe(2)
expect(files.every((f) => f.endsWith(".txt"))).toBe(true)
})
test("supports exclusion patterns", async () => {
const files = await getFileList(baseConfig, ["templates/**/*.txt"])
expect(files.some((f) => f.includes(".hidden"))).toBe(false)
expect(files.every((f) => f.endsWith(".txt"))).toBe(true)
})
},
),
)
describe(
"getTemplateFileInfo",
withMock(
{
input: {
"{{name}}.txt": "Hello {{name}}",
},
output: {},
},
() => {
test("calculates correct output path", async () => {
const config: ScaffoldConfig = { ...baseConfig, tmpDir: "." }
registerHelpers(config)
const info = await getTemplateFileInfo(config, {
templatePath: "input/{{name}}.txt",
basePath: "",
})
expect(info.inputPath).toEqual(path.resolve(process.cwd(), "input/{{name}}.txt"))
expect(info.outputPath).toContain("test_app.txt")
expect(info.exists).toBe(false)
})
test("detects existing output file", async () => {
mockFs.restore()
mockFs({
input: { "{{name}}.txt": "Hello {{name}}" },
output: { "test_app.txt": "existing" },
})
const config: ScaffoldConfig = { ...baseConfig, tmpDir: "." }
registerHelpers(config)
const info = await getTemplateFileInfo(config, {
templatePath: "input/{{name}}.txt",
basePath: "",
})
expect(info.exists).toBe(true)
})
},
),
)
describe(
"copyFileTransformed",
withMock(
{
"input.txt": "Hello {{name}}",
output: {},
},
() => {
test("writes transformed content", async () => {
const config: ScaffoldConfig = { ...baseConfig }
registerHelpers(config)
await createDirIfNotExists("output", { logLevel: LogLevel.none, dryRun: false })
await copyFileTransformed(config, {
exists: false,
overwrite: false,
outputPath: "output/result.txt",
inputPath: "input.txt",
})
const content = readFileSync("output/result.txt").toString()
expect(content).toEqual("Hello test_app")
})
test("does not write in dry run mode", async () => {
const config: ScaffoldConfig = { ...baseConfig, dryRun: true }
registerHelpers(config)
await copyFileTransformed(config, {
exists: false,
overwrite: false,
outputPath: "output/result.txt",
inputPath: "input.txt",
})
expect(await pathExists("output/result.txt")).toBe(false)
})
test("skips existing file without overwrite", async () => {
mockFs.restore()
mockFs({
"input.txt": "Hello {{name}}",
output: { "result.txt": "original" },
})
const config: ScaffoldConfig = { ...baseConfig }
registerHelpers(config)
await copyFileTransformed(config, {
exists: true,
overwrite: false,
outputPath: "output/result.txt",
inputPath: "input.txt",
})
const content = readFileSync("output/result.txt").toString()
expect(content).toEqual("original")
})
test("overwrites existing file with overwrite flag", async () => {
mockFs.restore()
mockFs({
"input.txt": "Hello {{name}}",
output: { "result.txt": "original" },
})
const config: ScaffoldConfig = { ...baseConfig }
registerHelpers(config)
await copyFileTransformed(config, {
exists: true,
overwrite: true,
outputPath: "output/result.txt",
inputPath: "input.txt",
})
const content = readFileSync("output/result.txt").toString()
expect(content).toEqual("Hello test_app")
})
test("calls beforeWrite callback", async () => {
const config: ScaffoldConfig = {
...baseConfig,
beforeWrite: (content) => content.toString().toUpperCase(),
}
registerHelpers(config)
await createDirIfNotExists("output", { logLevel: LogLevel.none, dryRun: false })
await copyFileTransformed(config, {
exists: false,
overwrite: false,
outputPath: "output/result.txt",
inputPath: "input.txt",
})
const content = readFileSync("output/result.txt").toString()
expect(content).toEqual("HELLO TEST_APP")
})
},
),
)
describe(
"handleTemplateFile",
withMock(
{
input: {
"{{name}}.txt": "Content for {{name}}",
},
output: {},
},
() => {
test("processes template file end to end", async () => {
const config: ScaffoldConfig = { ...baseConfig, tmpDir: "." }
registerHelpers(config)
await handleTemplateFile(config, {
templatePath: "input/{{name}}.txt",
basePath: "",
})
const content = readFileSync(path.join("output", "test_app.txt")).toString()
expect(content).toEqual("Content for test_app")
})
test("throws for non-existing template", async () => {
const config: ScaffoldConfig = { ...baseConfig, tmpDir: "." }
registerHelpers(config)
await expect(
handleTemplateFile(config, {
templatePath: "non-existing.txt",
basePath: "",
}),
).rejects.toThrow()
})
},
),
)
})

122
tests/ignore.test.ts Normal file
View File

@@ -0,0 +1,122 @@
import { describe, test, expect, beforeEach, afterEach } from "vitest"
import mockFs from "mock-fs"
import { Console } from "console"
import path from "node:path"
import { parseIgnoreFile, loadIgnorePatterns, filterIgnoredFiles } from "../src/ignore"
describe("ignore", () => {
describe("parseIgnoreFile", () => {
test("parses patterns", () => {
const result = parseIgnoreFile("node_modules\n*.log\n")
expect(result).toEqual(["node_modules", "*.log"])
})
test("skips comments", () => {
const result = parseIgnoreFile("# comment\nfoo\n# another\nbar")
expect(result).toEqual(["foo", "bar"])
})
test("skips empty lines", () => {
const result = parseIgnoreFile("foo\n\n\nbar\n")
expect(result).toEqual(["foo", "bar"])
})
test("trims whitespace", () => {
const result = parseIgnoreFile(" foo \n bar ")
expect(result).toEqual(["foo", "bar"])
})
test("handles empty file", () => {
expect(parseIgnoreFile("")).toEqual([])
})
test("handles comments-only file", () => {
expect(parseIgnoreFile("# just comments\n# nothing here")).toEqual([])
})
})
describe("filterIgnoredFiles", () => {
test("filters files matching patterns", () => {
const files = ["templates/file.txt", "templates/debug.log", "templates/other.js"]
const result = filterIgnoredFiles(files, ["*.log"], "templates")
expect(result).toEqual(["templates/file.txt", "templates/other.js"])
})
test("always excludes .scaffoldignore", () => {
const files = ["templates/file.txt", "templates/.scaffoldignore"]
const result = filterIgnoredFiles(files, [], "templates")
expect(result).toEqual(["templates/file.txt"])
})
test("matches by relative path", () => {
const files = ["templates/src/index.ts", "templates/dist/index.js", "templates/src/utils.ts"]
const result = filterIgnoredFiles(files, ["dist/**"], "templates")
expect(result).toEqual(["templates/src/index.ts", "templates/src/utils.ts"])
})
test("matches by basename", () => {
const files = ["templates/README.md", "templates/nested/README.md", "templates/file.txt"]
const result = filterIgnoredFiles(files, ["README.md"], "templates")
expect(result).toEqual(["templates/file.txt"])
})
test("handles multiple patterns", () => {
const files = ["tpl/a.txt", "tpl/b.log", "tpl/c.tmp", "tpl/d.ts"]
const result = filterIgnoredFiles(files, ["*.log", "*.tmp"], "tpl")
expect(result).toEqual(["tpl/a.txt", "tpl/d.ts"])
})
test("returns all files when no patterns", () => {
const files = ["tpl/a.txt", "tpl/b.txt"]
const result = filterIgnoredFiles(files, [], "tpl")
expect(result).toEqual(files)
})
test("handles glob patterns with directories", () => {
const files = [
path.join("tpl", "src", "index.ts"),
path.join("tpl", "node_modules", "pkg", "index.js"),
path.join("tpl", "file.txt"),
]
const result = filterIgnoredFiles(files, ["node_modules/**"], "tpl")
expect(result).toEqual([path.join("tpl", "src", "index.ts"), path.join("tpl", "file.txt")])
})
})
describe("loadIgnorePatterns", () => {
beforeEach(() => {
console = new Console(process.stdout, process.stderr)
})
afterEach(() => {
mockFs.restore()
})
test("returns empty array when no .scaffoldignore exists", async () => {
mockFs({ templates: { "file.txt": "content" } })
const result = await loadIgnorePatterns("templates")
expect(result).toEqual([])
})
test("reads and parses .scaffoldignore", async () => {
mockFs({
templates: {
".scaffoldignore": "*.log\nnode_modules\n",
"file.txt": "content",
},
})
const result = await loadIgnorePatterns("templates")
expect(result).toEqual(["*.log", "node_modules"])
})
test("ignores comments in .scaffoldignore", async () => {
mockFs({
templates: {
".scaffoldignore": "# This is a comment\n*.tmp\n",
},
})
const result = await loadIgnorePatterns("templates")
expect(result).toEqual(["*.tmp"])
})
})
})

115
tests/init.test.ts Normal file
View File

@@ -0,0 +1,115 @@
import { describe, test, expect, vi, beforeEach, afterEach } from "vitest"
import mockFs from "mock-fs"
import { Console } from "console"
import { readFileSync, existsSync } from "fs"
import path from "path"
vi.mock("@inquirer/input", () => ({
default: vi.fn(),
}))
vi.mock("@inquirer/select", () => ({
default: vi.fn(),
}))
import selectMock from "@inquirer/select"
import { initScaffold } from "../src/init"
describe("init", () => {
beforeEach(() => {
console = new Console(process.stdout, process.stderr)
vi.clearAllMocks()
mockFs({})
})
afterEach(() => {
mockFs.restore()
})
test("creates js config and example template", async () => {
await initScaffold({ format: "js", dir: process.cwd() })
expect(existsSync("scaffold.config.js")).toBe(true)
const config = readFileSync("scaffold.config.js", "utf-8")
expect(config).toContain("module.exports")
expect(config).toContain("templates/default")
expect(existsSync(path.join("templates", "default", "{{name}}.md"))).toBe(true)
const template = readFileSync(path.join("templates", "default", "{{name}}.md"), "utf-8")
expect(template).toContain("{{ name }}")
})
test("creates mjs config", async () => {
await initScaffold({ format: "mjs", dir: process.cwd() })
expect(existsSync("scaffold.config.mjs")).toBe(true)
const config = readFileSync("scaffold.config.mjs", "utf-8")
expect(config).toContain("export default")
})
test("creates json config", async () => {
await initScaffold({ format: "json", dir: process.cwd() })
expect(existsSync("scaffold.config.json")).toBe(true)
const config = readFileSync("scaffold.config.json", "utf-8")
const parsed = JSON.parse(config)
expect(parsed.default).toBeDefined()
expect(parsed.default.templates).toEqual(["templates/default"])
})
test("does not overwrite existing config", async () => {
mockFs.restore()
mockFs({
"scaffold.config.js": "// existing config",
})
await initScaffold({ format: "js", dir: process.cwd() })
const config = readFileSync("scaffold.config.js", "utf-8")
expect(config).toBe("// existing config")
})
test("does not overwrite existing template dir", async () => {
mockFs.restore()
mockFs({
templates: {
default: {
"existing.md": "# Existing",
},
},
})
await initScaffold({ format: "js", dir: process.cwd() })
expect(existsSync(path.join("templates", "default", "existing.md"))).toBe(true)
expect(existsSync(path.join("templates", "default", "{{name}}.md"))).toBe(false)
})
test("skips example template when createExample is false", async () => {
await initScaffold({ format: "js", dir: process.cwd(), createExample: false })
expect(existsSync("scaffold.config.js")).toBe(true)
expect(existsSync("templates")).toBe(false)
})
test("prompts for format when not provided", async () => {
vi.mocked(selectMock).mockResolvedValue("js")
await initScaffold({ dir: process.cwd() })
expect(selectMock).toHaveBeenCalledOnce()
expect(existsSync("scaffold.config.js")).toBe(true)
})
test("creates config in custom directory", async () => {
mockFs.restore()
mockFs({
"my-project": {},
})
await initScaffold({ format: "js", dir: path.resolve("my-project") })
expect(existsSync(path.join("my-project", "scaffold.config.js"))).toBe(true)
expect(existsSync(path.join("my-project", "templates", "default", "{{name}}.md"))).toBe(true)
})
})

175
tests/logger.test.ts Normal file
View File

@@ -0,0 +1,175 @@
import { describe, test, expect, beforeEach, afterEach, vi, type MockInstance } from "vitest"
import { log, logInitStep, logInputFile } from "../src/logger"
import { LogLevel, ScaffoldConfig } from "../src/types"
describe("logger", () => {
let consoleSpy: {
log: MockInstance
warn: MockInstance
error: MockInstance
}
beforeEach(() => {
consoleSpy = {
log: vi.spyOn(console, "log").mockImplementation(() => void 0),
warn: vi.spyOn(console, "warn").mockImplementation(() => void 0),
error: vi.spyOn(console, "error").mockImplementation(() => void 0),
}
})
afterEach(() => {
consoleSpy.log.mockRestore()
consoleSpy.warn.mockRestore()
consoleSpy.error.mockRestore()
})
describe("log", () => {
test("does not log when logLevel is none", () => {
log({ logLevel: LogLevel.none }, LogLevel.info, "test")
expect(consoleSpy.log).not.toHaveBeenCalled()
})
test("logs info messages with console.log", () => {
log({ logLevel: LogLevel.info }, LogLevel.info, "test message")
expect(consoleSpy.log).toHaveBeenCalled()
})
test("logs warning messages with console.warn", () => {
log({ logLevel: LogLevel.warning }, LogLevel.warning, "warning message")
expect(consoleSpy.warn).toHaveBeenCalled()
})
test("logs error messages with console.error", () => {
log({ logLevel: LogLevel.error }, LogLevel.error, "error message")
expect(consoleSpy.error).toHaveBeenCalled()
})
test("filters out messages below configured level", () => {
log({ logLevel: LogLevel.warning }, LogLevel.info, "should be filtered")
expect(consoleSpy.log).not.toHaveBeenCalled()
})
test("filters out debug messages when level is info", () => {
log({ logLevel: LogLevel.info }, LogLevel.debug, "debug message")
expect(consoleSpy.log).not.toHaveBeenCalled()
})
test("shows debug messages when level is debug", () => {
log({ logLevel: LogLevel.debug }, LogLevel.debug, "debug message")
expect(consoleSpy.log).toHaveBeenCalled()
})
test("shows all levels when configured as debug", () => {
log({ logLevel: LogLevel.debug }, LogLevel.debug, "d")
log({ logLevel: LogLevel.debug }, LogLevel.info, "i")
log({ logLevel: LogLevel.debug }, LogLevel.warning, "w")
log({ logLevel: LogLevel.debug }, LogLevel.error, "e")
expect(consoleSpy.log).toHaveBeenCalledTimes(2) // debug + info
expect(consoleSpy.warn).toHaveBeenCalledTimes(1)
expect(consoleSpy.error).toHaveBeenCalledTimes(1)
})
test("handles Error objects", () => {
log({ logLevel: LogLevel.error }, LogLevel.error, new Error("test error"))
expect(consoleSpy.error).toHaveBeenCalled()
})
test("handles objects with util.inspect", () => {
log({ logLevel: LogLevel.info }, LogLevel.info, { key: "value" })
expect(consoleSpy.log).toHaveBeenCalled()
})
test("handles multiple arguments", () => {
log({ logLevel: LogLevel.info }, LogLevel.info, "a", "b", "c")
expect(consoleSpy.log).toHaveBeenCalled()
// First call, should have 3 arguments
expect(consoleSpy.log.mock.calls[0].length).toBe(3)
})
test("defaults to info when logLevel is undefined", () => {
log({ logLevel: undefined as unknown as LogLevel }, LogLevel.info, "test")
expect(consoleSpy.log).toHaveBeenCalled()
})
test("error level shows when logLevel is info", () => {
log({ logLevel: LogLevel.info }, LogLevel.error, "error")
expect(consoleSpy.error).toHaveBeenCalled()
})
test("warning level shows when logLevel is info", () => {
log({ logLevel: LogLevel.info }, LogLevel.warning, "warning")
expect(consoleSpy.warn).toHaveBeenCalled()
})
})
describe("logInitStep", () => {
test("logs config at debug level", () => {
const config: ScaffoldConfig = {
name: "test",
output: "output",
templates: ["input"],
logLevel: LogLevel.debug,
data: { name: "test" },
}
logInitStep(config)
expect(consoleSpy.log).toHaveBeenCalled()
})
test("does not log at info level (debug only)", () => {
const config: ScaffoldConfig = {
name: "test",
output: "output",
templates: ["input"],
logLevel: LogLevel.info,
data: { name: "test" },
}
logInitStep(config)
// Full config is debug-only, nothing logged at info
expect(consoleSpy.log).not.toHaveBeenCalled()
})
})
describe("logInputFile", () => {
test("logs file info at debug level", () => {
const config: ScaffoldConfig = {
name: "test",
output: "output",
templates: ["input"],
logLevel: LogLevel.debug,
data: { name: "test" },
}
logInputFile(config, {
originalTemplate: "input",
relativePath: ".",
parsedTemplate: "input/**/*",
inputFilePath: "input/file.txt",
nonGlobTemplate: "input",
basePath: "",
isDirOrGlob: true,
isGlob: false,
})
expect(consoleSpy.log).toHaveBeenCalled()
})
test("does not log at info level", () => {
const config: ScaffoldConfig = {
name: "test",
output: "output",
templates: ["input"],
logLevel: LogLevel.info,
data: { name: "test" },
}
logInputFile(config, {
originalTemplate: "input",
relativePath: ".",
parsedTemplate: "input/**/*",
inputFilePath: "input/file.txt",
nonGlobTemplate: "input",
basePath: "",
isDirOrGlob: true,
isGlob: false,
})
expect(consoleSpy.log).not.toHaveBeenCalled()
})
})
})

429
tests/parser.test.ts Normal file
View File

@@ -0,0 +1,429 @@
import { describe, test, expect, beforeAll, afterAll } from "vitest"
import { ScaffoldConfig } from "../src/types"
import path from "node:path"
import * as dateFns from "date-fns"
import { dateHelper, defaultHelpers, handlebarsParse, nowHelper, registerHelpers } from "../src/parser"
const blankConf: ScaffoldConfig = {
logLevel: "none",
name: "",
output: "",
templates: [],
data: { name: "test" },
}
describe("parser", () => {
describe("handlebarsParse", () => {
let origSep: string
describe("windows paths", () => {
beforeAll(() => {
origSep = path.sep
Object.defineProperty(path, "sep", { value: "\\" })
})
afterAll(() => {
Object.defineProperty(path, "sep", { value: origSep })
})
test("should work for windows paths", async () => {
expect(handlebarsParse(blankConf, "C:\\exports\\{{name}}.txt", { asPath: true }).toString()).toEqual(
"C:\\exports\\test.txt",
)
})
})
describe("non-windows paths", () => {
beforeAll(() => {
origSep = path.sep
Object.defineProperty(path, "sep", { value: "/" })
})
afterAll(() => {
Object.defineProperty(path, "sep", { value: origSep })
})
test("should work for non-windows paths", async () => {
expect(handlebarsParse(blankConf, "/home/test/{{name}}.txt", { asPath: true })).toEqual(
Buffer.from("/home/test/test.txt"),
)
})
})
test("should not do path escaping on non-path compiles", async () => {
expect(
handlebarsParse(
{ ...blankConf, data: { ...blankConf.data, escaped: "value" } },
"/home/test/{{name}} \\{{escaped}}.txt",
{
asPath: false,
},
),
).toEqual(Buffer.from("/home/test/test {{escaped}}.txt"))
})
test("should replace name token in content", () => {
const result = handlebarsParse(blankConf, "Hello {{name}}")
expect(result.toString()).toEqual("Hello test")
})
test("should replace multiple tokens", () => {
const config: ScaffoldConfig = {
...blankConf,
data: { name: "app", version: "1.0" },
}
expect(handlebarsParse(config, "{{name}} v{{version}}").toString()).toEqual("app v1.0")
})
test("should return Buffer", () => {
expect(Buffer.isBuffer(handlebarsParse(blankConf, "test"))).toBe(true)
})
test("should handle Buffer input", () => {
expect(handlebarsParse(blankConf, Buffer.from("Hello {{name}}")).toString()).toEqual("Hello test")
})
test("should return original content on handlebars error", () => {
const result = handlebarsParse(blankConf, "{{#if}}invalid{{/unless}}")
expect(Buffer.isBuffer(result)).toBe(true)
expect(result.toString()).toEqual("{{#if}}invalid{{/unless}}")
})
test("should handle empty template", () => {
expect(handlebarsParse(blankConf, "").toString()).toEqual("")
})
test("should handle template with no tokens", () => {
expect(handlebarsParse(blankConf, "no tokens here").toString()).toEqual("no tokens here")
})
test("should not escape HTML chars (noEscape)", () => {
const config: ScaffoldConfig = {
...blankConf,
data: { name: "<div>test</div>" },
}
expect(handlebarsParse(config, "{{name}}").toString()).toEqual("<div>test</div>")
})
test("should handle nested data", () => {
const config: ScaffoldConfig = {
...blankConf,
data: { name: "test", nested: { key: "value" } },
}
expect(handlebarsParse(config, "{{nested.key}}").toString()).toEqual("value")
})
test("should handle handlebars conditionals", () => {
const config: ScaffoldConfig = {
...blankConf,
data: { name: "test", showExtra: true },
}
registerHelpers(config)
expect(handlebarsParse(config, "{{#if showExtra}}extra{{/if}} content").toString()).toEqual("extra content")
})
test("should handle handlebars conditionals when false", () => {
const config: ScaffoldConfig = {
...blankConf,
data: { name: "test", showExtra: false },
}
registerHelpers(config)
expect(handlebarsParse(config, "{{#if showExtra}}extra{{/if}}content").toString()).toEqual("content")
})
test("should handle handlebars each loops", () => {
const config: ScaffoldConfig = {
...blankConf,
data: { name: "test", items: ["a", "b", "c"] },
}
registerHelpers(config)
expect(handlebarsParse(config, "{{#each items}}{{this}},{{/each}}").toString()).toEqual("a,b,c,")
})
test("should render empty for undefined data token", () => {
expect(handlebarsParse(blankConf, "{{undefinedVar}}").toString()).toEqual("")
})
})
describe("Helpers", () => {
describe("string helpers", () => {
test("camelCase", () => {
expect(defaultHelpers.camelCase("test string")).toEqual("testString")
expect(defaultHelpers.camelCase("test_string")).toEqual("testString")
expect(defaultHelpers.camelCase("test-string")).toEqual("testString")
expect(defaultHelpers.camelCase("testString")).toEqual("testString")
expect(defaultHelpers.camelCase("TestString")).toEqual("testString")
expect(defaultHelpers.camelCase("Test____String")).toEqual("testString")
})
test("pascalCase", () => {
expect(defaultHelpers.pascalCase("test string")).toEqual("TestString")
expect(defaultHelpers.pascalCase("test_string")).toEqual("TestString")
expect(defaultHelpers.pascalCase("test-string")).toEqual("TestString")
expect(defaultHelpers.pascalCase("testString")).toEqual("TestString")
expect(defaultHelpers.pascalCase("TestString")).toEqual("TestString")
expect(defaultHelpers.pascalCase("Test____String")).toEqual("TestString")
})
test("snakeCase", () => {
expect(defaultHelpers.snakeCase("test string")).toEqual("test_string")
expect(defaultHelpers.snakeCase("test_string")).toEqual("test_string")
expect(defaultHelpers.snakeCase("test-string")).toEqual("test_string")
expect(defaultHelpers.snakeCase("testString")).toEqual("test_string")
expect(defaultHelpers.snakeCase("TestString")).toEqual("test_string")
expect(defaultHelpers.snakeCase("Test____String")).toEqual("test_string")
})
test("kebabCase", () => {
expect(defaultHelpers.kebabCase("test string")).toEqual("test-string")
expect(defaultHelpers.kebabCase("test_string")).toEqual("test-string")
expect(defaultHelpers.kebabCase("test-string")).toEqual("test-string")
expect(defaultHelpers.kebabCase("testString")).toEqual("test-string")
expect(defaultHelpers.kebabCase("TestString")).toEqual("test-string")
expect(defaultHelpers.kebabCase("Test____String")).toEqual("test-string")
})
test("startCase", () => {
expect(defaultHelpers.startCase("test string")).toEqual("Test String")
expect(defaultHelpers.startCase("test_string")).toEqual("Test String")
expect(defaultHelpers.startCase("test-string")).toEqual("Test String")
expect(defaultHelpers.startCase("testString")).toEqual("Test String")
expect(defaultHelpers.startCase("TestString")).toEqual("Test String")
expect(defaultHelpers.startCase("Test____String")).toEqual("Test String")
})
})
describe("string helpers edge cases", () => {
test("camelCase single word", () => {
expect(defaultHelpers.camelCase("hello")).toEqual("hello")
})
test("camelCase empty string", () => {
expect(defaultHelpers.camelCase("")).toEqual("")
})
test("camelCase all uppercase", () => {
expect(defaultHelpers.camelCase("HELLO WORLD")).toEqual("helloWorld")
})
test("pascalCase single word", () => {
expect(defaultHelpers.pascalCase("hello")).toEqual("Hello")
})
test("pascalCase empty string", () => {
expect(defaultHelpers.pascalCase("")).toEqual("")
})
test("snakeCase single word", () => {
expect(defaultHelpers.snakeCase("hello")).toEqual("hello")
})
test("snakeCase empty string", () => {
expect(defaultHelpers.snakeCase("")).toEqual("")
})
test("kebabCase single word", () => {
expect(defaultHelpers.kebabCase("hello")).toEqual("hello")
})
test("kebabCase empty string", () => {
expect(defaultHelpers.kebabCase("")).toEqual("")
})
test("startCase single word", () => {
expect(defaultHelpers.startCase("hello")).toEqual("Hello")
})
test("startCase empty string", () => {
expect(defaultHelpers.startCase("")).toEqual("")
})
test("hyphenCase is same as kebabCase", () => {
expect(defaultHelpers.hyphenCase("testString")).toEqual(defaultHelpers.kebabCase("testString"))
expect(defaultHelpers.hyphenCase("test_string")).toEqual(defaultHelpers.kebabCase("test_string"))
})
test("lowerCase lowercases everything", () => {
expect(defaultHelpers.lowerCase("HELLO")).toEqual("hello")
expect(defaultHelpers.lowerCase("Hello World")).toEqual("hello world")
})
test("upperCase uppercases everything", () => {
expect(defaultHelpers.upperCase("hello")).toEqual("HELLO")
expect(defaultHelpers.upperCase("hello world")).toEqual("HELLO WORLD")
})
test("camelCase handles numbers in string", () => {
expect(defaultHelpers.camelCase("item1_name")).toEqual("item1Name")
})
test("pascalCase handles multiple separators", () => {
expect(defaultHelpers.pascalCase("a--b__c d")).toEqual("ABCD")
})
test("snakeCase handles mixed separators", () => {
expect(defaultHelpers.snakeCase("myApp-name_here")).toEqual("my_app_name_here")
})
test("kebabCase handles mixed separators", () => {
expect(defaultHelpers.kebabCase("myApp-name_here")).toEqual("my-app-name-here")
})
test("single character inputs", () => {
expect(defaultHelpers.camelCase("a")).toEqual("a")
expect(defaultHelpers.pascalCase("a")).toEqual("A")
expect(defaultHelpers.snakeCase("a")).toEqual("a")
expect(defaultHelpers.kebabCase("a")).toEqual("a")
expect(defaultHelpers.startCase("a")).toEqual("A")
})
})
describe("date helpers", () => {
describe("now", () => {
test("should work without extra params", () => {
const now = new Date()
const fmt = "yyyy-MM-dd HH:mm"
expect(nowHelper(fmt)).toEqual(dateFns.format(now, fmt))
})
})
describe("date", () => {
test("should work with no offset params", () => {
const now = new Date()
const fmt = "yyyy-MM-dd HH:mm"
expect(dateHelper(now.toISOString(), fmt)).toEqual(dateFns.format(now, fmt))
})
test("should work with offset params", () => {
const now = new Date()
const fmt = "yyyy-MM-dd HH:mm"
expect(dateHelper(now.toISOString(), fmt, -1, "days")).toEqual(
dateFns.format(dateFns.add(now, { days: -1 }), fmt),
)
expect(dateHelper(now.toISOString(), fmt, 1, "months")).toEqual(
dateFns.format(dateFns.add(now, { months: 1 }), fmt),
)
})
test("should work with years offset", () => {
const dateStr = "2024-01-15T12:00:00.000Z"
const date = dateFns.parseISO(dateStr)
expect(dateHelper(dateStr, "yyyy", 1, "years")).toEqual(
dateFns.format(dateFns.add(date, { years: 1 }), "yyyy"),
)
})
test("should work with weeks offset", () => {
const dateStr = "2024-01-15T12:00:00.000Z"
const date = dateFns.parseISO(dateStr)
expect(dateHelper(dateStr, "yyyy-MM-dd", 2, "weeks")).toEqual(
dateFns.format(dateFns.add(date, { weeks: 2 }), "yyyy-MM-dd"),
)
})
test("should work with minutes offset", () => {
const dateStr = "2024-01-15T12:00:00.000Z"
const date = dateFns.parseISO(dateStr)
expect(dateHelper(dateStr, "HH:mm", 30, "minutes")).toEqual(
dateFns.format(dateFns.add(date, { minutes: 30 }), "HH:mm"),
)
})
test("should work with seconds offset", () => {
const dateStr = "2024-01-15T12:00:00.000Z"
const date = dateFns.parseISO(dateStr)
expect(dateHelper(dateStr, "HH:mm:ss", 45, "seconds")).toEqual(
dateFns.format(dateFns.add(date, { seconds: 45 }), "HH:mm:ss"),
)
})
})
describe("now edge cases", () => {
test("should work with different format tokens", () => {
const now = new Date()
expect(nowHelper("yyyy")).toEqual(dateFns.format(now, "yyyy"))
expect(nowHelper("MM")).toEqual(dateFns.format(now, "MM"))
expect(nowHelper("dd")).toEqual(dateFns.format(now, "dd"))
})
test("should work with positive offset", () => {
const now = new Date()
const result = nowHelper("yyyy-MM-dd", 1, "days")
const expected = dateFns.format(dateFns.add(now, { days: 1 }), "yyyy-MM-dd")
expect(result).toEqual(expected)
})
test("should work with hours offset", () => {
const now = new Date()
const result = nowHelper("HH", 2, "hours")
const expected = dateFns.format(dateFns.add(now, { hours: 2 }), "HH")
expect(result).toEqual(expected)
})
})
})
})
describe("registerHelpers", () => {
test("registers default helpers", () => {
const config: ScaffoldConfig = { ...blankConf }
registerHelpers(config)
const result = handlebarsParse(
{ ...config, data: { name: "hello_world" } },
"{{camelCase name}}",
)
expect(result.toString()).toEqual("helloWorld")
})
test("registers custom helpers", () => {
const config: ScaffoldConfig = {
...blankConf,
helpers: {
reverse: (text: string) => text.split("").reverse().join(""),
},
}
registerHelpers(config)
const result = handlebarsParse(
{ ...config, data: { name: "hello" } },
"{{reverse name}}",
)
expect(result.toString()).toEqual("olleh")
})
test("custom helpers override default helpers", () => {
const config: ScaffoldConfig = {
...blankConf,
helpers: {
camelCase: () => "OVERRIDDEN",
},
}
registerHelpers(config)
const result = handlebarsParse(
{ ...config, data: { name: "test" } },
"{{camelCase name}}",
)
expect(result.toString()).toEqual("OVERRIDDEN")
})
})
describe("default helpers completeness", () => {
test("all expected helpers are defined", () => {
const expectedHelpers = [
"camelCase", "snakeCase", "startCase", "kebabCase",
"hyphenCase", "pascalCase", "lowerCase", "upperCase",
"now", "date",
]
for (const helper of expectedHelpers) {
expect(defaultHelpers).toHaveProperty(helper)
expect(typeof defaultHelpers[helper as keyof typeof defaultHelpers]).toBe("function")
}
})
test("has exactly 10 helpers", () => {
expect(Object.keys(defaultHelpers).length).toBe(10)
})
})
})

529
tests/prompts.test.ts Normal file
View File

@@ -0,0 +1,529 @@
import { describe, test, expect, vi, beforeEach } from "vitest"
import { LogLevel, ScaffoldCmdConfig, ScaffoldConfig } from "../src/types"
vi.mock("@inquirer/input", () => ({
default: vi.fn(),
}))
vi.mock("@inquirer/select", () => ({
default: vi.fn(),
}))
vi.mock("@inquirer/confirm", () => ({
default: vi.fn(),
}))
vi.mock("@inquirer/number", () => ({
default: vi.fn(),
}))
import inputMock from "@inquirer/input"
import selectMock from "@inquirer/select"
import confirmMock from "@inquirer/confirm"
import numberMock from "@inquirer/number"
import {
promptForName,
promptForTemplateKey,
promptForOutput,
promptForTemplates,
promptForMissingConfig,
promptForInputs,
resolveInputs,
isInteractive,
} from "../src/prompts"
function mockTTY(value: boolean) {
Object.defineProperty(process.stdin, "isTTY", { value, configurable: true })
}
const blankConfig: ScaffoldCmdConfig = {
logLevel: LogLevel.none,
name: "",
output: "",
templates: [],
data: {},
overwrite: false,
subdir: false,
dryRun: false,
quiet: false,
version: false,
}
describe("prompts", () => {
beforeEach(() => {
vi.clearAllMocks()
})
describe("promptForName", () => {
test("calls input prompt and returns result", async () => {
vi.mocked(inputMock).mockResolvedValue("my-component")
const result = await promptForName()
expect(result).toEqual("my-component")
expect(inputMock).toHaveBeenCalledOnce()
})
})
describe("promptForTemplateKey", () => {
test("calls select prompt when multiple keys", async () => {
vi.mocked(selectMock).mockResolvedValue("component")
const result = await promptForTemplateKey({
default: { name: "d", templates: [], output: "" },
component: { name: "c", templates: [], output: "" },
})
expect(result).toEqual("component")
expect(selectMock).toHaveBeenCalledOnce()
})
test("returns single key without prompting", async () => {
const result = await promptForTemplateKey({
default: { name: "d", templates: [], output: "" },
})
expect(result).toEqual("default")
expect(selectMock).not.toHaveBeenCalled()
})
test("throws when config map is empty", async () => {
await expect(promptForTemplateKey({})).rejects.toThrow("No templates found")
})
test("presents all keys as choices", async () => {
vi.mocked(selectMock).mockResolvedValue("b")
await promptForTemplateKey({
a: { name: "a", templates: [], output: "" },
b: { name: "b", templates: [], output: "" },
c: { name: "c", templates: [], output: "" },
})
const call = vi.mocked(selectMock).mock.calls[0][0] as {
choices: { name: string; value: string }[]
}
expect(call.choices).toEqual([
{ name: "a", value: "a" },
{ name: "b", value: "b" },
{ name: "c", value: "c" },
])
})
})
describe("promptForOutput", () => {
test("calls input prompt and returns result", async () => {
vi.mocked(inputMock).mockResolvedValue("./dist")
const result = await promptForOutput()
expect(result).toEqual("./dist")
expect(inputMock).toHaveBeenCalledOnce()
})
})
describe("promptForTemplates", () => {
test("parses comma-separated input into array", async () => {
vi.mocked(inputMock).mockResolvedValue("src/templates, lib/other")
const result = await promptForTemplates()
expect(result).toEqual(["src/templates", "lib/other"])
})
test("handles single template", async () => {
vi.mocked(inputMock).mockResolvedValue("src/templates")
const result = await promptForTemplates()
expect(result).toEqual(["src/templates"])
})
test("trims whitespace and filters empty entries", async () => {
vi.mocked(inputMock).mockResolvedValue(" a , , b , ")
const result = await promptForTemplates()
expect(result).toEqual(["a", "b"])
})
})
describe("isInteractive", () => {
test("returns a boolean", () => {
expect(typeof isInteractive()).toBe("boolean")
})
})
describe("promptForMissingConfig", () => {
test("prompts for all missing values when interactive", async () => {
mockTTY(true)
vi.mocked(inputMock)
.mockResolvedValueOnce("my-app") // name
.mockResolvedValueOnce("./output") // output
.mockResolvedValueOnce("src/tpl") // templates
const config = { ...blankConfig }
const result = await promptForMissingConfig(config)
expect(result.name).toEqual("my-app")
expect(result.output).toEqual("./output")
expect(result.templates).toEqual(["src/tpl"])
expect(inputMock).toHaveBeenCalledTimes(3)
})
test("does not prompt for values already provided", async () => {
mockTTY(true)
const config = {
...blankConfig,
name: "already-set",
output: "./out",
templates: ["tpl"],
}
const result = await promptForMissingConfig(config)
expect(result.name).toEqual("already-set")
expect(result.output).toEqual("./out")
expect(result.templates).toEqual(["tpl"])
expect(inputMock).not.toHaveBeenCalled()
})
test("prompts for template key when multiple templates and no key", async () => {
mockTTY(true)
vi.mocked(inputMock)
.mockResolvedValueOnce("name") // name
.mockResolvedValueOnce("./output") // output
.mockResolvedValueOnce("src/tpl") // templates
vi.mocked(selectMock).mockResolvedValue("component")
const configMap = {
default: { name: "d", templates: [], output: "" },
component: { name: "c", templates: [], output: "" },
}
const config = { ...blankConfig }
const result = await promptForMissingConfig(config, configMap)
expect(result.key).toEqual("component")
})
test("does not prompt for template key when already set", async () => {
mockTTY(true)
const configMap = {
default: { name: "d", templates: [], output: "" },
component: { name: "c", templates: [], output: "" },
}
const config = {
...blankConfig,
name: "test",
output: "./out",
templates: ["tpl"],
key: "default",
}
const result = await promptForMissingConfig(config, configMap)
expect(result.key).toEqual("default")
expect(selectMock).not.toHaveBeenCalled()
})
test("does not prompt for template key when only one template", async () => {
mockTTY(true)
const configMap = {
default: { name: "d", templates: [], output: "" },
}
const config = { ...blankConfig, name: "test", output: "./out", templates: ["tpl"] }
const result = await promptForMissingConfig(config, configMap)
expect(result.key).toBeUndefined()
expect(selectMock).not.toHaveBeenCalled()
})
test("does not prompt in non-interactive mode", async () => {
mockTTY(false)
const config = { ...blankConfig }
const result = await promptForMissingConfig(config)
expect(result.name).toEqual("")
expect(result.output).toEqual("")
expect(result.templates).toEqual([])
expect(inputMock).not.toHaveBeenCalled()
})
test("does not prompt for config key when no config map provided", async () => {
mockTTY(true)
vi.mocked(inputMock)
.mockResolvedValueOnce("name")
.mockResolvedValueOnce("./out")
.mockResolvedValueOnce("tpl")
const config = { ...blankConfig }
const result = await promptForMissingConfig(config)
expect(result.key).toBeUndefined()
expect(selectMock).not.toHaveBeenCalled()
})
test("only prompts for missing values, not provided ones", async () => {
mockTTY(true)
vi.mocked(inputMock).mockResolvedValueOnce("src/tpl") // only templates missing
const config = { ...blankConfig, name: "app", output: "./out" }
const result = await promptForMissingConfig(config)
expect(result.name).toEqual("app")
expect(result.output).toEqual("./out")
expect(result.templates).toEqual(["src/tpl"])
expect(inputMock).toHaveBeenCalledOnce()
})
})
describe("promptForInputs", () => {
test("prompts for required inputs not in existing data", async () => {
vi.mocked(inputMock).mockResolvedValueOnce("John")
const result = await promptForInputs(
{ author: { message: "Author name", required: true } },
{},
)
expect(result.author).toEqual("John")
expect(inputMock).toHaveBeenCalledOnce()
})
test("skips inputs already provided in data", async () => {
const result = await promptForInputs(
{ author: { message: "Author name", required: true } },
{ author: "Jane" },
)
expect(result.author).toEqual("Jane")
expect(inputMock).not.toHaveBeenCalled()
})
test("applies default value for optional inputs not in data", async () => {
const result = await promptForInputs({ license: { default: "MIT" } }, {})
expect(result.license).toEqual("MIT")
expect(inputMock).not.toHaveBeenCalled()
})
test("does not apply default when value already exists", async () => {
const result = await promptForInputs(
{ license: { default: "MIT" } },
{ license: "Apache-2.0" },
)
expect(result.license).toEqual("Apache-2.0")
})
test("uses input key as message fallback", async () => {
vi.mocked(inputMock).mockResolvedValueOnce("val")
await promptForInputs({ myField: { required: true } }, {})
const call = vi.mocked(inputMock).mock.calls[0][0] as { message: string }
expect(call.message).toContain("myField")
})
test("prompts multiple required inputs in order", async () => {
vi.mocked(inputMock).mockResolvedValueOnce("John").mockResolvedValueOnce("2.0")
const result = await promptForInputs(
{
author: { message: "Author", required: true },
version: { message: "Version", required: true },
},
{},
)
expect(result.author).toEqual("John")
expect(result.version).toEqual("2.0")
expect(inputMock).toHaveBeenCalledTimes(2)
})
test("mixes prompts, defaults, and existing data", async () => {
vi.mocked(inputMock).mockResolvedValueOnce("John")
const result = await promptForInputs(
{
author: { message: "Author", required: true },
license: { default: "MIT" },
description: { message: "Desc", required: true },
},
{ description: "My project" },
)
expect(result.author).toEqual("John")
expect(result.license).toEqual("MIT")
expect(result.description).toEqual("My project")
expect(inputMock).toHaveBeenCalledOnce()
})
test("preserves existing data keys not in inputs", async () => {
const result = await promptForInputs({ license: { default: "MIT" } }, { extra: "value" })
expect(result.extra).toEqual("value")
expect(result.license).toEqual("MIT")
})
test("required input with default pre-fills prompt", async () => {
vi.mocked(inputMock).mockResolvedValueOnce("custom")
await promptForInputs({ author: { required: true, default: "Anonymous" } }, {})
const call = vi.mocked(inputMock).mock.calls[0][0] as { default?: string }
expect(call.default).toEqual("Anonymous")
})
test("select input prompts with options", async () => {
vi.mocked(selectMock).mockResolvedValueOnce("MIT")
const result = await promptForInputs(
{
license: {
type: "select",
message: "License",
options: ["MIT", "Apache-2.0", "GPL-3.0"],
},
},
{},
)
expect(result.license).toEqual("MIT")
expect(selectMock).toHaveBeenCalledOnce()
const call = vi.mocked(selectMock).mock.calls[0][0] as {
choices: { name: string; value: string }[]
}
expect(call.choices).toEqual([
{ name: "MIT", value: "MIT" },
{ name: "Apache-2.0", value: "Apache-2.0" },
{ name: "GPL-3.0", value: "GPL-3.0" },
])
})
test("select input with object options", async () => {
vi.mocked(selectMock).mockResolvedValueOnce("mit")
const result = await promptForInputs(
{
license: {
type: "select",
options: [
{ name: "MIT License", value: "mit" },
{ name: "Apache 2.0", value: "apache" },
],
},
},
{},
)
expect(result.license).toEqual("mit")
})
test("select input throws when no options", async () => {
await expect(promptForInputs({ license: { type: "select" } }, {})).rejects.toThrow(
"no options defined",
)
})
test("select input skipped when value already provided", async () => {
const result = await promptForInputs(
{ license: { type: "select", options: ["MIT", "Apache"] } },
{ license: "MIT" },
)
expect(result.license).toEqual("MIT")
expect(selectMock).not.toHaveBeenCalled()
})
test("confirm input prompts and returns boolean", async () => {
vi.mocked(confirmMock).mockResolvedValueOnce(true)
const result = await promptForInputs(
{ private: { type: "confirm", message: "Private?" } },
{},
)
expect(result.private).toBe(true)
expect(confirmMock).toHaveBeenCalledOnce()
})
test("confirm input with default false", async () => {
vi.mocked(confirmMock).mockResolvedValueOnce(false)
await promptForInputs({ private: { type: "confirm", default: false } }, {})
const call = vi.mocked(confirmMock).mock.calls[0][0] as { default?: boolean }
expect(call.default).toBe(false)
})
test("confirm input skipped when value already provided", async () => {
const result = await promptForInputs({ private: { type: "confirm" } }, { private: true })
expect(result.private).toBe(true)
expect(confirmMock).not.toHaveBeenCalled()
})
test("number input prompts and returns number", async () => {
vi.mocked(numberMock).mockResolvedValueOnce(8080)
const result = await promptForInputs(
{ port: { type: "number", message: "Port", required: true } },
{},
)
expect(result.port).toBe(8080)
expect(numberMock).toHaveBeenCalledOnce()
})
test("number input with default", async () => {
vi.mocked(numberMock).mockResolvedValueOnce(3000)
await promptForInputs({ port: { type: "number", default: 3000, required: true } }, {})
const call = vi.mocked(numberMock).mock.calls[0][0] as { default?: number }
expect(call.default).toBe(3000)
})
test("number input skipped when value already provided", async () => {
const result = await promptForInputs(
{ port: { type: "number", required: true } },
{ port: 9090 },
)
expect(result.port).toBe(9090)
expect(numberMock).not.toHaveBeenCalled()
})
test("mixed input types in one config", async () => {
vi.mocked(inputMock).mockResolvedValueOnce("John")
vi.mocked(selectMock).mockResolvedValueOnce("MIT")
vi.mocked(confirmMock).mockResolvedValueOnce(true)
vi.mocked(numberMock).mockResolvedValueOnce(3000)
const result = await promptForInputs(
{
author: { type: "text", message: "Author", required: true },
license: { type: "select", message: "License", options: ["MIT", "Apache"] },
private: { type: "confirm", message: "Private?" },
port: { type: "number", message: "Port", required: true },
},
{},
)
expect(result.author).toEqual("John")
expect(result.license).toEqual("MIT")
expect(result.private).toBe(true)
expect(result.port).toBe(3000)
})
})
describe("resolveInputs", () => {
test("returns config unchanged when no inputs defined", async () => {
const config: ScaffoldConfig = {
name: "test",
output: "out",
templates: [],
data: { foo: "bar" },
}
const result = await resolveInputs(config)
expect(result.data).toEqual({ foo: "bar" })
})
test("applies defaults in non-interactive mode", async () => {
mockTTY(false)
const config: ScaffoldConfig = {
name: "test",
output: "out",
templates: [],
data: {},
inputs: {
license: { default: "MIT" },
},
}
const result = await resolveInputs(config)
expect(result.data?.license).toEqual("MIT")
expect(inputMock).not.toHaveBeenCalled()
})
test("does not overwrite existing data with defaults in non-interactive mode", async () => {
mockTTY(false)
const config: ScaffoldConfig = {
name: "test",
output: "out",
templates: [],
data: { license: "Apache-2.0" },
inputs: {
license: { default: "MIT" },
},
}
const result = await resolveInputs(config)
expect(result.data?.license).toEqual("Apache-2.0")
})
test("prompts for required inputs in interactive mode", async () => {
mockTTY(true)
vi.mocked(inputMock).mockResolvedValueOnce("John")
const config: ScaffoldConfig = {
name: "test",
output: "out",
templates: [],
data: {},
inputs: {
author: { message: "Author", required: true },
},
}
const result = await resolveInputs(config)
expect(result.data?.author).toEqual("John")
})
})
})

File diff suppressed because it is too large Load Diff

3
tests/test-config.d.ts vendored Normal file
View File

@@ -0,0 +1,3 @@
declare const config: import("../dist").ScaffoldConfigFile;
export = config;

24
tests/test-config.js Normal file
View File

@@ -0,0 +1,24 @@
// @ts-check
/** @type {import('../dist').ScaffoldConfigFile} */
// eslint-disable-next-line no-undef
module.exports = (conf) => {
// eslint-disable-next-line no-undef
console.log("Config:", conf)
return {
default: {
templates: ["examples/test-input/Component"],
output: "examples/test-output",
data: { property: "myProp", value: "10" },
},
component: {
templates: ["examples/test-input/Component"],
output: "examples/test-output/component",
data: { property: "myProp", value: "10" },
},
configs: {
templates: ["examples/test-input/**/.*"],
output: "examples/test-output/configs",
name: "---",
},
}
}

View File

@@ -1,125 +1,207 @@
import { ScaffoldCmdConfig, ScaffoldConfig } from "../src/types"
import path from "path"
import * as dateFns from "date-fns"
import { OptionsBase } from "massarg/types"
import { dateHelper, handlebarsParse, nowHelper } from "../src/parser"
import { parseAppendData } from "../src/config"
const blankConf: ScaffoldConfig = {
verbose: 0,
name: "",
output: "",
templates: [],
data: { name: "test" },
}
const blankCliConf: ScaffoldCmdConfig & OptionsBase = {
verbose: 0,
name: "",
output: "",
templates: [],
data: { name: "test" },
overwrite: false,
createSubFolder: false,
dryRun: false,
quiet: false,
extras: [],
help: false,
}
describe("Utils", () => {
describe("handlebarsParse", () => {
let origSep: any
describe("windows paths", () => {
beforeAll(() => {
origSep = path.sep
Object.defineProperty(path, "sep", { value: "\\" })
})
afterAll(() => {
Object.defineProperty(path, "sep", { value: origSep })
})
test("should work for windows paths", async () => {
expect(handlebarsParse(blankConf, "C:\\exports\\{{name}}.txt", { isPath: true })).toEqual(
Buffer.from("C:\\exports\\test.txt"),
)
})
import { describe, test, expect } from "vitest"
import { handleErr, resolve, wrapNoopResolver, colorize, TermColor } from "../src/utils"
describe("utils", () => {
describe("resolve", () => {
test("should resolve function", () => {
expect(resolve(() => 1, null)).toBe(1)
expect(resolve((x) => x, 2)).toBe(2)
})
describe("non-windows paths", () => {
beforeAll(() => {
origSep = path.sep
Object.defineProperty(path, "sep", { value: "/" })
})
afterAll(() => {
Object.defineProperty(path, "sep", { value: origSep })
})
test("should work for non-windows paths", async () => {
expect(handlebarsParse(blankConf, "/home/test/{{name}}.txt", { isPath: true })).toEqual(
Buffer.from("/home/test/test.txt"),
)
})
test("should resolve value", () => {
expect(resolve(1, null)).toBe(1)
expect(resolve(2, 1)).toBe(2)
})
test("should not do path escaping on non-path compiles", async () => {
expect(
handlebarsParse(
{ ...blankConf, data: { ...blankConf.data, escaped: "value" } },
"/home/test/{{name}} \\{{escaped}}.txt",
{
isPath: false,
},
),
).toEqual(Buffer.from("/home/test/test {{escaped}}.txt"))
test("should resolve function with argument transformation", () => {
expect(resolve((x: number) => x * 2, 5)).toBe(10)
})
test("should resolve static string", () => {
expect(resolve("hello", null)).toBe("hello")
})
test("should resolve static boolean", () => {
expect(resolve(true, null)).toBe(true)
expect(resolve(false, null)).toBe(false)
})
test("should resolve static object", () => {
const obj = { key: "value" }
expect(resolve(obj, null)).toBe(obj)
})
test("should resolve function returning object", () => {
expect(resolve(() => ({ key: "value" }), null)).toEqual({ key: "value" })
})
test("should pass argument to function", () => {
const fn = (config: { name: string }) => config.name
expect(resolve(fn, { name: "test" })).toBe("test")
})
test("should resolve zero", () => {
expect(resolve(0, null)).toBe(0)
})
test("should resolve null", () => {
expect(resolve(null, "anything")).toBe(null)
})
test("should resolve undefined", () => {
expect(resolve(undefined, "anything")).toBe(undefined)
})
})
describe("Helpers", () => {
describe("date helpers", () => {
describe("now", () => {
test("should work without extra params", () => {
const now = new Date()
const fmt = "yyyy-MM-dd HH:mm"
describe("handleErr", () => {
test("should throw error", () => {
expect(() => handleErr({ name: "test", message: "test" })).toThrow()
expect(() => handleErr(null as never)).not.toThrow()
})
expect(nowHelper(fmt)).toEqual(dateFns.format(now, fmt))
})
})
describe("date", () => {
test("should work with no offset params", () => {
const now = new Date()
const fmt = "yyyy-MM-dd HH:mm"
expect(dateHelper(now.toISOString(), fmt)).toEqual(dateFns.format(now, fmt))
})
test("should work with offset params", () => {
const now = new Date()
const fmt = "yyyy-MM-dd HH:mm"
expect(dateHelper(now.toISOString(), fmt, -1, "days")).toEqual(
dateFns.format(dateFns.add(now, { days: -1 }), fmt),
)
expect(dateHelper(now.toISOString(), fmt, 1, "months")).toEqual(
dateFns.format(dateFns.add(now, { months: 1 }), fmt),
)
})
})
test("should throw the provided error", () => {
const err = new Error("test error")
expect(() => handleErr(err as unknown as NodeJS.ErrnoException)).toThrow("test error")
})
})
describe("parseAppendData", () => {
test('works for "key=value"', () => {
expect(parseAppendData("key=value", blankCliConf)).toEqual({ key: "value", name: "test" })
describe("wrapNoopResolver", () => {
test("should wrap static value in function", () => {
const wrapped = wrapNoopResolver("hello")
expect(typeof wrapped).toBe("function")
expect((wrapped as (_: unknown) => unknown)("anything")).toBe("hello")
})
test('works for "key:=value"', () => {
expect(parseAppendData("key:=123", blankCliConf)).toEqual({ key: 123, name: "test" })
test("should return function as-is", () => {
const fn = (x: string) => x.toUpperCase()
const wrapped = wrapNoopResolver(fn)
expect(wrapped).toBe(fn)
})
test("overwrites existing value", () => {
expect(parseAppendData("name:=123", blankCliConf)).toEqual({ name: 123 })
test("should wrap object value", () => {
const obj = { key: "value" }
const wrapped = wrapNoopResolver(obj)
expect(typeof wrapped).toBe("function")
expect((wrapped as (_: unknown) => unknown)("anything")).toBe(obj)
})
test("works with quotes", () => {
expect(parseAppendData('key="value test"', blankCliConf)).toEqual({ key: "value test", name: "test" })
test("should wrap boolean value", () => {
const wrapped = wrapNoopResolver(true)
expect(typeof wrapped).toBe("function")
expect((wrapped as (_: unknown) => unknown)(null)).toBe(true)
})
test("should wrap number value", () => {
const wrapped = wrapNoopResolver(42)
expect(typeof wrapped).toBe("function")
expect((wrapped as (_: unknown) => unknown)(null)).toBe(42)
})
})
})
describe("colorize", () => {
test("should colorize text with red color", () => {
const result = colorize("Hello", "red")
expect(result).toBe("\x1b[31mHello\x1b[0m")
})
test("should colorize text with bold", () => {
const result = colorize("Hello", "bold")
expect(result).toBe("\x1b[1mHello\x1b[23m")
})
test("should reset color", () => {
const result = colorize("Hello", "reset")
expect(result).toBe("\x1b[0mHello\x1b[0m")
})
test("should have all color functions", () => {
const colors: TermColor[] = [
"reset",
"dim",
"bold",
"italic",
"underline",
"red",
"green",
"yellow",
"blue",
"magenta",
"cyan",
"white",
"gray",
]
colors.forEach((color) => {
expect(typeof colorize[color]).toBe("function")
})
})
test("should colorize text using colorize.red", () => {
const result = colorize.red("Hello")
expect(result).toBe("\x1b[31mHello\x1b[0m")
})
test("should colorize text using template strings with colorize.blue", () => {
const result = colorize.blue`Hello ${"World"}`
expect(result).toBe("\x1b[34mHello World\x1b[0m")
})
test("should colorize with green", () => {
expect(colorize("Hello", "green")).toBe("\x1b[32mHello\x1b[0m")
})
test("should colorize with yellow", () => {
expect(colorize("Hello", "yellow")).toBe("\x1b[33mHello\x1b[0m")
})
test("should colorize with magenta", () => {
expect(colorize("Hello", "magenta")).toBe("\x1b[35mHello\x1b[0m")
})
test("should colorize with cyan", () => {
expect(colorize("Hello", "cyan")).toBe("\x1b[36mHello\x1b[0m")
})
test("should colorize with white", () => {
expect(colorize("Hello", "white")).toBe("\x1b[37mHello\x1b[0m")
})
test("should colorize with gray", () => {
expect(colorize("Hello", "gray")).toBe("\x1b[90mHello\x1b[0m")
})
test("should colorize with dim", () => {
expect(colorize("Hello", "dim")).toBe("\x1b[2mHello\x1b[22m")
})
test("should colorize with italic", () => {
expect(colorize("Hello", "italic")).toBe("\x1b[3mHello\x1b[23m")
})
test("should colorize with underline", () => {
expect(colorize("Hello", "underline")).toBe("\x1b[4mHello\x1b[24m")
})
test("color functions work as template strings", () => {
const name = "World"
expect(colorize.green`Hello ${name}`).toBe("\x1b[32mHello World\x1b[0m")
})
test("color functions work with direct call", () => {
expect(colorize.yellow("warning")).toBe("\x1b[33mwarning\x1b[0m")
expect(colorize.cyan("info")).toBe("\x1b[36minfo\x1b[0m")
})
test("handles empty string", () => {
expect(colorize("", "red")).toBe("\x1b[31m\x1b[0m")
})
test("handles special characters", () => {
expect(colorize("hello\nworld", "blue")).toBe("\x1b[34mhello\nworld\x1b[0m")
})
test("template string with multiple interpolations", () => {
const a = "one"
const b = "two"
expect(colorize.red`${a} and ${b}`).toBe("\x1b[31mone and two\x1b[0m")
})
})

231
tests/validate.test.ts Normal file
View File

@@ -0,0 +1,231 @@
import { describe, test, expect, beforeEach, afterEach } from "vitest"
import mockFs from "mock-fs"
import { Console } from "console"
import { validateConfig, validateTemplatePaths, assertConfigValid } from "../src/validate"
const validConfig = {
name: "test",
templates: ["templates"],
output: "output",
}
describe("validate", () => {
describe("validateConfig", () => {
test("returns no errors for valid config", () => {
expect(validateConfig(validConfig)).toEqual([])
})
test("returns no errors with all optional fields", () => {
const errors = validateConfig({
...validConfig,
subdir: true,
subdirHelper: "camelCase",
data: { key: "value" },
logLevel: "debug",
dryRun: true,
overwrite: true,
inputs: {
author: { type: "text", message: "Author", required: true },
},
})
expect(errors).toEqual([])
})
test("errors on missing name", () => {
const errors = validateConfig({ ...validConfig, name: "" })
expect(errors.length).toBeGreaterThan(0)
expect(errors[0]).toContain("name")
})
test("errors on missing templates", () => {
const errors = validateConfig({ ...validConfig, templates: [] })
expect(errors.length).toBeGreaterThan(0)
expect(errors[0]).toContain("templates")
})
test("errors on invalid logLevel", () => {
const errors = validateConfig({ ...validConfig, logLevel: "verbose" })
expect(errors.length).toBeGreaterThan(0)
expect(errors[0]).toContain("logLevel")
})
test("errors on subdirHelper without subdir", () => {
const errors = validateConfig({
...validConfig,
subdirHelper: "camelCase",
})
expect(errors.length).toBeGreaterThan(0)
expect(errors[0]).toContain("subdirHelper")
})
test("no error on subdirHelper with subdir", () => {
const errors = validateConfig({
...validConfig,
subdir: true,
subdirHelper: "camelCase",
})
expect(errors).toEqual([])
})
test("errors on select input without options", () => {
const errors = validateConfig({
...validConfig,
inputs: {
license: { type: "select" },
},
})
expect(errors.length).toBeGreaterThan(0)
expect(errors.some((e) => e.includes("select") && e.includes("options"))).toBe(true)
})
test("no error on select input with options", () => {
const errors = validateConfig({
...validConfig,
inputs: {
license: { type: "select", options: ["MIT", "Apache"] },
},
})
expect(errors).toEqual([])
})
test("errors on confirm input with non-boolean default", () => {
const errors = validateConfig({
...validConfig,
inputs: {
flag: { type: "confirm", default: "yes" },
},
})
expect(errors.length).toBeGreaterThan(0)
expect(errors.some((e) => e.includes("confirm") && e.includes("boolean"))).toBe(true)
})
test("errors on number input with non-number default", () => {
const errors = validateConfig({
...validConfig,
inputs: {
port: { type: "number", default: "3000" },
},
})
expect(errors.length).toBeGreaterThan(0)
expect(errors.some((e) => e.includes("number"))).toBe(true)
})
test("valid input types pass", () => {
const errors = validateConfig({
...validConfig,
inputs: {
a: { type: "text", required: true },
b: { type: "select", options: ["x", "y"] },
c: { type: "confirm", default: false },
d: { type: "number", default: 42 },
},
})
expect(errors).toEqual([])
})
test("accepts function output", () => {
const errors = validateConfig({
...validConfig,
output: () => "dynamic-output",
})
expect(errors).toEqual([])
})
test("accepts function overwrite", () => {
const errors = validateConfig({
...validConfig,
overwrite: () => true,
})
expect(errors).toEqual([])
})
test("accepts afterScaffold string", () => {
const errors = validateConfig({
...validConfig,
afterScaffold: "npm install",
})
expect(errors).toEqual([])
})
test("accepts afterScaffold function", () => {
const errors = validateConfig({
...validConfig,
afterScaffold: () => {},
})
expect(errors).toEqual([])
})
})
describe("validateTemplatePaths", () => {
beforeEach(() => {
console = new Console(process.stdout, process.stderr)
mockFs({
templates: { "file.txt": "content" },
})
})
afterEach(() => {
mockFs.restore()
})
test("returns no errors for existing paths", async () => {
const errors = await validateTemplatePaths(["templates"])
expect(errors).toEqual([])
})
test("returns error for missing paths", async () => {
const errors = await validateTemplatePaths(["nonexistent"])
expect(errors.length).toBe(1)
expect(errors[0]).toContain("nonexistent")
})
test("skips glob patterns", async () => {
const errors = await validateTemplatePaths(["templates/**/*"])
expect(errors).toEqual([])
})
test("skips negation patterns", async () => {
const errors = await validateTemplatePaths(["!excluded"])
expect(errors).toEqual([])
})
})
describe("assertConfigValid", () => {
beforeEach(() => {
console = new Console(process.stdout, process.stderr)
mockFs({
templates: { "file.txt": "content" },
})
})
afterEach(() => {
mockFs.restore()
})
test("does not throw for valid config", async () => {
await expect(assertConfigValid(validConfig)).resolves.toBeUndefined()
})
test("throws formatted error for invalid config", async () => {
await expect(assertConfigValid({ name: "", templates: [], output: "" })).rejects.toThrow(
"Invalid scaffold config",
)
})
test("includes all errors in message", async () => {
try {
await assertConfigValid({ name: "", templates: [], output: "out" })
} catch (e) {
const msg = (e as Error).message
expect(msg).toContain("name")
expect(msg).toContain("templates")
}
})
test("checks template path existence", async () => {
await expect(
assertConfigValid({ name: "test", templates: ["missing"], output: "out" }),
).rejects.toThrow("does not exist")
})
})
})

View File

@@ -1,16 +1,29 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
"target": "ES2022",
"target": "ESNext",
"module": "commonjs",
"moduleResolution": "node",
"esModuleInterop": true,
"lib": ["ES2022"],
"lib": [
"ESNext"
],
"declaration": true,
"outDir": "dist",
"strict": true,
"sourceMap": true,
"removeComments": false
"removeComments": false,
"paths": {
"@/*": [
"./src/*"
],
},
},
"include": ["src/index.ts", "src/cmd.ts"],
"exclude": ["tests/*"]
"include": [
"src/index.ts",
"src/cmd.ts"
],
"exclude": [
"tests/*"
],
}

View File

@@ -1,4 +1,4 @@
const path = require("path")
const path = require("node:path")
/** @type {import('typedoc').TypeDocOptions} */
module.exports = {

44
vite.config.ts Normal file
View File

@@ -0,0 +1,44 @@
import { defineConfig } from "vite"
import path from "node:path"
export default defineConfig({
build: {
lib: {
entry: {
index: path.resolve(__dirname, "src/index.ts"),
cmd: path.resolve(__dirname, "src/cmd.ts"),
},
formats: ["cjs"],
},
outDir: "dist",
target: "node20",
rollupOptions: {
// Externalize all node builtins and all dependencies
external: [
/^node:/, // Node builtins
/^[^./]/, // All bare imports (dependencies)
],
output: {
exports: "named",
},
},
sourcemap: true,
minify: false,
},
resolve: {
alias: {
"@": path.resolve(__dirname, "src"),
},
},
test: {
globals: true,
clearMocks: true,
coverage: {
enabled: true,
provider: "v8",
reportsDirectory: "coverage",
exclude: ["node_modules/", "scaffold.config.js", "dist/", "docs/"],
},
exclude: ["node_modules", "dist", "docs"],
},
})