Compare commits
24 Commits
v3.1.1
...
v1.1.0-alp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
374b51e482 | ||
|
|
5e175e6815 | ||
|
|
0742fc8771 | ||
|
|
0ecf07de18 | ||
|
|
51081cf2ee | ||
|
|
5a39c08a9f | ||
|
|
83e5724152 | ||
|
|
7455ac1ead | ||
|
|
f315373d18 | ||
|
|
38d557d450 | ||
|
|
a27389e262 | ||
|
|
ce9fededf0 | ||
|
|
4653428bda | ||
|
|
19b2203f30 | ||
|
|
3f103cd4c2 | ||
|
|
ec5503bc3e | ||
|
|
19ad60c3cb | ||
|
|
f04dd7e487 | ||
|
|
4c9a838ebe | ||
|
|
6700708e1d | ||
|
|
2727bd526c | ||
|
|
834a805794 | ||
|
|
ba9434f504 | ||
|
|
59353f4121 |
@@ -3,6 +3,3 @@ tab_width = 2
|
||||
indent_style = space
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
5
.github/FUNDING.yml
vendored
@@ -1,6 +1,6 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: chenasraf
|
||||
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||
patreon: # Replace with a single Patreon username
|
||||
open_collective: # Replace with a single Open Collective username
|
||||
ko_fi: casraf
|
||||
@@ -9,5 +9,4 @@ community_bridge: # Replace with a single Community Bridge project-name e.g., cl
|
||||
liberapay: # Replace with a single Liberapay username
|
||||
issuehunt: # Replace with a single IssueHunt username
|
||||
otechie: # Replace with a single Otechie username
|
||||
custom:
|
||||
- "https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=TSH3C3ABGQM22¤cy_code=ILS&source=url"
|
||||
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||
|
||||
46
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,47 +1,37 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: "[BUG] "
|
||||
title: ''
|
||||
labels: bug, needs-triage
|
||||
assignees: chenasraf
|
||||
|
||||
---
|
||||
|
||||
#### Describe the bug
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
#### How To Reproduce
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
|
||||
1. Prepare templates:
|
||||
|
||||
```text
|
||||
This is my {{ template }}
|
||||
```
|
||||
|
||||
```
|
||||
template contents
|
||||
```
|
||||
2. Run with args/config:
|
||||
|
||||
```shell
|
||||
npx simple-scaffold@latest -t input -o output TplName
|
||||
```
|
||||
|
||||
#### Expected behavior\*\*
|
||||
|
||||
```
|
||||
npx simple-scaffold@latest -t ... -o output TplName
|
||||
```
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
#### Logs
|
||||
**Logs**
|
||||
If applicable, paste your logs to help explain your problem.
|
||||
To see more logs, run the scaffold with `-v 1` to enable debug logging.
|
||||
|
||||
If applicable, paste your logs to help explain your problem. To see more logs, run the scaffold with
|
||||
`-v 1` to enable debug logging.
|
||||
|
||||
#### Desktop (please complete the following information):
|
||||
|
||||
- OS: [e.g. macOS, Windows, Linux]
|
||||
- OS Version: [e.g. Big Sur, 11, Ubuntu 20.04]
|
||||
**Desktop (please complete the following information):**
|
||||
- OS: [e.g. iOS]
|
||||
- Node.js: [e.g. 16.8]
|
||||
- Simple Scaffold Version [e.g. 1.1.2]
|
||||
|
||||
#### Additional context
|
||||
- Simple Scaffold Version [e.g. 1.0.4]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
|
||||
18
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -1,24 +1,20 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: "[FEATURE] "
|
||||
title: ''
|
||||
labels: enhancement, needs-triage
|
||||
assignees: chenasraf
|
||||
|
||||
---
|
||||
|
||||
#### Is your feature request related to a problem? Please describe.
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
#### Describe the solution you'd like
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
#### Describe alternatives you've considered
|
||||
|
||||
A clear and concise description of any alternative solutions or features you've considered, if
|
||||
applicable.
|
||||
|
||||
#### Additional context
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
|
||||
52
.github/workflows/develop.yml
vendored
Normal file
@@ -0,0 +1,52 @@
|
||||
name: Alpha Releases
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [develop]
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
if: "!contains(github.event.head_commit.message, '[skip ci]')"
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: "12.x"
|
||||
- run: yarn install --frozen-lockfile
|
||||
- run: yarn test
|
||||
- run: yarn build
|
||||
- run: cd ./dist && yarn pack --filename=../package.tgz
|
||||
if: "!contains(github.event.head_commit.message, '[skip publish]')"
|
||||
- uses: Klemensas/action-autotag@stable
|
||||
if: "!contains(github.event.head_commit.message, '[skip publish]')"
|
||||
id: update_tag
|
||||
with:
|
||||
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
||||
tag_prefix: "v"
|
||||
- name: Publish on NPM
|
||||
uses: JS-DevTools/npm-publish@v1
|
||||
if: "!contains(github.event.head_commit.message, '[skip publish]')"
|
||||
with:
|
||||
package: ./dist/package.json
|
||||
token: "${{ secrets.NPM_TOKEN }}"
|
||||
- name: Create Release
|
||||
if: steps.update_tag.outputs.tagname && !contains(github.event.head_commit.message, '[skip publish]')
|
||||
uses: actions/create-release@v1
|
||||
id: create_release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
prerelease: true
|
||||
tag_name: ${{ steps.update_tag.outputs.tagname }}
|
||||
release_name: Release ${{ steps.update_tag.outputs.tagname }}
|
||||
- name: Upload Release Asset
|
||||
if: steps.update_tag.outputs.tagname && !contains(github.event.head_commit.message, '[skip publish]')
|
||||
id: upload-release-asset
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: ./package.tgz
|
||||
asset_name: simple-scaffold ${{ steps.update_tag.outputs.tagname }}.tgz
|
||||
asset_content_type: application/tgz
|
||||
17
.github/workflows/pull_requests.yml
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
name: Pull Requests
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [master, develop]
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
if: "!contains(github.event.head_commit.message, '[skip ci]')"
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: "12.x"
|
||||
- run: yarn install --frozen-lockfile
|
||||
- run: yarn build
|
||||
- run: yarn test
|
||||
126
.github/workflows/release.yml
vendored
@@ -1,102 +1,50 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
id-token: write
|
||||
|
||||
branches: [master]
|
||||
jobs:
|
||||
test:
|
||||
name: Test
|
||||
runs-on: ubuntu-latest
|
||||
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: pnpm test
|
||||
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
if: "!contains(github.event.head_commit.message, '[skip ci]')"
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v4
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
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
|
||||
node-version: "12.x"
|
||||
- run: yarn install --frozen-lockfile
|
||||
- run: yarn test
|
||||
- run: yarn build
|
||||
- run: cd ./dist && yarn pack --filename=../package.tgz
|
||||
if: "!contains(github.event.head_commit.message, '[skip publish]')"
|
||||
- uses: Klemensas/action-autotag@stable
|
||||
if: "!contains(github.event.head_commit.message, '[skip publish]')"
|
||||
id: update_tag
|
||||
with:
|
||||
token: ${{ secrets.RELEASE_PLEASE_TOKEN }}
|
||||
release-type: node
|
||||
target-branch: master
|
||||
|
||||
publish:
|
||||
name: NPM Publish
|
||||
needs: release
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ needs.release.outputs.release_created }}
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v4
|
||||
- uses: actions/setup-node@v4
|
||||
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
||||
tag_prefix: "v"
|
||||
- name: Publish on NPM
|
||||
uses: JS-DevTools/npm-publish@v1
|
||||
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
|
||||
package: ./dist/package.json
|
||||
token: "${{ secrets.NPM_TOKEN }}"
|
||||
- name: Create Release
|
||||
if: steps.update_tag.outputs.tagname && !contains(github.event.head_commit.message, '[skip publish]')
|
||||
uses: actions/create-release@v1
|
||||
id: create_release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
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
|
||||
tag_name: ${{ steps.update_tag.outputs.tagname }}
|
||||
release_name: Release ${{ steps.update_tag.outputs.tagname }}
|
||||
- name: Upload Release Asset
|
||||
if: steps.update_tag.outputs.tagname && !contains(github.event.head_commit.message, '[skip publish]')
|
||||
id: upload-release-asset
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
publish_dir: ./docs/build
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: ./package.tgz
|
||||
asset_name: simple-scaffold ${{ steps.update_tag.outputs.tagname }}.tgz
|
||||
asset_content_type: application/tgz
|
||||
|
||||
5
.gitignore
vendored
@@ -42,9 +42,6 @@ typings/
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# NPM
|
||||
.npmrc
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
@@ -64,5 +61,3 @@ examples/test-output/**/*
|
||||
dist/
|
||||
.DS_Store
|
||||
tmp/
|
||||
.nvmrc
|
||||
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
pnpm lint-staged
|
||||
@@ -1,9 +1,8 @@
|
||||
{
|
||||
"line-length": {
|
||||
"MD013": {
|
||||
"line_length": 100,
|
||||
"tables": false,
|
||||
"code_blocks": false
|
||||
},
|
||||
"no-inline-html": false,
|
||||
"first-line-h1": false
|
||||
"MD033": false
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
docs/docs/api/
|
||||
examples/
|
||||
.github/
|
||||
CHANGELOG.md
|
||||
pnpm-lock.yaml
|
||||
15
.prettierrc
@@ -1,16 +1,5 @@
|
||||
{
|
||||
"semi": false,
|
||||
"singleQuote": false,
|
||||
"trailingComma": "all",
|
||||
"tabWidth": 2,
|
||||
"printWidth": 100,
|
||||
"overrides": [
|
||||
{
|
||||
"files": "*.md",
|
||||
"options": {
|
||||
"printWidth": 100,
|
||||
"proseWrap": "always"
|
||||
}
|
||||
}
|
||||
]
|
||||
"printWidth": 120,
|
||||
"tabWidth": 2
|
||||
}
|
||||
|
||||
50
.vscode/launch.json
vendored
@@ -1,27 +1,29 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Debug Scaffold",
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"protocol": "inspector",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"program": "${workspaceFolder}/test.ts",
|
||||
"outFiles": ["${workspaceRoot}/dist/test.js"],
|
||||
"env": {
|
||||
"NODE_ENV": "develop"
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Debug Scaffold",
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"protocol": "inspector",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"program": "${workspaceFolder}/test.ts",
|
||||
"outFiles": [
|
||||
"${workspaceRoot}/dist/test.js"
|
||||
],
|
||||
"env": {
|
||||
"NODE_ENV": "develop"
|
||||
},
|
||||
"sourceMaps": true,
|
||||
},
|
||||
"sourceMaps": true
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "attach",
|
||||
"name": "Attach by Process ID",
|
||||
"processId": "${command:PickProcess}"
|
||||
}
|
||||
]
|
||||
{
|
||||
"type": "node",
|
||||
"request": "attach",
|
||||
"name": "Attach by Process ID",
|
||||
"processId": "${command:PickProcess}"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
9
.vscode/settings.json
vendored
@@ -3,17 +3,12 @@
|
||||
"npm.packageManager": "yarn",
|
||||
"cSpell.words": [
|
||||
"massarg",
|
||||
"MYCOMPONENT",
|
||||
"myname",
|
||||
"nobrace",
|
||||
"nocomment",
|
||||
"nodir",
|
||||
"noext",
|
||||
"nonegate",
|
||||
"subdir",
|
||||
"variabletoken"
|
||||
],
|
||||
"[markdown]": {
|
||||
"editor.rulers": [87, 100]
|
||||
}
|
||||
"subdir"
|
||||
]
|
||||
}
|
||||
|
||||
30
.vscode/tasks.json
vendored
@@ -5,55 +5,49 @@
|
||||
"script": "build",
|
||||
"label": "build",
|
||||
"type": "npm",
|
||||
"problemMatcher": []
|
||||
"problemMatcher": [],
|
||||
},
|
||||
{
|
||||
"script": "dev",
|
||||
"label": "dev",
|
||||
"type": "npm",
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"command": "pnpm typedoc --watch",
|
||||
"label": "typedoc --watch",
|
||||
"type": "shell",
|
||||
"problemMatcher": []
|
||||
"problemMatcher": [],
|
||||
},
|
||||
{
|
||||
"script": "start",
|
||||
"label": "start",
|
||||
"type": "npm",
|
||||
"problemMatcher": []
|
||||
"problemMatcher": [],
|
||||
},
|
||||
{
|
||||
"script": "test",
|
||||
"label": "test",
|
||||
"type": "npm",
|
||||
"problemMatcher": []
|
||||
"problemMatcher": [],
|
||||
},
|
||||
{
|
||||
"command": "pnpm test --watchAll",
|
||||
"label": "pnpm test --watchAll",
|
||||
"command": "yarn test --watchAll",
|
||||
"label": "yarn test --watchAll",
|
||||
"type": "shell",
|
||||
"problemMatcher": []
|
||||
"problemMatcher": [],
|
||||
},
|
||||
{
|
||||
"script": "cmd",
|
||||
"label": "cmd",
|
||||
"type": "npm",
|
||||
"problemMatcher": []
|
||||
"problemMatcher": [],
|
||||
},
|
||||
{
|
||||
"script": "build-test",
|
||||
"label": "build-test",
|
||||
"type": "npm",
|
||||
"problemMatcher": []
|
||||
"problemMatcher": [],
|
||||
},
|
||||
{
|
||||
"script": "build-cmd",
|
||||
"label": "build-cmd",
|
||||
"type": "npm",
|
||||
"problemMatcher": []
|
||||
}
|
||||
]
|
||||
"problemMatcher": [],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
713
CHANGELOG.md
@@ -1,713 +0,0 @@
|
||||
# Change Log
|
||||
|
||||
## [3.1.1](https://github.com/chenasraf/simple-scaffold/compare/v3.1.0...v3.1.1) (2026-03-26)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **deps:** update dependencies ([f5ed38a](https://github.com/chenasraf/simple-scaffold/commit/f5ed38ad950e3738b173cd39baf2acc9e35bf8de))
|
||||
|
||||
## [3.1.0](https://github.com/chenasraf/simple-scaffold/compare/v3.0.0...v3.1.0) (2026-03-23)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* 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
|
||||
|
||||
* interactive inputs with existing config/cli options ([c860e46](https://github.com/chenasraf/simple-scaffold/commit/c860e4644a3f3d4bd6b7dab9974accdf8bae9463))
|
||||
|
||||
## [3.0.0](https://github.com/chenasraf/simple-scaffold/compare/v2.3.3...v3.0.0) (2026-03-23)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* 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
|
||||
|
||||
* string helpers to words parts conversion ([af33c05](https://github.com/chenasraf/simple-scaffold/commit/af33c059b91d3f463a5d174ab3a0119c577880c5))
|
||||
|
||||
## [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)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* 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)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* `list` command ([d579c09](https://github.com/chenasraf/simple-scaffold/commit/d579c09c11f2149fe7bb4515297c1287fa67083e))
|
||||
* add `--before-write` cli option ([#89](https://github.com/chenasraf/simple-scaffold/issues/89)) ([5f810e2](https://github.com/chenasraf/simple-scaffold/commit/5f810e21160816bc683cc0f375de318ff874871c))
|
||||
|
||||
# [2.1.0](https://github.com/chenasraf/simple-scaffold/compare/v2.0.2...v2.1.0) (2024-02-12)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* 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
|
||||
|
||||
* try to await scaffold before finally ([1b70897](https://github.com/chenasraf/simple-scaffold/commit/1b70897f9840e6365ff800490fbb813b9840177d))
|
||||
|
||||
## [2.0.1](https://github.com/chenasraf/simple-scaffold/compare/v2.0.0...v2.0.1) (2024-02-02)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 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))
|
||||
|
||||
# [2.0.0](https://github.com/chenasraf/simple-scaffold/compare/v1.9.0...v2.0.0) (2024-01-31)
|
||||
|
||||
* 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))
|
||||
|
||||
|
||||
|
||||
## 1.9.0 (2024-01-02)
|
||||
|
||||
* 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))
|
||||
|
||||
|
||||
|
||||
## 1.8.0 (2023-11-29)
|
||||
|
||||
* 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))
|
||||
|
||||
|
||||
|
||||
## <small>1.7.2 (2023-08-20)</small>
|
||||
|
||||
* chore(release): 1.7.2 [skip ci] ([d62eeeb](https://github.com/chenasraf/simple-scaffold/commit/d62eeeb))
|
||||
|
||||
|
||||
|
||||
## <small>1.7.2-pre.1 (2023-08-15)</small>
|
||||
|
||||
* 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))
|
||||
|
||||
|
||||
|
||||
## <small>1.7.1 (2023-06-07)</small>
|
||||
|
||||
* 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))
|
||||
|
||||
|
||||
|
||||
## <small>1.7.1-develop.1 (2023-06-07)</small>
|
||||
|
||||
* chore(release): 1.7.1-develop.1 [skip ci] ([77be7c0](https://github.com/chenasraf/simple-scaffold/commit/77be7c0))
|
||||
|
||||
|
||||
|
||||
## 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))
|
||||
21
MIGRATION.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# Migrating from 0.x to 1.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`.
|
||||
|
||||
## 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.
|
||||
602
README.md
@@ -1,334 +1,366 @@
|
||||
<p align="center">
|
||||
<img src="https://chenasraf.github.io//simple-scaffold/img/logo-lg.png" alt="Logo" />
|
||||
</p>
|
||||
# Simple Scaffold
|
||||
|
||||
<h2 align="center">
|
||||
Simple Scaffold allows you to generate any set of files in the easiest way possible with simple commands.
|
||||
|
||||
[GitHub](https://github.com/chenasraf/simple-scaffold) |
|
||||
[Documentation](https://chenasraf.github.io/simple-scaffold) |
|
||||
[NPM](https://npmjs.com/package/simple-scaffold) | [casraf.dev](https://casraf.dev)
|
||||
It is completely framework agnostic so you can use it for anything from a few simple files to an
|
||||
entire app boilerplate setup.
|
||||
|
||||

|
||||

|
||||
Simply organize your commonly-created files in their original structure, and running Simple Scaffold
|
||||
will copy the files to the output path, while replacing values (such as component or app name, or
|
||||
other custom data) inside the paths or contents of the files using Handlebars.js syntax.
|
||||
|
||||
</h2>
|
||||
<br />
|
||||
|
||||
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.
|
||||
<details>
|
||||
<summary>Table of contents</summary>
|
||||
|
||||
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.
|
||||
- [Simple Scaffold](#simple-scaffold)
|
||||
- [Install](#install)
|
||||
- [Use as a command line tool](#use-as-a-command-line-tool)
|
||||
- [Command Line Options](#command-line-options)
|
||||
- [Use in Node.js](#use-in-nodejs)
|
||||
- [Node-specific options](#node-specific-options)
|
||||
- [Preparing files](#preparing-files)
|
||||
- [Template files](#template-files)
|
||||
- [Variable/token replacement](#variabletoken-replacement)
|
||||
- [Built-in Helpers](#built-in-helpers)
|
||||
- [Capitalization Helpers](#capitalization-helpers)
|
||||
- [Date helpers](#date-helpers)
|
||||
- [Custom Helpers](#custom-helpers)
|
||||
- [Examples](#examples)
|
||||
- [Command Example](#command-example)
|
||||
- [Example Scaffold Input](#example-scaffold-input)
|
||||
- [Input Directory structure](#input-directory-structure)
|
||||
- [Contents of `project/scaffold/{{Name}}.jsx`](#contents-of-projectscaffoldnamejsx)
|
||||
- [Example Scaffold Output](#example-scaffold-output)
|
||||
- [Output directory structure](#output-directory-structure)
|
||||
- [Contents of `project/scaffold/MyComponent/MyComponent.jsx`](#contents-of-projectscaffoldmycomponentmycomponentjsx)
|
||||
- [Contributing](#contributing)
|
||||
|
||||
<div align="center">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
> **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).
|
||||
## Install
|
||||
|
||||
## Table of Contents
|
||||
You can either use it as a command line tool or import into your own code and run from there.
|
||||
|
||||
- [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)
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Install
|
||||
|
||||
```sh
|
||||
npm install -D simple-scaffold
|
||||
# or use directly with npx
|
||||
npx simple-scaffold
|
||||
```bash
|
||||
# npm
|
||||
npm install [-g] simple-scaffold
|
||||
# yarn
|
||||
yarn [global] add simple-scaffold
|
||||
# run without installing
|
||||
npx simple-scaffold@latest <...args>
|
||||
```
|
||||
|
||||
### Initialize a Project
|
||||
## Use as a command line tool
|
||||
|
||||
Run `init` to create a config file and an example template:
|
||||
### Command Line Options
|
||||
|
||||
```sh
|
||||
npx simple-scaffold init
|
||||
```plaintext
|
||||
Usage: simple-scaffold [options]
|
||||
|
||||
Create structured files based on templates.
|
||||
|
||||
Options:
|
||||
|
||||
--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.
|
||||
|
||||
--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.
|
||||
|
||||
--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)
|
||||
```
|
||||
|
||||
This creates `scaffold.config.js` and `templates/default/{{name}}.md`. Now generate files:
|
||||
You can also add this as a script in your `package.json`:
|
||||
|
||||
```sh
|
||||
npx simple-scaffold MyProject
|
||||
```
|
||||
|
||||
### One-off Usage (No Config)
|
||||
|
||||
Generate files from a template directory without a config file:
|
||||
|
||||
```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 = {
|
||||
component: {
|
||||
templates: ["templates/component"],
|
||||
output: "src/components",
|
||||
},
|
||||
page: {
|
||||
templates: ["templates/page"],
|
||||
output: "src/pages",
|
||||
subdir: true,
|
||||
},
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"scaffold": "npx simple-scaffold@latest -t scaffolds/component/**/* -o src/components -d '{\"myProp\": \"propName\", \"myVal\": 123}'"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Then run:
|
||||
## Use in Node.js
|
||||
|
||||
```sh
|
||||
npx simple-scaffold -k component MyComponent
|
||||
npx simple-scaffold -k page Dashboard
|
||||
You can also build the scaffold yourself, if you want to create more complex arguments or scaffold groups.
|
||||
Simply pass a config object to the Scaffold function when you are ready to start.
|
||||
The config takes similar arguments to the command line:
|
||||
|
||||
```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)
|
||||
```
|
||||
|
||||
Use the key `default` to skip the `-k` flag entirely.
|
||||
### Node-specific options
|
||||
|
||||
### Listing Available Templates
|
||||
In addition to all the options available in the command line, there are some Node/JS-specific
|
||||
options available:
|
||||
|
||||
```sh
|
||||
npx simple-scaffold list
|
||||
npx simple-scaffold list -c path/to/config.js
|
||||
| Option | Type | Description |
|
||||
| ------------- | -------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `output` | | In addition to being passed the same as CLI, it may also be passed a function for each input file to output into a dynamic path: `{ output: (fullPath, baseDir, baseName) => path.resolve(baseDir, baseName) }` |
|
||||
| `helpers` | `Record<string, (string) => string>` | Helpers are simple functions that transform your `data` variables into other values. See [Helpers](#helpers) for the list of default helpers, or add your own to be loaded into the template parser. |
|
||||
| `beforeWrite` | `(content: Buffer, rawContent: Buffer, outputPath: string) => Promise<String \| Buffer \| undefined> \| String \| Buffer \| undefined` | Supply this function to override the final output contents of each of your files. The return value of this function will replace the output content of the respective file, which you may discriminate (if needed) using the `outputPath` argument. |
|
||||
|
||||
## Preparing files
|
||||
|
||||
### 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 | Output path(s) |
|
||||
| ---------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------ |
|
||||
| `./templates/{{ name }}.txt` | `src/AppName.txt` |
|
||||
| `./templates/directory` <br /><br /> Directory contents:<br /> <ol><li>`outer/{{name}}.txt`</li></li><li>`outer2/inner/{{name.txt}}`</li></ol> | `src/outer/AppName.txt`,<br />`src/outer2/inner/AppName.txt` |
|
||||
| `./templates/others/**/*.txt` <br /><br /> Directory contents:<br /> <ol><li>`outer/{{name}}.jpg`</li></li><li>`outer2/inner/{{name.txt}}`</li></ol> | `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
|
||||
```
|
||||
|
||||
## Templates
|
||||
Will output a file with the path:
|
||||
|
||||
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.
|
||||
```plaintext
|
||||
<working_dir>/src/components/MyComponent.jsx
|
||||
```
|
||||
|
||||
### Example Template
|
||||
The contents of the file will be transformed in a similar fashion.
|
||||
|
||||
`templates/component/{{pascalCase name}}.tsx`
|
||||
Your `data` will be pre-populated with the following:
|
||||
|
||||
```tsx
|
||||
// Created: {{ now 'yyyy-MM-dd' }}
|
||||
import React from "react"
|
||||
- `{{Name}}`: PascalCase of the component name
|
||||
- `{{name}}`: raw name of the component as you entered it
|
||||
|
||||
export default {{ pascalCase name }}: React.FC = (props) => {
|
||||
> 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.
|
||||
|
||||
### 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.
|
||||
|
||||
## Examples
|
||||
|
||||
### Command Example
|
||||
|
||||
```bash
|
||||
simple-scaffold MyComponent \
|
||||
-t project/scaffold/**/* \
|
||||
-o src/components \
|
||||
-d '{"className": "myClassName"}'
|
||||
MyComponent
|
||||
```
|
||||
|
||||
### Example Scaffold Input
|
||||
|
||||
#### Input Directory structure
|
||||
|
||||
```plaintext
|
||||
- project
|
||||
- scaffold
|
||||
- {{Name}}.js
|
||||
- src
|
||||
- components
|
||||
- ...
|
||||
```
|
||||
|
||||
#### Contents of `project/scaffold/{{Name}}.jsx`
|
||||
|
||||
```typescriptreact
|
||||
import React from 'react'
|
||||
|
||||
export default {{camelCase ame}}: React.FC = (props) => {
|
||||
return (
|
||||
<div className="{{ camelCase name }}">{{ pascalCase name }} Component</div>
|
||||
<div className="{{className}}">{{camelCase name}} Component</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
Running `npx simple-scaffold -t templates/component -o src/components PageWrapper` produces
|
||||
`src/components/PageWrapper.tsx` with all tokens replaced.
|
||||
### Example Scaffold Output
|
||||
|
||||
### Glob Patterns & Exclusions
|
||||
### Output directory structure
|
||||
|
||||
Template paths support globs and negation:
|
||||
|
||||
```js
|
||||
{
|
||||
templates: ["templates/component/**", "!templates/component/README.md"]
|
||||
}
|
||||
```plaintext
|
||||
- project
|
||||
- src
|
||||
- components
|
||||
- MyComponent
|
||||
- MyComponent.js
|
||||
- ...
|
||||
```
|
||||
|
||||
### .scaffoldignore
|
||||
With `createSubFolder = false`:
|
||||
|
||||
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 },
|
||||
},
|
||||
},
|
||||
}
|
||||
```plaintext
|
||||
- project
|
||||
- src
|
||||
- components
|
||||
- MyComponent.js
|
||||
- ...
|
||||
```
|
||||
|
||||
**Input types:** `text` (default), `select`, `confirm`, `number`
|
||||
#### Contents of `project/scaffold/MyComponent/MyComponent.jsx`
|
||||
|
||||
Pre-fill inputs from the command line to skip prompts:
|
||||
```typescriptreact
|
||||
import React from 'react'
|
||||
|
||||
```sh
|
||||
npx simple-scaffold -k component -D author=John -D license=MIT MyComponent
|
||||
```
|
||||
|
||||
## Remote Templates
|
||||
|
||||
Use 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
|
||||
```
|
||||
|
||||
The repository is cloned to a temporary directory, used, and cleaned up automatically.
|
||||
|
||||
## CLI Reference
|
||||
|
||||
### Commands
|
||||
|
||||
| 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 |
|
||||
|
||||
### 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() + "!!!",
|
||||
},
|
||||
},
|
||||
export default MyComponent: React.FC = (props) => {
|
||||
return (
|
||||
<div className="myClassName">MyComponent Component</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
I am developing this package on my free time, so any support, whether code, issues, or just stars is
|
||||
very helpful to sustaining its life. If you are feeling incredibly generous and would like to donate
|
||||
just a small amount to help sustain this project, I would be very very thankful!
|
||||
I am developing this package on my free time, so any support, whether code, issues, or just stars
|
||||
is very helpful to sustaining its life. If you would like to donate a bit to help keep the project
|
||||
alive, I would be very thankful!
|
||||
|
||||
<a href='https://ko-fi.com/casraf' target='_blank'>
|
||||
<img
|
||||
height='36'
|
||||
<img height='36' style='border:0px;height:36px;'
|
||||
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,
|
||||
@@ -337,8 +369,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 `pnpm install`
|
||||
3. Run `pnpm dev` to start file watch mode
|
||||
2. Run `yarn install`
|
||||
3. Run `yarn 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)
|
||||
@@ -346,9 +378,13 @@ If you are a developer and want to contribute code, here are some starting tips:
|
||||
|
||||
Some tips on getting around the code:
|
||||
|
||||
- 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
|
||||
- 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
|
||||
- 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. Either use `yarn dev` to watch
|
||||
> for changes and build, or `yarn build` before running this, or use `yarn build-cmd` instead,
|
||||
> which triggers a build right before running the command with the rest of the given arguments.
|
||||
|
||||
3
babel.config.js
Normal file
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
plugins: ["@babel/plugin-proposal-nullish-coalescing-operator"],
|
||||
}
|
||||
22
docs/.gitignore
vendored
@@ -1,22 +0,0 @@
|
||||
# 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
|
||||
@@ -1,44 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,3 +0,0 @@
|
||||
module.exports = {
|
||||
presets: [require.resolve("@docusaurus/core/lib/babel/preset")],
|
||||
}
|
||||
@@ -1,134 +0,0 @@
|
||||
---
|
||||
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).
|
||||
@@ -1,205 +0,0 @@
|
||||
---
|
||||
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()
|
||||
```
|
||||
@@ -1,162 +0,0 @@
|
||||
---
|
||||
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
|
||||
```
|
||||
@@ -1,208 +0,0 @@
|
||||
---
|
||||
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()
|
||||
```
|
||||
@@ -1,186 +0,0 @@
|
||||
---
|
||||
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
|
||||
```
|
||||
@@ -1,64 +0,0 @@
|
||||
---
|
||||
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 +0,0 @@
|
||||
label: "Usage"
|
||||
@@ -1,18 +0,0 @@
|
||||
---
|
||||
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
|
||||
@@ -1,191 +0,0 @@
|
||||
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
|
||||
@@ -1,51 +0,0 @@
|
||||
{
|
||||
"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
@@ -1,28 +0,0 @@
|
||||
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
|
||||
@@ -1,72 +0,0 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
.features {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 2rem 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.featureSvg {
|
||||
height: 200px;
|
||||
width: 200px;
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
title: Markdown page example
|
||||
---
|
||||
|
||||
# Markdown page example
|
||||
|
||||
You don't need React to write simple standalone pages.
|
||||
0
docs/static/.nojekyll
vendored
BIN
docs/static/img/docusaurus-social-card.jpg
vendored
|
Before Width: | Height: | Size: 54 KiB |
BIN
docs/static/img/docusaurus.png
vendored
|
Before Width: | Height: | Size: 5.0 KiB |
BIN
docs/static/img/favicon.ico
vendored
|
Before Width: | Height: | Size: 3.5 KiB |
BIN
docs/static/img/favicon.png
vendored
|
Before Width: | Height: | Size: 14 KiB |
20
docs/static/img/favicon.svg
vendored
|
Before Width: | Height: | Size: 265 KiB |
BIN
docs/static/img/intro.gif
vendored
|
Before Width: | Height: | Size: 385 KiB |
BIN
docs/static/img/logo-lg.png
vendored
|
Before Width: | Height: | Size: 22 KiB |
20
docs/static/img/logo-lg.svg
vendored
|
Before Width: | Height: | Size: 281 KiB |
BIN
docs/static/img/logo.png
vendored
|
Before Width: | Height: | Size: 24 KiB |
20
docs/static/img/logo.svg
vendored
|
Before Width: | Height: | Size: 432 KiB |
171
docs/static/img/undraw_docusaurus_mountain.svg
vendored
@@ -1,171 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 31 KiB |
170
docs/static/img/undraw_docusaurus_react.svg
vendored
@@ -1,170 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 35 KiB |
40
docs/static/img/undraw_docusaurus_tree.svg
vendored
@@ -1,40 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 12 KiB |
@@ -1,7 +0,0 @@
|
||||
{
|
||||
// This file is not used in compilation. It is here just for a nice editor experience.
|
||||
"extends": "@docusaurus/tsconfig",
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
},
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
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,5 +0,0 @@
|
||||
# {{ name }} Readme
|
||||
|
||||
TO DO:
|
||||
|
||||
- [ ] ...
|
||||
|
Before Width: | Height: | Size: 3.9 KiB |
17
examples/test-input/Component/{{Name}}.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
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}}
|
||||
@@ -1,17 +0,0 @@
|
||||
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}}
|
||||
196
jest.config.ts
Normal file
@@ -0,0 +1,196 @@
|
||||
/*
|
||||
* 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 and instances between 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,
|
||||
|
||||
// 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",
|
||||
// "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: {},
|
||||
|
||||
// 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 between 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 between 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: "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",
|
||||
|
||||
// This option sets the URL for the jsdom environment. It is reflected in properties such as location.href
|
||||
// testURL: "http://localhost",
|
||||
|
||||
// Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout"
|
||||
// timers: "real",
|
||||
|
||||
// A map from regular expressions to paths to transformers
|
||||
// transform: {},
|
||||
|
||||
// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
|
||||
// transformIgnorePatterns: [
|
||||
// "/node_modules/",
|
||||
// "\\.pnp\\.[^\\/]+$"
|
||||
// ],
|
||||
|
||||
// An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
|
||||
// unmockedModulePathPatterns: undefined,
|
||||
|
||||
// Indicates whether each individual test should be reported during the run
|
||||
// verbose: undefined,
|
||||
|
||||
// An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
|
||||
// watchPathIgnorePatterns: [],
|
||||
|
||||
// Whether to use watchman for file crawling
|
||||
// watchman: true,
|
||||
|
||||
// extensionsToTreatAsEsm: [".ts"],
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"ignore": ["**/*.test.ts", "**/*.spec.ts", ".git", "node_modules"],
|
||||
"watch": ["src"],
|
||||
"exec": "node -r tsconfig-paths/register -r ts-node/register ./src/index.ts",
|
||||
"ext": "ts, js"
|
||||
}
|
||||
91
package.json
@@ -1,19 +1,12 @@
|
||||
{
|
||||
"name": "simple-scaffold",
|
||||
"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": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/chenasraf/simple-scaffold.git"
|
||||
},
|
||||
"author": "Chen Asraf <contact@casraf.dev> (https://casraf.dev)",
|
||||
"version": "1.1.0-alpha.6",
|
||||
"description": "Create files based on templates",
|
||||
"repository": "https://github.com/chenasraf/simple-scaffold.git",
|
||||
"author": "Chen Asraf <inbox@casraf.com>",
|
||||
"license": "MIT",
|
||||
"main": "index.js",
|
||||
"bin": {
|
||||
"simple-scaffold": "cmd.js"
|
||||
},
|
||||
"packageManager": "pnpm@9.9.0",
|
||||
"bin": "cmd.js",
|
||||
"keywords": [
|
||||
"javascript",
|
||||
"cli",
|
||||
@@ -26,57 +19,35 @@
|
||||
"scaffolding"
|
||||
],
|
||||
"scripts": {
|
||||
"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",
|
||||
"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"
|
||||
]
|
||||
"clean": "rimraf dist/",
|
||||
"build": "yarn clean && tsc && chmod -R +x ./dist && cp ./package.json ./README.md ./dist/",
|
||||
"dev": "tsc --watch",
|
||||
"start": "node dist/scaffold.js",
|
||||
"test": "jest --verbose",
|
||||
"cmd": "node --trace-warnings dist/cmd.js",
|
||||
"build-test": "yarn build && yarn test",
|
||||
"build-cmd": "yarn build && yarn cmd"
|
||||
},
|
||||
"dependencies": {
|
||||
"@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"
|
||||
"chalk": "^4.1.2",
|
||||
"date-fns": "^2.28.0",
|
||||
"glob": "^7.1.3",
|
||||
"handlebars": "^4.7.7",
|
||||
"lodash": "^4.17.21",
|
||||
"massarg": "^1.0.5",
|
||||
"util.promisify": "^1.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@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"
|
||||
"@types/glob": "^7.1.1",
|
||||
"@types/jest": "^26.0.24",
|
||||
"@types/lodash": "^4.14.171",
|
||||
"@types/mock-fs": "^4.13.1",
|
||||
"@types/node": "^14.14.22",
|
||||
"jest": "^27.0.6",
|
||||
"mock-fs": "^5.0.0",
|
||||
"rimraf": "^3.0.2",
|
||||
"ts-jest": "^27.0.3",
|
||||
"ts-node": "^10.1.0",
|
||||
"typescript": "^4.3.5"
|
||||
}
|
||||
}
|
||||
|
||||
2216
pnpm-lock.yaml
generated
@@ -1,22 +0,0 @@
|
||||
// @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: "---",
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
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
|
||||
}
|
||||
431
src/cmd.ts
@@ -1,349 +1,102 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import path from "node:path"
|
||||
import fs from "node:fs/promises"
|
||||
import { massarg } from "massarg"
|
||||
import { ListCommandCliOptions, LogLevel, ScaffoldCmdConfig, ScaffoldConfigMap } from "./types"
|
||||
import massarg from "massarg"
|
||||
import chalk from "chalk"
|
||||
import { LogLevel, ScaffoldCmdConfig } 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 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"],
|
||||
export function parseCliArgs(args = process.argv.slice(2)) {
|
||||
return (
|
||||
massarg<ScaffoldCmdConfig & { help: boolean; extras: string[] }>()
|
||||
.main(Scaffold)
|
||||
.option({
|
||||
name: "name",
|
||||
aliases: ["n"],
|
||||
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 })
|
||||
}
|
||||
},
|
||||
"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 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: [],
|
||||
.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: true,
|
||||
})
|
||||
.option({
|
||||
name: "templates",
|
||||
aliases: ["t"],
|
||||
array: true,
|
||||
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"))
|
||||
}
|
||||
},
|
||||
"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: true,
|
||||
})
|
||||
.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)
|
||||
.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: "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: "",
|
||||
// })
|
||||
.help({
|
||||
binName: "simple-scaffold",
|
||||
useGlobalColumns: true,
|
||||
usageExample: "[options]",
|
||||
header: "Create structured files based on templates.",
|
||||
footer: [
|
||||
`Copyright © Chen Asraf 2021`,
|
||||
`NPM: ${chalk.underline`https://npmjs.com/package/simple-scaffold`}`,
|
||||
`GitHub: ${chalk.underline`https://github.com/chenasraf/simple-scaffold`}`,
|
||||
].join("\n"),
|
||||
})
|
||||
.parse(args)
|
||||
)
|
||||
}
|
||||
|
||||
parseCliArgs()
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
/** 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>,
|
||||
),
|
||||
)
|
||||
206
src/config.ts
@@ -1,206 +0,0 @@
|
||||
import path from "node:path"
|
||||
import {
|
||||
ConfigLoadConfig,
|
||||
LogConfig,
|
||||
LogLevel,
|
||||
RemoteConfigLoadConfig,
|
||||
ScaffoldCmdConfig,
|
||||
ScaffoldConfig,
|
||||
ScaffoldConfigFile,
|
||||
ScaffoldConfigMap,
|
||||
} from "./types"
|
||||
import { log } from "./logger"
|
||||
import { resolve, wrapNoopResolver } from "./utils"
|
||||
import { getGitConfig } from "./git"
|
||||
import { isDir, pathExists } from "./fs-utils"
|
||||
import { wrapBeforeWrite } from "./before-write"
|
||||
|
||||
// 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(/:?=/)
|
||||
if (value.includes(":=") && !val.includes(":=")) {
|
||||
return { ...data, [key]: JSON.parse(val) }
|
||||
}
|
||||
return { ...data, [key]: isWrappedWithQuotes(val) ? val.substring(1, val.length - 1) : val }
|
||||
}
|
||||
|
||||
function isWrappedWithQuotes(string: string): boolean {
|
||||
return (
|
||||
(string.startsWith('"') && string.endsWith('"')) ||
|
||||
(string.startsWith("'") && string.endsWith("'"))
|
||||
)
|
||||
}
|
||||
|
||||
/** 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)
|
||||
}
|
||||
|
||||
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 ${config.config}`)
|
||||
}
|
||||
|
||||
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: {
|
||||
...imported.data,
|
||||
...config.data,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
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")) {
|
||||
gitUrl.pathname += ".git"
|
||||
}
|
||||
return gitUrl.toString()
|
||||
}
|
||||
|
||||
/** Loads a scaffold config from a local file or directory. @internal */
|
||||
export async function getLocalConfig(
|
||||
config: ConfigLoadConfig & Partial<LogConfig>,
|
||||
): Promise<ScaffoldConfigFile> {
|
||||
const { config: configFile, ...logConfig } = config as Required<typeof config>
|
||||
|
||||
const absolutePath = path.resolve(process.cwd(), configFile)
|
||||
|
||||
const _isDir = await isDir(absolutePath)
|
||||
|
||||
if (_isDir) {
|
||||
log(logConfig, LogLevel.debug, `Resolving config file from directory ${absolutePath}`)
|
||||
const file = await findConfigFile(absolutePath)
|
||||
const exists = await pathExists(file)
|
||||
if (!exists) {
|
||||
throw new Error(`Could not find config file in directory ${absolutePath}`)
|
||||
}
|
||||
log(logConfig, LogLevel.debug, `Loading config from: ${path.resolve(absolutePath, file)}`)
|
||||
return wrapNoopResolver(import(path.resolve(absolutePath, file)))
|
||||
}
|
||||
|
||||
log(logConfig, LogLevel.debug, `Loading config from: ${absolutePath}`)
|
||||
return wrapNoopResolver(import(absolutePath))
|
||||
}
|
||||
|
||||
/** Loads a scaffold config from a remote git repository. @internal */
|
||||
export async function getRemoteConfig(
|
||||
config: RemoteConfigLoadConfig & Partial<LogConfig>,
|
||||
): Promise<ScaffoldConfigFile> {
|
||||
const { config: configFile, git, tmpDir, ...logConfig } = config as Required<typeof config>
|
||||
|
||||
log(
|
||||
logConfig,
|
||||
LogLevel.debug,
|
||||
`Loading config from remote ${git}, config file ${configFile || "<auto-detect>"}`,
|
||||
)
|
||||
|
||||
const url = new URL(git!)
|
||||
const isHttp = url.protocol === "http:" || url.protocol === "https:"
|
||||
const isGit = url.protocol === "git:" || (isHttp && url.pathname.endsWith(".git"))
|
||||
|
||||
if (!isGit) {
|
||||
throw new Error(`Unsupported protocol ${url.protocol}`)
|
||||
}
|
||||
|
||||
return getGitConfig(url, configFile, tmpDir, logConfig)
|
||||
}
|
||||
|
||||
/** 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`)
|
||||
}
|
||||
55
src/docs.css
@@ -1,55 +0,0 @@
|
||||
.tsd-typography table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.tsd-typography table td,
|
||||
.tsd-typography table th {
|
||||
vertical-align: top;
|
||||
border: 1px solid var(--color-accent);
|
||||
padding: 6px;
|
||||
}
|
||||
.tsd-typography h1 + pre,
|
||||
.tsd-typography h2 + pre,
|
||||
.tsd-typography h3 + pre,
|
||||
.tsd-typography h4 + pre,
|
||||
.tsd-typography h5 + pre,
|
||||
.tsd-typography h6 + pre,
|
||||
/* */
|
||||
.tsd-typography h1 + table,
|
||||
.tsd-typography h2 + table,
|
||||
.tsd-typography h3 + table,
|
||||
.tsd-typography h4 + table,
|
||||
.tsd-typography h5 + table,
|
||||
.tsd-typography h6 + table {
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
.tsd-typography pre + a + h1,
|
||||
.tsd-typography pre + a + h2,
|
||||
.tsd-typography pre + a + h3,
|
||||
.tsd-typography pre + a + h4,
|
||||
.tsd-typography pre + a + h5,
|
||||
.tsd-typography pre + a + h6,
|
||||
/* */
|
||||
.tsd-typography table + a + h1,
|
||||
.tsd-typography table + a + h2,
|
||||
.tsd-typography table + a + h3,
|
||||
.tsd-typography table + a + h4,
|
||||
.tsd-typography table + a + h5,
|
||||
.tsd-typography table + a + h6 {
|
||||
margin-top: 2em;
|
||||
}
|
||||
|
||||
.tsd-index-accordion[data-key*="Configuration."] ul.tsd-nested-navigation,
|
||||
.tsd-index-accordion[data-key*="Configuration."] .tsd-accordion-summary > svg,
|
||||
.tsd-index-accordion[data-key="Changelog"] ul.tsd-nested-navigation,
|
||||
.tsd-index-accordion[data-key="Changelog"] .tsd-accordion-summary > svg,
|
||||
.tsd-index-accordion[data-key="Configuration"] li:nth-child(n + 6) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tsd-index-accordion[data-key*="Configuration."],
|
||||
.tsd-index-accordion[data-key="Changelog"] {
|
||||
margin-left: 0;
|
||||
}
|
||||
214
src/file.ts
@@ -1,214 +0,0 @@
|
||||
import path from "node:path"
|
||||
import fs from "node:fs/promises"
|
||||
import { FileResponse, FileResponseHandler, LogLevel, ScaffoldConfig } from "./types"
|
||||
import { glob, hasMagic } from "glob"
|
||||
import { log } from "./logger"
|
||||
import { handlebarsParse } from "./parser"
|
||||
import { handleErr } from "./utils"
|
||||
import { createDirIfNotExists, pathExists, isDir } from "./fs-utils"
|
||||
import { removeGlob } from "./path-utils"
|
||||
|
||||
const { readFile, writeFile } = fs
|
||||
|
||||
// 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()),
|
||||
)
|
||||
}
|
||||
|
||||
/** 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
|
||||
}
|
||||
|
||||
/** 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(templates, {
|
||||
dot: true,
|
||||
nodir: true,
|
||||
})
|
||||
).map(path.normalize)
|
||||
}
|
||||
|
||||
/** 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)
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
/** Complete information about a template file's output destination. */
|
||||
export interface OutputFileInfo {
|
||||
inputPath: string
|
||||
outputPathOpt: string
|
||||
outputDir: string
|
||||
outputPath: string
|
||||
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.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,
|
||||
{
|
||||
exists,
|
||||
overwrite,
|
||||
outputPath,
|
||||
inputPath,
|
||||
}: {
|
||||
exists: boolean
|
||||
overwrite: boolean
|
||||
outputPath: string
|
||||
inputPath: string
|
||||
},
|
||||
): Promise<void> {
|
||||
if (!exists || overwrite) {
|
||||
if (exists && overwrite) {
|
||||
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
|
||||
|
||||
if (!config.dryRun) {
|
||||
await writeFile(outputPath, finalOutputContents)
|
||||
} else {
|
||||
log(config, LogLevel.debug, "Dry run — output would be:")
|
||||
log(config, LogLevel.debug, finalOutputContents.toString())
|
||||
}
|
||||
} else if (exists) {
|
||||
log(config, LogLevel.debug, `Skipped ${outputPath} (already exists)`)
|
||||
}
|
||||
}
|
||||
|
||||
/** 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.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<string | null> {
|
||||
try {
|
||||
const { inputPath, outputPathOpt, outputDir, outputPath, exists } = await getTemplateFileInfo(
|
||||
config,
|
||||
{
|
||||
templatePath,
|
||||
basePath,
|
||||
},
|
||||
)
|
||||
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`,
|
||||
)
|
||||
|
||||
await createDirIfNotExists(path.dirname(outputPath), config)
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
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,65 +0,0 @@
|
||||
import path from "node:path"
|
||||
import { log } from "./logger"
|
||||
import { AsyncResolver, LogConfig, LogLevel, ScaffoldCmdConfig, ScaffoldConfigMap } from "./types"
|
||||
import { spawn } from "node:child_process"
|
||||
import { resolve, wrapNoopResolver } from "./utils"
|
||||
import { findConfigFile } from "./config"
|
||||
|
||||
export async function getGitConfig(
|
||||
url: URL,
|
||||
file: string,
|
||||
tmpPath: string,
|
||||
logConfig: LogConfig,
|
||||
): Promise<AsyncResolver<ScaffoldCmdConfig, ScaffoldConfigMap>> {
|
||||
const repoUrl = `${url.protocol}//${url.host}${url.pathname}`
|
||||
|
||||
log(logConfig, LogLevel.debug, `Cloning git repo ${repoUrl}`)
|
||||
|
||||
return new Promise((res, reject) => {
|
||||
log(logConfig, LogLevel.debug, `Cloning git repo to ${tmpPath}`)
|
||||
const clone = spawn("git", ["clone", "--recurse-submodules", "--depth", "1", repoUrl, tmpPath])
|
||||
|
||||
clone.on("error", reject)
|
||||
clone.on("close", async (code) => {
|
||||
if (code === 0) {
|
||||
res(await loadGitConfig({ logConfig, url: repoUrl, file, tmpPath }))
|
||||
return
|
||||
}
|
||||
|
||||
reject(new Error(`Git clone failed with code ${code}`))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export async function loadGitConfig({
|
||||
logConfig,
|
||||
url: repoUrl,
|
||||
file,
|
||||
tmpPath,
|
||||
}: {
|
||||
logConfig: LogConfig
|
||||
url: string
|
||||
file: string
|
||||
tmpPath: string
|
||||
}): Promise<AsyncResolver<ScaffoldCmdConfig, ScaffoldConfigMap>> {
|
||||
log(logConfig, LogLevel.debug, `Loading config from git repo: ${repoUrl}`)
|
||||
const filename = file || (await findConfigFile(tmpPath))
|
||||
const absolutePath = path.resolve(tmpPath, filename)
|
||||
log(logConfig, LogLevel.debug, `Resolving config file: ${absolutePath}`)
|
||||
const loadedConfig = await resolve(
|
||||
async () => (await import(absolutePath)).default as ScaffoldConfigMap,
|
||||
logConfig,
|
||||
)
|
||||
|
||||
log(logConfig, LogLevel.debug, `Loaded config from git`)
|
||||
log(logConfig, LogLevel.debug, `Raw config:`, loadedConfig)
|
||||
const fixedConfig: ScaffoldConfigMap = {}
|
||||
for (const [k, v] of Object.entries(loadedConfig)) {
|
||||
fixedConfig[k] = {
|
||||
...v,
|
||||
templates: v.templates.map((t) => path.resolve(tmpPath, t)),
|
||||
}
|
||||
}
|
||||
return wrapNoopResolver(fixedConfig)
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
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,6 +1,4 @@
|
||||
export * from "./scaffold"
|
||||
export * from "./types"
|
||||
export { validateConfig, assertConfigValid, scaffoldConfigSchema } from "./validate"
|
||||
import Scaffold from "./scaffold"
|
||||
|
||||
export default Scaffold
|
||||
|
||||
111
src/init.ts
@@ -1,111 +0,0 @@
|
||||
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()
|
||||
}
|
||||
135
src/logger.ts
@@ -1,135 +0,0 @@
|
||||
import util from "util"
|
||||
import path from "node:path"
|
||||
import { LogConfig, LogLevel, ScaffoldConfig } from "./types"
|
||||
import { colorize, TermColor } from "./colors"
|
||||
|
||||
/** 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 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
|
||||
? colorFn(i, JSON.stringify(i, undefined, 1), i.stack)
|
||||
: typeof i === "object"
|
||||
? 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: {
|
||||
originalTemplate: string
|
||||
relativePath: string
|
||||
parsedTemplate: string
|
||||
inputFilePath: string
|
||||
nonGlobTemplate: string
|
||||
basePath: string
|
||||
isDirOrGlob: boolean
|
||||
isGlob: boolean
|
||||
},
|
||||
): void {
|
||||
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:", {
|
||||
name: config.name,
|
||||
templates: config.templates,
|
||||
output: config.output,
|
||||
subdir: config.subdir,
|
||||
data: config.data,
|
||||
overwrite: config.overwrite,
|
||||
subdirHelper: config.subdirHelper,
|
||||
helpers: Object.keys(config.helpers ?? {}),
|
||||
logLevel: config.logLevel,
|
||||
dryRun: config.dryRun,
|
||||
beforeWrite: config.beforeWrite,
|
||||
} as Record<keyof ScaffoldConfig, unknown>)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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}`))
|
||||
}
|
||||
}
|
||||
140
src/parser.ts
@@ -1,140 +0,0 @@
|
||||
import path from "node:path"
|
||||
import { DefaultHelpers, Helper, LogLevel, ScaffoldConfig } from "./types"
|
||||
import Handlebars from "handlebars"
|
||||
import { add, format, parseISO, type Duration } from "date-fns"
|
||||
import { log } from "./logger"
|
||||
|
||||
const dateFns = { add, format, parseISO }
|
||||
|
||||
export const defaultHelpers: Record<DefaultHelpers, Helper> = {
|
||||
camelCase,
|
||||
snakeCase,
|
||||
startCase,
|
||||
kebabCase,
|
||||
hyphenCase: kebabCase,
|
||||
pascalCase,
|
||||
lowerCase: (text) => text.toLowerCase(),
|
||||
upperCase: (text) => text.toUpperCase(),
|
||||
now: nowHelper,
|
||||
date: dateHelper,
|
||||
}
|
||||
|
||||
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 {
|
||||
if (durationType && durationDifference !== undefined) {
|
||||
return dateFns.format(dateFns.add(date, { [durationType]: durationDifference }), formatString)
|
||||
}
|
||||
return dateFns.format(date, formatString)
|
||||
}
|
||||
|
||||
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 {
|
||||
return _dateHelper(new Date(), formatString, durationDifference!, durationType!)
|
||||
}
|
||||
|
||||
export function dateHelper(date: string, formatString: string): string
|
||||
export function dateHelper(
|
||||
date: string,
|
||||
formatString: string,
|
||||
durationDifference: number,
|
||||
durationType: keyof Duration,
|
||||
): string
|
||||
export function dateHelper(
|
||||
date: string,
|
||||
formatString: string,
|
||||
durationDifference?: number,
|
||||
durationType?: keyof Duration,
|
||||
): string {
|
||||
return _dateHelper(dateFns.parseISO(date), formatString, durationDifference!, durationType!)
|
||||
}
|
||||
|
||||
// splits by either non-alphanumeric character or capital letter boundaries
|
||||
function toWordParts(string: string): string[] {
|
||||
// First split on non-alphanumeric characters
|
||||
return string
|
||||
.split(/[^a-zA-Z0-9]/)
|
||||
.flatMap((segment) =>
|
||||
// Then split camelCase/PascalCase boundaries, handling consecutive uppercase (e.g. "HTMLParser" -> "HTML", "Parser")
|
||||
segment.split(/(?<=[a-z0-9])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])/),
|
||||
)
|
||||
.filter((s) => s.length > 0)
|
||||
}
|
||||
|
||||
function camelCase(s: string): string {
|
||||
return toWordParts(s).reduce((acc, part, i) => {
|
||||
if (i === 0) {
|
||||
return part.toLowerCase()
|
||||
}
|
||||
return acc + part[0].toUpperCase() + part.slice(1).toLowerCase()
|
||||
}, "")
|
||||
}
|
||||
|
||||
function snakeCase(s: string): string {
|
||||
return toWordParts(s).join("_").toLowerCase()
|
||||
}
|
||||
|
||||
function kebabCase(s: string): string {
|
||||
return toWordParts(s).join("-").toLowerCase()
|
||||
}
|
||||
|
||||
function startCase(s: string): string {
|
||||
return toWordParts(s)
|
||||
.map((part) => part[0].toUpperCase() + part.slice(1).toLowerCase())
|
||||
.join(" ")
|
||||
}
|
||||
|
||||
function pascalCase(s: string): string {
|
||||
return startCase(s).replace(/\s+/g, "")
|
||||
}
|
||||
|
||||
export function registerHelpers(config: ScaffoldConfig): void {
|
||||
const _helpers = { ...defaultHelpers, ...config.helpers }
|
||||
for (const helperName in _helpers) {
|
||||
log(config, LogLevel.debug, `Registering helper: ${helperName}`)
|
||||
Handlebars.registerHelper(helperName, _helpers[helperName as keyof typeof _helpers])
|
||||
}
|
||||
}
|
||||
|
||||
export function handlebarsParse(
|
||||
config: ScaffoldConfig,
|
||||
templateBuffer: Buffer | string,
|
||||
{ asPath = false }: { asPath?: boolean } = {},
|
||||
): Buffer {
|
||||
const { data } = config
|
||||
try {
|
||||
let str = templateBuffer.toString()
|
||||
if (asPath) {
|
||||
str = str.replace(/\\/g, "/")
|
||||
}
|
||||
const parser = Handlebars.compile(str, { noEscape: true })
|
||||
let outputContents = parser(data)
|
||||
if (asPath && path.sep !== "/") {
|
||||
outputContents = outputContents.replace(/\//g, "\\")
|
||||
}
|
||||
return Buffer.from(outputContents)
|
||||
} catch (e) {
|
||||
log(config, LogLevel.debug, e)
|
||||
log(config, LogLevel.debug, "Couldn't parse file with handlebars, returning original content")
|
||||
return Buffer.from(templateBuffer)
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
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
@@ -1,253 +0,0 @@
|
||||
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
|
||||
}
|
||||
295
src/scaffold.ts
@@ -1,23 +1,24 @@
|
||||
/**
|
||||
* @module
|
||||
* Simple Scaffold
|
||||
*
|
||||
* See [readme](README.md)
|
||||
*/
|
||||
import path from "node:path"
|
||||
import os from "node:os"
|
||||
import { exec } from "node:child_process"
|
||||
import path from "path"
|
||||
|
||||
import { handleErr, resolve } from "./utils"
|
||||
import { isDir, getTemplateGlobInfo, getFileList, handleTemplateFile, GlobInfo } from "./file"
|
||||
import { removeGlob, makeRelativePath, getBasePath } from "./path-utils"
|
||||
import { LogLevel, MinimalConfig, Resolver, ScaffoldCmdConfig, ScaffoldConfig } from "./types"
|
||||
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"
|
||||
import {
|
||||
createDirIfNotExists,
|
||||
getOptionValueForFile,
|
||||
handleErr,
|
||||
log,
|
||||
pascalCase,
|
||||
isDir,
|
||||
removeGlob,
|
||||
makeRelativePath,
|
||||
registerHelpers,
|
||||
getTemplateGlobInfo,
|
||||
getFileList,
|
||||
getBasePath,
|
||||
copyFileTransformed,
|
||||
getTemplateFileInfo,
|
||||
logInitStep,
|
||||
logInputFile,
|
||||
} from "./utils"
|
||||
import { LogLevel, ScaffoldConfig } from "./types"
|
||||
|
||||
/**
|
||||
* Create a scaffold using given `options`.
|
||||
@@ -30,194 +31,104 @@ import { assertConfigValid } from "./validate"
|
||||
* The contents and names will be replaced with the transformed values so you can use your original structure as a
|
||||
* boilerplate for other projects, components, modules, or even single files.
|
||||
*
|
||||
* The files will maintain their structure, starting from the directory containing the template (or the template itself
|
||||
* if it is already a directory), and will output from that directory into the directory defined by `config.output`.
|
||||
*
|
||||
* #### Helpers
|
||||
* Helpers are functions you can use to transform your `{{ var }}` contents into other values without having to
|
||||
* pre-define the data and use a duplicated key.
|
||||
* pre-define the data and use a duplicated key. Common cases are transforming name-case format
|
||||
* (e.g. `MyName` → `my_name`), so these have been provided as defaults:
|
||||
*
|
||||
* | Helper name | Example code | Example output |
|
||||
* | ----------- | ----------------------- | -------------- |
|
||||
* | 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 }}` | MYNAME |
|
||||
* | lowerCase | `{{ lowerCase name }}` | myname |
|
||||
*
|
||||
* Any functions you provide in `helpers` option will also be available to you to make custom formatting as you see fit
|
||||
* (for example, formatting a date)
|
||||
*
|
||||
* For available default values, see {@link DefaultHelpers}.
|
||||
*
|
||||
* @param {ScaffoldConfig} config The main configuration object
|
||||
* @return {Promise<void>} A promise that resolves when the scaffold is complete
|
||||
*
|
||||
* @see {@link DefaultHelpers}
|
||||
* @see {@link CaseHelpers}
|
||||
* @see {@link DateHelpers}
|
||||
*
|
||||
* @category Main
|
||||
*/
|
||||
export async function Scaffold(config: ScaffoldConfig): Promise<void> {
|
||||
config.output ??= process.cwd()
|
||||
export async function Scaffold({ ...options }: ScaffoldConfig): Promise<void> {
|
||||
options.output ??= process.cwd()
|
||||
|
||||
await assertConfigValid(config)
|
||||
config = await resolveInputs(config)
|
||||
registerHelpers(config)
|
||||
|
||||
const startTime = performance.now()
|
||||
const writtenFiles: string[] = []
|
||||
registerHelpers(options)
|
||||
try {
|
||||
config.data = { name: config.name, ...config.data }
|
||||
logInitStep(config)
|
||||
|
||||
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)
|
||||
options.data = { name: options.name, Name: pascalCase(options.name), ...options.data }
|
||||
logInitStep(options)
|
||||
for (let _template of options.templates) {
|
||||
try {
|
||||
const { nonGlobTemplate, origTemplate, isDirOrGlob, isGlob, template } = await getTemplateGlobInfo(
|
||||
options,
|
||||
_template
|
||||
)
|
||||
const files = await getFileList(options, 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(options, {
|
||||
origTemplate,
|
||||
relPath,
|
||||
template,
|
||||
inputFilePath,
|
||||
nonGlobTemplate,
|
||||
basePath,
|
||||
isDirOrGlob,
|
||||
isGlob,
|
||||
})
|
||||
await handleTemplateFile(options, options.data, {
|
||||
templatePath: inputFilePath,
|
||||
basePath,
|
||||
})
|
||||
}
|
||||
} catch (e: any) {
|
||||
handleErr(e)
|
||||
}
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
log(config, LogLevel.error, e)
|
||||
} catch (e: any) {
|
||||
log(options, 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) {
|
||||
async function handleTemplateFile(
|
||||
options: ScaffoldConfig,
|
||||
data: Record<string, string>,
|
||||
{ templatePath, basePath }: { templatePath: string; basePath: string }
|
||||
): Promise<void> {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
templates.push(await getTemplateGlobInfo(config, includedTemplate))
|
||||
} catch (e: unknown) {
|
||||
handleErr(e as NodeJS.ErrnoException)
|
||||
const { inputPath, outputPathOpt, outputDir, outputPath, exists } = await getTemplateFileInfo(options, data, {
|
||||
templatePath,
|
||||
basePath,
|
||||
})
|
||||
const overwrite = getOptionValueForFile(options, inputPath, options.overwrite ?? false)
|
||||
|
||||
log(
|
||||
options,
|
||||
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), options)
|
||||
|
||||
log(options, LogLevel.Info, `Writing to ${outputPath}`)
|
||||
await copyFileTransformed(options, { exists, overwrite, outputPath, inputPath })
|
||||
resolve()
|
||||
} catch (e: any) {
|
||||
handleErr(e)
|
||||
reject(e)
|
||||
}
|
||||
}
|
||||
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)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a scaffold based on a config file or URL.
|
||||
*
|
||||
* @param {string} pathOrUrl The path or URL to the config file
|
||||
* @param {Record<string, string>} config Information needed before loading the config
|
||||
* @param {Partial<Omit<ScaffoldConfig, 'name'>>} overrides Any overrides to the loaded config
|
||||
*
|
||||
* @see {@link Scaffold}
|
||||
* @category Main
|
||||
* @return {Promise<void>} A promise that resolves when the scaffold is complete
|
||||
*/
|
||||
Scaffold.fromConfig = async function (
|
||||
pathOrUrl: string,
|
||||
config: MinimalConfig,
|
||||
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(),
|
||||
logLevel: LogLevel.info,
|
||||
overwrite: false,
|
||||
templates: [],
|
||||
subdir: false,
|
||||
quiet: false,
|
||||
config: pathOrUrl,
|
||||
version: false,
|
||||
tmpDir: tmpPath,
|
||||
...config,
|
||||
}
|
||||
const _overrides = resolve(overrides, _cmdConfig)
|
||||
const _config = await parseConfigFile(_cmdConfig)
|
||||
return Scaffold({ ..._config, ..._overrides })
|
||||
}
|
||||
|
||||
export default Scaffold
|
||||
|
||||
524
src/types.ts
@@ -1,16 +1,33 @@
|
||||
import { HelperDelegate } from "handlebars/runtime"
|
||||
|
||||
/**
|
||||
* The config object for defining a scaffolding group.
|
||||
*
|
||||
* @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}
|
||||
*
|
||||
* @category Config
|
||||
*/
|
||||
export enum LogLevel {
|
||||
None = 0,
|
||||
Debug = 1,
|
||||
Info = 2,
|
||||
Warning = 3,
|
||||
Error = 4,
|
||||
}
|
||||
|
||||
export type FileResponseFn<T> = (fullPath: string, basedir: string, basename: string) => T
|
||||
|
||||
export type FileResponse<T> = T | FileResponseFn<T>
|
||||
|
||||
export type DefaultHelperKeys =
|
||||
| "camelCase"
|
||||
| "date"
|
||||
| "hyphenCase"
|
||||
| "kebabCase"
|
||||
| "lowerCase"
|
||||
| "now"
|
||||
| "pascalCase"
|
||||
| "snakeCase"
|
||||
| "startCase"
|
||||
| "upperCase"
|
||||
|
||||
export type HelperKeys<T> = DefaultHelperKeys | T
|
||||
|
||||
export type Helper = HelperDelegate
|
||||
|
||||
export interface ScaffoldConfig {
|
||||
/**
|
||||
* Name to be passed to the generated files. `{{name}}` and `{{Name}}` inside contents and file names will be replaced
|
||||
@@ -20,76 +37,46 @@ 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
|
||||
* or a glob pattern for multiple file matching easily. (default: current working directory)
|
||||
*/
|
||||
templates: string[]
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @see {@link FileResponse}
|
||||
* @see {@link FileResponseHandler}
|
||||
*/
|
||||
/** Path to output to. If `createSubFolder` is `true`, the subfolder will be created inside this path. */
|
||||
output: FileResponse<string>
|
||||
|
||||
/**
|
||||
* Whether to create subdir with the input name.
|
||||
*
|
||||
* When `true`, you may also use {@link subdirHelper} to determine a pre-process helper on
|
||||
* the directory name.
|
||||
*
|
||||
* @default `false`
|
||||
* Create subfolder with the input name (default: `false`)
|
||||
*/
|
||||
subdir?: boolean
|
||||
createSubFolder?: 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, unknown>
|
||||
data?: Record<string, string>
|
||||
|
||||
/**
|
||||
* Enable to override output files, even if they already exist.
|
||||
* Enable to override output files, even if they already exist. (default: `false`)
|
||||
*
|
||||
* You may supply a function to this option, which can take the arguments `(fullPath, baseDir, baseName)` and returns
|
||||
* a boolean for each file.
|
||||
*
|
||||
* May also be a {@link FileResponseHandler} which returns a boolean value per file.
|
||||
*
|
||||
* @see {@link FileResponse}
|
||||
* @see {@link FileResponseHandler}
|
||||
*
|
||||
* @default `false`
|
||||
* a string, to return a dynamic path for each file.
|
||||
*/
|
||||
overwrite?: FileResponse<boolean>
|
||||
|
||||
/** Suppress output logs (Same as `verbose: 0` or `verbose: LogLevel.None`) */
|
||||
quiet?: boolean
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @see {@link LogLevel}
|
||||
*
|
||||
* @default `2 (info)`
|
||||
* of the same level or higher. (default: `2 (info)`)
|
||||
* @see LogLevel
|
||||
*/
|
||||
logLevel?: LogLevel
|
||||
verbose?: 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.
|
||||
*
|
||||
* @default `false`
|
||||
* actual file contents or create directories. (default: `false`)
|
||||
*/
|
||||
dryRun?: boolean
|
||||
|
||||
@@ -97,7 +84,7 @@ export interface ScaffoldConfig {
|
||||
* Additional helpers to add to the template parser. Provide an object whose keys are the name of the function to add,
|
||||
* and the value is the helper function itself. The signature of helpers is as follows:
|
||||
* ```typescript
|
||||
* (text: string, ...args: any[]) => string
|
||||
* (text: string) => string
|
||||
* ```
|
||||
*
|
||||
* A full example might be:
|
||||
@@ -106,45 +93,30 @@ export interface ScaffoldConfig {
|
||||
* Scaffold({
|
||||
* //...
|
||||
* helpers: {
|
||||
* upperKebabCase: (text) => kebabCase(text).toUpperCase()
|
||||
* upperCamelCase: (text) => camelCase(text).toUpperCase()
|
||||
* }
|
||||
* })
|
||||
* ```
|
||||
*
|
||||
* Which will allow:
|
||||
*
|
||||
* ```
|
||||
* {{ upperKebabCase "my value" }}
|
||||
* ```
|
||||
*
|
||||
* To transform to:
|
||||
*
|
||||
* ```
|
||||
* MY-VALUE
|
||||
* ```
|
||||
*
|
||||
* See {@link DefaultHelpers} for a list of all the built-in available helpers.
|
||||
*
|
||||
* Simple Scaffold uses Handlebars.js, so all the syntax from there is supported. See
|
||||
* [their docs](https://handlebarsjs.com/guide/#custom-helpers) for more information.
|
||||
*
|
||||
* @see {@link DefaultHelpers}
|
||||
* @see {@link CaseHelpers}
|
||||
* @see {@link DateHelpers}
|
||||
* @see {@link https://chenasraf.github.io/simple-scaffold/docs/usage/templates| Templates}
|
||||
* */
|
||||
* Here are the built-in helpers available for use:
|
||||
* | Helper name | Example code | Example output |
|
||||
* | ----------- | ----------------------- | -------------- |
|
||||
* | 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 }}` | MYNAME |
|
||||
* | lowerCase | `{{ lowerCase name }}` | myname |
|
||||
*/
|
||||
helpers?: Record<string, Helper>
|
||||
|
||||
/**
|
||||
* 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 subdir}
|
||||
* @see {@link CaseHelpers}
|
||||
* @see {@link DefaultHelpers}
|
||||
* Default transformer to apply to subfolder name when using `createSubFolder: true`. Can be one of the default
|
||||
* helpers, or a custom one you provide to `helpers`. Defaults to `undefined`, which means no transformation is done.
|
||||
*/
|
||||
subdirHelper?: DefaultHelpers | string
|
||||
subFolderNameHelper?: DefaultHelperKeys | string
|
||||
|
||||
/**
|
||||
* This callback runs right before content is being written to the disk. If you supply this function, you may return
|
||||
@@ -157,389 +129,23 @@ export interface ScaffoldConfig {
|
||||
* @param rawContent The original template before token replacement
|
||||
* @param outputPath The final output path of the processed file
|
||||
*
|
||||
* @returns {Promise<String | Buffer | undefined> | String | Buffer | undefined} The final output of the file
|
||||
* contents-only, after further modifications - or `undefined` to use the original content (i.e. `content.toString()`)
|
||||
* @returns `String | Buffer | undefined` The final output of the file contents-only, after further modifications -
|
||||
* or `undefined` to use the original content (i.e. `content.toString()`)
|
||||
*/
|
||||
beforeWrite?(
|
||||
content: Buffer,
|
||||
rawContent: Buffer,
|
||||
outputPath: string,
|
||||
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 `subdirHelper`.
|
||||
*
|
||||
* | 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 |
|
||||
*
|
||||
* @see {@link DefaultHelpers}
|
||||
* @see {@link DateHelpers}
|
||||
* @see {@link ScaffoldConfig}
|
||||
* @see {@link ScaffoldConfig.subdirHelper}
|
||||
*
|
||||
* @category Helpers
|
||||
*/
|
||||
export type CaseHelpers =
|
||||
| "camelCase"
|
||||
| "hyphenCase"
|
||||
| "kebabCase"
|
||||
| "lowerCase"
|
||||
| "pascalCase"
|
||||
| "snakeCase"
|
||||
| "startCase"
|
||||
| "upperCase"
|
||||
|
||||
/**
|
||||
* The names of the available helper functions that relate to dates.
|
||||
*
|
||||
* | 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.
|
||||
*
|
||||
* @see {@link DefaultHelpers}
|
||||
* @see {@link CaseHelpers}
|
||||
* @see {@link ScaffoldConfig}
|
||||
*
|
||||
* @category Helpers
|
||||
*/
|
||||
export type DateHelpers = "date" | "now"
|
||||
|
||||
/**
|
||||
* The names of all the available helper functions in templates.
|
||||
* Simple-Scaffold provides some built-in text transformation filters usable by Handlebars.js.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* @see {@link CaseHelpers}
|
||||
* @see {@link DateHelpers}
|
||||
* @see {@link ScaffoldConfig}
|
||||
*
|
||||
* @category Helpers
|
||||
*/
|
||||
export type DefaultHelpers = CaseHelpers | DateHelpers
|
||||
|
||||
/**
|
||||
* Helper function, see https://handlebarsjs.com/guide/#custom-helpers
|
||||
*
|
||||
* @category Helpers
|
||||
*/
|
||||
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.
|
||||
*
|
||||
* 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 (const)
|
||||
*/
|
||||
|
||||
export const LogLevel = {
|
||||
/** Silent output */
|
||||
none: "none",
|
||||
/** Debugging information. Very verbose and only recommended for troubleshooting. */
|
||||
debug: "debug",
|
||||
/**
|
||||
* The regular level of logging. Major actions are logged to show the scaffold progress.
|
||||
*
|
||||
* @default
|
||||
*/
|
||||
info: "info",
|
||||
/** Warnings such as when file fails to replace token values properly in template. */
|
||||
warning: "warning",
|
||||
/** Errors, such as missing files, bad replacement token syntax, or un-writable directories. */
|
||||
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`
|
||||
*
|
||||
* @template T The return type for the function
|
||||
* @param {string} fullPath The full path of the current file
|
||||
* @param {string} basedir The directory containing the current file
|
||||
* @param {string} basename The name of the file
|
||||
*
|
||||
* @returns {T} A return value
|
||||
*
|
||||
* @category Config
|
||||
*/
|
||||
export type FileResponseHandler<T> = (fullPath: string, basedir: string, basename: string) => T
|
||||
|
||||
/**
|
||||
* Represents a response for file path information.
|
||||
* Can either be:
|
||||
*
|
||||
* 1. `T` - static value
|
||||
* 2. A function with the following signature which returns `T`:
|
||||
* ```typescript
|
||||
* (fullPath: string, basedir: string, basename: string) => T
|
||||
* ```
|
||||
*
|
||||
* @see {@link FileResponseHandler}
|
||||
*
|
||||
* @category Config
|
||||
* */
|
||||
export type FileResponse<T> = T | FileResponseHandler<T>
|
||||
|
||||
/**
|
||||
* 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. */
|
||||
export interface ScaffoldCmdConfig {
|
||||
name: string
|
||||
/** The templates to use for generation */
|
||||
templates: string[]
|
||||
/** The output path to write to */
|
||||
output: string
|
||||
/** 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 */
|
||||
createSubFolder: boolean
|
||||
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
|
||||
/**
|
||||
* 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. */
|
||||
verbose: LogLevel
|
||||
dryRun: boolean
|
||||
/** Config file path to use */
|
||||
config?: string
|
||||
/** The key of the template to use */
|
||||
key?: 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
|
||||
}
|
||||
|
||||
/**
|
||||
* A mapping of scaffold template keys to their configurations.
|
||||
*
|
||||
* Each configuration is a {@link ScaffoldConfig} object.
|
||||
*
|
||||
* The key is the name of the template, and the value is the configuration for that template.
|
||||
*
|
||||
* 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:
|
||||
* - 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)
|
||||
|
||||
/** @internal */
|
||||
export type AsyncResolver<T, R = T> = Resolver<T, Promise<R> | R>
|
||||
|
||||
/** @internal */
|
||||
export type LogConfig = Pick<ScaffoldConfig, "logLevel">
|
||||
|
||||
/** @internal */
|
||||
export type ConfigLoadConfig = LogConfig & Pick<ScaffoldCmdConfig, "config">
|
||||
|
||||
/** @internal */
|
||||
export type RemoteConfigLoadConfig = LogConfig &
|
||||
Pick<ScaffoldCmdConfig, "config" | "git" | "tmpDir">
|
||||
|
||||
/** @internal */
|
||||
export type MinimalConfig = Pick<ScaffoldCmdConfig, "name" | "key">
|
||||
|
||||
/** @internal */
|
||||
export type ListCommandCliOptions = Pick<ScaffoldCmdConfig, "config" | "git" | "logLevel" | "quiet">
|
||||
|
||||
377
src/utils.ts
@@ -1,23 +1,376 @@
|
||||
import { Resolver } from "./types"
|
||||
import path from "path"
|
||||
import { F_OK } from "constants"
|
||||
import { DefaultHelperKeys, FileResponse, FileResponseFn, Helper, LogLevel, ScaffoldConfig } from "./types"
|
||||
import camelCase from "lodash/camelCase"
|
||||
import snakeCase from "lodash/snakeCase"
|
||||
import kebabCase from "lodash/kebabCase"
|
||||
import startCase from "lodash/startCase"
|
||||
import Handlebars from "handlebars"
|
||||
import { promises as fsPromises } from "fs"
|
||||
import chalk from "chalk"
|
||||
const { stat, access, mkdir } = fsPromises
|
||||
import dtAdd from "date-fns/add"
|
||||
import dtFormat from "date-fns/format"
|
||||
import dtParseISO from "date-fns/parseISO"
|
||||
|
||||
// Re-export colors for backward compatibility
|
||||
export { colorize, type TermColor } from "./colors"
|
||||
const dateFns = {
|
||||
add: dtAdd,
|
||||
format: dtFormat,
|
||||
parseISO: dtParseISO,
|
||||
}
|
||||
|
||||
import { glob } from "glob"
|
||||
import { promisify } from "util"
|
||||
const { readFile, writeFile } = fsPromises
|
||||
|
||||
export const defaultHelpers: Record<DefaultHelperKeys, Helper> = {
|
||||
camelCase,
|
||||
snakeCase,
|
||||
startCase,
|
||||
kebabCase,
|
||||
hyphenCase: kebabCase,
|
||||
pascalCase,
|
||||
lowerCase: (text) => text.toLowerCase(),
|
||||
upperCase: (text) => text.toUpperCase(),
|
||||
now: nowHelper,
|
||||
date: dateHelper,
|
||||
}
|
||||
|
||||
export function _dateHelper(date: Date, formatString: string): string
|
||||
export function _dateHelper(
|
||||
date: Date,
|
||||
formatString: string,
|
||||
durationDifference: number,
|
||||
durationType: keyof Duration
|
||||
): string
|
||||
export function _dateHelper(
|
||||
date: Date,
|
||||
formatString: string,
|
||||
durationDifference?: number,
|
||||
durationType?: keyof Duration
|
||||
): string {
|
||||
if (durationType && durationDifference !== undefined) {
|
||||
return dateFns.format(dateFns.add(date, { [durationType]: durationDifference }), formatString)
|
||||
}
|
||||
return dateFns.format(date, formatString)
|
||||
}
|
||||
|
||||
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 {
|
||||
return _dateHelper(new Date(), formatString, durationDifference!, durationType!)
|
||||
}
|
||||
|
||||
export function dateHelper(date: string, formatString: string): string
|
||||
export function dateHelper(
|
||||
date: string,
|
||||
formatString: string,
|
||||
durationDifference: number,
|
||||
durationType: keyof Duration
|
||||
): string
|
||||
|
||||
export function dateHelper(
|
||||
date: string,
|
||||
formatString: string,
|
||||
durationDifference?: number,
|
||||
durationType?: keyof Duration
|
||||
): string {
|
||||
return _dateHelper(dateFns.parseISO(date), formatString, durationDifference!, durationType!)
|
||||
}
|
||||
|
||||
export function registerHelpers(options: ScaffoldConfig): void {
|
||||
const _helpers = { ...defaultHelpers, ...options.helpers }
|
||||
for (const helperName in _helpers) {
|
||||
log(options, LogLevel.Debug, `Registering helper: ${helperName}`)
|
||||
Handlebars.registerHelper(helperName, _helpers[helperName as keyof typeof _helpers])
|
||||
}
|
||||
}
|
||||
|
||||
/** 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)
|
||||
export function log(options: ScaffoldConfig, level: LogLevel, ...obj: any[]): void {
|
||||
if (options.quiet || options.verbose === LogLevel.None || level < (options.verbose ?? 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]
|
||||
logFn(
|
||||
...obj.map((i) =>
|
||||
i instanceof Error
|
||||
? chalkFn(i, JSON.stringify(i, undefined, 1), i.stack)
|
||||
: typeof i === "object"
|
||||
? chalkFn(JSON.stringify(i, undefined, 1))
|
||||
: chalkFn(i)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/** 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
|
||||
export async function createDirIfNotExists(dir: string, options: ScaffoldConfig): Promise<void> {
|
||||
const parentDir = path.dirname(dir)
|
||||
|
||||
if (!(await pathExists(parentDir))) {
|
||||
await createDirIfNotExists(parentDir, options)
|
||||
}
|
||||
|
||||
return (_) => value
|
||||
if (!(await pathExists(dir))) {
|
||||
try {
|
||||
log(options, LogLevel.Debug, `Creating dir ${dir}`)
|
||||
await mkdir(dir)
|
||||
return
|
||||
} catch (e: any) {
|
||||
if (e.code !== "EEXIST") {
|
||||
throw e
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function getOptionValueForFile<T>(
|
||||
options: ScaffoldConfig,
|
||||
filePath: string,
|
||||
fn: FileResponse<T>,
|
||||
defaultValue?: T
|
||||
): T {
|
||||
if (typeof fn !== "function") {
|
||||
return defaultValue ?? (fn as T)
|
||||
}
|
||||
return (fn as FileResponseFn<T>)(
|
||||
filePath,
|
||||
path.dirname(handlebarsParse(options, filePath, { isPath: true }).toString()),
|
||||
path.basename(handlebarsParse(options, filePath, { isPath: true }).toString())
|
||||
)
|
||||
}
|
||||
|
||||
export function handlebarsParse(
|
||||
options: ScaffoldConfig,
|
||||
templateBuffer: Buffer | string,
|
||||
{ isPath = false }: { isPath?: boolean } = {}
|
||||
): Buffer {
|
||||
const { data } = options
|
||||
try {
|
||||
let str = templateBuffer.toString()
|
||||
if (isPath) {
|
||||
str = str.replace(/\\/g, "/")
|
||||
}
|
||||
const parser = Handlebars.compile(str, { noEscape: true })
|
||||
let outputContents = parser(data)
|
||||
if (isPath && path.sep !== "/") {
|
||||
outputContents = outputContents.replace(/\//g, "\\")
|
||||
}
|
||||
return Buffer.from(outputContents)
|
||||
} catch (e) {
|
||||
log(options, LogLevel.Debug, e)
|
||||
log(options, LogLevel.Warning, "Couldn't parse file with handlebars, returning original content")
|
||||
return Buffer.from(templateBuffer)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
export function pascalCase(s: string): string {
|
||||
return startCase(s).replace(/\s+/g, "")
|
||||
}
|
||||
|
||||
export async function isDir(path: string): Promise<boolean> {
|
||||
const tplStat = await stat(path)
|
||||
return tplStat.isDirectory()
|
||||
}
|
||||
|
||||
export function removeGlob(template: string): string {
|
||||
return template.replace(/\*/g, "").replace(/(\/\/|\\\\)/g, path.sep)
|
||||
}
|
||||
|
||||
export function makeRelativePath(str: string): string {
|
||||
return str.startsWith(path.sep) ? str.slice(1) : str
|
||||
}
|
||||
|
||||
export function getBasePath(relPath: string): string {
|
||||
return path
|
||||
.resolve(process.cwd(), relPath)
|
||||
.replace(process.cwd() + path.sep, "")
|
||||
.replace(process.cwd(), "")
|
||||
}
|
||||
|
||||
export async function getFileList(options: ScaffoldConfig, template: string): Promise<string[]> {
|
||||
return (
|
||||
await promisify(glob)(template, {
|
||||
dot: true,
|
||||
debug: options.verbose === LogLevel.Debug,
|
||||
nodir: true,
|
||||
})
|
||||
).map((f) => f.replace(/\//g, path.sep))
|
||||
}
|
||||
|
||||
export interface GlobInfo {
|
||||
nonGlobTemplate: string
|
||||
origTemplate: string
|
||||
isDirOrGlob: boolean
|
||||
isGlob: boolean
|
||||
template: string
|
||||
}
|
||||
|
||||
export async function getTemplateGlobInfo(options: ScaffoldConfig, template: string): Promise<GlobInfo> {
|
||||
const isGlob = glob.hasMagic(template)
|
||||
log(options, LogLevel.Debug, "before isDir", "isGlob:", isGlob, template)
|
||||
let _template = template
|
||||
const nonGlobTemplate = isGlob ? removeGlob(template) : template
|
||||
const isDirOrGlob = isGlob ? true : await isDir(template)
|
||||
log(options, LogLevel.Debug, "after isDir", isDirOrGlob)
|
||||
const _shouldAddGlob = !isGlob && isDirOrGlob
|
||||
const origTemplate = template
|
||||
if (_shouldAddGlob) {
|
||||
_template = path.join(template, "**", "*")
|
||||
}
|
||||
return { nonGlobTemplate, origTemplate, isDirOrGlob, isGlob, template: _template }
|
||||
}
|
||||
|
||||
export interface OutputFileInfo {
|
||||
inputPath: string
|
||||
outputPathOpt: string
|
||||
outputDir: string
|
||||
outputPath: string
|
||||
exists: boolean
|
||||
}
|
||||
|
||||
export async function getTemplateFileInfo(
|
||||
options: ScaffoldConfig,
|
||||
data: Record<string, string>,
|
||||
{ templatePath, basePath }: { templatePath: string; basePath: string }
|
||||
): Promise<OutputFileInfo> {
|
||||
const inputPath = path.resolve(process.cwd(), templatePath)
|
||||
const outputPathOpt = getOptionValueForFile(options, inputPath, options.output)
|
||||
const outputDir = getOutputDir(options, outputPathOpt, basePath)
|
||||
const outputPath = handlebarsParse(options, path.join(outputDir, path.basename(inputPath)), {
|
||||
isPath: true,
|
||||
}).toString()
|
||||
const exists = await pathExists(outputPath)
|
||||
return { inputPath, outputPathOpt, outputDir, outputPath, exists }
|
||||
}
|
||||
|
||||
export async function copyFileTransformed(
|
||||
options: ScaffoldConfig,
|
||||
{
|
||||
exists,
|
||||
overwrite,
|
||||
outputPath,
|
||||
inputPath,
|
||||
}: {
|
||||
exists: boolean
|
||||
overwrite: boolean
|
||||
outputPath: string
|
||||
inputPath: string
|
||||
}
|
||||
): Promise<void> {
|
||||
if (!exists || overwrite) {
|
||||
if (exists && overwrite) {
|
||||
log(options, LogLevel.Info, `File ${outputPath} exists, overwriting`)
|
||||
}
|
||||
const templateBuffer = await readFile(inputPath)
|
||||
const unprocessedOutputContents = handlebarsParse(options, templateBuffer)
|
||||
const finalOutputContents = (
|
||||
(await options.beforeWrite?.(unprocessedOutputContents, templateBuffer, outputPath)) ?? unprocessedOutputContents
|
||||
).toString()
|
||||
|
||||
if (!options.dryRun) {
|
||||
await writeFile(outputPath, finalOutputContents)
|
||||
log(options, LogLevel.Info, "Done.")
|
||||
} else {
|
||||
log(options, LogLevel.Info, "Content output:")
|
||||
log(options, LogLevel.Info, finalOutputContents)
|
||||
}
|
||||
} else if (exists) {
|
||||
log(options, LogLevel.Info, `File ${outputPath} already exists, skipping`)
|
||||
}
|
||||
}
|
||||
|
||||
export function getOutputDir(options: ScaffoldConfig, outputPathOpt: string, basePath: string): string {
|
||||
return path.resolve(
|
||||
process.cwd(),
|
||||
...([
|
||||
outputPathOpt,
|
||||
basePath,
|
||||
options.createSubFolder
|
||||
? options.subFolderNameHelper
|
||||
? handlebarsParse(options, `{{ ${options.subFolderNameHelper} name }}`).toString()
|
||||
: options.name
|
||||
: undefined,
|
||||
].filter(Boolean) as string[])
|
||||
)
|
||||
}
|
||||
|
||||
export function logInputFile(
|
||||
options: ScaffoldConfig,
|
||||
{
|
||||
origTemplate,
|
||||
relPath,
|
||||
template,
|
||||
inputFilePath,
|
||||
nonGlobTemplate,
|
||||
basePath,
|
||||
isDirOrGlob,
|
||||
isGlob,
|
||||
}: {
|
||||
origTemplate: string
|
||||
relPath: string
|
||||
template: string
|
||||
inputFilePath: string
|
||||
nonGlobTemplate: string
|
||||
basePath: string
|
||||
isDirOrGlob: boolean
|
||||
isGlob: boolean
|
||||
}
|
||||
): void {
|
||||
log(
|
||||
options,
|
||||
LogLevel.Debug,
|
||||
`\nprocess.cwd(): ${process.cwd()}`,
|
||||
`\norigTemplate: ${origTemplate}`,
|
||||
`\nrelPath: ${relPath}`,
|
||||
`\ntemplate: ${template}`,
|
||||
`\ninputFilePath: ${inputFilePath}`,
|
||||
`\nnonGlobTemplate: ${nonGlobTemplate}`,
|
||||
`\nbasePath: ${basePath}`,
|
||||
`\nisDirOrGlob: ${isDirOrGlob}`,
|
||||
`\nisGlob: ${isGlob}`,
|
||||
`\n`
|
||||
)
|
||||
}
|
||||
|
||||
export function logInitStep(options: ScaffoldConfig): void {
|
||||
log(options, LogLevel.Debug, "Full config:", {
|
||||
name: options.name,
|
||||
templates: options.templates,
|
||||
output: options.output,
|
||||
createSubfolder: options.createSubFolder,
|
||||
data: options.data,
|
||||
overwrite: options.overwrite,
|
||||
quiet: options.quiet,
|
||||
subFolderTransformHelper: options.subFolderNameHelper,
|
||||
helpers: Object.keys(options.helpers ?? {}),
|
||||
verbose: `${options.verbose} (${Object.keys(LogLevel).find(
|
||||
(k) => (LogLevel[k as any] as unknown as number) === options.verbose!
|
||||
)})`,
|
||||
})
|
||||
log(options, LogLevel.Info, "Data:", options.data)
|
||||
}
|
||||
|
||||
171
src/validate.ts
@@ -1,171 +0,0 @@
|
||||
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,539 +0,0 @@
|
||||
import { describe, test, expect, beforeEach, afterEach, beforeAll, vi } from "vitest"
|
||||
import mockFs from "mock-fs"
|
||||
import FileSystem from "mock-fs/lib/filesystem"
|
||||
import { Console } from "console"
|
||||
import { LogLevel, ScaffoldCmdConfig, ScaffoldConfig } from "../src/types"
|
||||
import * as config from "../src/config"
|
||||
import { resolve } from "../src/utils"
|
||||
import configFile from "./test-config"
|
||||
import { findConfigFile, getOptionValueForFile } from "../src/config"
|
||||
import { registerHelpers } from "../src/parser"
|
||||
import path from "path"
|
||||
|
||||
vi.mock("../src/git", async () => {
|
||||
const actual = await vi.importActual<typeof import("../src/git")>("../src/git")
|
||||
return {
|
||||
...actual,
|
||||
getGitConfig: () => {
|
||||
return Promise.resolve(blankCliConf)
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const { githubPartToUrl, parseAppendData, parseConfigFile } = config
|
||||
|
||||
const blankCliConf: ScaffoldCmdConfig = {
|
||||
logLevel: LogLevel.none,
|
||||
name: "",
|
||||
output: "",
|
||||
templates: [],
|
||||
data: { name: "test" },
|
||||
overwrite: false,
|
||||
subdir: false,
|
||||
dryRun: false,
|
||||
quiet: false,
|
||||
version: false,
|
||||
}
|
||||
|
||||
const blankConfig: ScaffoldCmdConfig = {
|
||||
...blankCliConf,
|
||||
data: {},
|
||||
}
|
||||
|
||||
describe("config", () => {
|
||||
describe("parseAppendData", () => {
|
||||
test('works for "key=value"', () => {
|
||||
expect(parseAppendData("key=value", blankCliConf)).toEqual({ key: "value", name: "test" })
|
||||
})
|
||||
|
||||
test('works for "key:=value"', () => {
|
||||
expect(parseAppendData("key:=123", blankCliConf)).toEqual({ key: 123, name: "test" })
|
||||
})
|
||||
|
||||
test("overwrites existing value", () => {
|
||||
expect(parseAppendData("name:=123", blankCliConf)).toEqual({ name: 123 })
|
||||
})
|
||||
|
||||
test("works with quotes", () => {
|
||||
expect(parseAppendData('key="value test"', blankCliConf)).toEqual({
|
||||
key: "value test",
|
||||
name: "test",
|
||||
})
|
||||
})
|
||||
|
||||
test("handles JSON array values with :=", () => {
|
||||
expect(parseAppendData('items:=["a","b"]', blankCliConf)).toEqual({
|
||||
items: ["a", "b"],
|
||||
name: "test",
|
||||
})
|
||||
})
|
||||
|
||||
test("handles JSON boolean with :=", () => {
|
||||
expect(parseAppendData("flag:=true", blankCliConf)).toEqual({ flag: true, name: "test" })
|
||||
expect(parseAppendData("flag:=false", blankCliConf)).toEqual({ flag: false, name: "test" })
|
||||
})
|
||||
|
||||
test("handles JSON null with :=", () => {
|
||||
expect(parseAppendData("val:=null", blankCliConf)).toEqual({ val: null, name: "test" })
|
||||
})
|
||||
|
||||
test("handles JSON object with :=", () => {
|
||||
expect(parseAppendData('obj:={"a":1}', blankCliConf)).toEqual({ obj: { a: 1 }, name: "test" })
|
||||
})
|
||||
|
||||
test("handles single quoted values", () => {
|
||||
expect(parseAppendData("key='value test'", blankCliConf)).toEqual({
|
||||
key: "value test",
|
||||
name: "test",
|
||||
})
|
||||
})
|
||||
|
||||
test("handles empty string value", () => {
|
||||
expect(parseAppendData("key=", blankCliConf)).toEqual({ key: "", name: "test" })
|
||||
})
|
||||
|
||||
test("handles negative number with :=", () => {
|
||||
expect(parseAppendData("num:=-42", blankCliConf)).toEqual({ num: -42, name: "test" })
|
||||
})
|
||||
|
||||
test("handles float with :=", () => {
|
||||
expect(parseAppendData("num:=3.14", blankCliConf)).toEqual({ num: 3.14, name: "test" })
|
||||
})
|
||||
})
|
||||
|
||||
describe("githubPartToUrl", () => {
|
||||
test("works", () => {
|
||||
expect(githubPartToUrl("chenasraf/simple-scaffold")).toEqual(
|
||||
"https://github.com/chenasraf/simple-scaffold.git",
|
||||
)
|
||||
expect(githubPartToUrl("chenasraf/simple-scaffold.git")).toEqual(
|
||||
"https://github.com/chenasraf/simple-scaffold.git",
|
||||
)
|
||||
})
|
||||
|
||||
test("handles organization repos", () => {
|
||||
expect(githubPartToUrl("org/sub-repo")).toEqual("https://github.com/org/sub-repo.git")
|
||||
})
|
||||
|
||||
test("handles repos with dots in name", () => {
|
||||
expect(githubPartToUrl("user/my.repo")).toEqual("https://github.com/user/my.repo.git")
|
||||
})
|
||||
})
|
||||
|
||||
describe("parseConfigFile", () => {
|
||||
test("normal config does not change", async () => {
|
||||
const tmpDir = `/tmp/scaffold-config-${Date.now()}`
|
||||
const { quiet: _, tmpDir: _tmpDir, version: __, ...conf } = blankCliConf
|
||||
expect(
|
||||
await parseConfigFile({
|
||||
...blankCliConf,
|
||||
name: "-",
|
||||
tmpDir,
|
||||
}),
|
||||
).toEqual({ ...conf, name: "-", tmpDir, subdirHelper: undefined, beforeWrite: undefined })
|
||||
})
|
||||
|
||||
describe("appendData", () => {
|
||||
test("appends", async () => {
|
||||
const result = await parseConfigFile({
|
||||
...blankCliConf,
|
||||
name: "-",
|
||||
appendData: { key: "value" },
|
||||
tmpDir: `/tmp/scaffold-config-${Date.now()}`,
|
||||
})
|
||||
expect(result?.data?.key).toEqual("value")
|
||||
})
|
||||
|
||||
test("overwrites existing value", async () => {
|
||||
const result = await parseConfigFile({
|
||||
...blankCliConf,
|
||||
name: "-",
|
||||
data: { num: "123" },
|
||||
appendData: { num: "1234" },
|
||||
tmpDir: `/tmp/scaffold-config-${Date.now()}`,
|
||||
})
|
||||
expect(result?.data?.num).toEqual("1234")
|
||||
})
|
||||
|
||||
test("CLI output overrides config file output", async () => {
|
||||
const tmpDir = `/tmp/scaffold-config-${Date.now()}`
|
||||
|
||||
const result = await parseConfigFile({
|
||||
...blankCliConf,
|
||||
config: path.resolve(__dirname, "test-config.js"),
|
||||
key: "component",
|
||||
output: "examples/test-output/override",
|
||||
name: "Component",
|
||||
tmpDir,
|
||||
})
|
||||
|
||||
expect(result.output).toEqual("examples/test-output/override")
|
||||
})
|
||||
})
|
||||
|
||||
test("throws when name is missing", async () => {
|
||||
await expect(
|
||||
parseConfigFile({
|
||||
...blankCliConf,
|
||||
name: "",
|
||||
tmpDir: `/tmp/scaffold-config-${Date.now()}`,
|
||||
}),
|
||||
).rejects.toThrow("Missing required option: name")
|
||||
})
|
||||
|
||||
test("preserves dryRun setting", async () => {
|
||||
const result = await parseConfigFile({
|
||||
...blankCliConf,
|
||||
name: "test",
|
||||
dryRun: true,
|
||||
tmpDir: `/tmp/scaffold-config-${Date.now()}`,
|
||||
})
|
||||
expect(result.dryRun).toBe(true)
|
||||
})
|
||||
|
||||
test("preserves subdir setting", async () => {
|
||||
const result = await parseConfigFile({
|
||||
...blankCliConf,
|
||||
name: "test",
|
||||
subdir: true,
|
||||
tmpDir: `/tmp/scaffold-config-${Date.now()}`,
|
||||
})
|
||||
expect(result.subdir).toBe(true)
|
||||
})
|
||||
|
||||
test("preserves overwrite setting", async () => {
|
||||
const result = await parseConfigFile({
|
||||
...blankCliConf,
|
||||
name: "test",
|
||||
overwrite: true,
|
||||
tmpDir: `/tmp/scaffold-config-${Date.now()}`,
|
||||
})
|
||||
expect(result.overwrite).toBe(true)
|
||||
})
|
||||
|
||||
test("merges data from config and appendData", async () => {
|
||||
const result = await parseConfigFile({
|
||||
...blankCliConf,
|
||||
name: "test",
|
||||
data: { key1: "val1" },
|
||||
appendData: { key2: "val2" },
|
||||
tmpDir: `/tmp/scaffold-config-${Date.now()}`,
|
||||
})
|
||||
expect(result.data).toEqual({ key1: "val1", key2: "val2" })
|
||||
})
|
||||
|
||||
test("appendData overrides data", async () => {
|
||||
const result = await parseConfigFile({
|
||||
...blankCliConf,
|
||||
name: "test",
|
||||
data: { key: "original" },
|
||||
appendData: { key: "overridden" },
|
||||
tmpDir: `/tmp/scaffold-config-${Date.now()}`,
|
||||
})
|
||||
expect(result.data?.key).toEqual("overridden")
|
||||
})
|
||||
|
||||
test("sets subdirHelper from config", async () => {
|
||||
const result = await parseConfigFile({
|
||||
...blankCliConf,
|
||||
name: "test",
|
||||
subdirHelper: "pascalCase",
|
||||
tmpDir: `/tmp/scaffold-config-${Date.now()}`,
|
||||
})
|
||||
expect(result.subdirHelper).toEqual("pascalCase")
|
||||
})
|
||||
|
||||
test("handles empty templates array", async () => {
|
||||
const result = await parseConfigFile({
|
||||
...blankCliConf,
|
||||
name: "test",
|
||||
templates: [],
|
||||
tmpDir: `/tmp/scaffold-config-${Date.now()}`,
|
||||
})
|
||||
expect(result.templates).toEqual([])
|
||||
})
|
||||
|
||||
test("throws when config key not found", async () => {
|
||||
await expect(
|
||||
parseConfigFile({
|
||||
...blankCliConf,
|
||||
name: "test",
|
||||
config: path.resolve(__dirname, "test-config.js"),
|
||||
key: "nonexistent",
|
||||
tmpDir: `/tmp/scaffold-config-${Date.now()}`,
|
||||
}),
|
||||
).rejects.toThrow('Template "nonexistent" not found')
|
||||
})
|
||||
|
||||
test("uses default key when key not specified", async () => {
|
||||
const result = await parseConfigFile({
|
||||
...blankCliConf,
|
||||
name: "MyComponent",
|
||||
templates: undefined as unknown as string[],
|
||||
config: path.resolve(__dirname, "test-config.js"),
|
||||
tmpDir: `/tmp/scaffold-config-${Date.now()}`,
|
||||
})
|
||||
expect(result.templates.length).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe("getConfig", () => {
|
||||
test("gets git config", async () => {
|
||||
const resultFn = await config.getRemoteConfig({
|
||||
git: "https://github.com/chenasraf/simple-scaffold.git",
|
||||
logLevel: LogLevel.none,
|
||||
tmpDir: `/tmp/scaffold-config-${Date.now()}`,
|
||||
})
|
||||
const result = await resolve(resultFn, blankCliConf)
|
||||
expect(result).toEqual(blankCliConf)
|
||||
})
|
||||
|
||||
test("gets local file config", async () => {
|
||||
const resultFn = await config.getLocalConfig({
|
||||
config: path.join(__dirname, "test-config.js"),
|
||||
logLevel: LogLevel.none,
|
||||
})
|
||||
const result = (await resolve(resultFn, {} as ScaffoldCmdConfig)).default
|
||||
expect(result).toEqual(configFile)
|
||||
})
|
||||
})
|
||||
|
||||
describe("getRemoteConfig", () => {
|
||||
test("throws for unsupported protocol", async () => {
|
||||
await expect(
|
||||
config.getRemoteConfig({
|
||||
git: "ftp://example.com/repo.git",
|
||||
logLevel: LogLevel.none,
|
||||
tmpDir: `/tmp/scaffold-config-${Date.now()}`,
|
||||
}),
|
||||
).rejects.toThrow("Unsupported protocol")
|
||||
})
|
||||
})
|
||||
|
||||
describe("getOptionValueForFile", () => {
|
||||
const conf: ScaffoldConfig = {
|
||||
name: "test",
|
||||
output: "output",
|
||||
templates: [],
|
||||
logLevel: LogLevel.none,
|
||||
data: { name: "test" },
|
||||
}
|
||||
|
||||
beforeAll(() => {
|
||||
registerHelpers(conf)
|
||||
})
|
||||
|
||||
test("returns static string value", () => {
|
||||
expect(getOptionValueForFile(conf, "/some/path", "static-value")).toEqual("static-value")
|
||||
})
|
||||
|
||||
test("returns static boolean value", () => {
|
||||
expect(getOptionValueForFile(conf, "/some/path", true)).toBe(true)
|
||||
expect(getOptionValueForFile(conf, "/some/path", false)).toBe(false)
|
||||
})
|
||||
|
||||
test("calls function with file path info", () => {
|
||||
const fn = vi.fn().mockReturnValue("custom-output")
|
||||
const result = getOptionValueForFile(conf, "/home/user/file.txt", fn)
|
||||
expect(result).toEqual("custom-output")
|
||||
expect(fn).toHaveBeenCalledWith("/home/user/file.txt", expect.any(String), expect.any(String))
|
||||
})
|
||||
|
||||
test("returns default value when fn is not a function and no value", () => {
|
||||
expect(
|
||||
getOptionValueForFile(conf, "/some/path", undefined as unknown as string, "default"),
|
||||
).toEqual("default")
|
||||
})
|
||||
|
||||
test("function receives parsed basename", () => {
|
||||
const fn = (_fullPath: string, _basedir: string, basename: string) => basename
|
||||
const result = getOptionValueForFile(conf, "/home/user/{{name}}.txt", fn)
|
||||
expect(result).toEqual("test.txt")
|
||||
})
|
||||
})
|
||||
|
||||
describe("findConfigFile", () => {
|
||||
const struct1 = {
|
||||
"scaffold.config.js": `module.exports = '${JSON.stringify(blankConfig)}'`,
|
||||
}
|
||||
const struct2 = {
|
||||
"scaffold.js": `module.exports = '${JSON.stringify(blankConfig)}'`,
|
||||
}
|
||||
const struct3 = {
|
||||
"scaffold.cjs": `module.exports = '${JSON.stringify(blankConfig)}'`,
|
||||
}
|
||||
const struct4 = {
|
||||
"scaffold.json": JSON.stringify(blankConfig),
|
||||
}
|
||||
|
||||
function withMock(fileStruct: FileSystem.DirectoryItems, testFn: () => void): () => void {
|
||||
return () => {
|
||||
beforeEach(() => {
|
||||
// console.log("Mocking:", fileStruct)
|
||||
console = new Console(process.stdout, process.stderr)
|
||||
|
||||
mockFs(fileStruct)
|
||||
// logMock = jest.spyOn(console, 'log').mockImplementation((...args) => {
|
||||
// logsTemp.push(args)
|
||||
// })
|
||||
})
|
||||
testFn()
|
||||
afterEach(() => {
|
||||
// console.log("Restoring mock")
|
||||
mockFs.restore()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
for (const struct of [struct1, struct2, struct3, struct4]) {
|
||||
const [k] = Object.keys(struct)
|
||||
|
||||
describe(
|
||||
`finds config file ${k}`,
|
||||
withMock(struct, () => {
|
||||
test(`finds ${k}`, async () => {
|
||||
const result = await findConfigFile(process.cwd())
|
||||
expect(result).toEqual(k)
|
||||
})
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
describe(
|
||||
"finds .mjs config file",
|
||||
withMock({ "scaffold.config.mjs": "export default {}" }, () => {
|
||||
test("finds scaffold.config.mjs", async () => {
|
||||
const result = await findConfigFile(process.cwd())
|
||||
expect(result).toEqual("scaffold.config.mjs")
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
describe(
|
||||
"priority order",
|
||||
withMock(
|
||||
{
|
||||
"scaffold.config.js": "module.exports = {}",
|
||||
"scaffold.js": "module.exports = {}",
|
||||
},
|
||||
() => {
|
||||
test("prefers scaffold.config.js over scaffold.js", async () => {
|
||||
const result = await findConfigFile(process.cwd())
|
||||
expect(result).toEqual("scaffold.config.js")
|
||||
})
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
describe(
|
||||
"throws when no config found",
|
||||
withMock({ "unrelated-file.txt": "content" }, () => {
|
||||
test("throws error when no config file exists", async () => {
|
||||
await expect(findConfigFile(process.cwd())).rejects.toThrow("Could not find config file")
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
describe(
|
||||
"finds scaffold.config.cjs",
|
||||
withMock({ "scaffold.config.cjs": "module.exports = {}" }, () => {
|
||||
test("finds .cjs config file", async () => {
|
||||
const result = await findConfigFile(process.cwd())
|
||||
expect(result).toEqual("scaffold.config.cjs")
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
describe(
|
||||
"finds scaffold.config.json",
|
||||
withMock({ "scaffold.config.json": "{}" }, () => {
|
||||
test("finds .json config file", async () => {
|
||||
const result = await findConfigFile(process.cwd())
|
||||
expect(result).toEqual("scaffold.config.json")
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
describe(
|
||||
"finds scaffold.mjs",
|
||||
withMock({ "scaffold.mjs": "export default {}" }, () => {
|
||||
test("finds scaffold.mjs", async () => {
|
||||
const result = await findConfigFile(process.cwd())
|
||||
expect(result).toEqual("scaffold.mjs")
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
describe(
|
||||
"finds scaffold.cjs",
|
||||
withMock({ "scaffold.cjs": "module.exports = {}" }, () => {
|
||||
test("finds scaffold.cjs", async () => {
|
||||
const result = await findConfigFile(process.cwd())
|
||||
expect(result).toEqual("scaffold.cjs")
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
describe(
|
||||
"finds scaffold.json",
|
||||
withMock({ "scaffold.json": "{}" }, () => {
|
||||
test("finds scaffold.json", async () => {
|
||||
const result = await findConfigFile(process.cwd())
|
||||
expect(result).toEqual("scaffold.json")
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
describe(
|
||||
"finds .scaffold.js",
|
||||
withMock({ ".scaffold.js": "module.exports = {}" }, () => {
|
||||
test("finds dotfile config", async () => {
|
||||
const result = await findConfigFile(process.cwd())
|
||||
expect(result).toEqual(".scaffold.js")
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
describe(
|
||||
"finds .scaffold.json",
|
||||
withMock({ ".scaffold.json": "{}" }, () => {
|
||||
test("finds dotfile json config", async () => {
|
||||
const result = await findConfigFile(process.cwd())
|
||||
expect(result).toEqual(".scaffold.json")
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
describe(
|
||||
"prefers scaffold.config over .scaffold",
|
||||
withMock(
|
||||
{
|
||||
"scaffold.config.js": "module.exports = {}",
|
||||
".scaffold.js": "module.exports = {}",
|
||||
},
|
||||
() => {
|
||||
test("prefers scaffold.config.js over .scaffold.js", async () => {
|
||||
const result = await findConfigFile(process.cwd())
|
||||
expect(result).toEqual("scaffold.config.js")
|
||||
})
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
describe(
|
||||
"prefers scaffold over .scaffold",
|
||||
withMock(
|
||||
{
|
||||
"scaffold.js": "module.exports = {}",
|
||||
".scaffold.js": "module.exports = {}",
|
||||
},
|
||||
() => {
|
||||
test("prefers scaffold.js over .scaffold.js", async () => {
|
||||
const result = await findConfigFile(process.cwd())
|
||||
expect(result).toEqual("scaffold.js")
|
||||
})
|
||||
},
|
||||
),
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -1,456 +0,0 @@
|
||||
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()
|
||||
})
|
||||
},
|
||||
),
|
||||
)
|
||||
})
|
||||
@@ -1,122 +0,0 @@
|
||||
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"])
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,115 +0,0 @@
|
||||
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)
|
||||
})
|
||||
})
|
||||
@@ -1,175 +0,0 @@
|
||||
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,429 +0,0 @@
|
||||
import { describe, test, expect, beforeAll, afterAll } from "vitest"
|
||||
import { ScaffoldConfig } from "../src/types"
|
||||
import path from "node:path"
|
||||
import * as dateFns from "date-fns"
|
||||
import { dateHelper, defaultHelpers, handlebarsParse, nowHelper, registerHelpers } from "../src/parser"
|
||||
|
||||
const blankConf: ScaffoldConfig = {
|
||||
logLevel: "none",
|
||||
name: "",
|
||||
output: "",
|
||||
templates: [],
|
||||
data: { name: "test" },
|
||||
}
|
||||
|
||||
describe("parser", () => {
|
||||
describe("handlebarsParse", () => {
|
||||
let origSep: string
|
||||
|
||||
describe("windows paths", () => {
|
||||
beforeAll(() => {
|
||||
origSep = path.sep
|
||||
Object.defineProperty(path, "sep", { value: "\\" })
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
Object.defineProperty(path, "sep", { value: origSep })
|
||||
})
|
||||
|
||||
test("should work for windows paths", async () => {
|
||||
expect(handlebarsParse(blankConf, "C:\\exports\\{{name}}.txt", { asPath: true }).toString()).toEqual(
|
||||
"C:\\exports\\test.txt",
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("non-windows paths", () => {
|
||||
|
||||
beforeAll(() => {
|
||||
origSep = path.sep
|
||||
Object.defineProperty(path, "sep", { value: "/" })
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
Object.defineProperty(path, "sep", { value: origSep })
|
||||
})
|
||||
|
||||
test("should work for non-windows paths", async () => {
|
||||
expect(handlebarsParse(blankConf, "/home/test/{{name}}.txt", { asPath: true })).toEqual(
|
||||
Buffer.from("/home/test/test.txt"),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
test("should not do path escaping on non-path compiles", async () => {
|
||||
expect(
|
||||
handlebarsParse(
|
||||
{ ...blankConf, data: { ...blankConf.data, escaped: "value" } },
|
||||
"/home/test/{{name}} \\{{escaped}}.txt",
|
||||
{
|
||||
asPath: false,
|
||||
},
|
||||
),
|
||||
).toEqual(Buffer.from("/home/test/test {{escaped}}.txt"))
|
||||
})
|
||||
|
||||
test("should replace name token in content", () => {
|
||||
const result = handlebarsParse(blankConf, "Hello {{name}}")
|
||||
expect(result.toString()).toEqual("Hello test")
|
||||
})
|
||||
|
||||
test("should replace multiple tokens", () => {
|
||||
const config: ScaffoldConfig = {
|
||||
...blankConf,
|
||||
data: { name: "app", version: "1.0" },
|
||||
}
|
||||
expect(handlebarsParse(config, "{{name}} v{{version}}").toString()).toEqual("app v1.0")
|
||||
})
|
||||
|
||||
test("should return Buffer", () => {
|
||||
expect(Buffer.isBuffer(handlebarsParse(blankConf, "test"))).toBe(true)
|
||||
})
|
||||
|
||||
test("should handle Buffer input", () => {
|
||||
expect(handlebarsParse(blankConf, Buffer.from("Hello {{name}}")).toString()).toEqual("Hello test")
|
||||
})
|
||||
|
||||
test("should return original content on handlebars error", () => {
|
||||
const result = handlebarsParse(blankConf, "{{#if}}invalid{{/unless}}")
|
||||
expect(Buffer.isBuffer(result)).toBe(true)
|
||||
expect(result.toString()).toEqual("{{#if}}invalid{{/unless}}")
|
||||
})
|
||||
|
||||
test("should handle empty template", () => {
|
||||
expect(handlebarsParse(blankConf, "").toString()).toEqual("")
|
||||
})
|
||||
|
||||
test("should handle template with no tokens", () => {
|
||||
expect(handlebarsParse(blankConf, "no tokens here").toString()).toEqual("no tokens here")
|
||||
})
|
||||
|
||||
test("should not escape HTML chars (noEscape)", () => {
|
||||
const config: ScaffoldConfig = {
|
||||
...blankConf,
|
||||
data: { name: "<div>test</div>" },
|
||||
}
|
||||
expect(handlebarsParse(config, "{{name}}").toString()).toEqual("<div>test</div>")
|
||||
})
|
||||
|
||||
test("should handle nested data", () => {
|
||||
const config: ScaffoldConfig = {
|
||||
...blankConf,
|
||||
data: { name: "test", nested: { key: "value" } },
|
||||
}
|
||||
expect(handlebarsParse(config, "{{nested.key}}").toString()).toEqual("value")
|
||||
})
|
||||
|
||||
test("should handle handlebars conditionals", () => {
|
||||
const config: ScaffoldConfig = {
|
||||
...blankConf,
|
||||
data: { name: "test", showExtra: true },
|
||||
}
|
||||
registerHelpers(config)
|
||||
expect(handlebarsParse(config, "{{#if showExtra}}extra{{/if}} content").toString()).toEqual("extra content")
|
||||
})
|
||||
|
||||
test("should handle handlebars conditionals when false", () => {
|
||||
const config: ScaffoldConfig = {
|
||||
...blankConf,
|
||||
data: { name: "test", showExtra: false },
|
||||
}
|
||||
registerHelpers(config)
|
||||
expect(handlebarsParse(config, "{{#if showExtra}}extra{{/if}}content").toString()).toEqual("content")
|
||||
})
|
||||
|
||||
test("should handle handlebars each loops", () => {
|
||||
const config: ScaffoldConfig = {
|
||||
...blankConf,
|
||||
data: { name: "test", items: ["a", "b", "c"] },
|
||||
}
|
||||
registerHelpers(config)
|
||||
expect(handlebarsParse(config, "{{#each items}}{{this}},{{/each}}").toString()).toEqual("a,b,c,")
|
||||
})
|
||||
|
||||
test("should render empty for undefined data token", () => {
|
||||
expect(handlebarsParse(blankConf, "{{undefinedVar}}").toString()).toEqual("")
|
||||
})
|
||||
})
|
||||
|
||||
describe("Helpers", () => {
|
||||
describe("string helpers", () => {
|
||||
test("camelCase", () => {
|
||||
expect(defaultHelpers.camelCase("test string")).toEqual("testString")
|
||||
expect(defaultHelpers.camelCase("test_string")).toEqual("testString")
|
||||
expect(defaultHelpers.camelCase("test-string")).toEqual("testString")
|
||||
expect(defaultHelpers.camelCase("testString")).toEqual("testString")
|
||||
expect(defaultHelpers.camelCase("TestString")).toEqual("testString")
|
||||
expect(defaultHelpers.camelCase("Test____String")).toEqual("testString")
|
||||
})
|
||||
|
||||
test("pascalCase", () => {
|
||||
expect(defaultHelpers.pascalCase("test string")).toEqual("TestString")
|
||||
expect(defaultHelpers.pascalCase("test_string")).toEqual("TestString")
|
||||
expect(defaultHelpers.pascalCase("test-string")).toEqual("TestString")
|
||||
expect(defaultHelpers.pascalCase("testString")).toEqual("TestString")
|
||||
expect(defaultHelpers.pascalCase("TestString")).toEqual("TestString")
|
||||
expect(defaultHelpers.pascalCase("Test____String")).toEqual("TestString")
|
||||
})
|
||||
|
||||
test("snakeCase", () => {
|
||||
expect(defaultHelpers.snakeCase("test string")).toEqual("test_string")
|
||||
expect(defaultHelpers.snakeCase("test_string")).toEqual("test_string")
|
||||
expect(defaultHelpers.snakeCase("test-string")).toEqual("test_string")
|
||||
expect(defaultHelpers.snakeCase("testString")).toEqual("test_string")
|
||||
expect(defaultHelpers.snakeCase("TestString")).toEqual("test_string")
|
||||
expect(defaultHelpers.snakeCase("Test____String")).toEqual("test_string")
|
||||
})
|
||||
|
||||
test("kebabCase", () => {
|
||||
expect(defaultHelpers.kebabCase("test string")).toEqual("test-string")
|
||||
expect(defaultHelpers.kebabCase("test_string")).toEqual("test-string")
|
||||
expect(defaultHelpers.kebabCase("test-string")).toEqual("test-string")
|
||||
expect(defaultHelpers.kebabCase("testString")).toEqual("test-string")
|
||||
expect(defaultHelpers.kebabCase("TestString")).toEqual("test-string")
|
||||
expect(defaultHelpers.kebabCase("Test____String")).toEqual("test-string")
|
||||
})
|
||||
|
||||
test("startCase", () => {
|
||||
expect(defaultHelpers.startCase("test string")).toEqual("Test String")
|
||||
expect(defaultHelpers.startCase("test_string")).toEqual("Test String")
|
||||
expect(defaultHelpers.startCase("test-string")).toEqual("Test String")
|
||||
expect(defaultHelpers.startCase("testString")).toEqual("Test String")
|
||||
expect(defaultHelpers.startCase("TestString")).toEqual("Test String")
|
||||
expect(defaultHelpers.startCase("Test____String")).toEqual("Test String")
|
||||
})
|
||||
})
|
||||
|
||||
describe("string helpers edge cases", () => {
|
||||
test("camelCase single word", () => {
|
||||
expect(defaultHelpers.camelCase("hello")).toEqual("hello")
|
||||
})
|
||||
|
||||
test("camelCase empty string", () => {
|
||||
expect(defaultHelpers.camelCase("")).toEqual("")
|
||||
})
|
||||
|
||||
test("camelCase all uppercase", () => {
|
||||
expect(defaultHelpers.camelCase("HELLO WORLD")).toEqual("helloWorld")
|
||||
})
|
||||
|
||||
test("pascalCase single word", () => {
|
||||
expect(defaultHelpers.pascalCase("hello")).toEqual("Hello")
|
||||
})
|
||||
|
||||
test("pascalCase empty string", () => {
|
||||
expect(defaultHelpers.pascalCase("")).toEqual("")
|
||||
})
|
||||
|
||||
test("snakeCase single word", () => {
|
||||
expect(defaultHelpers.snakeCase("hello")).toEqual("hello")
|
||||
})
|
||||
|
||||
test("snakeCase empty string", () => {
|
||||
expect(defaultHelpers.snakeCase("")).toEqual("")
|
||||
})
|
||||
|
||||
test("kebabCase single word", () => {
|
||||
expect(defaultHelpers.kebabCase("hello")).toEqual("hello")
|
||||
})
|
||||
|
||||
test("kebabCase empty string", () => {
|
||||
expect(defaultHelpers.kebabCase("")).toEqual("")
|
||||
})
|
||||
|
||||
test("startCase single word", () => {
|
||||
expect(defaultHelpers.startCase("hello")).toEqual("Hello")
|
||||
})
|
||||
|
||||
test("startCase empty string", () => {
|
||||
expect(defaultHelpers.startCase("")).toEqual("")
|
||||
})
|
||||
|
||||
test("hyphenCase is same as kebabCase", () => {
|
||||
expect(defaultHelpers.hyphenCase("testString")).toEqual(defaultHelpers.kebabCase("testString"))
|
||||
expect(defaultHelpers.hyphenCase("test_string")).toEqual(defaultHelpers.kebabCase("test_string"))
|
||||
})
|
||||
|
||||
test("lowerCase lowercases everything", () => {
|
||||
expect(defaultHelpers.lowerCase("HELLO")).toEqual("hello")
|
||||
expect(defaultHelpers.lowerCase("Hello World")).toEqual("hello world")
|
||||
})
|
||||
|
||||
test("upperCase uppercases everything", () => {
|
||||
expect(defaultHelpers.upperCase("hello")).toEqual("HELLO")
|
||||
expect(defaultHelpers.upperCase("hello world")).toEqual("HELLO WORLD")
|
||||
})
|
||||
|
||||
test("camelCase handles numbers in string", () => {
|
||||
expect(defaultHelpers.camelCase("item1_name")).toEqual("item1Name")
|
||||
})
|
||||
|
||||
test("pascalCase handles multiple separators", () => {
|
||||
expect(defaultHelpers.pascalCase("a--b__c d")).toEqual("ABCD")
|
||||
})
|
||||
|
||||
test("snakeCase handles mixed separators", () => {
|
||||
expect(defaultHelpers.snakeCase("myApp-name_here")).toEqual("my_app_name_here")
|
||||
})
|
||||
|
||||
test("kebabCase handles mixed separators", () => {
|
||||
expect(defaultHelpers.kebabCase("myApp-name_here")).toEqual("my-app-name-here")
|
||||
})
|
||||
|
||||
test("single character inputs", () => {
|
||||
expect(defaultHelpers.camelCase("a")).toEqual("a")
|
||||
expect(defaultHelpers.pascalCase("a")).toEqual("A")
|
||||
expect(defaultHelpers.snakeCase("a")).toEqual("a")
|
||||
expect(defaultHelpers.kebabCase("a")).toEqual("a")
|
||||
expect(defaultHelpers.startCase("a")).toEqual("A")
|
||||
})
|
||||
})
|
||||
|
||||
describe("date helpers", () => {
|
||||
describe("now", () => {
|
||||
test("should work without extra params", () => {
|
||||
const now = new Date()
|
||||
const fmt = "yyyy-MM-dd HH:mm"
|
||||
|
||||
expect(nowHelper(fmt)).toEqual(dateFns.format(now, fmt))
|
||||
})
|
||||
})
|
||||
|
||||
describe("date", () => {
|
||||
test("should work with no offset params", () => {
|
||||
const now = new Date()
|
||||
const fmt = "yyyy-MM-dd HH:mm"
|
||||
|
||||
expect(dateHelper(now.toISOString(), fmt)).toEqual(dateFns.format(now, fmt))
|
||||
})
|
||||
|
||||
test("should work with offset params", () => {
|
||||
const now = new Date()
|
||||
const fmt = "yyyy-MM-dd HH:mm"
|
||||
|
||||
expect(dateHelper(now.toISOString(), fmt, -1, "days")).toEqual(
|
||||
dateFns.format(dateFns.add(now, { days: -1 }), fmt),
|
||||
)
|
||||
expect(dateHelper(now.toISOString(), fmt, 1, "months")).toEqual(
|
||||
dateFns.format(dateFns.add(now, { months: 1 }), fmt),
|
||||
)
|
||||
})
|
||||
|
||||
test("should work with years offset", () => {
|
||||
const dateStr = "2024-01-15T12:00:00.000Z"
|
||||
const date = dateFns.parseISO(dateStr)
|
||||
expect(dateHelper(dateStr, "yyyy", 1, "years")).toEqual(
|
||||
dateFns.format(dateFns.add(date, { years: 1 }), "yyyy"),
|
||||
)
|
||||
})
|
||||
|
||||
test("should work with weeks offset", () => {
|
||||
const dateStr = "2024-01-15T12:00:00.000Z"
|
||||
const date = dateFns.parseISO(dateStr)
|
||||
expect(dateHelper(dateStr, "yyyy-MM-dd", 2, "weeks")).toEqual(
|
||||
dateFns.format(dateFns.add(date, { weeks: 2 }), "yyyy-MM-dd"),
|
||||
)
|
||||
})
|
||||
|
||||
test("should work with minutes offset", () => {
|
||||
const dateStr = "2024-01-15T12:00:00.000Z"
|
||||
const date = dateFns.parseISO(dateStr)
|
||||
expect(dateHelper(dateStr, "HH:mm", 30, "minutes")).toEqual(
|
||||
dateFns.format(dateFns.add(date, { minutes: 30 }), "HH:mm"),
|
||||
)
|
||||
})
|
||||
|
||||
test("should work with seconds offset", () => {
|
||||
const dateStr = "2024-01-15T12:00:00.000Z"
|
||||
const date = dateFns.parseISO(dateStr)
|
||||
expect(dateHelper(dateStr, "HH:mm:ss", 45, "seconds")).toEqual(
|
||||
dateFns.format(dateFns.add(date, { seconds: 45 }), "HH:mm:ss"),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("now edge cases", () => {
|
||||
test("should work with different format tokens", () => {
|
||||
const now = new Date()
|
||||
expect(nowHelper("yyyy")).toEqual(dateFns.format(now, "yyyy"))
|
||||
expect(nowHelper("MM")).toEqual(dateFns.format(now, "MM"))
|
||||
expect(nowHelper("dd")).toEqual(dateFns.format(now, "dd"))
|
||||
})
|
||||
|
||||
test("should work with positive offset", () => {
|
||||
const now = new Date()
|
||||
const result = nowHelper("yyyy-MM-dd", 1, "days")
|
||||
const expected = dateFns.format(dateFns.add(now, { days: 1 }), "yyyy-MM-dd")
|
||||
expect(result).toEqual(expected)
|
||||
})
|
||||
|
||||
test("should work with hours offset", () => {
|
||||
const now = new Date()
|
||||
const result = nowHelper("HH", 2, "hours")
|
||||
const expected = dateFns.format(dateFns.add(now, { hours: 2 }), "HH")
|
||||
expect(result).toEqual(expected)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("registerHelpers", () => {
|
||||
test("registers default helpers", () => {
|
||||
const config: ScaffoldConfig = { ...blankConf }
|
||||
registerHelpers(config)
|
||||
const result = handlebarsParse(
|
||||
{ ...config, data: { name: "hello_world" } },
|
||||
"{{camelCase name}}",
|
||||
)
|
||||
expect(result.toString()).toEqual("helloWorld")
|
||||
})
|
||||
|
||||
test("registers custom helpers", () => {
|
||||
const config: ScaffoldConfig = {
|
||||
...blankConf,
|
||||
helpers: {
|
||||
reverse: (text: string) => text.split("").reverse().join(""),
|
||||
},
|
||||
}
|
||||
registerHelpers(config)
|
||||
const result = handlebarsParse(
|
||||
{ ...config, data: { name: "hello" } },
|
||||
"{{reverse name}}",
|
||||
)
|
||||
expect(result.toString()).toEqual("olleh")
|
||||
})
|
||||
|
||||
test("custom helpers override default helpers", () => {
|
||||
const config: ScaffoldConfig = {
|
||||
...blankConf,
|
||||
helpers: {
|
||||
camelCase: () => "OVERRIDDEN",
|
||||
},
|
||||
}
|
||||
registerHelpers(config)
|
||||
const result = handlebarsParse(
|
||||
{ ...config, data: { name: "test" } },
|
||||
"{{camelCase name}}",
|
||||
)
|
||||
expect(result.toString()).toEqual("OVERRIDDEN")
|
||||
})
|
||||
})
|
||||
|
||||
describe("default helpers completeness", () => {
|
||||
test("all expected helpers are defined", () => {
|
||||
const expectedHelpers = [
|
||||
"camelCase", "snakeCase", "startCase", "kebabCase",
|
||||
"hyphenCase", "pascalCase", "lowerCase", "upperCase",
|
||||
"now", "date",
|
||||
]
|
||||
for (const helper of expectedHelpers) {
|
||||
expect(defaultHelpers).toHaveProperty(helper)
|
||||
expect(typeof defaultHelpers[helper as keyof typeof defaultHelpers]).toBe("function")
|
||||
}
|
||||
})
|
||||
|
||||
test("has exactly 10 helpers", () => {
|
||||
expect(Object.keys(defaultHelpers).length).toBe(10)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,529 +0,0 @@
|
||||
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
@@ -1,3 +0,0 @@
|
||||
declare const config: import("../dist").ScaffoldConfigFile;
|
||||
export = config;
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
// @ts-check
|
||||
/** @type {import('../dist').ScaffoldConfigFile} */
|
||||
// eslint-disable-next-line no-undef
|
||||
module.exports = (conf) => {
|
||||
// eslint-disable-next-line no-undef
|
||||
console.log("Config:", conf)
|
||||
return {
|
||||
default: {
|
||||
templates: ["examples/test-input/Component"],
|
||||
output: "examples/test-output",
|
||||
data: { property: "myProp", value: "10" },
|
||||
},
|
||||
component: {
|
||||
templates: ["examples/test-input/Component"],
|
||||
output: "examples/test-output/component",
|
||||
data: { property: "myProp", value: "10" },
|
||||
},
|
||||
configs: {
|
||||
templates: ["examples/test-input/**/.*"],
|
||||
output: "examples/test-output/configs",
|
||||
name: "---",
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,207 +1,91 @@
|
||||
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)
|
||||
})
|
||||
import { dateHelper, handlebarsParse, nowHelper } from "../src/utils"
|
||||
import { ScaffoldConfig } from "../src/types"
|
||||
import path from "path"
|
||||
import * as dateFns from "date-fns"
|
||||
|
||||
test("should resolve value", () => {
|
||||
expect(resolve(1, null)).toBe(1)
|
||||
expect(resolve(2, 1)).toBe(2)
|
||||
})
|
||||
const blankConf: ScaffoldConfig = {
|
||||
verbose: 0,
|
||||
name: "",
|
||||
output: "",
|
||||
templates: [],
|
||||
data: { name: "test" },
|
||||
}
|
||||
|
||||
test("should resolve function with argument transformation", () => {
|
||||
expect(resolve((x: number) => x * 2, 5)).toBe(10)
|
||||
describe("Utils", () => {
|
||||
describe("handlebarsParse", () => {
|
||||
let origSep: any
|
||||
describe("windows paths", () => {
|
||||
beforeAll(() => {
|
||||
origSep = path.sep
|
||||
Object.defineProperty(path, "sep", { value: "\\" })
|
||||
})
|
||||
afterAll(() => {
|
||||
Object.defineProperty(path, "sep", { value: origSep })
|
||||
})
|
||||
test("should work for windows paths", async () => {
|
||||
expect(handlebarsParse(blankConf, "C:\\exports\\{{name}}.txt", { isPath: true })).toEqual(
|
||||
Buffer.from("C:\\exports\\test.txt")
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
test("should resolve static string", () => {
|
||||
expect(resolve("hello", null)).toBe("hello")
|
||||
describe("non-windows paths", () => {
|
||||
beforeAll(() => {
|
||||
origSep = path.sep
|
||||
Object.defineProperty(path, "sep", { value: "/" })
|
||||
})
|
||||
afterAll(() => {
|
||||
Object.defineProperty(path, "sep", { value: origSep })
|
||||
})
|
||||
test("should work for non-windows paths", async () => {
|
||||
expect(handlebarsParse(blankConf, "/home/test/{{name}}.txt", { isPath: true })).toEqual(
|
||||
Buffer.from("/home/test/test.txt")
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
test("should resolve 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)
|
||||
test("should not do path escaping on non-path compiles", async () => {
|
||||
expect(
|
||||
handlebarsParse(
|
||||
{ ...blankConf, data: { ...blankConf.data, escaped: "value" } },
|
||||
"/home/test/{{name}} \\{{escaped}}.txt",
|
||||
{
|
||||
isPath: false,
|
||||
}
|
||||
)
|
||||
).toEqual(Buffer.from("/home/test/test {{escaped}}.txt"))
|
||||
})
|
||||
})
|
||||
|
||||
describe("handleErr", () => {
|
||||
test("should throw error", () => {
|
||||
expect(() => handleErr({ name: "test", message: "test" })).toThrow()
|
||||
expect(() => handleErr(null as never)).not.toThrow()
|
||||
})
|
||||
describe("Helpers", () => {
|
||||
describe("date helpers", () => {
|
||||
describe("now", () => {
|
||||
test("should work without extra params", () => {
|
||||
const now = new Date()
|
||||
const fmt = "yyyy-MM-dd HH:mm"
|
||||
|
||||
test("should throw the provided error", () => {
|
||||
const err = new Error("test error")
|
||||
expect(() => handleErr(err as unknown as NodeJS.ErrnoException)).toThrow("test error")
|
||||
})
|
||||
})
|
||||
expect(nowHelper(fmt)).toEqual(dateFns.format(now, fmt))
|
||||
})
|
||||
})
|
||||
|
||||
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")
|
||||
})
|
||||
describe("date", () => {
|
||||
test("should work with no offset params", () => {
|
||||
const now = new Date()
|
||||
const fmt = "yyyy-MM-dd HH:mm"
|
||||
|
||||
test("should return function as-is", () => {
|
||||
const fn = (x: string) => x.toUpperCase()
|
||||
const wrapped = wrapNoopResolver(fn)
|
||||
expect(wrapped).toBe(fn)
|
||||
})
|
||||
expect(dateHelper(now.toISOString(), fmt)).toEqual(dateFns.format(now, fmt))
|
||||
})
|
||||
|
||||
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 work with offset params", () => {
|
||||
const now = new Date()
|
||||
const fmt = "yyyy-MM-dd HH:mm"
|
||||
|
||||
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)
|
||||
expect(dateHelper(now.toISOString(), fmt, -1, "days")).toEqual(
|
||||
dateFns.format(dateFns.add(now, { days: -1 }), fmt)
|
||||
)
|
||||
expect(dateHelper(now.toISOString(), fmt, 1, "months")).toEqual(
|
||||
dateFns.format(dateFns.add(now, { months: 1 }), fmt)
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
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")
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,231 +0,0 @@
|
||||
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,22 +1,23 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"rootDir": "./src",
|
||||
"target": "ES2019",
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"esModuleInterop": true,
|
||||
"lib": ["ESNext"],
|
||||
"lib": [
|
||||
"ES2019",
|
||||
],
|
||||
"declaration": true,
|
||||
"outDir": "dist",
|
||||
"strict": true,
|
||||
"sourceMap": true,
|
||||
"removeComments": false,
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
},
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["src/index.ts", "src/cmd.ts"],
|
||||
"exclude": ["tests/*"]
|
||||
"include": [
|
||||
"src/index.ts",
|
||||
"src/cmd.ts",
|
||||
],
|
||||
"exclude": [
|
||||
"tests/*"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
const path = require("node:path")
|
||||
|
||||
/** @type {import('typedoc').TypeDocOptions} */
|
||||
module.exports = {
|
||||
name: "Simple Scaffold",
|
||||
entryPoints: ["src/index.ts"],
|
||||
includeVersion: true,
|
||||
categorizeByGroup: false,
|
||||
sort: ["visibility"],
|
||||
categoryOrder: ["Main", "*"],
|
||||
media: "media",
|
||||
githubPages: true,
|
||||
entryPointStrategy: "expand",
|
||||
out: "docs",
|
||||
excludePrivate: true,
|
||||
excludeProtected: true,
|
||||
excludeInternal: true,
|
||||
gaID: "GTM-KHQS9TQ",
|
||||
validation: {
|
||||
invalidLink: true,
|
||||
},
|
||||
plugin: ["@knodes/typedoc-plugin-pages"],
|
||||
customCss: "src/docs.css",
|
||||
options: "typedoc.config.js",
|
||||
logLevel: "Verbose",
|
||||
pluginPages: {
|
||||
logLevel: "Verbose",
|
||||
pages: [
|
||||
{
|
||||
name: "Configuration",
|
||||
source: "README.md",
|
||||
childrenDir: path.join(process.cwd(), "pages"),
|
||||
childrenOutputDir: "./",
|
||||
children: [
|
||||
{
|
||||
name: "CLI usage",
|
||||
source: "cli.md",
|
||||
},
|
||||
{
|
||||
name: "Node.js usage",
|
||||
source: "node.md",
|
||||
},
|
||||
{
|
||||
name: "Templates",
|
||||
source: "templates.md",
|
||||
},
|
||||
{
|
||||
name: "Configuration Files",
|
||||
source: "configuration_files.md",
|
||||
},
|
||||
{
|
||||
name: "Migrating v0.x to v1.x",
|
||||
source: "migration.md",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Changelog",
|
||||
source: "./CHANGELOG.md",
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
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"],
|
||||
},
|
||||
})
|
||||