Compare commits
155 Commits
v1.8.0-pre
...
v3.1.1
| Author | SHA1 | Date | |
|---|---|---|---|
| 8834e49085 | |||
| c797893eb5 | |||
| 5b5a1fa9a4 | |||
| f5ed38ad95 | |||
| 6f15f3db14 | |||
| 02dcbdaf13 | |||
| 970e8e0b37 | |||
| c860e4644a | |||
| bb0248f91a | |||
| 972d199fbb | |||
| b5fd1df821 | |||
| f6408f221d | |||
| 0a4ead17c0 | |||
| 7926b15053 | |||
| 9cdea1c5ea | |||
| 4a79961ae5 | |||
| 51f422cc90 | |||
| 93f3a4caaf | |||
| 04e7e895d7 | |||
| 68e6d17fa9 | |||
| d64dd4f0e7 | |||
| 519ef273ac | |||
| 1431fda3db | |||
| 2229a9cda1 | |||
| 1f80a50185 | |||
| e82827d909 | |||
| 2e49448e59 | |||
| 0ffd7ef788 | |||
| d16fb17c38 | |||
| af33c059b9 | |||
| d487d36b04 | |||
| 429f12d1b8 | |||
| dcba30689b | |||
| 7745385573 | |||
| 29f2afe097 | |||
| 4b0b4e7380 | |||
|
|
c1536839e3 | ||
| 7e029fd122 | |||
| 41f4ca52f1 | |||
|
|
78d6bf186d | ||
|
|
80c92bfe84 | ||
| 162cc8cec1 | |||
|
|
db6177c200 | ||
| ae64db846f | |||
| 89dc43c73d | |||
| 2c43dc4daf | |||
| f4c907e6c9 | |||
| a275e688d4 | |||
| ff4ebf0a5b | |||
| ab9322e1ab | |||
|
|
35f0d014d9 | ||
| 8ad8cb4be1 | |||
| daaefaf54e | |||
| aefba4b773 | |||
|
|
8457f0996a | ||
|
|
adc95809ba | ||
| 98b326c843 | |||
| ddc115a037 | |||
| 19e7b0f0c3 | |||
|
|
f883571daa | ||
|
|
be3068a533 | ||
|
|
8acc660dea | ||
|
|
df6c351cb0 | ||
|
|
5f810e2116 | ||
| d579c09c11 | |||
| 3765398ab9 | |||
|
|
9be8a8b71b | ||
| 5cb8c3c081 | |||
| 3b52c255f0 | |||
| 80cf2076b0 | |||
| 4fd710b763 | |||
| 4e7ac34db9 | |||
| e48b832e0b | |||
| 06ffa656ae | |||
| 919fd54ebb | |||
|
|
dbc3283d5a | ||
|
|
0e567ec56f | ||
|
|
0ec671fe83 | ||
| 1b70897f98 | |||
| 43c9e8748f | |||
| 65b14bc707 | |||
| f583af662c | |||
| 600cc78186 | |||
| b4aea804cb | |||
| 795635dc61 | |||
| 6a026ce1a1 | |||
| 268e4d973c | |||
| a403d9df9b | |||
| 298beff355 | |||
| 8f1d58f2c2 | |||
| b10d69d2fe | |||
|
|
069c890ca3 | ||
| 5d7f449050 | |||
| cd29bd0521 | |||
| c9e5dad746 | |||
| 18f73b3eca | |||
| 4aa52c84bd | |||
| f748125ae6 | |||
|
|
43afe60ce8 | ||
| bc0a18dce0 | |||
|
|
179fabc579 | ||
| 909fe5bbea | |||
|
|
bc7d687977 | ||
| e6d9816a2f | |||
| e2353134d4 | |||
| 6c7e3e5068 | |||
| 81dd6e3b19 | |||
| b852a956ba | |||
| 0ecc2590c7 | |||
| af4b753a6d | |||
|
|
a8162f2315 | ||
| ff92fd7607 | |||
| 0dc1116141 | |||
| f36cf4c2f2 | |||
|
|
dd58c7fdda | ||
|
|
058f1d4afa | ||
| 583be2d9d2 | |||
|
|
18f1fac119 | ||
| 693b8c8ea4 | |||
| 901d5d76b4 | |||
| fac571f588 | |||
| a10f412337 | |||
| 819cd20644 | |||
| dc4c940559 | |||
| 816e2f9aa0 | |||
| 825bd096c2 | |||
| 939200c9f2 | |||
| 0b7653de72 | |||
| 89aacb58fd | |||
| 9ce2845ace | |||
| 5373495f80 | |||
| 70a080cc8e | |||
| ef7811d7e2 | |||
| 8b6b958480 | |||
| 2c73207784 | |||
| 5252642f6b | |||
| 361778a188 | |||
| a54b1d6297 | |||
| ae69eb52db | |||
| 3b60f1816d | |||
| c04d1cc42a | |||
| c6b185cd8f | |||
| 5abf528b81 | |||
| aeb3680ddf | |||
| 40e2381d05 | |||
|
|
4d1a6e1f0d | ||
| daae9c10f9 | |||
| 758719de5d | |||
| 1903055d46 | |||
|
|
cbaf130a0c | ||
| e26fe2a3d4 | |||
| 7e1acf0607 | |||
|
|
f666c357f4 | ||
| f5d55f234a | |||
|
|
746f924a22 |
29
.github/workflows/docs.yml
vendored
@@ -1,29 +0,0 @@
|
||||
name: Documentation
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
jobs:
|
||||
docs:
|
||||
runs-on: ubuntu-latest
|
||||
if: "!contains(github.event.head_commit.message, '[skip docs]')"
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: "18.x"
|
||||
- name: Install PNPM
|
||||
run: npm i -g pnpm
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: Build Docs
|
||||
run: pnpm build-docs
|
||||
- name: Deploy on GitHub Pages
|
||||
uses: peaceiris/actions-gh-pages@v3
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
publish_dir: ./docs
|
||||
26
.github/workflows/pull_requests.yml
vendored
@@ -1,26 +0,0 @@
|
||||
name: Pull Requests
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [master, pre, develop]
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
if: "!contains(github.event.head_commit.message, '[skip ci]')"
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: "18.x"
|
||||
- name: Install PNPM
|
||||
run: npm i -g pnpm
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: Run Tests
|
||||
run: pnpm test
|
||||
- name: Build Package
|
||||
run: pnpm build
|
||||
125
.github/workflows/release.yml
vendored
@@ -1,57 +1,102 @@
|
||||
name: Test & Build
|
||||
name: Release
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
push:
|
||||
branches: [master, pre, develop]
|
||||
branches:
|
||||
- master
|
||||
|
||||
permissions:
|
||||
contents: read # for checkout
|
||||
contents: write
|
||||
pull-requests: write
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Test
|
||||
permissions:
|
||||
contents: write # to be able to publish a GitHub release
|
||||
issues: write # to be able to comment on released issues
|
||||
pull-requests: write # to be able to comment on released pull requests
|
||||
id-token: write # to enable use of OIDC for npm provenance
|
||||
runs-on: ubuntu-latest
|
||||
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:
|
||||
run_install: |
|
||||
- recursive: true
|
||||
args: [--frozen-lockfile, --strict-peer-dependencies]
|
||||
node-version: 22
|
||||
cache: pnpm
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: pnpm test
|
||||
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
needs: test
|
||||
permissions:
|
||||
contents: write # to be able to publish a GitHub release
|
||||
issues: write # to be able to comment on released issues
|
||||
pull-requests: write # to be able to comment on released pull requests
|
||||
id-token: write # to enable use of OIDC for npm provenance
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v3
|
||||
node-version: 22
|
||||
cache: pnpm
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: pnpm build
|
||||
|
||||
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:
|
||||
node-version: "18.x"
|
||||
- name: Install PNPM
|
||||
run: npm i -g pnpm
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: Run Tests
|
||||
run: pnpm test
|
||||
- name: Build Package
|
||||
run: pnpm build
|
||||
- name: Pack
|
||||
run: cd ./dist && pnpm pack --pack-destination=../
|
||||
- name: Semantic Release
|
||||
run: npx semantic-release
|
||||
env:
|
||||
NPM_TOKEN: "${{ secrets.NPM_TOKEN }}"
|
||||
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
||||
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: |
|
||||
echo "node: $(node --version)"
|
||||
echo "npm: $(npm --version)"
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: pnpm build
|
||||
- run: npm publish ./dist --provenance
|
||||
|
||||
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: 22
|
||||
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
@@ -64,5 +64,5 @@ examples/test-output/**/*
|
||||
dist/
|
||||
.DS_Store
|
||||
tmp/
|
||||
docs/
|
||||
.nvmrc
|
||||
|
||||
|
||||
1
.husky/pre-commit
Normal file
@@ -0,0 +1 @@
|
||||
pnpm lint-staged
|
||||
5
.prettierignore
Normal file
@@ -0,0 +1,5 @@
|
||||
docs/docs/api/
|
||||
examples/
|
||||
.github/
|
||||
CHANGELOG.md
|
||||
pnpm-lock.yaml
|
||||
@@ -1,8 +1,9 @@
|
||||
{
|
||||
"semi": false,
|
||||
"singleQuote": false,
|
||||
"trailingComma": "all",
|
||||
"printWidth": 120,
|
||||
"tabWidth": 2,
|
||||
"printWidth": 100,
|
||||
"overrides": [
|
||||
{
|
||||
"files": "*.md",
|
||||
|
||||
764
CHANGELOG.md
@@ -1,197 +1,713 @@
|
||||
# Change Log
|
||||
|
||||
## [1.8.0-pre.1](https://github.com/chenasraf/simple-scaffold/compare/v1.7.2...v1.8.0-pre.1) (2023-11-27)
|
||||
## [3.1.1](https://github.com/chenasraf/simple-scaffold/compare/v3.1.0...v3.1.1) (2026-03-26)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **config:** fn config load ([457c904](https://github.com/chenasraf/simple-scaffold/commit/457c90470b0f138862469ff878c7e061c7afd18a)), closes [#63](https://github.com/chenasraf/simple-scaffold/issues/63)
|
||||
* **deps:** update dependencies ([f5ed38a](https://github.com/chenasraf/simple-scaffold/commit/f5ed38ad950e3738b173cd39baf2acc9e35bf8de))
|
||||
|
||||
## [1.7.2](https://github.com/chenasraf/simple-scaffold/compare/v1.7.1...v1.7.2) (2023-08-20)
|
||||
## [3.1.0](https://github.com/chenasraf/simple-scaffold/compare/v3.0.0...v3.1.0) (2026-03-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* windows path resolution ([98ee000](https://github.com/chenasraf/simple-scaffold/commit/98ee00031fc1ad67a53797a9e28e5c4759bc8bce))
|
||||
|
||||
## [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](https://github.com/chenasraf/simple-scaffold/commit/98ee00031fc1ad67a53797a9e28e5c4759bc8bce))
|
||||
|
||||
## [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](https://github.com/chenasraf/simple-scaffold/commit/2b7423993be06b2375631642455c801ae2acf75f))
|
||||
|
||||
## [1.7.0](https://github.com/chenasraf/simple-scaffold/compare/v1.7.0-develop.5...v1.7.0) (2023-05-17)
|
||||
|
||||
## [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](https://github.com/chenasraf/simple-scaffold/commit/2b7423993be06b2375631642455c801ae2acf75f))
|
||||
|
||||
## [1.7.0-develop.1](https://github.com/chenasraf/simple-scaffold/compare/v1.6.0...v1.7.0-develop.1) (2023-05-09)
|
||||
|
||||
### 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.6.0-develop.1...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)
|
||||
|
||||
## [1.6.0-develop.1](https://github.com/chenasraf/simple-scaffold/compare/v1.5.0...v1.6.0-develop.1) (2023-05-04)
|
||||
|
||||
### 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))
|
||||
|
||||
## [1.5.0](https://github.com/chenasraf/simple-scaffold/compare/v1.5.0-develop.1...v1.5.0) (2023-05-02)
|
||||
## [2.3.3](https://github.com/chenasraf/simple-scaffold/compare/v2.3.2...v2.3.3) (2025-06-18)
|
||||
|
||||
|
||||
### 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)
|
||||
|
||||
## [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))
|
||||
* remove chalk dependency ([ab9322e](https://github.com/chenasraf/simple-scaffold/commit/ab9322e1ab9c0a07cdab7275f3398286dee67a64))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* exclude globs ([89dc43c](https://github.com/chenasraf/simple-scaffold/commit/89dc43c73d9a8640f45ae77e5c89e4f08f7f99ad))
|
||||
|
||||
## [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.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))
|
||||
* `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))
|
||||
|
||||
## [1.3.2](https://github.com/chenasraf/simple-scaffold/compare/v1.3.1...v1.3.2) (2023-04-28)
|
||||
# [2.1.0](https://github.com/chenasraf/simple-scaffold/compare/v2.0.2...v2.1.0) (2024-02-12)
|
||||
|
||||
### 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))
|
||||
|
||||
## [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))
|
||||
|
||||
## [1.3.0](https://github.com/chenasraf/simple-scaffold/compare/v1.2.0...v1.3.0) (2023-04-25)
|
||||
|
||||
### Features
|
||||
|
||||
- load scaffold config from files
|
||||
([c398976](https://github.com/chenasraf/simple-scaffold/commit/c3989769fee445c9183ff5e5b3892c4e9fb66a9e))
|
||||
* 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)
|
||||
|
||||
|
||||
### 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))
|
||||
* try to await scaffold before finally ([1b70897](https://github.com/chenasraf/simple-scaffold/commit/1b70897f9840e6365ff800490fbb813b9840177d))
|
||||
|
||||
## [1.2.0](https://github.com/chenasraf/simple-scaffold/compare/v1.1.4...v1.2.0) (2023-04-24)
|
||||
## [2.0.1](https://github.com/chenasraf/simple-scaffold/compare/v2.0.0...v2.0.1) (2024-02-02)
|
||||
|
||||
### Features
|
||||
|
||||
- append-data cli flag
|
||||
([3c5c2de](https://github.com/chenasraf/simple-scaffold/commit/3c5c2ded02f61ff086e81ea4a7f40529bdff1c9d))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 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))
|
||||
* 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.1.3](https://github.com/chenasraf/simple-scaffold/compare/v1.1.2...v1.1.3) (2023-03-11)
|
||||
# [2.0.0](https://github.com/chenasraf/simple-scaffold/compare/v1.9.0...v2.0.0) (2024-01-31)
|
||||
|
||||
### Bug Fixes
|
||||
* 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))
|
||||
|
||||
- base path
|
||||
([943717a](https://github.com/chenasraf/simple-scaffold/commit/943717a76998ec0609f2072c886df6b4775f2ea2))
|
||||
- binary files + add tests
|
||||
([e450ad2](https://github.com/chenasraf/simple-scaffold/commit/e450ad242ed70ae928b19964da38cdcb1b6cf659))
|
||||
|
||||
## [1.1.0](https://github.com/chenasraf/simple-scaffold/compare/v1.0.4...v1.1.0) (2022-04-21)
|
||||
|
||||
## [1.0.3](https://github.com/chenasraf/simple-scaffold/compare/v1.0.2...v1.0.3) (2022-03-03)
|
||||
## 1.9.0 (2024-01-02)
|
||||
|
||||
## [1.0.1-pre.1](https://github.com/chenasraf/simple-scaffold/compare/v1.0.0...v1.0.1-pre.1) (2022-02-17)
|
||||
* 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))
|
||||
|
||||
## [0.7.5](https://github.com/chenasraf/simple-scaffold/compare/v0.7.4...v0.7.5) (2021-09-26)
|
||||
|
||||
## [0.7.4](https://github.com/chenasraf/simple-scaffold/compare/v0.7.3...v0.7.4) (2021-09-26)
|
||||
|
||||
## [0.7.3](https://github.com/chenasraf/simple-scaffold/compare/v0.7.2...v0.7.3) (2021-09-26)
|
||||
## 1.8.0 (2023-11-29)
|
||||
|
||||
## [0.7.2](https://github.com/chenasraf/simple-scaffold/compare/v0.6.1...v0.7.2) (2021-04-19)
|
||||
* 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))
|
||||
|
||||
## [0.6.1](https://github.com/chenasraf/simple-scaffold/compare/v0.6.0...v0.6.1) (2021-02-01)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- binary files
|
||||
([7c0c347](https://github.com/chenasraf/simple-scaffold/commit/7c0c3470020d7c166ea68a8effa6df65ec38f2c8))
|
||||
## <small>1.7.2 (2023-08-20)</small>
|
||||
|
||||
## [0.6.0](https://github.com/chenasraf/simple-scaffold/compare/v0.5.0...v0.6.0) (2021-02-01)
|
||||
* chore(release): 1.7.2 [skip ci] ([d62eeeb](https://github.com/chenasraf/simple-scaffold/commit/d62eeeb))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- support deeper file structure
|
||||
([4afafa5](https://github.com/chenasraf/simple-scaffold/commit/4afafa5a4af2e3f4b0af54f20811ecb2c8d98560))
|
||||
|
||||
## [0.5.0](https://github.com/chenasraf/simple-scaffold/compare/v0.4.5...v0.5.0) (2019-02-27)
|
||||
## <small>1.7.2-pre.1 (2023-08-15)</small>
|
||||
|
||||
## [0.4.5](https://github.com/chenasraf/simple-scaffold/compare/v0.4.4...v0.4.5) (2019-02-27)
|
||||
* 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.4.4](https://github.com/chenasraf/simple-scaffold/compare/v0.4.3...v0.4.4) (2019-02-27)
|
||||
|
||||
## [0.4.3](https://github.com/chenasraf/simple-scaffold/compare/v0.4.2...v0.4.3) (2019-02-27)
|
||||
|
||||
## [0.4.2](https://github.com/chenasraf/simple-scaffold/compare/v0.4.1...v0.4.2) (2019-02-25)
|
||||
## <small>1.7.1 (2023-06-07)</small>
|
||||
|
||||
## [0.4.1](https://github.com/chenasraf/simple-scaffold/compare/v0.3.1...v0.4.1) (2019-02-25)
|
||||
* 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))
|
||||
|
||||
## [0.3.1](https://github.com/chenasraf/simple-scaffold/compare/v0.3.0...v0.3.1) (2018-01-15)
|
||||
|
||||
## [0.3.0](https://github.com/chenasraf/simple-scaffold/compare/v0.2.0...v0.3.0) (2018-01-15)
|
||||
|
||||
## [0.2.0](https://github.com/chenasraf/simple-scaffold/compare/v0.1.5...v0.2.0) (2018-01-05)
|
||||
## <small>1.7.1-develop.1 (2023-06-07)</small>
|
||||
|
||||
## [0.1.5](https://github.com/chenasraf/simple-scaffold/compare/v0.1.4...v0.1.5) (2018-01-01)
|
||||
* chore(release): 1.7.1-develop.1 [skip ci] ([77be7c0](https://github.com/chenasraf/simple-scaffold/commit/77be7c0))
|
||||
|
||||
## [0.1.4](https://github.com/chenasraf/simple-scaffold/compare/v0.1.3...v0.1.4) (2018-01-01)
|
||||
|
||||
## [0.1.3](https://github.com/chenasraf/simple-scaffold/compare/v0.1.2...v0.1.3) (2018-01-01)
|
||||
|
||||
## 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.7 (2023-06-07)
|
||||
|
||||
* 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))
|
||||
|
||||
|
||||
|
||||
## 1.7.0-develop.6 (2023-05-27)
|
||||
|
||||
* 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))
|
||||
|
||||
|
||||
|
||||
## 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))
|
||||
|
||||
392
README.md
@@ -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,153 +8,314 @@
|
||||
[Documentation](https://chenasraf.github.io/simple-scaffold) |
|
||||
[NPM](https://npmjs.com/package/simple-scaffold) | [casraf.dev](https://casraf.dev)
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
</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">
|
||||
|
||||

|
||||

|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
> **Full documentation is available at
|
||||
> [chenasraf.github.io/simple-scaffold](https://chenasraf.github.io/simple-scaffold)** — including
|
||||
> detailed guides on [CLI usage](https://chenasraf.github.io/simple-scaffold/docs/usage/cli),
|
||||
> [Node.js API](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),
|
||||
> [examples](https://chenasraf.github.io/simple-scaffold/docs/usage/examples), and
|
||||
> [migration from v1/v2](https://chenasraf.github.io/simple-scaffold/docs/usage/migration).
|
||||
|
||||
### Local Templates
|
||||
## Table of Contents
|
||||
|
||||
The fastest way to get started is to use `npx` to immediately start a scaffold process.
|
||||
- [Getting Started](#getting-started)
|
||||
- [Configuration Files](#configuration-files)
|
||||
- [Templates](#templates)
|
||||
- [Interactive Mode & Inputs](#interactive-mode--inputs)
|
||||
- [Remote Templates](#remote-templates)
|
||||
- [CLI Reference](#cli-reference)
|
||||
- [Node.js API](#nodejs-api)
|
||||
- [Built-in Helpers](#built-in-helpers)
|
||||
- [Contributing](#contributing)
|
||||
|
||||
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.
|
||||
### Install
|
||||
|
||||
`templates/component/{{ pascalName name }}.tsx`
|
||||
|
||||
```tsx
|
||||
// Created: {{ now 'yyyy-MM-dd' }}
|
||||
import React from 'react'
|
||||
|
||||
export default {{pascalCase name}}: React.FC = (props) => {
|
||||
return (
|
||||
<div className="{{camelCase name}}">{{pascalCase name}} Component</div>
|
||||
)
|
||||
}
|
||||
```sh
|
||||
npm install -D simple-scaffold
|
||||
# or use directly with npx
|
||||
npx simple-scaffold
|
||||
```
|
||||
|
||||
To generate the template output, run:
|
||||
### Initialize a Project
|
||||
|
||||
```shell
|
||||
# generate single component
|
||||
$ npx simple-scaffold@latest \
|
||||
-t templates/component -o src/components PageWrapper
|
||||
Run `init` to create a config file and an example template:
|
||||
|
||||
```sh
|
||||
npx simple-scaffold init
|
||||
```
|
||||
|
||||
This will immediately create the following file: `src/components/PageWrapper.tsx`
|
||||
This creates `scaffold.config.js` and `templates/default/{{name}}.md`. Now generate files:
|
||||
|
||||
```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 MyProject
|
||||
```
|
||||
|
||||
### Configuration Files
|
||||
### One-off Usage (No Config)
|
||||
|
||||
You can also use a config file to more easily maintain all your scaffold definitions.
|
||||
Generate files from a template directory without a config file:
|
||||
|
||||
`scaffold.config.js`
|
||||
```sh
|
||||
npx simple-scaffold -t templates/component -o src/components MyComponent
|
||||
```
|
||||
|
||||
## Configuration Files
|
||||
|
||||
Config files let you define reusable 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}`
|
||||
|
||||
### Example
|
||||
|
||||
```js
|
||||
// scaffold.config.js
|
||||
module.exports = {
|
||||
// use "default" to avoid needing to specify key
|
||||
// in this case the key is "component"
|
||||
component: {
|
||||
templates: ["templates/component"],
|
||||
output: "src/components",
|
||||
data: {
|
||||
// ...
|
||||
},
|
||||
page: {
|
||||
templates: ["templates/page"],
|
||||
output: "src/pages",
|
||||
subdir: true,
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Then run:
|
||||
|
||||
```sh
|
||||
npx simple-scaffold -k component MyComponent
|
||||
npx simple-scaffold -k page Dashboard
|
||||
```
|
||||
|
||||
Use the key `default` to skip the `-k` flag entirely.
|
||||
|
||||
### Listing Available Templates
|
||||
|
||||
```sh
|
||||
npx simple-scaffold list
|
||||
npx simple-scaffold list -c path/to/config.js
|
||||
```
|
||||
|
||||
## Templates
|
||||
|
||||
Templates are regular files in a directory. Both **file names** and **file contents** support
|
||||
Handlebars syntax. Simple Scaffold preserves the directory structure of your template folder.
|
||||
|
||||
### Example Template
|
||||
|
||||
`templates/component/{{pascalCase name}}.tsx`
|
||||
|
||||
```tsx
|
||||
// Created: {{ now 'yyyy-MM-dd' }}
|
||||
import React from "react"
|
||||
|
||||
export default {{ pascalCase name }}: React.FC = (props) => {
|
||||
return (
|
||||
<div className="{{ camelCase name }}">{{ pascalCase name }} Component</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
Running `npx simple-scaffold -t templates/component -o src/components PageWrapper` produces
|
||||
`src/components/PageWrapper.tsx` with all tokens replaced.
|
||||
|
||||
### Glob Patterns & Exclusions
|
||||
|
||||
Template paths support globs and negation:
|
||||
|
||||
```js
|
||||
{
|
||||
templates: ["templates/component/**", "!templates/component/README.md"]
|
||||
}
|
||||
```
|
||||
|
||||
### .scaffoldignore
|
||||
|
||||
Place a `.scaffoldignore` file in your template directory to exclude files. It works like
|
||||
`.gitignore` — one pattern per line, `#` for comments.
|
||||
|
||||
## Interactive Mode & Inputs
|
||||
|
||||
When running in a terminal, Simple Scaffold prompts for any missing required values (name, output,
|
||||
template key). Config files can also define **inputs** — custom fields that are prompted
|
||||
interactively:
|
||||
|
||||
```js
|
||||
module.exports = {
|
||||
component: {
|
||||
templates: ["templates/component"],
|
||||
output: "src/components",
|
||||
inputs: {
|
||||
author: { type: "text", message: "Author name", required: true },
|
||||
license: { type: "select", message: "License", options: ["MIT", "Apache-2.0", "GPL-3.0"] },
|
||||
isPublic: { type: "confirm", message: "Public package?" },
|
||||
priority: { type: "number", message: "Priority level", default: 1 },
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Then call your scaffold like this:
|
||||
**Input types:** `text` (default), `select`, `confirm`, `number`
|
||||
|
||||
```shell
|
||||
$ npx simple-scaffold@latest -c scaffold.config.js PageWrapper
|
||||
Pre-fill inputs from the command line to skip prompts:
|
||||
|
||||
```sh
|
||||
npx simple-scaffold -k component -D author=John -D license=MIT MyComponent
|
||||
```
|
||||
|
||||
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.
|
||||
## Remote Templates
|
||||
|
||||
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)
|
||||
Use templates from any Git repository:
|
||||
|
||||
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).
|
||||
```sh
|
||||
# GitHub shorthand
|
||||
npx simple-scaffold -g username/repo -k component MyComponent
|
||||
|
||||
### Remote Configurations
|
||||
|
||||
Another quick way to start 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:
|
||||
|
||||
- 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`
|
||||
|
||||
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.
|
||||
|
||||
Here is an example for loading the example component templates in this very repository:
|
||||
|
||||
```shell
|
||||
$ npx simple-scaffold@latest \
|
||||
-gh chenasraf/simple-scaffold#scaffold.config.js:component \
|
||||
PageWrapper
|
||||
|
||||
# equivalent to:
|
||||
$ npx simple-scaffold@latest \
|
||||
-c https://github.com/chenasraf/simple-scaffold.git#scaffold.config.js:component \
|
||||
PageWrapper
|
||||
# Full Git URL (GitLab, Bitbucket, etc.)
|
||||
npx simple-scaffold -g https://gitlab.com/user/repo.git -k component MyComponent
|
||||
```
|
||||
|
||||
When template name (`:component`) is omitted, `default` is used.
|
||||
The repository is cloned to a temporary directory, used, and cleaned up automatically.
|
||||
|
||||
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).
|
||||
## CLI Reference
|
||||
|
||||
## Documentation
|
||||
### Commands
|
||||
|
||||
See full documentation [here](https://chenasraf.github.io/simple-scaffold).
|
||||
| Command | Description |
|
||||
| ------------- | ---------------------------------------- |
|
||||
| `[name]` | Generate files from a template (default) |
|
||||
| `init` | Create config file and example template |
|
||||
| `list` / `ls` | List available template keys in a config |
|
||||
|
||||
- [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)
|
||||
### Options
|
||||
|
||||
| Flag | Short | Description | Default |
|
||||
| ------------------------------ | --------- | ---------------------------------------------------- | ----------- |
|
||||
| `--config` | `-c` | Path to config file or directory | auto-detect |
|
||||
| `--git` | `-g` | Git URL or GitHub shorthand | |
|
||||
| `--key` | `-k` | Template key from config | `default` |
|
||||
| `--output` | `-o` | Output directory | |
|
||||
| `--templates` | `-t` | Template file paths or globs | |
|
||||
| `--data` | `-d` | Custom JSON data | |
|
||||
| `--append-data` | `-D` | Key-value data (`key=string`, `key:=raw`) | |
|
||||
| `--subdir`/`--no-subdir` | `-s`/`-S` | Create parent directory with input name | `false` |
|
||||
| `--subdir-helper` | `-H` | Helper to transform subdir name | |
|
||||
| `--overwrite`/`--no-overwrite` | `-w`/`-W` | Overwrite existing files | `false` |
|
||||
| `--dry-run` | `-dr` | Preview output without writing files | `false` |
|
||||
| `--before-write` | `-B` | Script to run before each file is written | |
|
||||
| `--after-scaffold` | `-A` | Shell command to run after scaffolding | |
|
||||
| `--quiet` | `-q` | Suppress output | |
|
||||
| `--log-level` | `-l` | Log level (`none`, `debug`, `info`, `warn`, `error`) | `info` |
|
||||
| `--version` | `-v` | Show version | |
|
||||
| `--help` | `-h` | Show help | |
|
||||
|
||||
## Node.js API
|
||||
|
||||
```js
|
||||
import Scaffold from "simple-scaffold"
|
||||
|
||||
// Basic usage
|
||||
const scaffold = new Scaffold({
|
||||
name: "MyComponent",
|
||||
templates: ["templates/component"],
|
||||
output: "src/components",
|
||||
})
|
||||
await scaffold.run()
|
||||
|
||||
// Load from config file
|
||||
const scaffold = await Scaffold.fromConfig("scaffold.config.js", {
|
||||
key: "component",
|
||||
name: "MyComponent",
|
||||
})
|
||||
await scaffold.run()
|
||||
```
|
||||
|
||||
### Config Options
|
||||
|
||||
| Option | Type | Description |
|
||||
| --------------- | -------------------------- | ------------------------------------- |
|
||||
| `name` | `string` | Name for generated files (required) |
|
||||
| `templates` | `string[]` | Template paths or globs (required) |
|
||||
| `output` | `string \| Function` | Output directory or per-file function |
|
||||
| `data` | `Record<string, unknown>` | Custom template data |
|
||||
| `inputs` | `Record<string, Input>` | Interactive input definitions |
|
||||
| `helpers` | `Record<string, Function>` | Custom Handlebars helpers |
|
||||
| `subdir` | `boolean` | Create parent directory with name |
|
||||
| `subdirHelper` | `string` | Helper for subdir name transformation |
|
||||
| `overwrite` | `boolean \| Function` | Overwrite existing files |
|
||||
| `dryRun` | `boolean` | Preview without writing |
|
||||
| `logLevel` | `string` | Log verbosity |
|
||||
| `beforeWrite` | `Function` | Async hook before each file write |
|
||||
| `afterScaffold` | `Function \| string` | Hook after all files are written |
|
||||
|
||||
## Built-in Helpers
|
||||
|
||||
All helpers work in both file names and file contents.
|
||||
|
||||
### Case Helpers
|
||||
|
||||
| Helper | Example Input | Output |
|
||||
| ------------ | ------------- | --------- |
|
||||
| `camelCase` | `my name` | `myName` |
|
||||
| `pascalCase` | `my name` | `MyName` |
|
||||
| `snakeCase` | `my name` | `my_name` |
|
||||
| `kebabCase` | `my name` | `my-name` |
|
||||
| `hyphenCase` | `my name` | `my-name` |
|
||||
| `startCase` | `my name` | `My Name` |
|
||||
| `upperCase` | `my name` | `MY NAME` |
|
||||
| `lowerCase` | `My Name` | `my name` |
|
||||
|
||||
### Date Helpers
|
||||
|
||||
```handlebars
|
||||
{{now "yyyy-MM-dd"}}
|
||||
{{now "yyyy-MM-dd HH:mm" -1 "hours"}}
|
||||
{{date myDateVar "yyyy-MM-dd"}}
|
||||
{{date "2077-01-01T00:00:00Z" "yyyy-MM-dd" 7 "days"}}
|
||||
```
|
||||
|
||||
### Custom Helpers
|
||||
|
||||
Add your own via config:
|
||||
|
||||
```js
|
||||
module.exports = {
|
||||
component: {
|
||||
templates: ["templates/component"],
|
||||
output: "src/components",
|
||||
helpers: {
|
||||
shout: (str) => str.toUpperCase() + "!!!",
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
@@ -161,9 +324,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 +337,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 +346,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
@@ -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
@@ -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
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
presets: [require.resolve("@docusaurus/core/lib/babel/preset")],
|
||||
}
|
||||
134
docs/docs/usage/01-templates.md
Normal file
@@ -0,0 +1,134 @@
|
||||
---
|
||||
title: Templates
|
||||
---
|
||||
|
||||
# Templates
|
||||
|
||||
Templates are regular files in a directory. Both **file names** and **file contents** support
|
||||
[Handlebars.js](https://handlebarsjs.com/) syntax for token replacement.
|
||||
|
||||
## How Templates Are Resolved
|
||||
|
||||
Each path in the `templates` array is resolved individually. If a path points to a directory or glob
|
||||
pattern containing multiple files, the first directory up the tree becomes the base path — files are
|
||||
then copied recursively into `output`, preserving their relative structure.
|
||||
|
||||
> In the examples below, `name` is `AppName` and `output` is `src`.
|
||||
|
||||
| Template path | Files found | Output |
|
||||
| ----------------------------- | ------------------------------------------------------ | ------------------------------------------------------------ |
|
||||
| `./templates/{{ name }}.txt` | `./templates/{{ name }}.txt` | `src/AppName.txt` |
|
||||
| `./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` |
|
||||
|
||||
### Glob Patterns & Exclusions
|
||||
|
||||
Template paths support glob patterns and negation with `!`:
|
||||
|
||||
```js
|
||||
{
|
||||
templates: ["templates/component/**", "!templates/component/README.md"]
|
||||
}
|
||||
```
|
||||
|
||||
## Ignoring Files
|
||||
|
||||
Place a `.scaffoldignore` file in your template directory to exclude files. It works like
|
||||
`.gitignore` — one pattern per line, `#` for comments.
|
||||
|
||||
```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` matches at any depth while `dist/**` only matches a `dist` directory at
|
||||
the template root.
|
||||
|
||||
## Token Replacement
|
||||
|
||||
Handlebars expressions like `{{ name }}` are replaced in both file names and file contents. The
|
||||
`name` variable is always available — it's the name you pass when running the scaffold.
|
||||
|
||||
Any additional data from `--data`, `-D`, `data` config, or [inputs](configuration_files#inputs) is
|
||||
also available.
|
||||
|
||||
```bash
|
||||
npx simple-scaffold \
|
||||
-t templates/component/{{name}}.jsx \
|
||||
-o src/components \
|
||||
MyComponent
|
||||
```
|
||||
|
||||
This produces `src/components/MyComponent.jsx`, with all tokens inside the file replaced as well.
|
||||
|
||||
All standard Handlebars features work — `{{#if}}`, `{{#each}}`, `{{#with}}`, and more. See
|
||||
[Handlebars.js Language Features](https://handlebarsjs.com/guide/#language-features) for details.
|
||||
|
||||
## Built-in Helpers
|
||||
|
||||
Simple Scaffold includes helpers you can use in templates and file names. Helpers can also be
|
||||
nested: `{{ pascalCase (snakeCase name) }}`.
|
||||
|
||||
### Case Helpers
|
||||
|
||||
| Helper | Usage | `my name` becomes |
|
||||
| ------------ | ----------------------- | ----------------- |
|
||||
| _(none)_ | `{{ name }}` | `my name` |
|
||||
| `camelCase` | `{{ camelCase name }}` | `myName` |
|
||||
| `pascalCase` | `{{ pascalCase name }}` | `MyName` |
|
||||
| `snakeCase` | `{{ snakeCase name }}` | `my_name` |
|
||||
| `kebabCase` | `{{ kebabCase name }}` | `my-name` |
|
||||
| `hyphenCase` | `{{ hyphenCase name }}` | `my-name` |
|
||||
| `startCase` | `{{ startCase name }}` | `My Name` |
|
||||
| `upperCase` | `{{ upperCase name }}` | `MY NAME` |
|
||||
| `lowerCase` | `{{ lowerCase name }}` | `my name` |
|
||||
|
||||
### Date Helpers
|
||||
|
||||
Both `now` and `date` use [`date-fns`](https://date-fns.org/docs/format) format tokens.
|
||||
|
||||
| Helper | Example | Output |
|
||||
| -------------------- | ---------------------------------------------------------------- | ------------------- |
|
||||
| `now` | `{{ now "yyyy-MM-dd HH:mm" }}` | `2042-01-01 15:00` |
|
||||
| `now` (with offset) | `{{ now "yyyy-MM-dd HH:mm" -1 "hours" }}` | `2042-01-01 14:00` |
|
||||
| `date` | `{{ date "2042-01-01T15:00:00Z" "yyyy-MM-dd HH:mm" }}` | `2042-01-01 15:00` |
|
||||
| `date` (with offset) | `{{ date "2042-01-01T15:00:00Z" "yyyy-MM-dd HH:mm" -1 "days" }}` | `2041-12-31 15:00` |
|
||||
| `date` (from data) | `{{ date myCustomDate "yyyy-MM-dd HH:mm" }}` | _(depends on data)_ |
|
||||
|
||||
**Signatures:**
|
||||
|
||||
```typescript
|
||||
now(format: string, offsetAmount?: number, offsetType?: "years" | "months" | "weeks" | "days" | "hours" | "minutes" | "seconds")
|
||||
|
||||
date(date: string, format: string, offsetAmount?: number, offsetType?: "years" | "months" | "weeks" | "days" | "hours" | "minutes" | "seconds")
|
||||
```
|
||||
|
||||
## Custom Helpers
|
||||
|
||||
You can register custom Handlebars helpers via the `helpers` config option:
|
||||
|
||||
```js
|
||||
module.exports = {
|
||||
component: {
|
||||
templates: ["templates/component"],
|
||||
output: "src/components",
|
||||
helpers: {
|
||||
shout: (text) => text.toUpperCase() + "!!!",
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Then use in templates: `{{ shout name }}`.
|
||||
|
||||
All helpers (built-in and custom) are also available as values for `subdirHelper` (`--subdir-helper`
|
||||
/ `-H`).
|
||||
|
||||
For more on Handlebars helpers, see the
|
||||
[Handlebars.js docs](https://handlebarsjs.com/guide/#custom-helpers).
|
||||
205
docs/docs/usage/02-configuration_files.md
Normal file
@@ -0,0 +1,205 @@
|
||||
---
|
||||
title: Configuration Files
|
||||
---
|
||||
|
||||
# Configuration Files
|
||||
|
||||
Config files let you define reusable scaffold definitions — template paths, output directories,
|
||||
custom data, inputs, and hooks — all in one place.
|
||||
|
||||
## Creating a Config File
|
||||
|
||||
The fastest way is to run `init`:
|
||||
|
||||
```sh
|
||||
npx simple-scaffold init
|
||||
```
|
||||
|
||||
This creates a `scaffold.config.js` and an example template in `templates/default/`. See
|
||||
[`init` command](cli#init) for options.
|
||||
|
||||
### Config Structure
|
||||
|
||||
A config file exports an object mapping **template keys** to scaffold configurations:
|
||||
|
||||
```js
|
||||
// scaffold.config.js
|
||||
module.exports = {
|
||||
component: {
|
||||
templates: ["templates/component"],
|
||||
output: "src/components",
|
||||
},
|
||||
page: {
|
||||
templates: ["templates/page"],
|
||||
output: "src/pages",
|
||||
subdir: true,
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
For the full list of options, see [ScaffoldConfig](../api/interfaces/ScaffoldConfig) or the
|
||||
[Node.js API](node) page.
|
||||
|
||||
### Dynamic Configs
|
||||
|
||||
JS config files can export a **function** that receives the CLI config and returns the scaffold map.
|
||||
This lets you pre-process arguments or add logic:
|
||||
|
||||
```js
|
||||
/** @type {import('simple-scaffold').ScaffoldConfigFile} */
|
||||
module.exports = (config) => ({
|
||||
component: {
|
||||
templates: ["templates/component"],
|
||||
output: "src/components",
|
||||
data: { timestamp: Date.now() },
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
The function can also be `async`.
|
||||
|
||||
### Supported Formats
|
||||
|
||||
- `.js` (CommonJS)
|
||||
- `.cjs` (CommonJS, explicit)
|
||||
- `.mjs` (ESM)
|
||||
- `.json`
|
||||
|
||||
:::note The correct extension may depend on your `package.json` `"type"` field. Packages with
|
||||
`"type": "module"` may require `.mjs` or `.cjs` instead of `.js`. :::
|
||||
|
||||
## Using a Config File
|
||||
|
||||
### Auto-detection
|
||||
|
||||
Simple Scaffold automatically searches the current directory for a config file — no `--config` flag
|
||||
needed. The following names are tried in order:
|
||||
|
||||
1. `scaffold.config.{mjs,cjs,js,json}`
|
||||
2. `scaffold.{mjs,cjs,js,json}`
|
||||
3. `.scaffold.{mjs,cjs,js,json}`
|
||||
|
||||
```sh
|
||||
# Just run from the project root — config is found automatically
|
||||
npx simple-scaffold -k component MyComponent
|
||||
```
|
||||
|
||||
### Explicit Path
|
||||
|
||||
Use `--config` (`-c`) to point to a specific file or directory:
|
||||
|
||||
```sh
|
||||
npx simple-scaffold -c path/to/scaffold.config.js -k component MyComponent
|
||||
```
|
||||
|
||||
When a directory is given, the auto-detection order above is used within that directory.
|
||||
|
||||
### Default Template Key
|
||||
|
||||
If you don't provide `--key`, the `default` key is used:
|
||||
|
||||
```js
|
||||
module.exports = {
|
||||
default: {
|
||||
templates: ["templates/default"],
|
||||
output: "src",
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
```sh
|
||||
# Uses the "default" key — no -k needed
|
||||
npx simple-scaffold MyProject
|
||||
```
|
||||
|
||||
If multiple keys exist and no `--key` is provided, you'll be prompted to select one interactively.
|
||||
|
||||
### Providing a Default Name
|
||||
|
||||
If your template doesn't need a dynamic name (e.g. common config files), set `name` in the config:
|
||||
|
||||
```js
|
||||
module.exports = {
|
||||
eslint: {
|
||||
name: ".eslintrc",
|
||||
templates: ["templates/eslint"],
|
||||
output: ".",
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
The name can still be overridden with `--name` on the command line.
|
||||
|
||||
## Inputs
|
||||
|
||||
Inputs define custom fields that are prompted interactively and become template data variables:
|
||||
|
||||
```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 },
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Use them in templates as `{{ author }}`, `{{ license }}`, `{{ private }}`, `{{ port }}`.
|
||||
|
||||
**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` |
|
||||
|
||||
**Behavior:**
|
||||
|
||||
- **Required** inputs are prompted 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
|
||||
- In non-interactive environments (CI, piped input), only defaults are applied
|
||||
|
||||
Pre-fill inputs from the CLI:
|
||||
|
||||
```sh
|
||||
npx simple-scaffold -k component -D author=John -D license=MIT MyComponent
|
||||
```
|
||||
|
||||
## Remote Templates (Git)
|
||||
|
||||
Load config files and templates from any Git repository:
|
||||
|
||||
```sh
|
||||
# GitHub shorthand
|
||||
npx simple-scaffold -g username/repo -k component MyComponent
|
||||
|
||||
# Full Git URL (GitLab, Bitbucket, etc.)
|
||||
npx simple-scaffold -g https://gitlab.com/user/repo.git -k component MyComponent
|
||||
```
|
||||
|
||||
When `--config` is omitted, the standard auto-detection order is used within the cloned repo. The
|
||||
repository is cloned to a temporary directory and cleaned up automatically.
|
||||
|
||||
### From Node.js
|
||||
|
||||
```ts
|
||||
import Scaffold from "simple-scaffold"
|
||||
|
||||
const scaffold = await Scaffold.fromConfig(
|
||||
"https://github.com/user/repo.git", // or a local file path
|
||||
{ name: "MyComponent", key: "component" },
|
||||
)
|
||||
await scaffold.run()
|
||||
```
|
||||
162
docs/docs/usage/03-cli.md
Normal file
@@ -0,0 +1,162 @@
|
||||
---
|
||||
title: CLI
|
||||
---
|
||||
|
||||
# CLI
|
||||
|
||||
```sh
|
||||
npx simple-scaffold [options] [name]
|
||||
```
|
||||
|
||||
Use `--help` (`-h`) to see all available options at any time.
|
||||
|
||||
## Commands
|
||||
|
||||
| Command | Description |
|
||||
| ------------- | ----------------------------------------- |
|
||||
| _(default)_ | Generate files from a template |
|
||||
| `init` | Create a config file and example template |
|
||||
| `list` / `ls` | List available template keys in a config |
|
||||
|
||||
### `init`
|
||||
|
||||
Scaffolds a config file and example template directory to get you started:
|
||||
|
||||
```sh
|
||||
npx simple-scaffold init
|
||||
npx simple-scaffold init --format mjs
|
||||
npx simple-scaffold init --dir packages/my-lib
|
||||
```
|
||||
|
||||
| Option | Description |
|
||||
| ----------------- | ----------------------------------------------------------------- |
|
||||
| `--dir` / `-d` | Directory to create the config in (defaults to current directory) |
|
||||
| `--format` / `-f` | Config format: `js`, `mjs`, or `json` (prompts if omitted) |
|
||||
|
||||
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.
|
||||
|
||||
### `list`
|
||||
|
||||
Lists all template keys defined in a config file:
|
||||
|
||||
```sh
|
||||
npx simple-scaffold list
|
||||
npx simple-scaffold list -c path/to/config.js
|
||||
npx simple-scaffold list -g username/repo
|
||||
```
|
||||
|
||||
## Options
|
||||
|
||||
| Flag | Short | Description | Default |
|
||||
| -------------------------------- | --------- | ----------------------------------------------------- | --------------- |
|
||||
| `--name` | `-n` | Name for generated files (can also be positional arg) | |
|
||||
| `--config` | `-c` | Path to config file or directory | _(auto-detect)_ |
|
||||
| `--git` | `-g` | Git URL or GitHub shorthand (`user/repo`) | |
|
||||
| `--key` | `-k` | Template key from config | `default` |
|
||||
| `--output` | `-o` | Output directory | |
|
||||
| `--templates` | `-t` | Template paths or glob patterns (repeatable) | |
|
||||
| `--data` | `-d` | Custom data as JSON string | |
|
||||
| `--append-data` | `-D` | Key-value data (`key=string`, `key:=raw`), repeatable | |
|
||||
| `--subdir` / `--no-subdir` | `-s`/`-S` | Create parent directory with the input name | `false` |
|
||||
| `--subdir-helper` | `-H` | Helper to transform subdir name | |
|
||||
| `--overwrite` / `--no-overwrite` | `-w`/`-W` | Overwrite existing files | `false` |
|
||||
| `--dry-run` | `-dr` | Preview output without writing files | `false` |
|
||||
| `--before-write` | `-B` | Command to run before each file is written | |
|
||||
| `--after-scaffold` | `-A` | Shell command to run after all files are written | |
|
||||
| `--quiet` | `-q` | Suppress output (same as `--log-level none`) | |
|
||||
| `--log-level` | `-l` | Log level: `none`, `debug`, `info`, `warn`, `error` | `info` |
|
||||
| `--version` | `-v` | Show version | |
|
||||
| `--help` | `-h` | Show help | |
|
||||
|
||||
## Interactive Mode
|
||||
|
||||
When running in a terminal (TTY), Simple Scaffold prompts for any missing required values:
|
||||
|
||||
- **Name** — if `--name` is not provided
|
||||
- **Template key** — if `--key` is not provided and the config has multiple templates
|
||||
- **Output directory** — if `--output` is not provided
|
||||
- **Template paths** — if `--templates` is not provided (comma-separated)
|
||||
|
||||
[Inputs](configuration_files#inputs) defined in config files are also prompted interactively.
|
||||
|
||||
In non-interactive environments (CI, piped input), missing required values cause an error.
|
||||
|
||||
## Hooks
|
||||
|
||||
### Before Write
|
||||
|
||||
Runs a command before each file is written. The command receives a temporary file path and should
|
||||
output the final content to stdout.
|
||||
|
||||
```sh
|
||||
# Appends file path automatically
|
||||
npx simple-scaffold -c . --before-write prettier
|
||||
|
||||
# Use tokens for explicit control
|
||||
npx simple-scaffold -c . --before-write 'cat {{path}} | my-linter'
|
||||
```
|
||||
|
||||
**Tokens:**
|
||||
|
||||
- `{{path}}` — temporary file path with Handlebars-processed contents
|
||||
- `{{rawpath}}` — temporary file path with raw (unprocessed) contents
|
||||
|
||||
If no tokens are found, `{{path}}` is appended automatically. Returning an empty string (after
|
||||
trimming) discards the result and writes the original contents.
|
||||
|
||||
### After Scaffold
|
||||
|
||||
Runs a shell command after all files are written. The command executes in the output directory:
|
||||
|
||||
```sh
|
||||
npx simple-scaffold -c . --after-scaffold 'npm install'
|
||||
npx simple-scaffold -c . --after-scaffold 'git init && git add .'
|
||||
```
|
||||
|
||||
See the [Node.js API](node#after-scaffold-hook) for the function-based equivalent.
|
||||
|
||||
## CLI Examples
|
||||
|
||||
```sh
|
||||
# Use auto-detected config, default key
|
||||
npx simple-scaffold MyProject
|
||||
|
||||
# Specify config and key
|
||||
npx simple-scaffold -c scaffold.config.js -k component MyComponent
|
||||
|
||||
# GitHub remote template
|
||||
npx simple-scaffold -g username/repo -k component MyComponent
|
||||
|
||||
# Full Git URL
|
||||
npx simple-scaffold -g https://gitlab.com/user/repo.git -k component MyComponent
|
||||
|
||||
# One-off (no config file)
|
||||
npx simple-scaffold -t templates/component -o src/components MyComponent
|
||||
|
||||
# With custom data
|
||||
npx simple-scaffold -k component -D author=John -D license:='"MIT"' MyComponent
|
||||
|
||||
# Dry run
|
||||
npx simple-scaffold -k component --dry-run MyComponent
|
||||
```
|
||||
|
||||
### package.json Scripts
|
||||
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"scaffold": "simple-scaffold -k component",
|
||||
"scaffold:page": "simple-scaffold -k page"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```sh
|
||||
npm run scaffold -- MyComponent
|
||||
npm run scaffold:page -- Dashboard
|
||||
```
|
||||
208
docs/docs/usage/04-node.md
Normal file
@@ -0,0 +1,208 @@
|
||||
---
|
||||
title: Node.js API
|
||||
---
|
||||
|
||||
# Node.js API
|
||||
|
||||
Use Simple Scaffold programmatically for more complex workflows, custom logic, or integration into
|
||||
build tools.
|
||||
|
||||
## Basic Usage
|
||||
|
||||
```typescript
|
||||
import Scaffold from "simple-scaffold"
|
||||
|
||||
const scaffold = new Scaffold({
|
||||
name: "MyComponent",
|
||||
templates: ["templates/component"],
|
||||
output: "src/components",
|
||||
})
|
||||
await scaffold.run()
|
||||
```
|
||||
|
||||
## Loading from a Config File
|
||||
|
||||
```typescript
|
||||
import Scaffold from "simple-scaffold"
|
||||
|
||||
const scaffold = await Scaffold.fromConfig(
|
||||
"scaffold.config.js", // local path or HTTPS Git URL
|
||||
{ name: "MyComponent", key: "component" },
|
||||
{
|
||||
/* optional config overrides */
|
||||
},
|
||||
)
|
||||
await scaffold.run()
|
||||
```
|
||||
|
||||
## Config Interface
|
||||
|
||||
```typescript
|
||||
interface ScaffoldConfig {
|
||||
name: string
|
||||
templates: string[]
|
||||
output: FileResponse<string>
|
||||
subdir?: boolean
|
||||
subdirHelper?: string
|
||||
data?: Record<string, unknown>
|
||||
overwrite?: FileResponse<boolean>
|
||||
logLevel?: "none" | "debug" | "info" | "warn" | "error"
|
||||
dryRun?: boolean
|
||||
helpers?: Record<string, Helper>
|
||||
inputs?: Record<string, ScaffoldInput>
|
||||
beforeWrite?(
|
||||
content: Buffer,
|
||||
rawContent: Buffer,
|
||||
outputPath: string,
|
||||
): string | Buffer | undefined | Promise<string | Buffer | undefined>
|
||||
afterScaffold?: AfterScaffoldHook
|
||||
}
|
||||
```
|
||||
|
||||
For the full API reference, see [ScaffoldConfig](../api/interfaces/ScaffoldConfig).
|
||||
|
||||
### Options
|
||||
|
||||
| Option | Type | Description |
|
||||
| --------------- | ------------------------------- | ------------------------------------------------------------------------ |
|
||||
| `name` | `string` | Name for generated files _(required)_ |
|
||||
| `templates` | `string[]` | Template paths or globs; prefix with `!` to exclude _(required)_ |
|
||||
| `output` | `string \| Function` | Output directory, or a function for per-file control |
|
||||
| `data` | `Record<string, unknown>` | Custom data available in templates |
|
||||
| `inputs` | `Record<string, ScaffoldInput>` | Interactive input definitions (see [Inputs](configuration_files#inputs)) |
|
||||
| `helpers` | `Record<string, Function>` | Custom Handlebars helpers |
|
||||
| `subdir` | `boolean` | Create a parent directory with the input name (default: `false`) |
|
||||
| `subdirHelper` | `string` | Helper to transform the subdir name (e.g. `"pascalCase"`) |
|
||||
| `overwrite` | `boolean \| Function` | Overwrite existing files (default: `false`); function for per-file logic |
|
||||
| `dryRun` | `boolean` | Preview without writing files (default: `false`) |
|
||||
| `logLevel` | `string` | Log verbosity (default: `"info"`) |
|
||||
| `beforeWrite` | `Function` | Async hook before each file is written |
|
||||
| `afterScaffold` | `Function \| string` | Hook after all files are written |
|
||||
|
||||
## Hooks
|
||||
|
||||
### Before Write
|
||||
|
||||
Runs before each file is written. Return a `string` or `Buffer` to replace the file contents, or
|
||||
`undefined` to keep the default (Handlebars-processed) output.
|
||||
|
||||
```typescript
|
||||
await new Scaffold({
|
||||
name: "MyComponent",
|
||||
templates: ["templates/component"],
|
||||
output: "src/components",
|
||||
beforeWrite: async (content, rawContent, outputPath) => {
|
||||
// Format the output, transform it, or return undefined to keep as-is
|
||||
return content.toString().trim()
|
||||
},
|
||||
}).run()
|
||||
```
|
||||
|
||||
### After Scaffold Hook
|
||||
|
||||
Runs after all files have been written. Pass a **function** for full control, or a **string** to run
|
||||
a shell command in the output directory.
|
||||
|
||||
**Function:**
|
||||
|
||||
```typescript
|
||||
await new 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.
|
||||
},
|
||||
}).run()
|
||||
```
|
||||
|
||||
**Shell command:**
|
||||
|
||||
```typescript
|
||||
await new Scaffold({
|
||||
name: "my-app",
|
||||
templates: ["templates/app"],
|
||||
output: "my-app",
|
||||
afterScaffold: "npm install && git init",
|
||||
}).run()
|
||||
```
|
||||
|
||||
The context object:
|
||||
|
||||
```typescript
|
||||
interface AfterScaffoldContext {
|
||||
config: ScaffoldConfig
|
||||
files: string[] // absolute paths of written files
|
||||
}
|
||||
```
|
||||
|
||||
In dry-run mode, the hook is still called but the `files` array will be empty.
|
||||
|
||||
## Inputs
|
||||
|
||||
Define interactive prompts that merge into template data:
|
||||
|
||||
```typescript
|
||||
await new 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 },
|
||||
},
|
||||
}).run()
|
||||
// In templates: {{ author }}, {{ license }}, {{ private }}, {{ port }}
|
||||
```
|
||||
|
||||
```typescript
|
||||
interface ScaffoldInput {
|
||||
type?: "text" | "select" | "confirm" | "number"
|
||||
message?: string
|
||||
required?: boolean
|
||||
default?: string | boolean | number
|
||||
options?: (string | { name: string; value: string })[] // for type: "select"
|
||||
}
|
||||
```
|
||||
|
||||
- **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
|
||||
|
||||
## Full Example
|
||||
|
||||
```typescript
|
||||
import path from "path"
|
||||
import Scaffold from "simple-scaffold"
|
||||
|
||||
await new Scaffold({
|
||||
name: "MyComponent",
|
||||
templates: [path.join(__dirname, "scaffolds", "component")],
|
||||
output: path.join(__dirname, "src", "components"),
|
||||
subdir: true,
|
||||
subdirHelper: "pascalCase",
|
||||
data: {
|
||||
property: "value",
|
||||
},
|
||||
helpers: {
|
||||
twice: (text) => [text, text].join(" "),
|
||||
},
|
||||
inputs: {
|
||||
author: { message: "Author name", required: true },
|
||||
license: { message: "License", default: "MIT" },
|
||||
},
|
||||
beforeWrite: (content, rawContent, outputPath) => {
|
||||
return content.toString().toUpperCase()
|
||||
},
|
||||
afterScaffold: async ({ config, files }) => {
|
||||
console.log(`Created ${files.length} files in ${config.output}`)
|
||||
},
|
||||
}).run()
|
||||
```
|
||||
186
docs/docs/usage/05-examples.md
Normal file
@@ -0,0 +1,186 @@
|
||||
---
|
||||
title: Examples
|
||||
---
|
||||
|
||||
# Examples
|
||||
|
||||
## React Component
|
||||
|
||||
### Template
|
||||
|
||||
**File:** `templates/component/{{pascalCase name}}.tsx`
|
||||
|
||||
```tsx
|
||||
/**
|
||||
* Author: {{ author }}
|
||||
* Date: {{ now "yyyy-MM-dd" }}
|
||||
*/
|
||||
import React from "react"
|
||||
|
||||
export default {{ pascalCase name }}: React.FC = (props) => {
|
||||
return (
|
||||
<div className="{{ camelCase name }}">{{ pascalCase name }} Component</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Config
|
||||
|
||||
```js
|
||||
// scaffold.config.js
|
||||
module.exports = {
|
||||
component: {
|
||||
templates: ["templates/component"],
|
||||
output: "src/components",
|
||||
data: {
|
||||
author: "My Name",
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### Running
|
||||
|
||||
```sh
|
||||
npx simple-scaffold -k component MyComponent
|
||||
```
|
||||
|
||||
### Output
|
||||
|
||||
**File:** `src/components/MyComponent.tsx`
|
||||
|
||||
```tsx
|
||||
/**
|
||||
* Author: My Name
|
||||
* Date: 2077-01-01
|
||||
*/
|
||||
import React from "react"
|
||||
|
||||
export default MyComponent: React.FC = (props) => {
|
||||
return (
|
||||
<div className="myComponent">MyComponent Component</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Subdir Variations
|
||||
|
||||
Given the template and config above, the output path changes based on `subdir` settings:
|
||||
|
||||
| Setting | Output path |
|
||||
| ------------------------------------------- | -------------------------------------------- |
|
||||
| `subdir: false` (default) | `src/components/MyComponent.tsx` |
|
||||
| `subdir: true` | `src/components/MyComponent/MyComponent.tsx` |
|
||||
| `subdir: true`, `subdirHelper: "upperCase"` | `src/components/MYCOMPONENT/MyComponent.tsx` |
|
||||
|
||||
## CLI One-liner (No Config)
|
||||
|
||||
```sh
|
||||
npx simple-scaffold \
|
||||
-t templates/component/**/* \
|
||||
-o src/components \
|
||||
-d '{"author": "My Name"}' \
|
||||
MyComponent
|
||||
```
|
||||
|
||||
## Node.js Equivalent
|
||||
|
||||
```typescript
|
||||
import Scaffold from "simple-scaffold"
|
||||
|
||||
await new Scaffold({
|
||||
name: "MyComponent",
|
||||
templates: ["templates/component"],
|
||||
output: "src/components",
|
||||
data: {
|
||||
author: "My Name",
|
||||
},
|
||||
}).run()
|
||||
```
|
||||
|
||||
## Reusable Config Files
|
||||
|
||||
### CommonJS (`scaffold.config.js`)
|
||||
|
||||
```js
|
||||
module.exports = {
|
||||
default: {
|
||||
templates: ["templates/component"],
|
||||
output: "src/components",
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### ESM (`scaffold.config.mjs`)
|
||||
|
||||
```js
|
||||
export default {
|
||||
default: {
|
||||
templates: ["templates/component"],
|
||||
output: "src/components",
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### Dynamic Config (with function)
|
||||
|
||||
```js
|
||||
module.exports = (config) => ({
|
||||
default: {
|
||||
templates: ["templates/component"],
|
||||
output: "src/components",
|
||||
data: {
|
||||
generatedAt: new Date().toISOString(),
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
## With Inputs
|
||||
|
||||
```js
|
||||
// scaffold.config.js
|
||||
module.exports = {
|
||||
package: {
|
||||
templates: ["templates/package"],
|
||||
output: "packages",
|
||||
subdir: true,
|
||||
inputs: {
|
||||
description: { message: "Package description", required: true },
|
||||
author: { message: "Author", default: "Team" },
|
||||
license: {
|
||||
type: "select",
|
||||
message: "License",
|
||||
options: ["MIT", "Apache-2.0", "ISC"],
|
||||
},
|
||||
private: { type: "confirm", message: "Private package?", default: true },
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
```sh
|
||||
# Interactive — prompts for each input
|
||||
npx simple-scaffold -k package my-lib
|
||||
|
||||
# Non-interactive — provide all values upfront
|
||||
npx simple-scaffold -k package -D description="A utility library" -D author=John my-lib
|
||||
```
|
||||
|
||||
## With Hooks
|
||||
|
||||
```js
|
||||
module.exports = {
|
||||
app: {
|
||||
templates: ["templates/app"],
|
||||
output: ".",
|
||||
subdir: true,
|
||||
afterScaffold: "cd {{name}} && npm install && git init",
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
```sh
|
||||
npx simple-scaffold -k app my-app
|
||||
# Files are generated, then npm install and git init run automatically
|
||||
```
|
||||
64
docs/docs/usage/06-migration.md
Normal file
@@ -0,0 +1,64 @@
|
||||
---
|
||||
title: Migration
|
||||
---
|
||||
|
||||
# Migration
|
||||
|
||||
## v1.x to v2.x
|
||||
|
||||
### CLI Changes
|
||||
|
||||
**Remote config syntax:**
|
||||
|
||||
- The `:template_key` suffix syntax has been removed. Use `-k template_key` instead.
|
||||
- `--github` (`-gh`) is now `--git` (`-g`) and supports any Git URL. GitHub shorthand still works:
|
||||
`-g username/project`.
|
||||
- The `#template_file` suffix syntax has been removed. Use `--config` (`-c`) to specify which file
|
||||
to look for inside the Git project.
|
||||
|
||||
**Renamed flags:**
|
||||
|
||||
| v1.x | v2.x |
|
||||
| ---------------------------------- | -------------------------------------------------------- |
|
||||
| `--create-sub-folder` / `-s` | `--subdir` / `-s` |
|
||||
| `--sub-folder-name-helper` / `-sh` | `--subdir-helper` / `-H` |
|
||||
| `--verbose` (true/false) | `--log-level` (`debug`, `info`, `warn`, `error`, `none`) |
|
||||
|
||||
**Boolean flags** no longer take a value. Use `-q` instead of `-q true`, `-s` instead of `-s 1`,
|
||||
etc.
|
||||
|
||||
### Behavior Changes
|
||||
|
||||
**`{{ Name }}` removed.** The auto-populated `Name` (PascalCase) variable is gone. Use
|
||||
`{{ pascalCase name }}` in your templates instead. If you need the old behavior, inject it manually
|
||||
via `data`:
|
||||
|
||||
```js
|
||||
module.exports = {
|
||||
default: {
|
||||
templates: ["templates/default"],
|
||||
output: "src",
|
||||
data: { Name: "{{ pascalCase name }}" }, // or set it programmatically
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## v0.x to v1.x
|
||||
|
||||
In v1.0, the codebase was overhauled but usage remained mostly the same.
|
||||
|
||||
### API Changes
|
||||
|
||||
| v0.x | v1.x |
|
||||
| -------------------------------- | ------------------------------------------ |
|
||||
| `new SimpleScaffold(opts).run()` | `SimpleScaffold(opts)` (returns a Promise) |
|
||||
| `locals` option | `data` option |
|
||||
| `--locals` / `-l` flag | `--data` / `-d` flag |
|
||||
|
||||
### Template Syntax
|
||||
|
||||
Templates still use Handlebars.js. v1.x added **built-in helpers** (case transformations, date
|
||||
formatting), removing the need to pre-process template data for common operations like `camelCase`,
|
||||
`snakeCase`, etc.
|
||||
|
||||
See [Templates](templates#built-in-helpers) for the full list of available helpers.
|
||||
1
docs/docs/usage/_category_.yml
Normal file
@@ -0,0 +1 @@
|
||||
label: "Usage"
|
||||
18
docs/docs/usage/index.md
Normal file
@@ -0,0 +1,18 @@
|
||||
---
|
||||
title: Usage
|
||||
sidebar_position: 0
|
||||
---
|
||||
|
||||
# Usage
|
||||
|
||||
Simple Scaffold can be used as a **CLI tool** or as a **Node.js library**. Both approaches share the
|
||||
same core concepts: templates, configuration files, and Handlebars-based token replacement.
|
||||
|
||||
- [Templates](./usage/templates) — how template files and directories work, built-in helpers, and
|
||||
custom helpers
|
||||
- [Configuration Files](./usage/configuration_files) — reusable scaffold definitions,
|
||||
auto-detection, inputs, and remote Git templates
|
||||
- [CLI](./usage/cli) — all commands, flags, interactive mode, and hooks
|
||||
- [Node.js API](./usage/node) — programmatic usage, full config interface, and hooks
|
||||
- [Examples](./usage/examples) — end-to-end examples for CLI and Node.js
|
||||
- [Migration](./usage/migration) — upgrading from v1.x or v0.x to the latest version
|
||||
191
docs/docusaurus.config.ts
Normal 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
@@ -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": "~6.0.2"
|
||||
},
|
||||
"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
28
docs/sidebars.ts
Normal 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
|
||||
72
docs/src/components/HomepageFeatures/index.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
11
docs/src/components/HomepageFeatures/styles.module.css
Normal 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
@@ -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);
|
||||
}
|
||||
34
docs/src/pages/index.module.css
Normal 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
@@ -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>
|
||||
)
|
||||
}
|
||||
7
docs/src/pages/markdown-page.md
Normal 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
BIN
docs/static/img/docusaurus-social-card.jpg
vendored
Normal file
|
After Width: | Height: | Size: 54 KiB |
BIN
docs/static/img/docusaurus.png
vendored
Normal file
|
After Width: | Height: | Size: 5.0 KiB |
BIN
docs/static/img/favicon.ico
vendored
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
BIN
docs/static/img/favicon.png
vendored
Normal file
|
After Width: | Height: | Size: 14 KiB |
20
docs/static/img/favicon.svg
vendored
Normal file
|
After Width: | Height: | Size: 265 KiB |
0
media/intro.gif → docs/static/img/intro.gif
vendored
|
Before Width: | Height: | Size: 385 KiB After Width: | Height: | Size: 385 KiB |
BIN
docs/static/img/logo-lg.png
vendored
Normal file
|
After Width: | Height: | Size: 22 KiB |
20
docs/static/img/logo-lg.svg
vendored
Normal file
|
After Width: | Height: | Size: 281 KiB |
BIN
docs/static/img/logo.png
vendored
Normal file
|
After Width: | Height: | Size: 24 KiB |
20
docs/static/img/logo.svg
vendored
Normal file
|
After Width: | Height: | Size: 432 KiB |
171
docs/static/img/undraw_docusaurus_mountain.svg
vendored
Normal 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 |
170
docs/static/img/undraw_docusaurus_react.svg
vendored
Normal 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 |
40
docs/static/img/undraw_docusaurus_tree.svg
vendored
Normal 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
@@ -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
@@ -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/'],
|
||||
},
|
||||
]
|
||||
@@ -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}}
|
||||
17
examples/test-input/Component/{{pascalCase name}}.tsx
Normal 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}}
|
||||
205
jest.config.ts
@@ -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: true,
|
||||
|
||||
// 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,
|
||||
}
|
||||
99
package.json
@@ -1,14 +1,19 @@
|
||||
{
|
||||
"name": "simple-scaffold",
|
||||
"version": "1.8.0-pre.1",
|
||||
"version": "3.1.1",
|
||||
"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.6.2",
|
||||
"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.30.0",
|
||||
"glob": "^10.3.3",
|
||||
"handlebars": "^4.7.7",
|
||||
"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.9",
|
||||
"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/exec": "^6.0.3",
|
||||
"@semantic-release/git": "^10.0.1",
|
||||
"@semantic-release/release-notes-generator": "^10.0.3",
|
||||
"@types/jest": "^29.5.1",
|
||||
"@types/mock-fs": "^4.13.1",
|
||||
"@types/node": "^18.16.0",
|
||||
"@types/semantic-release": "^20.0.1",
|
||||
"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.7",
|
||||
"typescript": "^5.0.4"
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@types/node": "^25.5.0",
|
||||
"@vitest/coverage-v8": "^4.1.2",
|
||||
"husky": "^9.1.7",
|
||||
"lint-staged": "^16.4.0",
|
||||
"mock-fs": "^5.5.0",
|
||||
"prettier": "^3.8.1",
|
||||
"rimraf": "^6.1.3",
|
||||
"typescript": "^6.0.2",
|
||||
"typescript-eslint": "^8.57.2",
|
||||
"vite": "^8.0.3",
|
||||
"vite-node": "^6.0.0",
|
||||
"vitest": "^4.1.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
79
pages/cli.md
@@ -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}'"
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -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
|
||||
},
|
||||
)
|
||||
```
|
||||
@@ -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.
|
||||
@@ -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)
|
||||
```
|
||||
@@ -1,224 +0,0 @@
|
||||
# Preparing template files
|
||||
|
||||
Put your template files anywhere, and fill them with tokens for replacement.
|
||||
|
||||
Each template (not file) in the config array is parsed individually, and copied to the output
|
||||
directory. If a single template path contains multiple files (e.g. if you use a folder path or a
|
||||
glob pattern), the first directory up the tree of that template will become the base inside the
|
||||
defined output path for that template, while copying files recursively and maintaining their
|
||||
relative structure.
|
||||
|
||||
Examples:
|
||||
|
||||
> In the following examples, the config `name` is `AppName`, and the config `output` is `src`.
|
||||
|
||||
| Input template | Files in template | Output path(s) |
|
||||
| ----------------------------- | ------------------------------------------------------ | ------------------------------------------------------------ |
|
||||
| `./templates/{{ name }}.txt` | `./templates/{{ name }}.txt` | `src/AppName.txt` |
|
||||
| `./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` |
|
||||
|
||||
## Variable/token replacement
|
||||
|
||||
Scaffolding will replace `{{ varName }}` in both the file name and its contents and put the
|
||||
transformed files in the output directory.
|
||||
|
||||
The data available for the template parser is the data you pass to the `data` config option (or
|
||||
`--data` argument in CLI).
|
||||
|
||||
For example, using the following command:
|
||||
|
||||
```bash
|
||||
npx simple-scaffold@latest \
|
||||
--templates templates/components/{{name}}.jsx \
|
||||
--output src/components \
|
||||
--create-sub-folder true \
|
||||
MyComponent
|
||||
```
|
||||
|
||||
Will output a file with the path:
|
||||
|
||||
```text
|
||||
<working_dir>/src/components/MyComponent.jsx
|
||||
```
|
||||
|
||||
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.
|
||||
> Any `data` you add in the config will be available for use with their names wrapped in `{{` and
|
||||
> `}}`. Other Handlebars built-ins such as `each`, `if` and `with` are also supported, see
|
||||
> [Handlebars.js Language Features](https://handlebarsjs.com/guide/#language-features) for more
|
||||
> information.
|
||||
|
||||
## Helpers
|
||||
|
||||
### Built-in Helpers
|
||||
|
||||
Simple-Scaffold provides some built-in text transformation filters usable by Handlebars.
|
||||
|
||||
For example, you may use `{{ snakeCase name }}` inside a template file or filename, and it will
|
||||
replace `My Name` with `my_name` when producing the final value.
|
||||
|
||||
#### Capitalization Helpers
|
||||
|
||||
| Helper name | Example code | Example output |
|
||||
| ------------ | ----------------------- | -------------- |
|
||||
| [None] | `{{ name }}` | my name |
|
||||
| `camelCase` | `{{ camelCase name }}` | myName |
|
||||
| `snakeCase` | `{{ snakeCase name }}` | my_name |
|
||||
| `startCase` | `{{ startCase name }}` | My Name |
|
||||
| `kebabCase` | `{{ kebabCase name }}` | my-name |
|
||||
| `hyphenCase` | `{{ hyphenCase name }}` | my-name |
|
||||
| `pascalCase` | `{{ pascalCase name }}` | MyName |
|
||||
| `upperCase` | `{{ upperCase name }}` | MY NAME |
|
||||
| `lowerCase` | `{{ lowerCase name }}` | my name |
|
||||
|
||||
#### Date helpers
|
||||
|
||||
| Helper name | Description | Example code | Example output |
|
||||
| -------------------------------- | ---------------------------------------------------------------- | ---------------------------------------------------------------- | ------------------ |
|
||||
| `now` | Current date with format | `{{ now "yyyy-MM-dd HH:mm" }}` | `2042-01-01 15:00` |
|
||||
| `now` (with offset) | Current date with format, and with offset | `{{ now "yyyy-MM-dd HH:mm" -1 "hours" }}` | `2042-01-01 14:00` |
|
||||
| `date` | Custom date with format | `{{ date "2042-01-01T15:00:00Z" "yyyy-MM-dd HH:mm" }}` | `2042-01-01 15:00` |
|
||||
| `date` (with offset) | Custom date with format, and with offset | `{{ date "2042-01-01T15:00:00Z" "yyyy-MM-dd HH:mm" -1 "days" }}` | `2041-31-12 15:00` |
|
||||
| `date` (with date from `--data`) | Custom date with format, with data from the `data` config option | `{{ date myCustomDate "yyyy-MM-dd HH:mm" }}` | `2042-01-01 12:00` |
|
||||
|
||||
Further details:
|
||||
|
||||
- We use [`date-fns`](https://date-fns.org/docs/) for parsing/manipulating the dates. If you want
|
||||
more information on the date tokens to use, refer to
|
||||
[their format documentation](https://date-fns.org/docs/format).
|
||||
|
||||
- The date helper format takes the following arguments:
|
||||
|
||||
```typescript
|
||||
(
|
||||
date: string,
|
||||
format: string,
|
||||
offsetAmount?: number,
|
||||
offsetType?: "years" | "months" | "weeks" | "days" | "hours" | "minutes" | "seconds"
|
||||
)
|
||||
```
|
||||
|
||||
- **The now helper** (for current time) takes the same arguments, minus the first one (`date`) as it
|
||||
is implicitly the current date.
|
||||
|
||||
### Custom Helpers
|
||||
|
||||
You may also add your own custom helpers using the `helpers` options when using the JS API (rather
|
||||
than the CLI). The `helpers` option takes an object whose keys are helper names, and values are the
|
||||
transformation functions. For example, `upperCase` is implemented like so:
|
||||
|
||||
```typescript
|
||||
config.helpers = {
|
||||
upperCase: (text) => text.toUpperCase(),
|
||||
}
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
> 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>
|
||||
)
|
||||
}
|
||||
```
|
||||
6523
pnpm-lock.yaml
generated
@@ -1,64 +0,0 @@
|
||||
/** @type {import('semantic-release').Options} */
|
||||
module.exports = {
|
||||
branches: ["master", { name: "pre", prerelease: true }],
|
||||
analyzeCommits: {
|
||||
path: "semantic-release-conventional-commits",
|
||||
},
|
||||
plugins: [
|
||||
[
|
||||
"@semantic-release/commit-analyzer",
|
||||
{
|
||||
preset: "conventionalcommits",
|
||||
parserOpts: {
|
||||
noteKeywords: ["breaking:", "breaking-fix:", "breaking-feat:"],
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
"@semantic-release/release-notes-generator",
|
||||
{
|
||||
preset: "conventionalcommits",
|
||||
parserOpts: {
|
||||
noteKeywords: ["breaking", "major"],
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
"@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: [
|
||||
{
|
||||
path: "*.tgz",
|
||||
name: "simple-scaffold.tgz",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
],
|
||||
}
|
||||
22
scaffold.config.cjs
Normal 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: "---",
|
||||
},
|
||||
}
|
||||
}
|
||||
86
src/before-write.ts
Normal 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
|
||||
}
|
||||
500
src/cmd.ts
@@ -1,169 +1,349 @@
|
||||
#!/usr/bin/env node
|
||||
import massarg from "massarg"
|
||||
import chalk from "chalk"
|
||||
import { LogLevel, ScaffoldCmdConfig } from "./types"
|
||||
import { Scaffold } from "./scaffold"
|
||||
|
||||
import path from "node:path"
|
||||
import fs from "node:fs/promises"
|
||||
import { parseAppendData, parseConfigFile } from "./config"
|
||||
import { massarg } from "massarg"
|
||||
import { ListCommandCliOptions, LogLevel, ScaffoldCmdConfig, ScaffoldConfigMap } from "./types"
|
||||
import { Scaffold } from "./scaffold"
|
||||
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 parseConfigFile(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
@@ -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>,
|
||||
),
|
||||
)
|
||||
230
src/config.ts
@@ -1,38 +1,27 @@
|
||||
import path from "node:path"
|
||||
import {
|
||||
ConfigLoadConfig,
|
||||
FileResponse,
|
||||
FileResponseHandler,
|
||||
LogConfig,
|
||||
LogLevel,
|
||||
RemoteConfigLoadConfig,
|
||||
ScaffoldCmdConfig,
|
||||
ScaffoldConfig,
|
||||
ScaffoldConfigFile,
|
||||
ScaffoldConfigMap,
|
||||
} from "./types"
|
||||
import { handlebarsParse } from "./parser"
|
||||
import { log } from "./logger"
|
||||
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"
|
||||
|
||||
/** 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) }
|
||||
}
|
||||
@@ -40,64 +29,109 @@ export function parseAppendData(value: string, options: ScaffoldCmdConfig): unkn
|
||||
}
|
||||
|
||||
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 parseConfigFile(config: ScaffoldCmdConfig): Promise<ScaffoldConfig> {
|
||||
let c: ScaffoldConfig = config
|
||||
if (config.github) {
|
||||
log(config, LogLevel.Info, `Loading config from github ${config.github}`)
|
||||
config.config = githubPartToUrl(config.github)
|
||||
/** 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 { configFile, key, isRemote } = parseConfigSelection(config.config, config.key)
|
||||
log(config, LogLevel.Info, `Loading config from ${configFile} with key ${key}`)
|
||||
const configPromise = await getConfig({
|
||||
config: configFile,
|
||||
isRemote,
|
||||
quiet: config.quiet,
|
||||
verbose: config.verbose,
|
||||
})
|
||||
let configImport = await resolve(configPromise, config)
|
||||
if (typeof configImport.default === "function" || configImport.default instanceof Promise) {
|
||||
configImport = await resolve(configImport.default, config)
|
||||
}
|
||||
const isGit = Boolean(config.git)
|
||||
const configFilename = config.config
|
||||
const configPath = isGit ? config.git : configFilename
|
||||
|
||||
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}`)
|
||||
}
|
||||
const importedKey = configImport[key]
|
||||
c = {
|
||||
...config,
|
||||
...importedKey,
|
||||
|
||||
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: {
|
||||
...(importedKey as any).data,
|
||||
...imported.data,
|
||||
...config.data,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
c.data = { ...c.data, ...config.appendData }
|
||||
delete config.appendData
|
||||
return c
|
||||
}
|
||||
|
||||
export function parseConfigSelection(
|
||||
config: string,
|
||||
key?: string,
|
||||
): { configFile: string; key: string; isRemote: boolean } {
|
||||
const isUrl = config.includes("://")
|
||||
|
||||
const hasColonToken = (!isUrl && config.includes(":")) || (isUrl && count(config, ":") > 1)
|
||||
const colonIndex = config.lastIndexOf(":")
|
||||
const [configFile, templateKey = "default"] = hasColonToken
|
||||
? [config.substring(0, colonIndex), config.substring(colonIndex + 1)]
|
||||
: [config, undefined]
|
||||
const _key = (key ?? templateKey) || "default"
|
||||
return { configFile, key: _key, isRemote: isUrl }
|
||||
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
|
||||
}
|
||||
|
||||
if (!output.name) {
|
||||
throw new Error("simple-scaffold: Missing required option: name")
|
||||
}
|
||||
|
||||
log(output, LogLevel.debug, "Parsed config", output)
|
||||
return output
|
||||
}
|
||||
|
||||
/** 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")) {
|
||||
@@ -106,31 +140,67 @@ export function githubPartToUrl(part: string): string {
|
||||
return gitUrl.toString()
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export async function getConfig(config: ConfigLoadConfig): Promise<ScaffoldConfigFile> {
|
||||
const { config: configFile, isRemote, ...logConfig } = config as Required<typeof config>
|
||||
/** 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>
|
||||
|
||||
if (!isRemote) {
|
||||
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)))
|
||||
}
|
||||
|
||||
const url = new URL(configFile)
|
||||
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 (isGit) {
|
||||
return getGitConfig(url, logConfig)
|
||||
}
|
||||
|
||||
if (!isHttp) {
|
||||
if (!isGit) {
|
||||
throw new Error(`Unsupported protocol ${url.protocol}`)
|
||||
}
|
||||
|
||||
return wrapNoopResolver(import(path.resolve(process.cwd(), configFile)))
|
||||
return getGitConfig(url, configFile, tmpDir, logConfig)
|
||||
}
|
||||
|
||||
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`)
|
||||
}
|
||||
|
||||
244
src/file.ts
@@ -1,108 +1,93 @@
|
||||
import path from "node:path"
|
||||
import { F_OK } from "node:constants"
|
||||
import { LogLevel, ScaffoldConfig } from "./types"
|
||||
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 { stat, access, mkdir, readFile, writeFile } = fs
|
||||
const { readFile, writeFile } = fs
|
||||
|
||||
export async function createDirIfNotExists(dir: string, config: ScaffoldConfig): 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: any) {
|
||||
if (e.code !== "EEXIST") {
|
||||
throw e
|
||||
}
|
||||
return
|
||||
}
|
||||
// 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"
|
||||
|
||||
/**
|
||||
* 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 path.normalize(template.replace(/\*/g, ""))
|
||||
}
|
||||
|
||||
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[]> {
|
||||
template = template.replaceAll(/[\\]+/g, "/")
|
||||
log(_config, LogLevel.Debug, `Getting file list for ${template}`)
|
||||
/** 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)
|
||||
const _shouldAddGlob = !isGlob && isDirOrGlob
|
||||
log(config, LogLevel.Debug, "after", { isDirOrGlob, _shouldAddGlob })
|
||||
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
|
||||
@@ -111,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,
|
||||
{
|
||||
@@ -141,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.toString())
|
||||
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
@@ -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
@@ -1,44 +1,28 @@
|
||||
import path from "node:path"
|
||||
import os from "node:os"
|
||||
import { log } from "./logger"
|
||||
import { AsyncResolver, LogConfig, LogLevel, ScaffoldCmdConfig, ScaffoldConfig, ScaffoldConfigMap } from "./types"
|
||||
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.Info, `Cloning git repo ${repoUrl}`)
|
||||
|
||||
const tmpPath = path.resolve(os.tmpdir(), `scaffold-config-${Date.now()}`)
|
||||
log(logConfig, LogLevel.debug, `Cloning git repo ${repoUrl}`)
|
||||
|
||||
return new Promise((res, reject) => {
|
||||
const clone = spawn("git", ["clone", "--depth", "1", repoUrl, tmpPath])
|
||||
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) {
|
||||
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 resolve(
|
||||
async () => (await import(absolutePath)).default as ScaffoldConfigMap,
|
||||
logConfig,
|
||||
)
|
||||
|
||||
log(logConfig, LogLevel.Info, `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)),
|
||||
}
|
||||
}
|
||||
res(wrapNoopResolver(fixedConfig))
|
||||
res(await loadGitConfig({ logConfig, url: repoUrl, file, tmpPath }))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -46,3 +30,36 @@ export async function getGitConfig(
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/** @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
@@ -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
|
||||
})
|
||||
}
|
||||
@@ -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
@@ -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()
|
||||
}
|
||||
122
src/logger.ts
@@ -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}`))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,10 @@
|
||||
import path from "node:path"
|
||||
import { DefaultHelpers, Helper, LogLevel, ScaffoldConfig } from "./types"
|
||||
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,
|
||||
@@ -26,7 +20,12 @@ export const defaultHelpers: Record<DefaultHelpers, Helper> = {
|
||||
}
|
||||
|
||||
function _dateHelper(date: Date, formatString: string): string
|
||||
function _dateHelper(date: Date, formatString: string, durationDifference: number, durationType: keyof Duration): string
|
||||
function _dateHelper(
|
||||
date: Date,
|
||||
formatString: string,
|
||||
durationDifference: number,
|
||||
durationType: keyof Duration,
|
||||
): string
|
||||
function _dateHelper(
|
||||
date: Date,
|
||||
formatString: string,
|
||||
@@ -40,8 +39,16 @@ 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,9 +68,16 @@ export function dateHelper(
|
||||
return _dateHelper(dateFns.parseISO(date), formatString, durationDifference!, durationType!)
|
||||
}
|
||||
|
||||
// splits by either non-alpha character or capital letter
|
||||
// splits by either non-alphanumeric character or capital letter boundaries
|
||||
function toWordParts(string: string): string[] {
|
||||
return string.split(/(?=[A-Z])|[^a-zA-Z]/).filter((s) => s.length > 0)
|
||||
// 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 {
|
||||
@@ -96,7 +110,7 @@ function pascalCase(s: string): string {
|
||||
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])
|
||||
}
|
||||
}
|
||||
@@ -104,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
@@ -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
@@ -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
|
||||
}
|
||||
191
src/scaffold.ts
@@ -5,20 +5,19 @@
|
||||
* See [readme](README.md)
|
||||
*/
|
||||
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 { defaultHelpers, registerHelpers } from "./parser"
|
||||
import { log, logInitStep, logInputFile } from "./logger"
|
||||
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`.
|
||||
@@ -55,47 +54,135 @@ import { parseConfigFile } 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: defaultHelpers.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)
|
||||
log(config, LogLevel.Debug, "Iterating files", { files, 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)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -109,23 +196,23 @@ export async function Scaffold(config: ScaffoldConfig): Promise<void> {
|
||||
* @category Main
|
||||
* @return {Promise<void>} A promise that resolves when the scaffold is complete
|
||||
*/
|
||||
Scaffold.fromConfig = async function(
|
||||
/** The path or URL to the config file */
|
||||
Scaffold.fromConfig = async function (
|
||||
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 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,
|
||||
config: pathOrUrl,
|
||||
version: false,
|
||||
tmpDir: tmpPath,
|
||||
...config,
|
||||
}
|
||||
const _overrides = resolve(overrides, _cmdConfig)
|
||||
|
||||
259
src/types.ts
@@ -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"> & { isRemote: boolean }
|
||||
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">
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
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
|
||||
|
||||
171
src/validate.ts
Normal 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")}`)
|
||||
}
|
||||
}
|
||||
@@ -1,31 +1,43 @@
|
||||
import { ScaffoldCmdConfig } from "../src/types"
|
||||
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"
|
||||
// @ts-ignore
|
||||
import * as configFile from "../scaffold.config"
|
||||
import configFile from "./test-config"
|
||||
import { findConfigFile, getOptionValueForFile } from "../src/config"
|
||||
import { registerHelpers } from "../src/parser"
|
||||
import path from "path"
|
||||
|
||||
jest.mock("../src/git", () => {
|
||||
vi.mock("../src/git", async () => {
|
||||
const actual = await vi.importActual<typeof import("../src/git")>("../src/git")
|
||||
return {
|
||||
__esModule: true,
|
||||
...jest.requireActual("../src/git"),
|
||||
...actual,
|
||||
getGitConfig: () => {
|
||||
return Promise.resolve(blankCliConf)
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const { githubPartToUrl, parseAppendData, parseConfigFile, parseConfigSelection } = config
|
||||
const { githubPartToUrl, parseAppendData, parseConfigFile } = config
|
||||
|
||||
const blankCliConf: ScaffoldCmdConfig = {
|
||||
verbose: 0,
|
||||
logLevel: LogLevel.none,
|
||||
name: "",
|
||||
output: "",
|
||||
templates: [],
|
||||
data: { name: "test" },
|
||||
overwrite: false,
|
||||
createSubFolder: false,
|
||||
subdir: false,
|
||||
dryRun: false,
|
||||
quiet: false,
|
||||
version: false,
|
||||
}
|
||||
|
||||
const blankConfig: ScaffoldCmdConfig = {
|
||||
...blankCliConf,
|
||||
data: {},
|
||||
}
|
||||
|
||||
describe("config", () => {
|
||||
@@ -43,107 +55,485 @@ describe("config", () => {
|
||||
})
|
||||
|
||||
test("works with quotes", () => {
|
||||
expect(parseAppendData('key="value test"', blankCliConf)).toEqual({ key: "value test", name: "test" })
|
||||
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")).toEqual(
|
||||
"https://github.com/chenasraf/simple-scaffold.git",
|
||||
)
|
||||
expect(githubPartToUrl("chenasraf/simple-scaffold.git")).toEqual(
|
||||
"https://github.com/chenasraf/simple-scaffold.git",
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("parseConfigSelection", () => {
|
||||
test("no key", () => {
|
||||
expect(parseConfigSelection("scaffold.config.js")).toEqual({
|
||||
configFile: "scaffold.config.js",
|
||||
key: "default",
|
||||
isRemote: false,
|
||||
})
|
||||
test("handles organization repos", () => {
|
||||
expect(githubPartToUrl("org/sub-repo")).toEqual("https://github.com/org/sub-repo.git")
|
||||
})
|
||||
test("separate key", () => {
|
||||
expect(parseConfigSelection("scaffold.config.js", "component")).toEqual({
|
||||
configFile: "scaffold.config.js",
|
||||
key: "component",
|
||||
isRemote: false,
|
||||
})
|
||||
})
|
||||
test("key override", () => {
|
||||
expect(parseConfigSelection("scaffold.config.js:component", "main")).toEqual({
|
||||
configFile: "scaffold.config.js",
|
||||
key: "main",
|
||||
isRemote: false,
|
||||
})
|
||||
})
|
||||
test("isRemote: false", () => {
|
||||
expect(parseConfigSelection("scaffold.config.js", "main")).toEqual({
|
||||
configFile: "scaffold.config.js",
|
||||
key: "main",
|
||||
isRemote: false,
|
||||
})
|
||||
})
|
||||
test("isRemote: true", () => {
|
||||
expect(
|
||||
parseConfigSelection("https://github.com/chenasraf/simple-scaffold.git#scaffold.config.js:component", "main"),
|
||||
).toEqual({
|
||||
configFile: "https://github.com/chenasraf/simple-scaffold.git#scaffold.config.js",
|
||||
key: "main",
|
||||
isRemote: true,
|
||||
})
|
||||
|
||||
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(blankCliConf)
|
||||
).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.getConfig({
|
||||
config: "https://github.com/chenasraf/simple-scaffold.git",
|
||||
isRemote: true,
|
||||
quiet: true,
|
||||
verbose: 0,
|
||||
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.getConfig({
|
||||
config: "scaffold.config.js",
|
||||
isRemote: false,
|
||||
quiet: true,
|
||||
verbose: 0,
|
||||
const resultFn = await config.getLocalConfig({
|
||||
config: path.join(__dirname, "test-config.js"),
|
||||
logLevel: LogLevel.none,
|
||||
})
|
||||
const result = await resolve(resultFn, {} as any)
|
||||
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
@@ -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
@@ -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
@@ -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
@@ -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()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,73 +1,149 @@
|
||||
import { ScaffoldCmdConfig, ScaffoldConfig } from "../src/types"
|
||||
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 { OptionsBase } from "massarg/types"
|
||||
import { dateHelper, defaultHelpers, handlebarsParse, nowHelper } from "../src/parser"
|
||||
import { dateHelper, defaultHelpers, handlebarsParse, nowHelper, registerHelpers } from "../src/parser"
|
||||
|
||||
const blankConf: ScaffoldConfig = {
|
||||
verbose: 0,
|
||||
logLevel: "none",
|
||||
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("parser", () => {
|
||||
describe("handlebarsParse", () => {
|
||||
let origSep: any
|
||||
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", { isPath: true }).toString()).toEqual(
|
||||
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", { isPath: true })).toEqual(
|
||||
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",
|
||||
{
|
||||
isPath: false,
|
||||
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", () => {
|
||||
@@ -80,6 +156,7 @@ describe("parser", () => {
|
||||
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")
|
||||
@@ -88,6 +165,7 @@ describe("parser", () => {
|
||||
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")
|
||||
@@ -96,6 +174,7 @@ describe("parser", () => {
|
||||
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")
|
||||
@@ -104,6 +183,7 @@ describe("parser", () => {
|
||||
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")
|
||||
@@ -113,6 +193,92 @@ describe("parser", () => {
|
||||
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", () => {
|
||||
@@ -142,7 +308,122 @@ describe("parser", () => {
|
||||
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
@@ -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")
|
||||
})
|
||||
})
|
||||
})
|
||||
3
tests/test-config.d.ts
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
declare const config: import("../dist").ScaffoldConfigFile;
|
||||
export = config;
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
/** @type {import('simple-scaffold').ScaffoldConfigFile} */
|
||||
// @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: {
|
||||
@@ -12,5 +15,10 @@ module.exports = (conf) => {
|
||||
output: "examples/test-output/component",
|
||||
data: { property: "myProp", value: "10" },
|
||||
},
|
||||
configs: {
|
||||
templates: ["examples/test-input/**/.*"],
|
||||
output: "examples/test-output/configs",
|
||||
name: "---",
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,55 @@
|
||||
import { handleErr, resolve } from "../src/utils"
|
||||
|
||||
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)
|
||||
})
|
||||
|
||||
test("should resolve value", () => {
|
||||
expect(resolve(1, null)).toBe(1)
|
||||
expect(resolve(2, 1)).toBe(2)
|
||||
})
|
||||
|
||||
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("handleErr", () => {
|
||||
@@ -17,5 +57,151 @@ describe("utils", () => {
|
||||
expect(() => handleErr({ name: "test", message: "test" })).toThrow()
|
||||
expect(() => handleErr(null as never)).not.toThrow()
|
||||
})
|
||||
|
||||
test("should throw the provided error", () => {
|
||||
const err = new Error("test error")
|
||||
expect(() => handleErr(err as unknown as NodeJS.ErrnoException)).toThrow("test error")
|
||||
})
|
||||
})
|
||||
|
||||
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("should return function as-is", () => {
|
||||
const fn = (x: string) => x.toUpperCase()
|
||||
const wrapped = wrapNoopResolver(fn)
|
||||
expect(wrapped).toBe(fn)
|
||||
})
|
||||
|
||||
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("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
@@ -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")
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,11 +1,12 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"rootDir": "./src",
|
||||
"esModuleInterop": true,
|
||||
"lib": ["ES2022"],
|
||||
"lib": ["ESNext"],
|
||||
"declaration": true,
|
||||
"outDir": "dist",
|
||||
"strict": true,
|
||||
@@ -13,7 +14,8 @@
|
||||
"removeComments": false,
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["src/index.ts", "src/cmd.ts"],
|
||||
"exclude": ["tests/*"]
|
||||
|
||||
44
vite.config.ts
Normal 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"],
|
||||
},
|
||||
})
|
||||