mirror of
https://github.com/chenasraf/simple-scaffold.git
synced 2026-05-18 01:29:09 +00:00
Compare commits
121 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4d1a6e1f0d | ||
| daae9c10f9 | |||
| 758719de5d | |||
| 1903055d46 | |||
|
|
cbaf130a0c | ||
| e26fe2a3d4 | |||
| 7e1acf0607 | |||
|
|
f666c357f4 | ||
| f5d55f234a | |||
|
|
746f924a22 | ||
| 807c3e27e2 | |||
| b048841dac | |||
| 457c90470b | |||
|
|
0fa1ad4db7 | ||
|
|
d62eeeb8e4 | ||
|
|
565d1f33aa | ||
|
|
9f58fff2cf | ||
| dbba81d053 | |||
| 9f5716e1b9 | |||
|
|
4cbb79bb3b | ||
| 98ee00031f | |||
| 8c3369a00d | |||
| 20ef0cea9b | |||
|
|
3413151358 | ||
|
|
d2a2fda1b1 | ||
|
|
486c07a55b | ||
|
|
de05bca546 | ||
|
|
72d4cf58c5 | ||
|
|
2cf31e827e | ||
|
|
3714e8b3bd | ||
|
|
77be7c09d5 | ||
|
|
1246b51cda | ||
|
|
fea6c0fb16 | ||
|
|
2b7423993b | ||
|
|
a47ba1186c | ||
|
|
06b1552dca | ||
|
|
4868925511 | ||
|
|
79cfdbed38 | ||
|
|
4f27b7b934 | ||
|
|
bee430a40d | ||
|
|
3dfc920455 | ||
|
|
68307d1378 | ||
|
|
33e1d569a3 | ||
|
|
c446439b1a | ||
|
|
773fd00674 | ||
|
|
22763f655e | ||
|
|
a54b1f9fa7 | ||
|
|
dac5527173 | ||
|
|
339459cad8 | ||
|
|
62b4a1cd2f | ||
|
|
5844c080ec | ||
|
|
9393c546aa | ||
|
|
263bf0bf36 | ||
|
|
e0ed371adb | ||
|
|
87934fb3d1 | ||
|
|
19b7ed5f06 | ||
|
|
c027e37d9a | ||
|
|
2251a9c727 | ||
|
|
02a8ba16cd | ||
|
|
565090a951 | ||
|
|
943aad1564 | ||
|
|
9fb4762c7b | ||
|
|
be92047d65 | ||
|
|
0940e843ac | ||
|
|
a880bc9445 | ||
|
|
6c8eb02cbb | ||
|
|
08b048845f | ||
|
|
77e477e07f | ||
|
|
5ba6034b2f | ||
|
|
95dafdf839 | ||
|
|
4c3f1c97d1 | ||
|
|
1799971949 | ||
|
|
4bf96742b3 | ||
|
|
ce5adbe0f8 | ||
|
|
9489f14db2 | ||
|
|
99318f7654 | ||
|
|
81743f1fff | ||
|
|
d916d88384 | ||
|
|
260ce6a8f3 | ||
|
|
ba984b6621 | ||
|
|
7b6260caf8 | ||
|
|
2ca91a7109 | ||
|
|
7be79fdcaf | ||
|
|
6afbcdc258 | ||
|
|
1a3fd3d610 | ||
|
|
7f10db0d6e | ||
|
|
93f5b4a004 | ||
|
|
b74411ddb8 | ||
|
|
f961c13da1 | ||
|
|
05487f4d1e | ||
|
|
c50518a19c | ||
|
|
10ea6b4132 | ||
|
|
ce399181ab | ||
|
|
83d38073f3 | ||
|
|
6c5ba0bc91 | ||
|
|
2c4eccd800 | ||
|
|
2c23fa9dbb | ||
|
|
0bef2df5f3 | ||
|
|
398a5d723b | ||
|
|
c7e2ef862c | ||
|
|
36dd27e480 | ||
|
|
6e19a86190 | ||
|
|
eba7897f84 | ||
|
|
dfdbca59a7 | ||
|
|
1d6643b7eb | ||
|
|
9b86499a04 | ||
|
|
f41ebfb1d0 | ||
|
|
873fa77c9a | ||
|
|
1ce4a41a97 | ||
|
|
4302eb5ce3 | ||
|
|
146086869f | ||
|
|
8a2207bec5 | ||
|
|
42568e0fa1 | ||
|
|
a4498f9539 | ||
|
|
5a2b187115 | ||
|
|
3db6a918f1 | ||
|
|
c3989769fe | ||
|
|
b3f7912760 | ||
|
|
1e0b73190c | ||
|
|
0c8e6e7573 | ||
|
|
f28280e815 |
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -16,7 +16,7 @@ Steps to reproduce the behavior:
|
||||
|
||||
1. Prepare templates:
|
||||
|
||||
```txt
|
||||
```text
|
||||
This is my {{ template }}
|
||||
```
|
||||
|
||||
|
||||
25
.github/workflows/docs.yml
vendored
25
.github/workflows/docs.yml
vendored
@@ -1,20 +1,29 @@
|
||||
name: Build Documentation
|
||||
name: Documentation
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master, develop]
|
||||
branches: [master]
|
||||
jobs:
|
||||
docs:
|
||||
runs-on: ubuntu-latest
|
||||
if: "!contains(github.event.head_commit.message, '[skip ci]') && !contains(github.event.head_commit.message, '[skip docs]')"
|
||||
if: "contains(github.event.head_commit.message, 'chore(release)')"
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: "18.x"
|
||||
- run: yarn install --frozen-lockfile
|
||||
- run: yarn typedoc
|
||||
- uses: peaceiris/actions-gh-pages@v3
|
||||
- name: Install PNPM
|
||||
run: npm i -g pnpm
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: Build Docs
|
||||
run: pnpm build-docs
|
||||
- name: Deploy on GitHub Pages
|
||||
uses: peaceiris/actions-gh-pages@v3
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
publish_dir: ./docs
|
||||
|
||||
22
.github/workflows/pull_requests.yml
vendored
22
.github/workflows/pull_requests.yml
vendored
@@ -2,16 +2,24 @@ name: Pull Requests
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [master, develop]
|
||||
branches: [master, pre, develop]
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
if: "!contains(github.event.head_commit.message, '[skip ci]')"
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: "18.x"
|
||||
- run: yarn install --frozen-lockfile
|
||||
- run: yarn build
|
||||
- run: yarn test
|
||||
- name: Install PNPM
|
||||
run: npm i -g pnpm
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: Run Tests
|
||||
run: pnpm test
|
||||
- name: Build Package
|
||||
run: pnpm build
|
||||
|
||||
42
.github/workflows/release.yml
vendored
42
.github/workflows/release.yml
vendored
@@ -1,24 +1,40 @@
|
||||
name: Semantic Release
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master, develop, feat/*, fix/*]
|
||||
branches: [master, pre, develop]
|
||||
permissions:
|
||||
contents: read # for checkout
|
||||
jobs:
|
||||
build:
|
||||
release:
|
||||
name: Release
|
||||
runs-on: ubuntu-latest
|
||||
if: "!contains(github.event.head_commit.message, '[skip ci]')"
|
||||
permissions:
|
||||
contents: write # to be able to publish a GitHub release
|
||||
issues: write # to be able to comment on released issues
|
||||
pull-requests: write # to be able to comment on released pull requests
|
||||
id-token: write # to enable use of OIDC for npm provenance
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: "18.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]')"
|
||||
- run: yarn semantic-release
|
||||
if: "!contains(github.event.head_commit.message, '[skip publish]')"
|
||||
- name: Install PNPM
|
||||
run: npm i -g pnpm
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: Run Tests
|
||||
run: pnpm test
|
||||
- name: Build Package
|
||||
run: pnpm build
|
||||
- name: Pack
|
||||
run: cd ./dist && pnpm pack --pack-destination=../
|
||||
- name: Semantic Release
|
||||
run: npx semantic-release
|
||||
env:
|
||||
NPM_TOKEN: "${{ secrets.NPM_TOKEN }}"
|
||||
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
||||
|
||||
7
.vscode/settings.json
vendored
7
.vscode/settings.json
vendored
@@ -14,9 +14,6 @@
|
||||
"variabletoken"
|
||||
],
|
||||
"[markdown]": {
|
||||
"editor.rulers": [
|
||||
87,
|
||||
100
|
||||
],
|
||||
},
|
||||
"editor.rulers": [87, 100]
|
||||
}
|
||||
}
|
||||
|
||||
6
.vscode/tasks.json
vendored
6
.vscode/tasks.json
vendored
@@ -14,7 +14,7 @@
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"command": "yarn typedoc --watch",
|
||||
"command": "pnpm typedoc --watch",
|
||||
"label": "typedoc --watch",
|
||||
"type": "shell",
|
||||
"problemMatcher": []
|
||||
@@ -32,8 +32,8 @@
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"command": "yarn test --watchAll",
|
||||
"label": "yarn test --watchAll",
|
||||
"command": "pnpm test --watchAll",
|
||||
"label": "pnpm test --watchAll",
|
||||
"type": "shell",
|
||||
"problemMatcher": []
|
||||
},
|
||||
|
||||
169
CHANGELOG.md
169
CHANGELOG.md
@@ -1,34 +1,153 @@
|
||||
# Change Log
|
||||
|
||||
## [1.2.0](https://github.com/chenasraf/simple-scaffold/compare/v1.1.4...v1.2.0) (2023-04-24)
|
||||
# [1.9.0](https://github.com/chenasraf/simple-scaffold/compare/v1.8.0...v1.9.0) (2024-01-02)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* append-data cli flag ([3c5c2de](https://github.com/chenasraf/simple-scaffold/commit/3c5c2ded02f61ff086e81ea4a7f40529bdff1c9d))
|
||||
* add --recurse-submodules to git clone ([cbaf130](https://github.com/chenasraf/simple-scaffold/commit/cbaf130a0cce45a2d20a3c2d132ac98e2f105b4f))
|
||||
|
||||
## [1.8.0](https://github.com/chenasraf/simple-scaffold/compare/v1.7.2...v1.8.0) (2023-11-29)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* ci node version ([767d34c](https://github.com/chenasraf/simple-scaffold/commit/767d34c684516d4cea865b25e87c27c779bb79ce))
|
||||
* semantic-release build dir ([f7956dd](https://github.com/chenasraf/simple-scaffold/commit/f7956ddc786018905c48ccf1f21a3bb4657c3d75))
|
||||
* support quote wrapping in append-data ([4fecca8](https://github.com/chenasraf/simple-scaffold/commit/4fecca848347312d45d704f82f2bcb3822da9b06))
|
||||
* **config:** fn config load ([457c904](https://github.com/chenasraf/simple-scaffold/commit/457c90470b0f138862469ff878c7e061c7afd18a)), closes [#63](https://github.com/chenasraf/simple-scaffold/issues/63)
|
||||
|
||||
### [1.1.4](https://github.com/chenasraf/simple-scaffold/compare/v1.1.3...v1.1.4) (2023-04-24)
|
||||
## [1.8.0-pre.1](https://github.com/chenasraf/simple-scaffold/compare/v1.7.2...v1.8.0-pre.1) (2023-11-27)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **config:** fn config load ([457c904](https://github.com/chenasraf/simple-scaffold/commit/457c90470b0f138862469ff878c7e061c7afd18a)), closes [#63](https://github.com/chenasraf/simple-scaffold/issues/63)
|
||||
|
||||
## [1.7.2](https://github.com/chenasraf/simple-scaffold/compare/v1.7.1...v1.7.2) (2023-08-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* windows path resolution ([98ee000](https://github.com/chenasraf/simple-scaffold/commit/98ee00031fc1ad67a53797a9e28e5c4759bc8bce))
|
||||
|
||||
## [1.7.2-pre.1](https://github.com/chenasraf/simple-scaffold/compare/v1.7.1...v1.7.2-pre.1) (2023-08-15)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* windows path resolution ([98ee000](https://github.com/chenasraf/simple-scaffold/commit/98ee00031fc1ad67a53797a9e28e5c4759bc8bce))
|
||||
|
||||
## [1.7.1](https://github.com/chenasraf/simple-scaffold/compare/v1.7.0...v1.7.1) (2023-06-07)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- local config file load error
|
||||
([2b74239](https://github.com/chenasraf/simple-scaffold/commit/2b7423993be06b2375631642455c801ae2acf75f))
|
||||
|
||||
## [1.7.0](https://github.com/chenasraf/simple-scaffold/compare/v1.7.0-develop.5...v1.7.0) (2023-05-17)
|
||||
|
||||
## [1.7.0-develop.7](https://github.com/chenasraf/simple-scaffold/compare/v1.7.0-develop.6...v1.7.0-develop.7) (2023-06-07)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- local config file load error
|
||||
([2b74239](https://github.com/chenasraf/simple-scaffold/commit/2b7423993be06b2375631642455c801ae2acf75f))
|
||||
|
||||
## [1.7.0-develop.1](https://github.com/chenasraf/simple-scaffold/compare/v1.6.0...v1.7.0-develop.1) (2023-05-09)
|
||||
|
||||
### Features
|
||||
|
||||
- function config file
|
||||
([02a8ba1](https://github.com/chenasraf/simple-scaffold/commit/02a8ba16cd6ee31806532845cb5ddbe0f5abf7de))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- use path.normalize
|
||||
([565090a](https://github.com/chenasraf/simple-scaffold/commit/565090a951e13dd222f2f802df717e7cb6ca0a73))
|
||||
|
||||
## [1.6.0](https://github.com/chenasraf/simple-scaffold/compare/v1.6.0-develop.1...v1.6.0) (2023-05-05)
|
||||
|
||||
## [1.6.0-develop.1](https://github.com/chenasraf/simple-scaffold/compare/v1.5.0...v1.6.0-develop.1) (2023-05-04)
|
||||
|
||||
### Features
|
||||
|
||||
- node.js function for remote configs
|
||||
([ce5adbe](https://github.com/chenasraf/simple-scaffold/commit/ce5adbe0f898a86db6046d7f66d83dfcaa519ad2))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- move dependency to dev dependency
|
||||
([d916d88](https://github.com/chenasraf/simple-scaffold/commit/d916d88384054e6c6b40e6299073f1d1acb4d29d))
|
||||
|
||||
## [1.5.0](https://github.com/chenasraf/simple-scaffold/compare/v1.5.0-develop.1...v1.5.0) (2023-05-02)
|
||||
|
||||
## [1.5.0-develop.1](https://github.com/chenasraf/simple-scaffold/compare/v1.4.0...v1.5.0-develop.1) (2023-05-02)
|
||||
|
||||
### Features
|
||||
|
||||
- add github remote templates
|
||||
([f961c13](https://github.com/chenasraf/simple-scaffold/commit/f961c13da15320b42540773ed958cdc3f97e4502))
|
||||
- support for remote template configs
|
||||
([05487f4](https://github.com/chenasraf/simple-scaffold/commit/05487f4d1e3b05f1d695242bb54427ee2fbdf247))
|
||||
|
||||
## [1.4.0](https://github.com/chenasraf/simple-scaffold/compare/v1.3.2...v1.4.0) (2023-04-28)
|
||||
|
||||
### Features
|
||||
|
||||
- add `--key` | `-k` to config loader
|
||||
([6c5ba0b](https://github.com/chenasraf/simple-scaffold/commit/6c5ba0bc916fb1d59240d2eaa1abedc74527a974))
|
||||
|
||||
## [1.3.2](https://github.com/chenasraf/simple-scaffold/compare/v1.3.1...v1.3.2) (2023-04-28)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- release build
|
||||
([2c23fa9](https://github.com/chenasraf/simple-scaffold/commit/2c23fa9dbb310cd0a31f09606798f96b95d66779))
|
||||
- release build asset
|
||||
([0bef2df](https://github.com/chenasraf/simple-scaffold/commit/0bef2df5f3aa800ad5f1094c0996108db9acce51))
|
||||
|
||||
## [1.3.1](https://github.com/chenasraf/simple-scaffold/compare/v1.3.0...v1.3.1) (2023-04-28)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- docs
|
||||
([6e19a86](https://github.com/chenasraf/simple-scaffold/commit/6e19a86190dd924058a48448aa6463569ef1125f))
|
||||
- remove old peer-dep
|
||||
([c7e2ef8](https://github.com/chenasraf/simple-scaffold/commit/c7e2ef862cb658feb1071ac120b185d8b34d6dd3))
|
||||
|
||||
## [1.3.0](https://github.com/chenasraf/simple-scaffold/compare/v1.2.0...v1.3.0) (2023-04-25)
|
||||
|
||||
### Features
|
||||
|
||||
- load scaffold config from files
|
||||
([c398976](https://github.com/chenasraf/simple-scaffold/commit/c3989769fee445c9183ff5e5b3892c4e9fb66a9e))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- config option should not be mandatory
|
||||
([3db6a91](https://github.com/chenasraf/simple-scaffold/commit/3db6a918f13d9300efa2fcb4a356d004475ab91c))
|
||||
- export config file type
|
||||
([4302eb5](https://github.com/chenasraf/simple-scaffold/commit/4302eb5ce35ed6cf1dc80dfb92790c3fdd96f963))
|
||||
|
||||
## [1.2.0](https://github.com/chenasraf/simple-scaffold/compare/v1.1.4...v1.2.0) (2023-04-24)
|
||||
|
||||
### Features
|
||||
|
||||
- append-data cli flag
|
||||
([2f4f7fc](https://github.com/chenasraf/simple-scaffold/commit/2f4f7fce47669e37ff49154fbec2acdfd0f5860e))
|
||||
([3c5c2de](https://github.com/chenasraf/simple-scaffold/commit/3c5c2ded02f61ff086e81ea4a7f40529bdff1c9d))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- ci node version
|
||||
([767d34c](https://github.com/chenasraf/simple-scaffold/commit/767d34c684516d4cea865b25e87c27c779bb79ce))
|
||||
- github action node version
|
||||
([7c19c53](https://github.com/chenasraf/simple-scaffold/commit/7c19c533376dc6904231e5cc51c7a4b2658c66e0))
|
||||
- github action node version
|
||||
([94fec76](https://github.com/chenasraf/simple-scaffold/commit/94fec766165f7540c578dbf2d0aeeb6ea3969ad8))
|
||||
- semantic-release build dir
|
||||
([f7956dd](https://github.com/chenasraf/simple-scaffold/commit/f7956ddc786018905c48ccf1f21a3bb4657c3d75))
|
||||
- support quote wrapping in append-data
|
||||
([4fecca8](https://github.com/chenasraf/simple-scaffold/commit/4fecca848347312d45d704f82f2bcb3822da9b06))
|
||||
|
||||
### [1.1.3](https://github.com/chenasraf/simple-scaffold/compare/v1.1.2...v1.1.3) (2023-03-11)
|
||||
## [1.1.3](https://github.com/chenasraf/simple-scaffold/compare/v1.1.2...v1.1.3) (2023-03-11)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -39,19 +158,19 @@
|
||||
|
||||
## [1.1.0](https://github.com/chenasraf/simple-scaffold/compare/v1.0.4...v1.1.0) (2022-04-21)
|
||||
|
||||
### [1.0.3](https://github.com/chenasraf/simple-scaffold/compare/v1.0.2...v1.0.3) (2022-03-03)
|
||||
## [1.0.3](https://github.com/chenasraf/simple-scaffold/compare/v1.0.2...v1.0.3) (2022-03-03)
|
||||
|
||||
### [1.0.1-pre.1](https://github.com/chenasraf/simple-scaffold/compare/v1.0.0...v1.0.1-pre.1) (2022-02-17)
|
||||
## [1.0.1-pre.1](https://github.com/chenasraf/simple-scaffold/compare/v1.0.0...v1.0.1-pre.1) (2022-02-17)
|
||||
|
||||
### [0.7.5](https://github.com/chenasraf/simple-scaffold/compare/v0.7.4...v0.7.5) (2021-09-26)
|
||||
## [0.7.5](https://github.com/chenasraf/simple-scaffold/compare/v0.7.4...v0.7.5) (2021-09-26)
|
||||
|
||||
### [0.7.4](https://github.com/chenasraf/simple-scaffold/compare/v0.7.3...v0.7.4) (2021-09-26)
|
||||
## [0.7.4](https://github.com/chenasraf/simple-scaffold/compare/v0.7.3...v0.7.4) (2021-09-26)
|
||||
|
||||
### [0.7.3](https://github.com/chenasraf/simple-scaffold/compare/v0.7.2...v0.7.3) (2021-09-26)
|
||||
## [0.7.3](https://github.com/chenasraf/simple-scaffold/compare/v0.7.2...v0.7.3) (2021-09-26)
|
||||
|
||||
### [0.7.2](https://github.com/chenasraf/simple-scaffold/compare/v0.6.1...v0.7.2) (2021-04-19)
|
||||
## [0.7.2](https://github.com/chenasraf/simple-scaffold/compare/v0.6.1...v0.7.2) (2021-04-19)
|
||||
|
||||
### [0.6.1](https://github.com/chenasraf/simple-scaffold/compare/v0.6.0...v0.6.1) (2021-02-01)
|
||||
## [0.6.1](https://github.com/chenasraf/simple-scaffold/compare/v0.6.0...v0.6.1) (2021-02-01)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -67,26 +186,26 @@
|
||||
|
||||
## [0.5.0](https://github.com/chenasraf/simple-scaffold/compare/v0.4.5...v0.5.0) (2019-02-27)
|
||||
|
||||
### [0.4.5](https://github.com/chenasraf/simple-scaffold/compare/v0.4.4...v0.4.5) (2019-02-27)
|
||||
## [0.4.5](https://github.com/chenasraf/simple-scaffold/compare/v0.4.4...v0.4.5) (2019-02-27)
|
||||
|
||||
### [0.4.4](https://github.com/chenasraf/simple-scaffold/compare/v0.4.3...v0.4.4) (2019-02-27)
|
||||
## [0.4.4](https://github.com/chenasraf/simple-scaffold/compare/v0.4.3...v0.4.4) (2019-02-27)
|
||||
|
||||
### [0.4.3](https://github.com/chenasraf/simple-scaffold/compare/v0.4.2...v0.4.3) (2019-02-27)
|
||||
## [0.4.3](https://github.com/chenasraf/simple-scaffold/compare/v0.4.2...v0.4.3) (2019-02-27)
|
||||
|
||||
### [0.4.2](https://github.com/chenasraf/simple-scaffold/compare/v0.4.1...v0.4.2) (2019-02-25)
|
||||
## [0.4.2](https://github.com/chenasraf/simple-scaffold/compare/v0.4.1...v0.4.2) (2019-02-25)
|
||||
|
||||
### [0.4.1](https://github.com/chenasraf/simple-scaffold/compare/v0.3.1...v0.4.1) (2019-02-25)
|
||||
## [0.4.1](https://github.com/chenasraf/simple-scaffold/compare/v0.3.1...v0.4.1) (2019-02-25)
|
||||
|
||||
### [0.3.1](https://github.com/chenasraf/simple-scaffold/compare/v0.3.0...v0.3.1) (2018-01-15)
|
||||
## [0.3.1](https://github.com/chenasraf/simple-scaffold/compare/v0.3.0...v0.3.1) (2018-01-15)
|
||||
|
||||
## [0.3.0](https://github.com/chenasraf/simple-scaffold/compare/v0.2.0...v0.3.0) (2018-01-15)
|
||||
|
||||
## [0.2.0](https://github.com/chenasraf/simple-scaffold/compare/v0.1.5...v0.2.0) (2018-01-05)
|
||||
|
||||
### [0.1.5](https://github.com/chenasraf/simple-scaffold/compare/v0.1.4...v0.1.5) (2018-01-01)
|
||||
## [0.1.5](https://github.com/chenasraf/simple-scaffold/compare/v0.1.4...v0.1.5) (2018-01-01)
|
||||
|
||||
### [0.1.4](https://github.com/chenasraf/simple-scaffold/compare/v0.1.3...v0.1.4) (2018-01-01)
|
||||
## [0.1.4](https://github.com/chenasraf/simple-scaffold/compare/v0.1.3...v0.1.4) (2018-01-01)
|
||||
|
||||
### [0.1.3](https://github.com/chenasraf/simple-scaffold/compare/v0.1.2...v0.1.3) (2018-01-01)
|
||||
## [0.1.3](https://github.com/chenasraf/simple-scaffold/compare/v0.1.2...v0.1.3) (2018-01-01)
|
||||
|
||||
### 0.1.2 (2018-01-01)
|
||||
## 0.1.2 (2018-01-01)
|
||||
|
||||
441
README.md
441
README.md
@@ -3,21 +3,25 @@
|
||||
<h2 align="center">
|
||||
|
||||
[GitHub](https://github.com/chenasraf/simple-scaffold) |
|
||||
[Documentation](https://casraf.dev/simple-scaffold) |
|
||||
[Documentation](https://chenasraf.github.io/simple-scaffold) |
|
||||
[NPM](https://npmjs.com/package/simple-scaffold) | [casraf.dev](https://casraf.dev)
|
||||
|
||||

|
||||

|
||||
|
||||
</h2>
|
||||
Generate any set of files in the easiest way possible with simple commands.
|
||||
|
||||
It is completely agnostic and un-opinionated so you can use it for anything from a few simple files
|
||||
to an entire app boilerplate setup.
|
||||
Looking to streamline your workflow and get your projects up and running quickly? Look no further
|
||||
than Simple Scaffold - the easy-to-use NPM package that simplifies the process of organizing and
|
||||
copying your commonly-created files.
|
||||
|
||||
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.
|
||||
With its agnostic and un-opinionated approach, Simple Scaffold can handle anything from a few simple
|
||||
files to an entire app boilerplate setup. Plus, with the power of **Handlebars.js** syntax, you can
|
||||
easily replace custom data and personalize your files to fit your exact needs. But that's not all -
|
||||
you can also use it to loop through data, use conditions, and write custom functions using helpers.
|
||||
|
||||
Don't waste any more time manually copying and pasting files - let Simple Scaffold do the heavy
|
||||
lifting for you and start building your projects faster and more efficiently today!
|
||||
|
||||
<div align="center">
|
||||
|
||||
@@ -25,385 +29,130 @@ name, or other custom data) inside the paths or contents of the files using Hand
|
||||
|
||||
</div>
|
||||
|
||||
<br />
|
||||
|
||||
<details>
|
||||
<summary>Table of contents</summary>
|
||||
|
||||
- [Install](#install)
|
||||
- [Command Line Interface (CLI)](#command-line-interface-cli)
|
||||
- [Available flags](#available-flags)
|
||||
- [Node module](#node-module)
|
||||
- [Node-specific options](#node-specific-options)
|
||||
- [Preparing files](#preparing-files)
|
||||
- [Template files](#template-files)
|
||||
- [Variable/token replacement](#variabletoken-replacement)
|
||||
- [Helpers](#helpers)
|
||||
- [Built-in Helpers](#built-in-helpers)
|
||||
- [Capitalization Helpers](#capitalization-helpers)
|
||||
- [Date helpers](#date-helpers)
|
||||
- [Custom Helpers](#custom-helpers)
|
||||
- [Examples](#examples)
|
||||
- [Run](#run)
|
||||
- [Command Example](#command-example)
|
||||
- [Equivalent Node Module Example](#equivalent-node-module-example)
|
||||
- [Files](#files)
|
||||
- [Input](#input)
|
||||
- [Output](#output)
|
||||
- [Contributing](#contributing)
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
## Install
|
||||
## Quick Start
|
||||
|
||||
You can either use it as a command line tool or import into your own code and run from there.
|
||||
### Local Templates
|
||||
|
||||
```bash
|
||||
# npm
|
||||
npm install [-g] simple-scaffold
|
||||
# yarn
|
||||
yarn [global] add simple-scaffold
|
||||
# run without installing
|
||||
npx simple-scaffold@latest <...args>
|
||||
```
|
||||
The fastest way to get started is to use `npx` to immediately start a scaffold process.
|
||||
|
||||
## Command Line Interface (CLI)
|
||||
Prepare any templates you want to use - for example, in the directory `templates/component`; and use
|
||||
that in the CLI args. Here is a simple example file:
|
||||
|
||||
### Available flags
|
||||
Simple Scaffold will maintain any file and directory structure you try to generate.
|
||||
|
||||
The following is the help text from the `simple-scaffold` binary. To see this and more information
|
||||
anytime, add the `-h` or `--help` flag to your call, e.g. `npx simple-scaffold@latest -h`.
|
||||
`templates/component/{{ pascalName name }}.tsx`
|
||||
|
||||
```text
|
||||
Usage: simple-scaffold [options]
|
||||
```tsx
|
||||
// Created: {{ now 'yyyy-MM-dd' }}
|
||||
import React from 'react'
|
||||
|
||||
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.
|
||||
|
||||
--append-data|-D Append additional custom data to the templates, which
|
||||
will overwrite --data, using an alternate syntax, which is
|
||||
easier to use with CLI: -D key1=string -D key2:=raw
|
||||
|
||||
--create-sub-folder|-s Create subfolder with the input name (default: false)
|
||||
|
||||
--sub-folder-name-helper|-sh Default helper to apply to subfolder name when using
|
||||
`--create-sub-folder true`.
|
||||
|
||||
--quiet|-q Suppress output logs (Same as --verbose 0)
|
||||
(default: false)
|
||||
|
||||
--verbose|-v Determine amount of logs to display. The values are:
|
||||
0 (none) | 1 (debug) | 2 (info) | 3 (warn) | 4
|
||||
(error). The provided level will display messages of
|
||||
the same level or higher. (default:
|
||||
2)
|
||||
|
||||
--dry-run|-dr Don't emit files. This is good for testing your
|
||||
scaffolds and making sure they don't fail, without
|
||||
having to write actual file contents or create
|
||||
directories. (default: false)
|
||||
```
|
||||
|
||||
You can also add this as a script in your `package.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"scaffold": "npx simple-scaffold@latest -t scaffolds/component/**/* -o src/components -d '{\"myProp\": \"propName\", \"myVal\": 123}'"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Node module
|
||||
|
||||
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)
|
||||
```
|
||||
|
||||
### Node-specific options
|
||||
|
||||
In addition to all the options available in the command line, there are some Node/JS-specific
|
||||
options available:
|
||||
|
||||
| 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, allowing you to add more pre-processing to your generator pipeline. 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. |
|
||||
| `append-data` | | Does not exist in Node.JS options, use `data` instead. |
|
||||
|
||||
## 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 | Files in template | Output path(s) |
|
||||
| ----------------------------- | ------------------------------------------------------ | ------------------------------------------------------------ |
|
||||
| `./templates/{{ name }}.txt` | `./templates/{{ name }}.txt` | `src/AppName.txt` |
|
||||
| `./templates/directory` | `outer/{{name}}.txt`,<br />`outer2/inner/{{name}}.txt` | `src/outer/AppName.txt`,<br />`src/outer2/inner/AppName.txt` |
|
||||
| `./templates/others/**/*.txt` | `outer/{{name}}.jpg`,<br />`outer2/inner/{{name}}.txt` | `src/outer2/inner/AppName.txt` |
|
||||
|
||||
### Variable/token replacement
|
||||
|
||||
Scaffolding will replace `{{ varName }}` in both the file name and its contents and put the
|
||||
transformed files in the output directory.
|
||||
|
||||
The data available for the template parser is the data you pass to the `data` config option (or
|
||||
`--data` argument in CLI).
|
||||
|
||||
For example, using the following command:
|
||||
|
||||
```bash
|
||||
npx simple-scaffold@latest \
|
||||
--templates templates/components/{{name}}.jsx \
|
||||
--output src/components \
|
||||
--create-sub-folder true \
|
||||
MyComponent
|
||||
```
|
||||
|
||||
Will output a file with the path:
|
||||
|
||||
```text
|
||||
<working_dir>/src/components/MyComponent.jsx
|
||||
```
|
||||
|
||||
The contents of the file will be transformed in a similar fashion.
|
||||
|
||||
Your `data` will be pre-populated with the following:
|
||||
|
||||
- `{{Name}}`: PascalCase of the component name
|
||||
- `{{name}}`: raw name of the component as you entered it
|
||||
|
||||
> Simple-Scaffold uses [Handlebars.js](https://handlebarsjs.com/) for outputting the file contents.
|
||||
> Any `data` you add in the config will be available for use with their names wrapped in `{{` and
|
||||
> `}}`. Other Handlebars built-ins such as `each`, `if` and `with` are also supported, see
|
||||
> [Handlebars.js Language Features](https://handlebarsjs.com/guide/#language-features) for more
|
||||
> information.
|
||||
|
||||
### Helpers
|
||||
|
||||
#### Built-in Helpers
|
||||
|
||||
Simple-Scaffold provides some built-in text transformation filters usable by Handlebars.
|
||||
|
||||
For example, you may use `{{ snakeCase name }}` inside a template file or filename, and it will
|
||||
replace `My Name` with `my_name` when producing the final value.
|
||||
|
||||
##### Capitalization Helpers
|
||||
|
||||
| Helper name | Example code | Example output |
|
||||
| ------------ | ----------------------- | -------------- |
|
||||
| [None] | `{{ name }}` | my name |
|
||||
| `camelCase` | `{{ camelCase name }}` | myName |
|
||||
| `snakeCase` | `{{ snakeCase name }}` | my_name |
|
||||
| `startCase` | `{{ startCase name }}` | My Name |
|
||||
| `kebabCase` | `{{ kebabCase name }}` | my-name |
|
||||
| `hyphenCase` | `{{ hyphenCase name }}` | my-name |
|
||||
| `pascalCase` | `{{ pascalCase name }}` | MyName |
|
||||
| `upperCase` | `{{ upperCase name }}` | MY NAME |
|
||||
| `lowerCase` | `{{ lowerCase name }}` | my name |
|
||||
|
||||
##### Date helpers
|
||||
|
||||
| Helper name | Description | Example code | Example output |
|
||||
| -------------------------------- | ---------------------------------------------------------------- | ---------------------------------------------------------------- | ------------------ |
|
||||
| `now` | Current date with format | `{{ now "yyyy-MM-dd HH:mm" }}` | `2042-01-01 15:00` |
|
||||
| `now` (with offset) | Current date with format, and with offset | `{{ now "yyyy-MM-dd HH:mm" -1 "hours" }}` | `2042-01-01 14:00` |
|
||||
| `date` | Custom date with format | `{{ date "2042-01-01T15:00:00Z" "yyyy-MM-dd HH:mm" }}` | `2042-01-01 15:00` |
|
||||
| `date` (with offset) | Custom date with format, and with offset | `{{ date "2042-01-01T15:00:00Z" "yyyy-MM-dd HH:mm" -1 "days" }}` | `2041-31-12 15:00` |
|
||||
| `date` (with date from `--data`) | Custom date with format, with data from the `data` config option | `{{ date myCustomDate "yyyy-MM-dd HH:mm" }}` | `2042-01-01 12:00` |
|
||||
|
||||
Further details:
|
||||
|
||||
- We use [`date-fns`](https://date-fns.org/docs/) for parsing/manipulating the dates. If you want
|
||||
more information on the date tokens to use, refer to
|
||||
[their format documentation](https://date-fns.org/docs/format).
|
||||
|
||||
- The date helper format takes the following arguments:
|
||||
|
||||
```typescript
|
||||
(
|
||||
date: string,
|
||||
format: string,
|
||||
offsetAmount?: number,
|
||||
offsetType?: "years" | "months" | "weeks" | "days" | "hours" | "minutes" | "seconds"
|
||||
export default {{pascalCase name}}: React.FC = (props) => {
|
||||
return (
|
||||
<div className="{{camelCase name}}">{{pascalCase name}} Component</div>
|
||||
)
|
||||
```
|
||||
|
||||
- **The now helper** (for current time) takes the same arguments, minus the first one (`date`) as it
|
||||
is implicitly the current date.
|
||||
|
||||
#### Custom Helpers
|
||||
|
||||
You may also add your own custom helpers using the `helpers` options when using the JS API (rather
|
||||
than the CLI). The `helpers` option takes an object whose keys are helper names, and values are the
|
||||
transformation functions. For example, `upperCase` is implemented like so:
|
||||
|
||||
```typescript
|
||||
config.helpers = {
|
||||
upperCase: (text) => text.toUpperCase(),
|
||||
}
|
||||
```
|
||||
|
||||
All of the above helpers (built in and custom) will also be available to you when using
|
||||
`subFolderNameHelper` (`--sub-folder-name-helper`/`-sh`) as a possible value.
|
||||
To generate the template output, run:
|
||||
|
||||
> To see more information on how helpers work and more features, see
|
||||
> [Handlebars.js docs](https://handlebarsjs.com/guide/#custom-helpers).
|
||||
|
||||
## Examples
|
||||
|
||||
### Run
|
||||
|
||||
#### Command Example
|
||||
|
||||
```bash
|
||||
simple-scaffold MyComponent \
|
||||
-t project/scaffold/**/* \
|
||||
-o src/components \
|
||||
-d '{"className": "myClassName","author": "Chen Asraf"}'
|
||||
MyComponent
|
||||
```shell
|
||||
# generate single component
|
||||
$ npx simple-scaffold@latest \
|
||||
-t templates/component -o src/components PageWrapper
|
||||
```
|
||||
|
||||
#### Equivalent Node Module Example
|
||||
This will immediately create the following file: `src/components/PageWrapper.tsx`
|
||||
|
||||
```typescript
|
||||
import Scaffold from "simple-scaffold"
|
||||
```tsx
|
||||
// Created: 2077-01-01
|
||||
import React from 'react'
|
||||
|
||||
async function main() {
|
||||
await Scaffold({
|
||||
name: "MyComponent",
|
||||
templates: ["project/scaffold/**/*"],
|
||||
output: ["src/components"],
|
||||
export default PageWrapper: React.FC = (props) => {
|
||||
return (
|
||||
<div className="pageWrapper">PageWrapper Component</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Configuration Files
|
||||
|
||||
You can also use a config file to more easily maintain all your scaffold definitions.
|
||||
|
||||
`scaffold.config.js`
|
||||
|
||||
```js
|
||||
module.exports = {
|
||||
// use "default" to avoid needing to specify key
|
||||
// in this case the key is "component"
|
||||
component: {
|
||||
templates: ["templates/component"],
|
||||
output: "src/components",
|
||||
data: {
|
||||
className: "myClassName",
|
||||
author: "Chen Asraf",
|
||||
// ...
|
||||
},
|
||||
})
|
||||
console.log("Done.")
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### Files
|
||||
Then call your scaffold like this:
|
||||
|
||||
#### Input
|
||||
```shell
|
||||
$ npx simple-scaffold@latest -c scaffold.config.js PageWrapper
|
||||
```
|
||||
|
||||
- Input file path:
|
||||
This will allow you to avoid needing to remember which configs are needed or to store them in a
|
||||
1-liner in `packqge.json` which can get pretty long and messy, which is harder to maintain.
|
||||
|
||||
```text
|
||||
project → scaffold → {{Name}}.js → src → components
|
||||
```
|
||||
Also, this allows you to define more complex scaffolds with logic without having to use the Node.js
|
||||
API directly. (Of course you always have the option to still do so if you wish)
|
||||
|
||||
- Input file contents:
|
||||
See more at the [CLI documentation](https://chenasraf.github.io/simple-scaffold/pages/cli.html) and
|
||||
[Configuration Files](https://chenasraf.github.io/simple-scaffold/pages/configuration_files.html).
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* Author: {{ author }}
|
||||
* Date: {{ now "yyyy-MM-dd" }}
|
||||
*/
|
||||
import React from 'react'
|
||||
### Remote Configurations
|
||||
|
||||
export default {{camelCase name}}: React.FC = (props) => {
|
||||
return (
|
||||
<div className="{{className}}">{{camelCase name}} Component</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
Another quick way to start is to re-use someone else's (or your own) work using a template
|
||||
repository.
|
||||
|
||||
#### Output
|
||||
A remote config can be loaded in one of these ways:
|
||||
|
||||
- Output file path:
|
||||
- If it's on GitHub, you can use `-gh user/repository_name`
|
||||
- If it's on another git server (such as GitLab), you can use
|
||||
`-c https://example.com/user/repository_name.git`
|
||||
|
||||
- With `createSubFolder = false` (default):
|
||||
Configurations can hold multiple scaffold groups. Each group can be accessed using its key by
|
||||
supplying the `--key` or `-k` argument, or by appending a hash and then the key name, like so:
|
||||
`-gh user/repository_name#key_name` - this also works for the `-c` flag.
|
||||
|
||||
```text
|
||||
project → src → components → MyComponent.js
|
||||
```
|
||||
Here is an example for loading the example component templates in this very repository:
|
||||
|
||||
- With `createSubFolder = true`:
|
||||
```shell
|
||||
$ npx simple-scaffold@latest \
|
||||
-gh chenasraf/simple-scaffold#scaffold.config.js:component \
|
||||
PageWrapper
|
||||
|
||||
```text
|
||||
project → src → components → MyComponent → MyComponent.js
|
||||
```
|
||||
# equivalent to:
|
||||
$ npx simple-scaffold@latest \
|
||||
-c https://github.com/chenasraf/simple-scaffold.git#scaffold.config.js:component \
|
||||
PageWrapper
|
||||
```
|
||||
|
||||
- With `createSubFolder = true` and `subFolderNameHelper = 'upperCase'`:
|
||||
When template name (`:component`) is omitted, `default` is used.
|
||||
|
||||
```text
|
||||
project → src → components → MYCOMPONENT → MyComponent.js
|
||||
```
|
||||
See more at the [CLI documentation](https://chenasraf.github.io/simple-scaffold/pages/cli.html) and
|
||||
[Configuration Files](https://chenasraf.github.io/simple-scaffold/pages/configuration_files.html).
|
||||
|
||||
- Output file contents:
|
||||
## Documentation
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* Author: Chen Asraf
|
||||
* Date: 2022-01-01
|
||||
*/
|
||||
import React from 'react'
|
||||
See full documentation [here](https://chenasraf.github.io/simple-scaffold).
|
||||
|
||||
export default MyComponent: React.FC = (props) => {
|
||||
return (
|
||||
<div className="myClassName">MyComponent Component</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
- [Command Line Interface (CLI) usage](https://chenasraf.github.io/simple-scaffold/pages/cli.html)
|
||||
- [Node.js usage](https://chenasraf.github.io/simple-scaffold/pages/node.html)
|
||||
- [Templates](https://chenasraf.github.io/simple-scaffold/pages/templates.html)
|
||||
- [Configuration Files](https://chenasraf.github.io/simple-scaffold/pages/configuration_files.html)
|
||||
- [Migrating v0.x to v1.x](https://chenasraf.github.io/simple-scaffold/pages/migration.html)
|
||||
|
||||
## Contributing
|
||||
|
||||
|
||||
5
examples/.dotdir/README.md
Executable file
5
examples/.dotdir/README.md
Executable file
@@ -0,0 +1,5 @@
|
||||
# {{ name }} Readme
|
||||
|
||||
TO DO:
|
||||
|
||||
- [ ] ...
|
||||
@@ -195,7 +195,7 @@ export default {
|
||||
// unmockedModulePathPatterns: undefined,
|
||||
|
||||
// Indicates whether each individual test should be reported during the run
|
||||
// verbose: undefined,
|
||||
verbose: true,
|
||||
|
||||
// An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
|
||||
// watchPathIgnorePatterns: [],
|
||||
|
||||
6
nodemon.json
Normal file
6
nodemon.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"ignore": ["**/*.test.ts", "**/*.spec.ts", ".git", "node_modules"],
|
||||
"watch": ["src"],
|
||||
"exec": "node -r tsconfig-paths/register -r ts-node/register ./src/index.ts",
|
||||
"ext": "ts, js"
|
||||
}
|
||||
43
package.json
43
package.json
@@ -1,13 +1,14 @@
|
||||
{
|
||||
"name": "simple-scaffold",
|
||||
"version": "1.2.0",
|
||||
"description": "A simple command to generate any file structure, from single components to entire app boilerplates.",
|
||||
"homepage": "https://casraf.dev/simple-scaffold",
|
||||
"version": "1.9.0",
|
||||
"description": "Generate any file structure - from single components to entire app boilerplates, with a single command.",
|
||||
"homepage": "https://chenasraf.github.io/simple-scaffold",
|
||||
"repository": "https://github.com/chenasraf/simple-scaffold.git",
|
||||
"author": "Chen Asraf <inbox@casraf.com>",
|
||||
"author": "Chen Asraf <contact@casraf.dev>",
|
||||
"license": "MIT",
|
||||
"main": "index.js",
|
||||
"bin": "cmd.js",
|
||||
"packageManager": "pnpm@8.6.2",
|
||||
"keywords": [
|
||||
"javascript",
|
||||
"cli",
|
||||
@@ -20,50 +21,46 @@
|
||||
"scaffolding"
|
||||
],
|
||||
"scripts": {
|
||||
"clean": "rimraf dist/",
|
||||
"build": "yarn clean && tsc && chmod -R +x ./dist && cp ./package.json ./README.md ./dist/",
|
||||
"clean": "rm -rf dist/",
|
||||
"build": "pnpm clean && tsc && chmod -R +x ./dist && cp ./package.json ./README.md ./dist/",
|
||||
"dev": "tsc --watch",
|
||||
"start": "node dist/scaffold.js",
|
||||
"test": "jest",
|
||||
"cmd": "node --trace-warnings dist/cmd.js",
|
||||
"build-test": "yarn build && yarn test",
|
||||
"build-cmd": "yarn build && yarn cmd",
|
||||
"build-test": "pnpm build && pnpm test",
|
||||
"build-cmd": "pnpm build && pnpm cmd",
|
||||
"build-docs": "typedoc",
|
||||
"watch-docs": "yarn typedoc --watch",
|
||||
"audit-fix": "npm_config_yes=true npx yarn-audit-fix --flow=convert",
|
||||
"changelog": "conventional-changelog -p conventionalcommits -i CHANGELOG.md -s -r 0"
|
||||
"watch-docs": "pnpm typedoc --watch",
|
||||
"audit-fix": "pnpm audit --fix",
|
||||
"changelog": "conventional-changelog -p conventionalcommits -i CHANGELOG.md -s -r 0; echo \"# Change Log\n\n$(cat CHANGELOG.md)\" > CHANGELOG.md"
|
||||
},
|
||||
"dependencies": {
|
||||
"chalk": "^4.1.2",
|
||||
"date-fns": "^2.29.3",
|
||||
"glob": "^9.2.1",
|
||||
"date-fns": "^2.30.0",
|
||||
"glob": "^10.3.3",
|
||||
"handlebars": "^4.7.7",
|
||||
"lodash": "^4.17.21",
|
||||
"massarg": "^1.0.7-pre.1",
|
||||
"semantic-release-conventional-commits": "^3.0.0",
|
||||
"util.promisify": "^1.1.1"
|
||||
"massarg": "^1.0.7-pre.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@knodes/typedoc-plugin-pages": "^0.23.4",
|
||||
"@semantic-release/changelog": "^6.0.3",
|
||||
"@semantic-release/exec": "^6.0.3",
|
||||
"@semantic-release/git": "^10.0.1",
|
||||
"@semantic-release/release-notes-generator": "^10.0.3",
|
||||
"@types/jest": "^29.5.1",
|
||||
"@types/lodash": "^4.14.171",
|
||||
"@types/mock-fs": "^4.13.1",
|
||||
"@types/node": "^18.16.0",
|
||||
"@types/semantic-release": "^20.0.1",
|
||||
"conventional-changelog": "^3.1.25",
|
||||
"conventional-changelog-cli": "^2.2.2",
|
||||
"conventional-changelog-conventionalcommits": "^5.0.0",
|
||||
"jest": "^29.5.0",
|
||||
"mock-fs": "^5.2.0",
|
||||
"rimraf": "^5.0.0",
|
||||
"semantic-release": "^21.0.1",
|
||||
"semantic-release-conventional-commits": "^3.0.0",
|
||||
"ts-jest": "^29.1.0",
|
||||
"ts-node": "^10.9.1",
|
||||
"typedoc": "^0.24.6",
|
||||
"typedoc": "^0.24.7",
|
||||
"typescript": "^5.0.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"doc-theme": "file:./doc-theme"
|
||||
}
|
||||
}
|
||||
|
||||
7
pages/README.md
Normal file
7
pages/README.md
Normal file
@@ -0,0 +1,7 @@
|
||||
See full documentation [here](https://chenasraf.github.io/simple-scaffold).
|
||||
|
||||
- [Command Line Interface (CLI) usage](https://chenasraf.github.io/simple-scaffold/pages/cli.html)
|
||||
- [Node.js usage](https://chenasraf.github.io/simple-scaffold/pages/node.html)
|
||||
- [Templates](https://chenasraf.github.io/simple-scaffold/pages/templates.html)
|
||||
- [Configuration Files](https://chenasraf.github.io/simple-scaffold/pages/configuration_files.html)
|
||||
- [Migrating v0.x to v1.x](https://chenasraf.github.io/simple-scaffold/pages/migration.html)
|
||||
79
pages/cli.md
Normal file
79
pages/cli.md
Normal file
@@ -0,0 +1,79 @@
|
||||
## Available flags
|
||||
|
||||
```text
|
||||
Usage: simple-scaffold [options]
|
||||
```
|
||||
|
||||
To see this and more information anytime, add the `-h` or `--help` flag to your call, e.g.
|
||||
`npx simple-scaffold@latest -h`.
|
||||
|
||||
| Command \| alias | |
|
||||
| --------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `--help`\|`-h` | Display help information |
|
||||
| `--name`\|`-n` | Name to be passed to the generated files. {{name}} and {{Name}} inside contents and file names will be replaced accordingly. |
|
||||
| `--config`\|`-c` | Filename or HTTPS git URL to load config from instead of passing arguments to CLI or using a Node.js script. |
|
||||
| `--github`\|`-gh` | GitHub path to load config from instead of passing arguments to CLI or using a Node.js script. |
|
||||
| `--key`\|`-k` | Key to load inside the config file. This overwrites the config key provided after the colon in --config (e.g. --config scaffold.cmd.js:component) |
|
||||
| `--output`\|`-o` | Path to output to. If --create-sub-folder is enabled, the subfolder will be created inside this path. (default: current dir) |
|
||||
| `--templates`\|`-t` | Template files to use as input. You may provide multiple files, each of which can be a relative or absolute path, or a glob pattern for multiple file matching easily. |
|
||||
| `--overwrite`\|`-w` | Enable to override output files, even if they already exist. (default: false) |
|
||||
| `--data`\|`-d` | Add custom data to the templates. By default, only your app name is included. |
|
||||
| `--append-data`\|`-D` | Append additional custom data to the templates, which will overwrite --data, using an alternate syntax, which is easier to use with CLI: -D key1=string -D key2:=raw |
|
||||
| `--create-sub-folder`\|`-s` | Create subfolder with the input name (default: false) |
|
||||
| `--sub-folder-name-helper`\|`-sh` | Default helper to apply to subfolder name when using `--create-sub-folder true`. |
|
||||
| `--quiet`\|`-q` | Suppress output logs (Same as --verbose 0) (default: false) |
|
||||
| `--verbose`\|`-v` | Determine amount of logs to display. The values are: 0 (none) \| 1 (debug) \| 2 (info) \| 3 (warn) \| 4 (error). The provided level will display messages of the same level or higher. (default: 2) |
|
||||
| `--dry-run`\|`-dr` | Don't emit files. This is good for testing your scaffolds and making sure they don't fail, without having to write actual file contents or create directories. (default: false) |
|
||||
|
||||
## Examples:
|
||||
|
||||
> See
|
||||
> [Configuration Files](https://chenasraf.github.io/simple-scaffold/pages/docs/configuration_files.md)
|
||||
> for organizing multiple scaffold types into easy-to-maintain files
|
||||
|
||||
Usage with config file
|
||||
|
||||
```shell
|
||||
$ simple-scaffold -c scaffold.cmd.js --key component
|
||||
```
|
||||
|
||||
Usage with GitHub config file
|
||||
|
||||
```shell
|
||||
$ simple-scaffold -gh chenasraf/simple-scaffold --key component
|
||||
```
|
||||
|
||||
Usage with https git URL (for non-GitHub)
|
||||
|
||||
```shell
|
||||
$ simple-scaffold -c \
|
||||
https://example.com/user/template.git#scaffold.cmd.js --key component
|
||||
```
|
||||
|
||||
Full syntax with config path and template key (applicable to all above methods)
|
||||
|
||||
```shell
|
||||
$ simple-scaffold -c scaffold.cmd.js:component MyComponent
|
||||
```
|
||||
|
||||
Excluded template key, assumes 'default' key
|
||||
|
||||
```shell
|
||||
$ simple-scaffold -c scaffold.cmd.js MyComponent
|
||||
```
|
||||
|
||||
Shortest syntax for GitHub, assumes file 'scaffold.cmd.js' and template key 'default'
|
||||
|
||||
```shell
|
||||
$ simple-scaffold -gh chenasraf/simple-scaffold MyComponent
|
||||
```
|
||||
|
||||
You can also add this as a script in your `package.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"scaffold": "npx simple-scaffold@latest -t scaffolds/component/**/* -o src/components -d '{\"myProp\": \"propName\", \"myVal\": 123}'"
|
||||
}
|
||||
}
|
||||
```
|
||||
172
pages/configuration_files.md
Normal file
172
pages/configuration_files.md
Normal file
@@ -0,0 +1,172 @@
|
||||
If you want to have reusable configurations which are complex and don't fit into command lines
|
||||
easily, or just want to manage your templates easier, you can use configuration files to load your
|
||||
scaffolding configurations.
|
||||
|
||||
## Creating config files
|
||||
|
||||
Configuration files should be valid `.js`/`.json` files that contain valid Scaffold configurations.
|
||||
|
||||
Each file hold multiple scaffolds. Each scaffold is a key, and its value is the configuration. For
|
||||
example:
|
||||
|
||||
```json
|
||||
{
|
||||
"component": {
|
||||
"templates": ["templates/component"],
|
||||
"output": "src/components"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The configuration contents are identical to the
|
||||
[Node.js configuration structure](https://chenasraf.github.io/simple-scaffold/pages/node.md):
|
||||
|
||||
```ts
|
||||
interface ScaffoldConfig {
|
||||
name: string
|
||||
templates: string[]
|
||||
output: FileResponse<string>
|
||||
createSubFolder?: boolean
|
||||
data?: Record<string, any>
|
||||
overwrite?: FileResponse<boolean>
|
||||
quiet?: boolean
|
||||
verbose?: LogLevel
|
||||
dryRun?: boolean
|
||||
helpers?: Record<string, Helper>
|
||||
subFolderNameHelper?: DefaultHelpers | string
|
||||
beforeWrite?(
|
||||
content: Buffer,
|
||||
rawContent: Buffer,
|
||||
outputPath: string,
|
||||
): string | Buffer | undefined | Promise<string | Buffer | undefined>
|
||||
}
|
||||
```
|
||||
|
||||
If you want to supply functions inside the configurations, you must use a `.js` file as JSON does
|
||||
not support non-primitives.
|
||||
|
||||
A `.js` file can be just like a `.json` file, make sure to export the final configuration:
|
||||
|
||||
```js
|
||||
/** @type {import('simple-scaffold').ScaffoldConfigFile} */
|
||||
module.exports = {
|
||||
component: {
|
||||
templates: ["templates/component"],
|
||||
output: "src/components",
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Another feature of using a JS file is you can export a function which will be loaded with the CMD
|
||||
config provided to Simple Scaffold. The `extras` key contains any values not consumed by built-in
|
||||
flags, so you can pre-process your args before outputting a config:
|
||||
|
||||
```js
|
||||
/** @type {import('simple-scaffold').ScaffoldConfigFile} */
|
||||
module.exports = (config) => {
|
||||
console.log("Config:", config)
|
||||
return {
|
||||
component: {
|
||||
templates: ["templates/component"],
|
||||
output: "src/components",
|
||||
},
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Using a config file
|
||||
|
||||
Once your config is created, you can use it by providing the file name to the `--config` (or `-c`
|
||||
for brevity), optionally followed by a colon, then your scaffold config name.
|
||||
|
||||
```shell
|
||||
simple-scaffold -c <file>[:<template_key>]
|
||||
```
|
||||
|
||||
For example:
|
||||
|
||||
```shell
|
||||
simple-scaffold -c scaffold.json:component MyComponentName
|
||||
```
|
||||
|
||||
If you don't want to supply a template/config name (e.g. `component`), you can omit the colon and
|
||||
the name, and it will use the configuration named `default`:
|
||||
|
||||
```js
|
||||
/** @type {import('simple-scaffold').ScaffoldConfigFile} */
|
||||
module.exports = {
|
||||
default: {
|
||||
// ...
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
And then:
|
||||
|
||||
```shell
|
||||
# will use 'default' template
|
||||
simple-scaffold -c scaffold.json MyComponentName
|
||||
```
|
||||
|
||||
## Remote Templates
|
||||
|
||||
You can load template groups remotely, similar to how you would pass a config normally.
|
||||
|
||||
The main difference is the templates will be hosted on a remote location such as a git server, and
|
||||
not locally in your project. This can be done to easily share & reuse templates.
|
||||
|
||||
When passing a git URL to `--config`, you will clone that repo and use the files there as template.
|
||||
|
||||
The syntax is as follows:
|
||||
|
||||
```shell
|
||||
simple-scaffold -c <git_url>[#<git_file>][:<template_key>]
|
||||
```
|
||||
|
||||
For example, to use this repository's example as base:
|
||||
|
||||
```shell
|
||||
simple-scaffold -c https://github.com/chenasraf/simple-scaffold.git#scaffold.config.js:component
|
||||
```
|
||||
|
||||
When the `filename` is omitted, `/scaffold.config.js` will be used as default.
|
||||
|
||||
When the `template_key` is ommitted, `default` will be used as default.
|
||||
|
||||
### GitHub Templates
|
||||
|
||||
As a shorter alternative to the above example, you can use `--github` or `-gh` to reference a GitHub
|
||||
URL without specifying the whole path.
|
||||
|
||||
The syntax is as follows:
|
||||
|
||||
```shell
|
||||
simple-scaffold -gh <username>/<project_name>[#<git_file>][:<template_key>]
|
||||
```
|
||||
|
||||
This example is equivalent to the above, just shorter to write:
|
||||
|
||||
```shell
|
||||
simple-scaffold -c chenasraf/simple-scaffold#scaffold.config.js:component
|
||||
```
|
||||
|
||||
## Use In Node.js
|
||||
|
||||
You can also start a scaffold from Node.js with a remote file or URL config.
|
||||
|
||||
Just use the `Scaffold.fromConfig` function:
|
||||
|
||||
```ts
|
||||
Scaffold.fromConfig(
|
||||
"scaffold.config.js", // file or HTTPS git URL
|
||||
{
|
||||
// name of the generated component
|
||||
name: "My Component",
|
||||
// key to load from the config
|
||||
key: "component",
|
||||
},
|
||||
{
|
||||
// other config overrides
|
||||
},
|
||||
)
|
||||
```
|
||||
@@ -1,5 +1,3 @@
|
||||
# Migrating from 0.x to 1.x
|
||||
|
||||
In Simple Scaffold v1.0, the entire codebase was overhauled, yet usage remains mostly the same
|
||||
between versions. With these notable exceptions:
|
||||
|
||||
@@ -13,8 +11,10 @@ between versions. With these notable exceptions:
|
||||
|
||||
- `locals` has been renamed to `data`. The appropriate command line args have been updated as well
|
||||
to `--data` | `-d`.
|
||||
- Additional options have been added to both CLI and Node interfaces. See the [readme](/README.md)
|
||||
for more information.
|
||||
- Additional options have been added to both CLI and Node interfaces. See
|
||||
[Command Line Interface (CLI) usage](https://chenasraf.github.io/simple-scaffold/pages/cli.html)
|
||||
and [Node.js usage](https://chenasraf.github.io/simple-scaffold/pages/node.html) for more
|
||||
information.
|
||||
|
||||
## Template syntax changes
|
||||
|
||||
53
pages/node.md
Normal file
53
pages/node.md
Normal file
@@ -0,0 +1,53 @@
|
||||
You can build the scaffold yourself, if you want to create more complex arguments, scaffold groups,
|
||||
etc - simply pass a config object to the Scaffold function when you are ready to start.
|
||||
|
||||
The config takes similar arguments to the command line. The full type definitions can be found in
|
||||
[src/types.ts](https://github.com/chenasraf/simple-scaffold/blob/develop/src/types.ts#L13).
|
||||
|
||||
See the full
|
||||
[documentation](https://chenasraf.github.io/simple-scaffold/interfaces/ScaffoldConfig.html) for the
|
||||
configuration options and their behavior.
|
||||
|
||||
```ts
|
||||
interface ScaffoldConfig {
|
||||
name: string
|
||||
templates: string[]
|
||||
output: FileResponse<string>
|
||||
createSubFolder?: boolean
|
||||
data?: Record<string, any>
|
||||
overwrite?: FileResponse<boolean>
|
||||
quiet?: boolean
|
||||
verbose?: LogLevel
|
||||
dryRun?: boolean
|
||||
helpers?: Record<string, Helper>
|
||||
subFolderNameHelper?: DefaultHelpers | string
|
||||
beforeWrite?(
|
||||
content: Buffer,
|
||||
rawContent: Buffer,
|
||||
outputPath: string,
|
||||
): string | Buffer | undefined | Promise<string | Buffer | undefined>
|
||||
}
|
||||
```
|
||||
|
||||
This is an example of loading a complete scaffold via Node.js:
|
||||
|
||||
```typescript
|
||||
import Scaffold from "simple-scaffold"
|
||||
|
||||
const config = {
|
||||
name: "component",
|
||||
templates: [path.join(__dirname, "scaffolds", "component")],
|
||||
output: path.join(__dirname, "src", "components"),
|
||||
createSubFolder: true,
|
||||
subFolderNameHelper: "upperCase"
|
||||
data: {
|
||||
property: "value",
|
||||
},
|
||||
helpers: {
|
||||
twice: (text) => [text, text].join(" ")
|
||||
},
|
||||
beforeWrite: (content, rawContent, outputPath) => content.toString().toUpperCase()
|
||||
}
|
||||
|
||||
const scaffold = Scaffold(config)
|
||||
```
|
||||
224
pages/templates.md
Normal file
224
pages/templates.md
Normal file
@@ -0,0 +1,224 @@
|
||||
# Preparing template files
|
||||
|
||||
Put your template files anywhere, and fill them with tokens for replacement.
|
||||
|
||||
Each template (not file) in the config array is parsed individually, and copied to the output
|
||||
directory. If a single template path contains multiple files (e.g. if you use a folder path or a
|
||||
glob pattern), the first directory up the tree of that template will become the base inside the
|
||||
defined output path for that template, while copying files recursively and maintaining their
|
||||
relative structure.
|
||||
|
||||
Examples:
|
||||
|
||||
> In the following examples, the config `name` is `AppName`, and the config `output` is `src`.
|
||||
|
||||
| Input template | Files in template | Output path(s) |
|
||||
| ----------------------------- | ------------------------------------------------------ | ------------------------------------------------------------ |
|
||||
| `./templates/{{ name }}.txt` | `./templates/{{ name }}.txt` | `src/AppName.txt` |
|
||||
| `./templates/directory` | `outer/{{name}}.txt`,<br />`outer2/inner/{{name}}.txt` | `src/outer/AppName.txt`,<br />`src/outer2/inner/AppName.txt` |
|
||||
| `./templates/others/**/*.txt` | `outer/{{name}}.jpg`,<br />`outer2/inner/{{name}}.txt` | `src/outer2/inner/AppName.txt` |
|
||||
|
||||
## Variable/token replacement
|
||||
|
||||
Scaffolding will replace `{{ varName }}` in both the file name and its contents and put the
|
||||
transformed files in the output directory.
|
||||
|
||||
The data available for the template parser is the data you pass to the `data` config option (or
|
||||
`--data` argument in CLI).
|
||||
|
||||
For example, using the following command:
|
||||
|
||||
```bash
|
||||
npx simple-scaffold@latest \
|
||||
--templates templates/components/{{name}}.jsx \
|
||||
--output src/components \
|
||||
--create-sub-folder true \
|
||||
MyComponent
|
||||
```
|
||||
|
||||
Will output a file with the path:
|
||||
|
||||
```text
|
||||
<working_dir>/src/components/MyComponent.jsx
|
||||
```
|
||||
|
||||
The contents of the file will be transformed in a similar fashion.
|
||||
|
||||
Your `data` will be pre-populated with the following:
|
||||
|
||||
- `{{Name}}`: PascalCase of the component name
|
||||
- `{{name}}`: raw name of the component as you entered it
|
||||
|
||||
> Simple-Scaffold uses [Handlebars.js](https://handlebarsjs.com/) for outputting the file contents.
|
||||
> Any `data` you add in the config will be available for use with their names wrapped in `{{` and
|
||||
> `}}`. Other Handlebars built-ins such as `each`, `if` and `with` are also supported, see
|
||||
> [Handlebars.js Language Features](https://handlebarsjs.com/guide/#language-features) for more
|
||||
> information.
|
||||
|
||||
## Helpers
|
||||
|
||||
### Built-in Helpers
|
||||
|
||||
Simple-Scaffold provides some built-in text transformation filters usable by Handlebars.
|
||||
|
||||
For example, you may use `{{ snakeCase name }}` inside a template file or filename, and it will
|
||||
replace `My Name` with `my_name` when producing the final value.
|
||||
|
||||
#### Capitalization Helpers
|
||||
|
||||
| Helper name | Example code | Example output |
|
||||
| ------------ | ----------------------- | -------------- |
|
||||
| [None] | `{{ name }}` | my name |
|
||||
| `camelCase` | `{{ camelCase name }}` | myName |
|
||||
| `snakeCase` | `{{ snakeCase name }}` | my_name |
|
||||
| `startCase` | `{{ startCase name }}` | My Name |
|
||||
| `kebabCase` | `{{ kebabCase name }}` | my-name |
|
||||
| `hyphenCase` | `{{ hyphenCase name }}` | my-name |
|
||||
| `pascalCase` | `{{ pascalCase name }}` | MyName |
|
||||
| `upperCase` | `{{ upperCase name }}` | MY NAME |
|
||||
| `lowerCase` | `{{ lowerCase name }}` | my name |
|
||||
|
||||
#### Date helpers
|
||||
|
||||
| Helper name | Description | Example code | Example output |
|
||||
| -------------------------------- | ---------------------------------------------------------------- | ---------------------------------------------------------------- | ------------------ |
|
||||
| `now` | Current date with format | `{{ now "yyyy-MM-dd HH:mm" }}` | `2042-01-01 15:00` |
|
||||
| `now` (with offset) | Current date with format, and with offset | `{{ now "yyyy-MM-dd HH:mm" -1 "hours" }}` | `2042-01-01 14:00` |
|
||||
| `date` | Custom date with format | `{{ date "2042-01-01T15:00:00Z" "yyyy-MM-dd HH:mm" }}` | `2042-01-01 15:00` |
|
||||
| `date` (with offset) | Custom date with format, and with offset | `{{ date "2042-01-01T15:00:00Z" "yyyy-MM-dd HH:mm" -1 "days" }}` | `2041-31-12 15:00` |
|
||||
| `date` (with date from `--data`) | Custom date with format, with data from the `data` config option | `{{ date myCustomDate "yyyy-MM-dd HH:mm" }}` | `2042-01-01 12:00` |
|
||||
|
||||
Further details:
|
||||
|
||||
- We use [`date-fns`](https://date-fns.org/docs/) for parsing/manipulating the dates. If you want
|
||||
more information on the date tokens to use, refer to
|
||||
[their format documentation](https://date-fns.org/docs/format).
|
||||
|
||||
- The date helper format takes the following arguments:
|
||||
|
||||
```typescript
|
||||
(
|
||||
date: string,
|
||||
format: string,
|
||||
offsetAmount?: number,
|
||||
offsetType?: "years" | "months" | "weeks" | "days" | "hours" | "minutes" | "seconds"
|
||||
)
|
||||
```
|
||||
|
||||
- **The now helper** (for current time) takes the same arguments, minus the first one (`date`) as it
|
||||
is implicitly the current date.
|
||||
|
||||
### Custom Helpers
|
||||
|
||||
You may also add your own custom helpers using the `helpers` options when using the JS API (rather
|
||||
than the CLI). The `helpers` option takes an object whose keys are helper names, and values are the
|
||||
transformation functions. For example, `upperCase` is implemented like so:
|
||||
|
||||
```typescript
|
||||
config.helpers = {
|
||||
upperCase: (text) => text.toUpperCase(),
|
||||
}
|
||||
```
|
||||
|
||||
All of the above helpers (built in and custom) will also be available to you when using
|
||||
`subFolderNameHelper` (`--sub-folder-name-helper`/`-sh`) as a possible value.
|
||||
|
||||
> To see more information on how helpers work and more features, see
|
||||
> [Handlebars.js docs](https://handlebarsjs.com/guide/#custom-helpers).
|
||||
|
||||
# Examples
|
||||
|
||||
## Run
|
||||
|
||||
### Command Example
|
||||
|
||||
```bash
|
||||
simple-scaffold MyComponent \
|
||||
-t project/scaffold/**/* \
|
||||
-o src/components \
|
||||
-d '{"className": "myClassName","author": "Chen Asraf"}'
|
||||
MyComponent
|
||||
```
|
||||
|
||||
### Equivalent Node Module Example
|
||||
|
||||
```typescript
|
||||
import Scaffold from "simple-scaffold"
|
||||
|
||||
async function main() {
|
||||
await Scaffold({
|
||||
name: "MyComponent",
|
||||
templates: ["project/scaffold/**/*"],
|
||||
output: ["src/components"],
|
||||
data: {
|
||||
className: "myClassName",
|
||||
author: "Chen Asraf",
|
||||
},
|
||||
})
|
||||
console.log("Done.")
|
||||
}
|
||||
```
|
||||
|
||||
## Files
|
||||
|
||||
### Input
|
||||
|
||||
- Input file path:
|
||||
|
||||
```text
|
||||
project → scaffold → {{Name}}.js → src → components
|
||||
```
|
||||
|
||||
- Input file contents:
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* Author: {{ author }}
|
||||
* Date: {{ now "yyyy-MM-dd" }}
|
||||
*/
|
||||
import React from 'react'
|
||||
|
||||
export default {{camelCase name}}: React.FC = (props) => {
|
||||
return (
|
||||
<div className="{{className}}">{{camelCase name}} Component</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Output
|
||||
|
||||
- Output file path:
|
||||
|
||||
- With `createSubFolder = false` (default):
|
||||
|
||||
```text
|
||||
project → src → components → MyComponent.js
|
||||
```
|
||||
|
||||
- With `createSubFolder = true`:
|
||||
|
||||
```text
|
||||
project → src → components → MyComponent → MyComponent.js
|
||||
```
|
||||
|
||||
- With `createSubFolder = true` and `subFolderNameHelper = 'upperCase'`:
|
||||
|
||||
```text
|
||||
project → src → components → MYCOMPONENT → MyComponent.js
|
||||
```
|
||||
|
||||
- Output file contents:
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* Author: Chen Asraf
|
||||
* Date: 2077-01-01
|
||||
*/
|
||||
import React from 'react'
|
||||
|
||||
export default MyComponent: React.FC = (props) => {
|
||||
return (
|
||||
<div className="myClassName">MyComponent Component</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
4767
pnpm-lock.yaml
generated
Normal file
4767
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,60 +1,12 @@
|
||||
/** @type {import('semantic-release').Options} */
|
||||
module.exports = {
|
||||
branches: [
|
||||
"+([0-9])?(.{+([0-9]),x}).x",
|
||||
"master",
|
||||
"next",
|
||||
"next-major",
|
||||
{ name: "beta", prerelease: true },
|
||||
{ name: "alpha", prerelease: true },
|
||||
],
|
||||
branches: ["master", { name: "pre", prerelease: true }],
|
||||
analyzeCommits: {
|
||||
path: "semantic-release-conventional-commits",
|
||||
majorTypes: ["major", "breaking"],
|
||||
minorTypes: ["minor", "feat", "feature"],
|
||||
patchTypes: ["patch", "fix", "bugfix", "refactor", "perf", "revert"],
|
||||
},
|
||||
plugins: [
|
||||
[
|
||||
"@semantic-release/commit-analyzer",
|
||||
{
|
||||
preset: "conventionalcommits",
|
||||
parserOpts: {
|
||||
noteKeywords: ["breaking:", "breaking-fix:", "breaking-feat:"],
|
||||
},
|
||||
releaseRules: [
|
||||
{ type: "feat", section: "Features", release: "minor" },
|
||||
{ type: "docs", section: "Misc", release: "patch" },
|
||||
{ type: "fix", section: "Bug Fixes", release: "patch" },
|
||||
{ type: "refactor", section: "Misc", release: "patch" },
|
||||
{ type: "docs", section: "Misc", release: "patch" },
|
||||
{ type: "perf", section: "Misc", release: "patch" },
|
||||
{ type: "build", section: "Misc", release: "patch" },
|
||||
{ type: "chore", section: "Misc", release: "patch" },
|
||||
{ type: "test", section: "Misc", release: "patch" },
|
||||
],
|
||||
},
|
||||
],
|
||||
[
|
||||
"@semantic-release/release-notes-generator",
|
||||
{
|
||||
preset: "conventionalcommits",
|
||||
parserOpts: {
|
||||
noteKeywords: ["breaking"],
|
||||
types: [
|
||||
{ type: "feat", section: "Features", release: "minor" },
|
||||
{ type: "docs", section: "Misc", release: "patch" },
|
||||
{ type: "fix", section: "Bug Fixes", release: "patch" },
|
||||
{ type: "refactor", section: "Misc", release: "patch" },
|
||||
{ type: "docs", section: "Misc", release: "patch" },
|
||||
{ type: "perf", section: "Misc", release: "patch" },
|
||||
{ type: "build", section: "Misc", release: "patch" },
|
||||
{ type: "chore", section: "Misc", release: "patch" },
|
||||
{ type: "test", section: "Misc", release: "patch" },
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
"@semantic-release/commit-analyzer",
|
||||
"@semantic-release/release-notes-generator",
|
||||
[
|
||||
"@semantic-release/changelog",
|
||||
{
|
||||
@@ -65,13 +17,14 @@ module.exports = {
|
||||
[
|
||||
"@semantic-release/npm",
|
||||
{
|
||||
packageRoot: "dist",
|
||||
npmPublish: false,
|
||||
},
|
||||
],
|
||||
[
|
||||
"@semantic-release/github",
|
||||
"@semantic-release/npm",
|
||||
{
|
||||
assets: ["package.tgz"],
|
||||
npmPublish: true,
|
||||
pkgRoot: "dist",
|
||||
},
|
||||
],
|
||||
[
|
||||
@@ -80,5 +33,11 @@ module.exports = {
|
||||
assets: ["CHANGELOG.md", "package.json"],
|
||||
},
|
||||
],
|
||||
[
|
||||
"@semantic-release/github",
|
||||
{
|
||||
assets: ["*.tgz"],
|
||||
},
|
||||
],
|
||||
],
|
||||
}
|
||||
|
||||
16
scaffold.config.js
Normal file
16
scaffold.config.js
Normal file
@@ -0,0 +1,16 @@
|
||||
/** @type {import('simple-scaffold').ScaffoldConfigFile} */
|
||||
module.exports = (conf) => {
|
||||
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" },
|
||||
},
|
||||
}
|
||||
}
|
||||
67
src/cmd.ts
67
src/cmd.ts
@@ -3,19 +3,21 @@ import massarg from "massarg"
|
||||
import chalk from "chalk"
|
||||
import { LogLevel, ScaffoldCmdConfig } from "./types"
|
||||
import { Scaffold } from "./scaffold"
|
||||
import path from "path"
|
||||
import fs from "fs/promises"
|
||||
import { parseAppendData } from "./utils"
|
||||
import path from "node:path"
|
||||
import fs from "node:fs/promises"
|
||||
import { parseAppendData, parseConfigFile } from "./config"
|
||||
|
||||
export async function parseCliArgs(args = process.argv.slice(2)) {
|
||||
const pkg = JSON.parse((await fs.readFile(path.join(__dirname, "package.json"))).toString())
|
||||
const pkgFile = await fs.readFile(path.join(__dirname, "package.json"))
|
||||
const pkg = JSON.parse(pkgFile.toString())
|
||||
const isConfigProvided =
|
||||
args.includes("--config") || args.includes("-c") || args.includes("--github") || args.includes("-gh")
|
||||
|
||||
return (
|
||||
massarg<ScaffoldCmdConfig>()
|
||||
.main((config) => {
|
||||
config.data = { ...config.data, ...config.appendData }
|
||||
delete config.appendData
|
||||
return Scaffold(config)
|
||||
.main(async (config) => {
|
||||
const _config = await parseConfigFile(config)
|
||||
return Scaffold(_config)
|
||||
})
|
||||
.option({
|
||||
name: "name",
|
||||
@@ -25,11 +27,29 @@ export async function parseCliArgs(args = process.argv.slice(2)) {
|
||||
isDefault: true,
|
||||
required: true,
|
||||
})
|
||||
.option({
|
||||
name: "config",
|
||||
aliases: ["c"],
|
||||
description:
|
||||
"Filename or https git URL to load config from instead of passing arguments to CLI or using a Node.js script. See examples for syntax.",
|
||||
})
|
||||
.option({
|
||||
name: "github",
|
||||
aliases: ["gh"],
|
||||
description:
|
||||
"GitHub path to load config from instead of passing arguments to CLI or using a Node.js script. See examples for syntax.",
|
||||
})
|
||||
.option({
|
||||
name: "key",
|
||||
aliases: ["k"],
|
||||
description:
|
||||
"Key to load inside the config file. This overwrites the config key provided after the colon in --config (e.g. --config scaffold.cmd.js:component)",
|
||||
})
|
||||
.option({
|
||||
name: "output",
|
||||
aliases: ["o"],
|
||||
description: `Path to output to. If --create-sub-folder is enabled, the subfolder will be created inside this path. ${chalk.reset`${chalk.white`(default: current dir)`}`}`,
|
||||
required: true,
|
||||
required: !isConfigProvided,
|
||||
})
|
||||
.option({
|
||||
name: "templates",
|
||||
@@ -38,7 +58,7 @@ export async function parseCliArgs(args = process.argv.slice(2)) {
|
||||
description:
|
||||
"Template files to use as input. You may provide multiple files, each of which can be a relative or absolute path, " +
|
||||
"or a glob pattern for multiple file matching easily.",
|
||||
required: true,
|
||||
required: !isConfigProvided,
|
||||
})
|
||||
.option({
|
||||
name: "overwrite",
|
||||
@@ -103,16 +123,41 @@ export async function parseCliArgs(args = process.argv.slice(2)) {
|
||||
// description: "Usage",
|
||||
// output: "",
|
||||
// })
|
||||
.example({
|
||||
description: "Usage with config file",
|
||||
input: "simple-scaffold -c scaffold.cmd.js --key component",
|
||||
})
|
||||
.example({
|
||||
description: "Usage with GitHub config file",
|
||||
input: "simple-scaffold -gh chenasraf/simple-scaffold --key component",
|
||||
})
|
||||
.example({
|
||||
description: "Usage with https git URL (for non-GitHub)",
|
||||
input: "simple-scaffold -c https://example.com/user/template.git#scaffold.cmd.js --key component",
|
||||
})
|
||||
.example({
|
||||
description: "Full syntax with config path and template key (applicable to all above methods)",
|
||||
input: "simple-scaffold -c scaffold.cmd.js:component MyComponent",
|
||||
})
|
||||
.example({
|
||||
description: "Excluded template key, assumes 'default' key",
|
||||
input: "simple-scaffold -c scaffold.cmd.js MyComponent",
|
||||
})
|
||||
.example({
|
||||
description: "Shortest syntax for GitHub, assumes file 'scaffold.cmd.js' and template key 'default'",
|
||||
input: "simple-scaffold -gh chenasraf/simple-scaffold MyComponent",
|
||||
})
|
||||
.help({
|
||||
binName: "simple-scaffold",
|
||||
useGlobalColumns: true,
|
||||
usageExample: "[options]",
|
||||
printWidth: 100,
|
||||
header: [`Create structured files based on templates.`].join("\n"),
|
||||
footer: [
|
||||
`Version: ${pkg.version}`,
|
||||
`Copyright © Chen Asraf 2017-${new Date().getFullYear()}`,
|
||||
``,
|
||||
`Documentation: ${chalk.underline`https://casraf.dev/simple-scaffold`}`,
|
||||
`Documentation: ${chalk.underline`https://chenasraf.github.io/simple-scaffold`}`,
|
||||
`NPM: ${chalk.underline`https://npmjs.com/package/simple-scaffold`}`,
|
||||
`GitHub: ${chalk.underline`https://github.com/chenasraf/simple-scaffold`}`,
|
||||
].join("\n"),
|
||||
|
||||
136
src/config.ts
Normal file
136
src/config.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
import path from "node:path"
|
||||
import {
|
||||
ConfigLoadConfig,
|
||||
FileResponse,
|
||||
FileResponseHandler,
|
||||
LogLevel,
|
||||
ScaffoldCmdConfig,
|
||||
ScaffoldConfig,
|
||||
ScaffoldConfigFile,
|
||||
} from "./types"
|
||||
import { handlebarsParse } from "./parser"
|
||||
import { log } from "./logger"
|
||||
import { resolve, wrapNoopResolver } from "./utils"
|
||||
import { getGitConfig } from "./git"
|
||||
|
||||
export function getOptionValueForFile<T>(
|
||||
config: ScaffoldConfig,
|
||||
filePath: string,
|
||||
fn: FileResponse<T>,
|
||||
defaultValue?: T,
|
||||
): T {
|
||||
if (typeof fn !== "function") {
|
||||
return defaultValue ?? (fn as T)
|
||||
}
|
||||
return (fn as FileResponseHandler<T>)(
|
||||
filePath,
|
||||
path.dirname(handlebarsParse(config, filePath, { isPath: true }).toString()),
|
||||
path.basename(handlebarsParse(config, filePath, { isPath: true }).toString()),
|
||||
)
|
||||
}
|
||||
|
||||
export function parseAppendData(value: string, options: ScaffoldCmdConfig): unknown {
|
||||
const data = options.data ?? {}
|
||||
const [key, val] = value.split(/\:?=/)
|
||||
// raw
|
||||
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("'"))
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export async function parseConfigFile(config: ScaffoldCmdConfig): Promise<ScaffoldConfig> {
|
||||
let c: ScaffoldConfig = config
|
||||
if (config.github) {
|
||||
log(config, LogLevel.Info, `Loading config from github ${config.github}`)
|
||||
config.config = githubPartToUrl(config.github)
|
||||
}
|
||||
|
||||
if (config.config) {
|
||||
const { configFile, key, isRemote } = parseConfigSelection(config.config, config.key)
|
||||
log(config, LogLevel.Info, `Loading config from ${configFile} with key ${key}`)
|
||||
const configPromise = await getConfig({
|
||||
config: configFile,
|
||||
isRemote,
|
||||
quiet: config.quiet,
|
||||
verbose: config.verbose,
|
||||
})
|
||||
let configImport = await resolve(configPromise, config)
|
||||
if (typeof configImport.default === "function" || configImport.default instanceof Promise) {
|
||||
configImport = await resolve(configImport.default, config)
|
||||
}
|
||||
if (!configImport[key]) {
|
||||
throw new Error(`Template "${key}" not found in ${configFile}`)
|
||||
}
|
||||
const importedKey = configImport[key]
|
||||
c = {
|
||||
...config,
|
||||
...importedKey,
|
||||
data: {
|
||||
...(importedKey as any).data,
|
||||
...config.data,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
c.data = { ...c.data, ...config.appendData }
|
||||
delete config.appendData
|
||||
return c
|
||||
}
|
||||
|
||||
export function parseConfigSelection(
|
||||
config: string,
|
||||
key?: string,
|
||||
): { configFile: string; key: string; isRemote: boolean } {
|
||||
const isUrl = config.includes("://")
|
||||
|
||||
const hasColonToken = (!isUrl && config.includes(":")) || (isUrl && count(config, ":") > 1)
|
||||
const colonIndex = config.lastIndexOf(":")
|
||||
const [configFile, templateKey = "default"] = hasColonToken
|
||||
? [config.substring(0, colonIndex), config.substring(colonIndex + 1)]
|
||||
: [config, undefined]
|
||||
const _key = (key ?? templateKey) || "default"
|
||||
return { configFile, key: _key, isRemote: isUrl }
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export async function getConfig(config: ConfigLoadConfig): Promise<ScaffoldConfigFile> {
|
||||
const { config: configFile, isRemote, ...logConfig } = config as Required<typeof config>
|
||||
|
||||
if (!isRemote) {
|
||||
log(logConfig, LogLevel.Info, `Loading config from file ${configFile}`)
|
||||
const absolutePath = path.resolve(process.cwd(), configFile)
|
||||
return wrapNoopResolver(import(absolutePath))
|
||||
}
|
||||
|
||||
const url = new URL(configFile)
|
||||
const isHttp = url.protocol === "http:" || url.protocol === "https:"
|
||||
const isGit = url.protocol === "git:" || (isHttp && url.pathname.endsWith(".git"))
|
||||
|
||||
if (isGit) {
|
||||
return getGitConfig(url, logConfig)
|
||||
}
|
||||
|
||||
if (!isHttp) {
|
||||
throw new Error(`Unsupported protocol ${url.protocol}`)
|
||||
}
|
||||
|
||||
return wrapNoopResolver(import(path.resolve(process.cwd(), configFile)))
|
||||
}
|
||||
|
||||
function count(string: string, substring: string): number {
|
||||
return string.split(substring).length - 1
|
||||
}
|
||||
35
src/docs.css
35
src/docs.css
@@ -1,7 +1,6 @@
|
||||
.tsd-typography table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
box-sizing
|
||||
}
|
||||
|
||||
.tsd-typography table td,
|
||||
@@ -10,32 +9,29 @@
|
||||
border: 1px solid var(--color-accent);
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.tsd-typography h1 + table,
|
||||
.tsd-typography h2 + table,
|
||||
.tsd-typography h3 + table,
|
||||
.tsd-typography h4 + table,
|
||||
.tsd-typography h5 + table,
|
||||
.tsd-typography h6 + table,
|
||||
|
||||
.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 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,
|
||||
@@ -44,3 +40,16 @@
|
||||
.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;
|
||||
}
|
||||
|
||||
212
src/file.ts
Normal file
212
src/file.ts
Normal file
@@ -0,0 +1,212 @@
|
||||
import path from "node:path"
|
||||
import { F_OK } from "node:constants"
|
||||
import { LogLevel, ScaffoldConfig } from "./types"
|
||||
import fs from "node:fs/promises"
|
||||
import { glob, hasMagic } from "glob"
|
||||
import { log } from "./logger"
|
||||
import { getOptionValueForFile } from "./config"
|
||||
import { handlebarsParse } from "./parser"
|
||||
import { handleErr } from "./utils"
|
||||
|
||||
const { stat, access, mkdir, readFile, writeFile } = fs
|
||||
|
||||
export async function createDirIfNotExists(dir: string, config: ScaffoldConfig): Promise<void> {
|
||||
if (config.dryRun) {
|
||||
log(config, LogLevel.Info, `Dry Run. Not creating dir ${dir}`)
|
||||
return
|
||||
}
|
||||
const parentDir = path.dirname(dir)
|
||||
|
||||
if (!(await pathExists(parentDir))) {
|
||||
await createDirIfNotExists(parentDir, config)
|
||||
}
|
||||
|
||||
if (!(await pathExists(dir))) {
|
||||
try {
|
||||
log(config, LogLevel.Debug, `Creating dir ${dir}`)
|
||||
await mkdir(dir)
|
||||
return
|
||||
} catch (e: any) {
|
||||
if (e.code !== "EEXIST") {
|
||||
throw e
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 async function isDir(path: string): Promise<boolean> {
|
||||
const tplStat = await stat(path)
|
||||
return tplStat.isDirectory()
|
||||
}
|
||||
|
||||
export function removeGlob(template: string): string {
|
||||
return path.normalize(template.replace(/\*/g, ""))
|
||||
}
|
||||
|
||||
export function makeRelativePath(str: string): string {
|
||||
return str.startsWith(path.sep) ? str.slice(1) : str
|
||||
}
|
||||
|
||||
export function getBasePath(relPath: string): string {
|
||||
return path
|
||||
.resolve(process.cwd(), relPath)
|
||||
.replace(process.cwd() + path.sep, "")
|
||||
.replace(process.cwd(), "")
|
||||
}
|
||||
|
||||
export async function getFileList(_config: ScaffoldConfig, template: string): Promise<string[]> {
|
||||
template = template.replaceAll(/[\\]+/g, "/")
|
||||
log(_config, LogLevel.Debug, `Getting file list for ${template}`)
|
||||
return (
|
||||
await glob(template, {
|
||||
dot: true,
|
||||
nodir: true,
|
||||
// debug: config.verbose === LogLevel.Debug,
|
||||
})
|
||||
).map(path.normalize)
|
||||
}
|
||||
|
||||
export interface GlobInfo {
|
||||
nonGlobTemplate: string
|
||||
origTemplate: string
|
||||
isDirOrGlob: boolean
|
||||
isGlob: boolean
|
||||
template: string
|
||||
}
|
||||
|
||||
export async function getTemplateGlobInfo(config: ScaffoldConfig, template: string): Promise<GlobInfo> {
|
||||
const isGlob = hasMagic(template)
|
||||
log(config, LogLevel.Debug, "before isDir", "isGlob:", isGlob, template)
|
||||
let _template = template
|
||||
let nonGlobTemplate = isGlob ? removeGlob(template) : template
|
||||
nonGlobTemplate = path.normalize(nonGlobTemplate)
|
||||
const isDirOrGlob = isGlob ? true : await isDir(template)
|
||||
const _shouldAddGlob = !isGlob && isDirOrGlob
|
||||
log(config, LogLevel.Debug, "after", { isDirOrGlob, _shouldAddGlob })
|
||||
const origTemplate = template
|
||||
if (_shouldAddGlob) {
|
||||
_template = path.join(template, "**", "*")
|
||||
}
|
||||
return { nonGlobTemplate, origTemplate, isDirOrGlob, isGlob, template: _template }
|
||||
}
|
||||
|
||||
export interface OutputFileInfo {
|
||||
inputPath: string
|
||||
outputPathOpt: string
|
||||
outputDir: string
|
||||
outputPath: string
|
||||
exists: boolean
|
||||
}
|
||||
|
||||
export async function getTemplateFileInfo(
|
||||
config: ScaffoldConfig,
|
||||
{ templatePath, basePath }: { templatePath: string; basePath: string },
|
||||
): Promise<OutputFileInfo> {
|
||||
const inputPath = path.resolve(process.cwd(), templatePath)
|
||||
const outputPathOpt = getOptionValueForFile(config, inputPath, config.output)
|
||||
const outputDir = getOutputDir(config, outputPathOpt, basePath)
|
||||
const outputPath = handlebarsParse(config, path.join(outputDir, path.basename(inputPath)), {
|
||||
isPath: true,
|
||||
}).toString()
|
||||
const exists = await pathExists(outputPath)
|
||||
return { inputPath, outputPathOpt, outputDir, outputPath, exists }
|
||||
}
|
||||
|
||||
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.Info, `File ${outputPath} exists, overwriting`)
|
||||
}
|
||||
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)
|
||||
log(config, LogLevel.Info, "Done.")
|
||||
} else {
|
||||
log(config, LogLevel.Info, "Dry Run. Output should be:")
|
||||
log(config, LogLevel.Info, finalOutputContents.toString())
|
||||
}
|
||||
} else if (exists) {
|
||||
log(config, LogLevel.Info, `File ${outputPath} already exists, skipping`)
|
||||
}
|
||||
}
|
||||
|
||||
export function getOutputDir(config: ScaffoldConfig, outputPathOpt: string, basePath: string): string {
|
||||
return path.resolve(
|
||||
process.cwd(),
|
||||
...([
|
||||
outputPathOpt,
|
||||
basePath,
|
||||
config.createSubFolder
|
||||
? config.subFolderNameHelper
|
||||
? handlebarsParse(config, `{{ ${config.subFolderNameHelper} name }}`).toString()
|
||||
: config.name
|
||||
: undefined,
|
||||
].filter(Boolean) as string[]),
|
||||
)
|
||||
}
|
||||
|
||||
export async function handleTemplateFile(
|
||||
config: ScaffoldConfig,
|
||||
{ templatePath, basePath }: { templatePath: string; basePath: string },
|
||||
): Promise<void> {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
const { inputPath, outputPathOpt, outputDir, outputPath, exists } = await getTemplateFileInfo(config, {
|
||||
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)
|
||||
|
||||
log(config, LogLevel.Info, `Writing to ${outputPath}`)
|
||||
await copyFileTransformed(config, { exists, overwrite, outputPath, inputPath })
|
||||
resolve()
|
||||
} catch (e: any) {
|
||||
handleErr(e)
|
||||
reject(e)
|
||||
}
|
||||
})
|
||||
}
|
||||
48
src/git.ts
Normal file
48
src/git.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import path from "node:path"
|
||||
import os from "node:os"
|
||||
import { log } from "./logger"
|
||||
import { AsyncResolver, LogConfig, LogLevel, ScaffoldCmdConfig, ScaffoldConfig, ScaffoldConfigMap } from "./types"
|
||||
import { spawn } from "node:child_process"
|
||||
import { resolve, wrapNoopResolver } from "./utils"
|
||||
|
||||
export async function getGitConfig(
|
||||
url: URL,
|
||||
logConfig: LogConfig,
|
||||
): Promise<AsyncResolver<ScaffoldCmdConfig, ScaffoldConfigMap>> {
|
||||
const repoUrl = `${url.protocol}//${url.host}${url.pathname}`
|
||||
|
||||
log(logConfig, LogLevel.Info, `Cloning git repo ${repoUrl}`)
|
||||
|
||||
const tmpPath = path.resolve(os.tmpdir(), `scaffold-config-${Date.now()}`)
|
||||
|
||||
return new Promise((res, reject) => {
|
||||
const clone = spawn("git", ["clone", "--recurse-submodules", "--depth", "1", repoUrl, tmpPath])
|
||||
|
||||
clone.on("error", reject)
|
||||
clone.on("close", async (code) => {
|
||||
if (code === 0) {
|
||||
log(logConfig, LogLevel.Info, `Loading config from git repo: ${repoUrl}`)
|
||||
const hashPath = url.hash?.replace("#", "") || "scaffold.config.js"
|
||||
const absolutePath = path.resolve(tmpPath, hashPath)
|
||||
const loadedConfig = await resolve(
|
||||
async () => (await import(absolutePath)).default as ScaffoldConfigMap,
|
||||
logConfig,
|
||||
)
|
||||
|
||||
log(logConfig, LogLevel.Info, `Loaded config from git`)
|
||||
log(logConfig, LogLevel.Debug, `Raw config:`, loadedConfig)
|
||||
const fixedConfig: ScaffoldConfigMap = {}
|
||||
for (const [k, v] of Object.entries(loadedConfig)) {
|
||||
fixedConfig[k] = {
|
||||
...v,
|
||||
templates: v.templates.map((t) => path.resolve(tmpPath, t)),
|
||||
}
|
||||
}
|
||||
res(wrapNoopResolver(fixedConfig))
|
||||
return
|
||||
}
|
||||
|
||||
reject(new Error(`Git clone failed with code ${code}`))
|
||||
})
|
||||
})
|
||||
}
|
||||
65
src/logger.ts
Normal file
65
src/logger.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { LogConfig, LogLevel, ScaffoldConfig } from "./types"
|
||||
import chalk from "chalk"
|
||||
|
||||
export function log(config: LogConfig, level: LogLevel, ...obj: any[]): void {
|
||||
if (config.quiet || config.verbose === LogLevel.None || level < (config.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),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
export function logInitStep(config: ScaffoldConfig): void {
|
||||
log(config, LogLevel.Debug, "Full config:", {
|
||||
name: config.name,
|
||||
templates: config.templates,
|
||||
output: config.output,
|
||||
createSubFolder: config.createSubFolder,
|
||||
data: config.data,
|
||||
overwrite: config.overwrite,
|
||||
quiet: config.quiet,
|
||||
subFolderNameHelper: config.subFolderNameHelper,
|
||||
helpers: Object.keys(config.helpers ?? {}),
|
||||
verbose: `${config.verbose} (${Object.keys(LogLevel).find(
|
||||
(k) => (LogLevel[k as any] as unknown as number) === config.verbose!,
|
||||
)})`,
|
||||
dryRun: config.dryRun,
|
||||
beforeWrite: config.beforeWrite,
|
||||
} as Record<keyof ScaffoldConfig, unknown>)
|
||||
log(config, LogLevel.Info, "Data:", config.data)
|
||||
}
|
||||
126
src/parser.ts
Normal file
126
src/parser.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
import path from "node:path"
|
||||
import { DefaultHelpers, Helper, LogLevel, ScaffoldConfig } from "./types"
|
||||
import Handlebars from "handlebars"
|
||||
import dtAdd from "date-fns/add"
|
||||
import dtFormat from "date-fns/format"
|
||||
import dtParseISO from "date-fns/parseISO"
|
||||
import { log } from "./logger"
|
||||
|
||||
const dateFns = {
|
||||
add: dtAdd,
|
||||
format: dtFormat,
|
||||
parseISO: dtParseISO,
|
||||
}
|
||||
|
||||
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-alpha character or capital letter
|
||||
function toWordParts(string: string): string[] {
|
||||
return string.split(/(?=[A-Z])|[^a-zA-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,
|
||||
{ isPath = false }: { isPath?: boolean } = {},
|
||||
): Buffer {
|
||||
const { data } = config
|
||||
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(config, LogLevel.Debug, e)
|
||||
log(config, LogLevel.Warning, "Couldn't parse file with handlebars, returning original content")
|
||||
return Buffer.from(templateBuffer)
|
||||
}
|
||||
}
|
||||
@@ -4,27 +4,21 @@
|
||||
*
|
||||
* See [readme](README.md)
|
||||
*/
|
||||
import path from "path"
|
||||
|
||||
import path from "node:path"
|
||||
import { handleErr, resolve } from "./utils"
|
||||
import {
|
||||
createDirIfNotExists,
|
||||
getOptionValueForFile,
|
||||
handleErr,
|
||||
log,
|
||||
pascalCase,
|
||||
isDir,
|
||||
removeGlob,
|
||||
makeRelativePath,
|
||||
registerHelpers,
|
||||
getTemplateGlobInfo,
|
||||
getFileList,
|
||||
getBasePath,
|
||||
copyFileTransformed,
|
||||
getTemplateFileInfo,
|
||||
logInitStep,
|
||||
logInputFile,
|
||||
} from "./utils"
|
||||
import { LogLevel, ScaffoldConfig } from "./types"
|
||||
handleTemplateFile,
|
||||
} from "./file"
|
||||
import { LogLevel, MinimalConfig, Resolver, ScaffoldCmdConfig, ScaffoldConfig } from "./types"
|
||||
import { defaultHelpers, registerHelpers } from "./parser"
|
||||
import { log, logInitStep, logInputFile } from "./logger"
|
||||
import { parseConfigFile } from "./config"
|
||||
|
||||
/**
|
||||
* Create a scaffold using given `options`.
|
||||
@@ -50,6 +44,7 @@ import { LogLevel, ScaffoldConfig } from "./types"
|
||||
* 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}
|
||||
@@ -62,7 +57,7 @@ export async function Scaffold(config: ScaffoldConfig): Promise<void> {
|
||||
|
||||
registerHelpers(config)
|
||||
try {
|
||||
config.data = { name: config.name, Name: pascalCase(config.name), ...config.data }
|
||||
config.data = { name: config.name, Name: defaultHelpers.pascalCase(config.name), ...config.data }
|
||||
logInitStep(config)
|
||||
for (let _template of config.templates) {
|
||||
try {
|
||||
@@ -71,6 +66,7 @@ export async function Scaffold(config: ScaffoldConfig): Promise<void> {
|
||||
_template,
|
||||
)
|
||||
const files = await getFileList(config, template)
|
||||
log(config, LogLevel.Debug, "Iterating files", { files, template })
|
||||
for (const inputFilePath of files) {
|
||||
if (await isDir(inputFilePath)) {
|
||||
continue
|
||||
@@ -78,9 +74,9 @@ export async function Scaffold(config: ScaffoldConfig): Promise<void> {
|
||||
const relPath = makeRelativePath(path.dirname(removeGlob(inputFilePath).replace(nonGlobTemplate, "")))
|
||||
const basePath = getBasePath(relPath)
|
||||
logInputFile(config, {
|
||||
origTemplate,
|
||||
relPath,
|
||||
template,
|
||||
originalTemplate: origTemplate,
|
||||
relativePath: relPath,
|
||||
parsedTemplate: template,
|
||||
inputFilePath,
|
||||
nonGlobTemplate,
|
||||
basePath,
|
||||
@@ -101,40 +97,40 @@ export async function Scaffold(config: ScaffoldConfig): Promise<void> {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
async function handleTemplateFile(
|
||||
config: ScaffoldConfig,
|
||||
{ templatePath, basePath }: { templatePath: string; basePath: string },
|
||||
|
||||
/**
|
||||
* 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(
|
||||
/** The path or URL to the config file */
|
||||
pathOrUrl: string,
|
||||
/** Information needed before loading the config */
|
||||
config: MinimalConfig,
|
||||
/** Any overrides to the loaded config */
|
||||
overrides?: Resolver<ScaffoldCmdConfig, Partial<Omit<ScaffoldConfig, "name">>>,
|
||||
): Promise<void> {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
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)
|
||||
|
||||
log(config, LogLevel.Info, `Writing to ${outputPath}`)
|
||||
await copyFileTransformed(config, { exists, overwrite, outputPath, inputPath })
|
||||
resolve()
|
||||
} catch (e: any) {
|
||||
handleErr(e)
|
||||
reject(e)
|
||||
}
|
||||
})
|
||||
const _cmdConfig: ScaffoldCmdConfig = {
|
||||
dryRun: false,
|
||||
output: process.cwd(),
|
||||
verbose: LogLevel.Info,
|
||||
overwrite: false,
|
||||
templates: [],
|
||||
createSubFolder: false,
|
||||
quiet: false,
|
||||
config: pathOrUrl,
|
||||
...config,
|
||||
}
|
||||
const _overrides = resolve(overrides, _cmdConfig)
|
||||
const _config = await parseConfigFile(_cmdConfig)
|
||||
return Scaffold({ ..._config, ..._overrides })
|
||||
}
|
||||
|
||||
export default Scaffold
|
||||
|
||||
55
src/types.ts
55
src/types.ts
@@ -3,7 +3,8 @@ import { HelperDelegate } from "handlebars/runtime"
|
||||
/**
|
||||
* The config object for defining a scaffolding group.
|
||||
*
|
||||
* @see https://github.com/chenasraf/simple-scaffold#readme
|
||||
* @see {@link https://chenasraf.github.io/simple-scaffold/pages/node.html | Node.js usage}
|
||||
* @see {@link https://chenasraf.github.io/simple-scaffold/pages/cli.html | CLI usage}
|
||||
* @see {@link DefaultHelpers}
|
||||
* @see {@link CaseHelpers}
|
||||
* @see {@link DateHelpers}
|
||||
@@ -56,7 +57,7 @@ export interface ScaffoldConfig {
|
||||
* Enable to override output files, even if they already exist.
|
||||
*
|
||||
* You may supply a function to this option, which can take the arguments `(fullPath, baseDir, baseName)` and returns
|
||||
* a string, to return a dynamic path for each file.
|
||||
* a boolean for each file.
|
||||
*
|
||||
* May also be a {@link FileResponseHandler} which returns a boolean value per file.
|
||||
*
|
||||
@@ -131,10 +132,8 @@ export interface ScaffoldConfig {
|
||||
* @see {@link DefaultHelpers}
|
||||
* @see {@link CaseHelpers}
|
||||
* @see {@link DateHelpers}
|
||||
* @see https://casraf.dev/simple-scaffold#helpers
|
||||
* @see https://casraf.dev/simple-scaffold#built-in-helpers
|
||||
* @see https://handlebarsjs.com/guide/#custom-helpers
|
||||
*/
|
||||
* @see {@link https://chenasraf.github.io/simple-scaffold/pages/templates.html | Templates}
|
||||
* */
|
||||
helpers?: Record<string, Helper>
|
||||
|
||||
/**
|
||||
@@ -326,6 +325,10 @@ export type FileResponse<T> = T | FileResponseHandler<T>
|
||||
|
||||
/** @internal */
|
||||
export interface ScaffoldCmdConfig {
|
||||
/**
|
||||
* Name to be passed to the generated files. `{{name}}` and `{{Name}}` inside contents and file names will be replaced
|
||||
* accordingly.
|
||||
*/
|
||||
name: string
|
||||
templates: string[]
|
||||
output: string
|
||||
@@ -336,4 +339,44 @@ export interface ScaffoldCmdConfig {
|
||||
quiet: boolean
|
||||
verbose: LogLevel
|
||||
dryRun: boolean
|
||||
config?: string
|
||||
/** The key to use for the file which contains the template configurations. */
|
||||
key?: string
|
||||
github?: 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}
|
||||
*/
|
||||
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
|
||||
*/
|
||||
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, "quiet" | "verbose">
|
||||
|
||||
/** @internal */
|
||||
export type ConfigLoadConfig = LogConfig & Pick<ScaffoldCmdConfig, "config"> & { isRemote: boolean }
|
||||
|
||||
/** @internal */
|
||||
export type MinimalConfig = Pick<ScaffoldCmdConfig, "name" | "key">
|
||||
|
||||
394
src/utils.ts
394
src/utils.ts
@@ -1,397 +1,17 @@
|
||||
import path from "path"
|
||||
import { F_OK } from "constants"
|
||||
import {
|
||||
DefaultHelpers,
|
||||
FileResponse,
|
||||
FileResponseHandler,
|
||||
Helper,
|
||||
LogLevel,
|
||||
ScaffoldCmdConfig,
|
||||
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"
|
||||
import { glob, hasMagic } from "glob"
|
||||
import { OptionsBase } from "massarg/types"
|
||||
|
||||
const dateFns = {
|
||||
add: dtAdd,
|
||||
format: dtFormat,
|
||||
parseISO: dtParseISO,
|
||||
}
|
||||
|
||||
const { readFile, writeFile } = fsPromises
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
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(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])
|
||||
}
|
||||
}
|
||||
import { Resolver } from "./types"
|
||||
|
||||
export function handleErr(err: NodeJS.ErrnoException | null): void {
|
||||
if (err) throw err
|
||||
}
|
||||
|
||||
export function log(config: ScaffoldConfig, level: LogLevel, ...obj: any[]): void {
|
||||
if (config.quiet || config.verbose === LogLevel.None || level < (config.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),
|
||||
),
|
||||
)
|
||||
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 async function createDirIfNotExists(dir: string, config: ScaffoldConfig): Promise<void> {
|
||||
const parentDir = path.dirname(dir)
|
||||
|
||||
if (!(await pathExists(parentDir))) {
|
||||
await createDirIfNotExists(parentDir, config)
|
||||
export function wrapNoopResolver<T, R = T>(value: Resolver<T, R>): Resolver<T, R> {
|
||||
if (typeof value === "function") {
|
||||
return value
|
||||
}
|
||||
|
||||
if (!(await pathExists(dir))) {
|
||||
try {
|
||||
log(config, LogLevel.Debug, `Creating dir ${dir}`)
|
||||
await mkdir(dir)
|
||||
return
|
||||
} catch (e: any) {
|
||||
if (e.code !== "EEXIST") {
|
||||
throw e
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function getOptionValueForFile<T>(
|
||||
config: ScaffoldConfig,
|
||||
filePath: string,
|
||||
fn: FileResponse<T>,
|
||||
defaultValue?: T,
|
||||
): T {
|
||||
if (typeof fn !== "function") {
|
||||
return defaultValue ?? (fn as T)
|
||||
}
|
||||
return (fn as FileResponseHandler<T>)(
|
||||
filePath,
|
||||
path.dirname(handlebarsParse(config, filePath, { isPath: true }).toString()),
|
||||
path.basename(handlebarsParse(config, filePath, { isPath: true }).toString()),
|
||||
)
|
||||
}
|
||||
|
||||
export function handlebarsParse(
|
||||
config: ScaffoldConfig,
|
||||
templateBuffer: Buffer | string,
|
||||
{ isPath = false }: { isPath?: boolean } = {},
|
||||
): Buffer {
|
||||
const { data } = config
|
||||
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(config, LogLevel.Debug, e)
|
||||
log(config, 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(config: ScaffoldConfig, template: string): Promise<string[]> {
|
||||
return (
|
||||
await glob(template, {
|
||||
dot: true,
|
||||
nodir: true,
|
||||
// debug: config.verbose === LogLevel.Debug,
|
||||
})
|
||||
).map((f) => f.replace(/\//g, path.sep))
|
||||
}
|
||||
|
||||
export interface GlobInfo {
|
||||
nonGlobTemplate: string
|
||||
origTemplate: string
|
||||
isDirOrGlob: boolean
|
||||
isGlob: boolean
|
||||
template: string
|
||||
}
|
||||
|
||||
export async function getTemplateGlobInfo(config: ScaffoldConfig, template: string): Promise<GlobInfo> {
|
||||
const isGlob = hasMagic(template)
|
||||
log(config, LogLevel.Debug, "before isDir", "isGlob:", isGlob, template)
|
||||
let _template = template
|
||||
let nonGlobTemplate = isGlob ? removeGlob(template) : template
|
||||
nonGlobTemplate = path.normalize(nonGlobTemplate)
|
||||
const isDirOrGlob = isGlob ? true : await isDir(template)
|
||||
log(config, LogLevel.Debug, "after isDir", isDirOrGlob)
|
||||
const _shouldAddGlob = !isGlob && isDirOrGlob
|
||||
const origTemplate = template
|
||||
if (_shouldAddGlob) {
|
||||
_template = path.join(template, "**", "*")
|
||||
}
|
||||
return { nonGlobTemplate, origTemplate, isDirOrGlob, isGlob, template: _template }
|
||||
}
|
||||
|
||||
export interface OutputFileInfo {
|
||||
inputPath: string
|
||||
outputPathOpt: string
|
||||
outputDir: string
|
||||
outputPath: string
|
||||
exists: boolean
|
||||
}
|
||||
|
||||
export async function getTemplateFileInfo(
|
||||
config: ScaffoldConfig,
|
||||
{ templatePath, basePath }: { templatePath: string; basePath: string },
|
||||
): Promise<OutputFileInfo> {
|
||||
const inputPath = path.resolve(process.cwd(), templatePath)
|
||||
const outputPathOpt = getOptionValueForFile(config, inputPath, config.output)
|
||||
const outputDir = getOutputDir(config, outputPathOpt, basePath)
|
||||
const outputPath = handlebarsParse(config, path.join(outputDir, path.basename(inputPath)), {
|
||||
isPath: true,
|
||||
}).toString()
|
||||
const exists = await pathExists(outputPath)
|
||||
return { inputPath, outputPathOpt, outputDir, outputPath, exists }
|
||||
}
|
||||
|
||||
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.Info, `File ${outputPath} exists, overwriting`)
|
||||
}
|
||||
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)
|
||||
log(config, LogLevel.Info, "Done.")
|
||||
} else {
|
||||
log(config, LogLevel.Info, "Content output:")
|
||||
log(config, LogLevel.Info, finalOutputContents)
|
||||
}
|
||||
} else if (exists) {
|
||||
log(config, LogLevel.Info, `File ${outputPath} already exists, skipping`)
|
||||
}
|
||||
}
|
||||
|
||||
export function getOutputDir(config: ScaffoldConfig, outputPathOpt: string, basePath: string): string {
|
||||
return path.resolve(
|
||||
process.cwd(),
|
||||
...([
|
||||
outputPathOpt,
|
||||
basePath,
|
||||
config.createSubFolder
|
||||
? config.subFolderNameHelper
|
||||
? handlebarsParse(config, `{{ ${config.subFolderNameHelper} name }}`).toString()
|
||||
: config.name
|
||||
: undefined,
|
||||
].filter(Boolean) as string[]),
|
||||
)
|
||||
}
|
||||
|
||||
export function logInputFile(
|
||||
config: 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(
|
||||
config,
|
||||
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(config: ScaffoldConfig): void {
|
||||
log(config, LogLevel.Debug, "Full config:", {
|
||||
name: config.name,
|
||||
templates: config.templates,
|
||||
output: config.output,
|
||||
createSubfolder: config.createSubFolder,
|
||||
data: config.data,
|
||||
overwrite: config.overwrite,
|
||||
quiet: config.quiet,
|
||||
subFolderTransformHelper: config.subFolderNameHelper,
|
||||
helpers: Object.keys(config.helpers ?? {}),
|
||||
verbose: `${config.verbose} (${Object.keys(LogLevel).find(
|
||||
(k) => (LogLevel[k as any] as unknown as number) === config.verbose!,
|
||||
)})`,
|
||||
})
|
||||
log(config, LogLevel.Info, "Data:", config.data)
|
||||
}
|
||||
|
||||
export function parseAppendData(value: string, options: ScaffoldCmdConfig & OptionsBase): unknown {
|
||||
const data = options.data ?? {}
|
||||
const [key, val] = value.split(/\:?=/)
|
||||
// raw
|
||||
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("'"))
|
||||
return (_) => value
|
||||
}
|
||||
|
||||
149
tests/config.test.ts
Normal file
149
tests/config.test.ts
Normal file
@@ -0,0 +1,149 @@
|
||||
import { ScaffoldCmdConfig } from "../src/types"
|
||||
import * as config from "../src/config"
|
||||
import { resolve } from "../src/utils"
|
||||
// @ts-ignore
|
||||
import * as configFile from "../scaffold.config"
|
||||
|
||||
jest.mock("../src/git", () => {
|
||||
return {
|
||||
__esModule: true,
|
||||
...jest.requireActual("../src/git"),
|
||||
getGitConfig: () => {
|
||||
return Promise.resolve(blankCliConf)
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const { githubPartToUrl, parseAppendData, parseConfigFile, parseConfigSelection } = config
|
||||
|
||||
const blankCliConf: ScaffoldCmdConfig = {
|
||||
verbose: 0,
|
||||
name: "",
|
||||
output: "",
|
||||
templates: [],
|
||||
data: { name: "test" },
|
||||
overwrite: false,
|
||||
createSubFolder: false,
|
||||
dryRun: false,
|
||||
quiet: false,
|
||||
}
|
||||
|
||||
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" })
|
||||
})
|
||||
})
|
||||
|
||||
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",
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("parseConfigSelection", () => {
|
||||
test("no key", () => {
|
||||
expect(parseConfigSelection("scaffold.config.js")).toEqual({
|
||||
configFile: "scaffold.config.js",
|
||||
key: "default",
|
||||
isRemote: false,
|
||||
})
|
||||
})
|
||||
test("separate key", () => {
|
||||
expect(parseConfigSelection("scaffold.config.js", "component")).toEqual({
|
||||
configFile: "scaffold.config.js",
|
||||
key: "component",
|
||||
isRemote: false,
|
||||
})
|
||||
})
|
||||
test("key override", () => {
|
||||
expect(parseConfigSelection("scaffold.config.js:component", "main")).toEqual({
|
||||
configFile: "scaffold.config.js",
|
||||
key: "main",
|
||||
isRemote: false,
|
||||
})
|
||||
})
|
||||
test("isRemote: false", () => {
|
||||
expect(parseConfigSelection("scaffold.config.js", "main")).toEqual({
|
||||
configFile: "scaffold.config.js",
|
||||
key: "main",
|
||||
isRemote: false,
|
||||
})
|
||||
})
|
||||
test("isRemote: true", () => {
|
||||
expect(
|
||||
parseConfigSelection("https://github.com/chenasraf/simple-scaffold.git#scaffold.config.js:component", "main"),
|
||||
).toEqual({
|
||||
configFile: "https://github.com/chenasraf/simple-scaffold.git#scaffold.config.js",
|
||||
key: "main",
|
||||
isRemote: true,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("parseConfigFile", () => {
|
||||
test("normal config does not change", async () => {
|
||||
expect(
|
||||
await parseConfigFile({
|
||||
...blankCliConf,
|
||||
}),
|
||||
).toEqual(blankCliConf)
|
||||
})
|
||||
describe("appendData", () => {
|
||||
test("appends", async () => {
|
||||
const result = await parseConfigFile({
|
||||
...blankCliConf,
|
||||
appendData: { key: "value" },
|
||||
})
|
||||
expect(result?.data?.key).toEqual("value")
|
||||
})
|
||||
test("overwrites existing value", async () => {
|
||||
const result = await parseConfigFile({
|
||||
...blankCliConf,
|
||||
data: { num: "123" },
|
||||
appendData: { num: "1234" },
|
||||
})
|
||||
expect(result?.data?.num).toEqual("1234")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("getConfig", () => {
|
||||
test("gets git config", async () => {
|
||||
const resultFn = await config.getConfig({
|
||||
config: "https://github.com/chenasraf/simple-scaffold.git",
|
||||
isRemote: true,
|
||||
quiet: true,
|
||||
verbose: 0,
|
||||
})
|
||||
const result = await resolve(resultFn, blankCliConf)
|
||||
expect(result).toEqual(blankCliConf)
|
||||
})
|
||||
|
||||
test("gets local file config", async () => {
|
||||
const resultFn = await config.getConfig({
|
||||
config: "scaffold.config.js",
|
||||
isRemote: false,
|
||||
quiet: true,
|
||||
verbose: 0,
|
||||
})
|
||||
const result = await resolve(resultFn, {} as any)
|
||||
expect(result).toEqual(configFile)
|
||||
})
|
||||
})
|
||||
})
|
||||
148
tests/parser.test.ts
Normal file
148
tests/parser.test.ts
Normal file
@@ -0,0 +1,148 @@
|
||||
import { ScaffoldCmdConfig, ScaffoldConfig } from "../src/types"
|
||||
import path from "node:path"
|
||||
import * as dateFns from "date-fns"
|
||||
import { OptionsBase } from "massarg/types"
|
||||
import { dateHelper, defaultHelpers, handlebarsParse, nowHelper } from "../src/parser"
|
||||
|
||||
const blankConf: ScaffoldConfig = {
|
||||
verbose: 0,
|
||||
name: "",
|
||||
output: "",
|
||||
templates: [],
|
||||
data: { name: "test" },
|
||||
}
|
||||
|
||||
const blankCliConf: ScaffoldCmdConfig & OptionsBase = {
|
||||
verbose: 0,
|
||||
name: "",
|
||||
output: "",
|
||||
templates: [],
|
||||
data: { name: "test" },
|
||||
overwrite: false,
|
||||
createSubFolder: false,
|
||||
dryRun: false,
|
||||
quiet: false,
|
||||
extras: [],
|
||||
help: false,
|
||||
}
|
||||
|
||||
describe("parser", () => {
|
||||
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 }).toString()).toEqual(
|
||||
"C:\\exports\\test.txt",
|
||||
)
|
||||
})
|
||||
})
|
||||
describe("non-windows paths", () => {
|
||||
beforeAll(() => {
|
||||
origSep = path.sep
|
||||
Object.defineProperty(path, "sep", { value: "/" })
|
||||
})
|
||||
afterAll(() => {
|
||||
Object.defineProperty(path, "sep", { value: origSep })
|
||||
})
|
||||
test("should work for non-windows paths", async () => {
|
||||
expect(handlebarsParse(blankConf, "/home/test/{{name}}.txt", { isPath: true })).toEqual(
|
||||
Buffer.from("/home/test/test.txt"),
|
||||
)
|
||||
})
|
||||
})
|
||||
test("should not do path escaping on non-path compiles", async () => {
|
||||
expect(
|
||||
handlebarsParse(
|
||||
{ ...blankConf, data: { ...blankConf.data, escaped: "value" } },
|
||||
"/home/test/{{name}} \\{{escaped}}.txt",
|
||||
{
|
||||
isPath: false,
|
||||
},
|
||||
),
|
||||
).toEqual(Buffer.from("/home/test/test {{escaped}}.txt"))
|
||||
})
|
||||
})
|
||||
|
||||
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("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),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -3,7 +3,7 @@ import FileSystem from "mock-fs/lib/filesystem"
|
||||
import Scaffold from "../src/scaffold"
|
||||
import { readdirSync, readFileSync } from "fs"
|
||||
import { Console } from "console"
|
||||
import { defaultHelpers } from "../src/utils"
|
||||
import { defaultHelpers } from "../src/parser"
|
||||
import { join } from "path"
|
||||
import * as dateFns from "date-fns"
|
||||
import crypto from "crypto"
|
||||
|
||||
@@ -1,124 +1,21 @@
|
||||
import { dateHelper, handlebarsParse, nowHelper, parseAppendData } from "../src/utils"
|
||||
import { ScaffoldCmdConfig, ScaffoldConfig } from "../src/types"
|
||||
import path from "path"
|
||||
import * as dateFns from "date-fns"
|
||||
import { OptionsBase } from "massarg/types"
|
||||
import { handleErr, resolve } from "../src/utils"
|
||||
|
||||
const blankConf: ScaffoldConfig = {
|
||||
verbose: 0,
|
||||
name: "",
|
||||
output: "",
|
||||
templates: [],
|
||||
data: { name: "test" },
|
||||
}
|
||||
|
||||
const blankCliConf: ScaffoldCmdConfig & OptionsBase = {
|
||||
verbose: 0,
|
||||
name: "",
|
||||
output: "",
|
||||
templates: [],
|
||||
data: { name: "test" },
|
||||
overwrite: false,
|
||||
createSubFolder: false,
|
||||
dryRun: false,
|
||||
quiet: false,
|
||||
extras: [],
|
||||
help: false,
|
||||
}
|
||||
|
||||
describe("Utils", () => {
|
||||
describe("handlebarsParse", () => {
|
||||
let origSep: any
|
||||
describe("windows paths", () => {
|
||||
beforeAll(() => {
|
||||
origSep = path.sep
|
||||
Object.defineProperty(path, "sep", { value: "\\" })
|
||||
})
|
||||
afterAll(() => {
|
||||
Object.defineProperty(path, "sep", { value: origSep })
|
||||
})
|
||||
test("should work for windows paths", async () => {
|
||||
expect(handlebarsParse(blankConf, "C:\\exports\\{{name}}.txt", { isPath: true })).toEqual(
|
||||
Buffer.from("C:\\exports\\test.txt"),
|
||||
)
|
||||
})
|
||||
describe("utils", () => {
|
||||
describe("resolve", () => {
|
||||
test("should resolve function", () => {
|
||||
expect(resolve(() => 1, null)).toBe(1)
|
||||
expect(resolve((x) => x, 2)).toBe(2)
|
||||
})
|
||||
describe("non-windows paths", () => {
|
||||
beforeAll(() => {
|
||||
origSep = path.sep
|
||||
Object.defineProperty(path, "sep", { value: "/" })
|
||||
})
|
||||
afterAll(() => {
|
||||
Object.defineProperty(path, "sep", { value: origSep })
|
||||
})
|
||||
test("should work for non-windows paths", async () => {
|
||||
expect(handlebarsParse(blankConf, "/home/test/{{name}}.txt", { isPath: true })).toEqual(
|
||||
Buffer.from("/home/test/test.txt"),
|
||||
)
|
||||
})
|
||||
})
|
||||
test("should not do path escaping on non-path compiles", async () => {
|
||||
expect(
|
||||
handlebarsParse(
|
||||
{ ...blankConf, data: { ...blankConf.data, escaped: "value" } },
|
||||
"/home/test/{{name}} \\{{escaped}}.txt",
|
||||
{
|
||||
isPath: false,
|
||||
},
|
||||
),
|
||||
).toEqual(Buffer.from("/home/test/test {{escaped}}.txt"))
|
||||
test("should resolve value", () => {
|
||||
expect(resolve(1, null)).toBe(1)
|
||||
expect(resolve(2, 1)).toBe(2)
|
||||
})
|
||||
})
|
||||
|
||||
describe("Helpers", () => {
|
||||
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),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
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" })
|
||||
describe("handleErr", () => {
|
||||
test("should throw error", () => {
|
||||
expect(() => handleErr({ name: "test", message: "test" })).toThrow()
|
||||
expect(() => handleErr(null as never)).not.toThrow()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"compilerOptions": {
|
||||
"target": "ES2019",
|
||||
"target": "ES2022",
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"esModuleInterop": true,
|
||||
"lib": ["ES2019"],
|
||||
"lib": ["ES2022"],
|
||||
"declaration": true,
|
||||
"outDir": "dist",
|
||||
"strict": true,
|
||||
"sourceMap": true,
|
||||
"removeComments": false
|
||||
"removeComments": false,
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src/index.ts", "src/cmd.ts"],
|
||||
"exclude": ["tests/*"]
|
||||
|
||||
63
typedoc.config.js
Normal file
63
typedoc.config.js
Normal file
@@ -0,0 +1,63 @@
|
||||
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",
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
27
typedoc.json
27
typedoc.json
@@ -1,27 +0,0 @@
|
||||
{
|
||||
"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
|
||||
},
|
||||
"customCss": "src/docs.css",
|
||||
}
|
||||
Reference in New Issue
Block a user