mirror of
https://github.com/chenasraf/simple-scaffold.git
synced 2026-05-18 01:29:09 +00:00
Compare commits
164 Commits
v1.0.0-alp
...
v1.3.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
398a5d723b | ||
|
|
c7e2ef862c | ||
|
|
36dd27e480 | ||
|
|
6e19a86190 | ||
|
|
eba7897f84 | ||
|
|
dfdbca59a7 | ||
|
|
1d6643b7eb | ||
|
|
9b86499a04 | ||
|
|
f41ebfb1d0 | ||
|
|
873fa77c9a | ||
|
|
1ce4a41a97 | ||
|
|
4302eb5ce3 | ||
|
|
146086869f | ||
|
|
8a2207bec5 | ||
|
|
42568e0fa1 | ||
|
|
a4498f9539 | ||
|
|
5a2b187115 | ||
|
|
3db6a918f1 | ||
|
|
c3989769fe | ||
|
|
b3f7912760 | ||
|
|
1e0b73190c | ||
|
|
0c8e6e7573 | ||
|
|
f28280e815 | ||
|
|
7da786a463 | ||
|
|
4fecca8483 | ||
|
|
222e1a04ca | ||
|
|
f7956ddc78 | ||
|
|
029f260a4c | ||
|
|
7a4c0abd33 | ||
|
|
ed385ecfee | ||
|
|
833ea9d404 | ||
|
|
8fb508f32f | ||
|
|
75641e5e20 | ||
|
|
f4c745bfeb | ||
|
|
47b4c427ac | ||
|
|
7ef6d583a8 | ||
|
|
59a46b0455 | ||
|
|
767d34c684 | ||
|
|
2050ea38d5 | ||
|
|
1bfcafad46 | ||
|
|
3c5c2ded02 | ||
|
|
8f5bee8da6 | ||
|
|
7d1dc2a808 | ||
|
|
8e432bfb0b | ||
|
|
7c19c53337 | ||
|
|
94fec76616 | ||
|
|
20400bd81d | ||
|
|
c334396d74 | ||
|
|
78c8e693a8 | ||
|
|
7865f27471 | ||
|
|
ee4e52c4a0 | ||
|
|
d7d2b13888 | ||
|
|
6b6baefbce | ||
|
|
943717a769 | ||
|
|
e28c4db323 | ||
|
|
5308ef9618 | ||
|
|
a8e9f71c11 | ||
|
|
6fa2a8b03b | ||
|
|
7c69010e95 | ||
|
|
b569f2b0a1 | ||
|
|
adba6490e8 | ||
|
|
1d0c20cba9 | ||
|
|
cd68ab41d4 | ||
|
|
1b37a8eb40 | ||
|
|
576798cfeb | ||
|
|
ac8af8e129 | ||
|
|
96b93d8b60 | ||
|
|
11edb0d464 | ||
|
|
35262b5a44 | ||
|
|
3b77e6938c | ||
|
|
3cf93598c3 | ||
|
|
8957b595da | ||
|
|
2841ebd3d2 | ||
|
|
0364247904 | ||
|
|
ac2c0d7ffb | ||
|
|
a002402088 | ||
|
|
bdc23e7074 | ||
|
|
6160a04009 | ||
|
|
7414baf2ab | ||
|
|
c45d870233 | ||
|
|
8c8cedee48 | ||
|
|
40606edaee | ||
|
|
e450ad242e | ||
|
|
df60922154 | ||
|
|
256d919253 | ||
|
|
f8c31a1ecf | ||
|
|
5571aba8dc | ||
|
|
12f8bca7b5 | ||
|
|
643431d333 | ||
|
|
0042c12ee7 | ||
|
|
869911bba0 | ||
|
|
9bd6219aa5 | ||
|
|
27a6ba4dda | ||
|
|
923b5316be | ||
|
|
bf10fb8ec3 | ||
|
|
4e2fa01bed | ||
|
|
af65ecadb9 | ||
|
|
0d359b1917 | ||
|
|
f2a75c9fb0 | ||
|
|
3c57638903 | ||
|
|
a3deda2d80 | ||
|
|
b2799d0bad | ||
|
|
f4997c6289 | ||
|
|
fe871eb97e | ||
|
|
dffa81fde1 | ||
|
|
56be5f32cd | ||
|
|
4a4e024aec | ||
|
|
86a7a2c063 | ||
|
|
d3259c44aa | ||
|
|
e26a434dba | ||
|
|
1783ddf230 | ||
|
|
4575f73d69 | ||
|
|
f07df79e82 | ||
|
|
cb6e06f1c9 | ||
|
|
a043a05bc9 | ||
|
|
52cb3e7353 | ||
|
|
89d7897f4e | ||
|
|
d6e1693074 | ||
|
|
56f1340093 | ||
|
|
8782f18a73 | ||
|
|
21c4ab6e1a | ||
|
|
d797e5b640 | ||
|
|
c3835a7b04 | ||
|
|
81ba5f50fd | ||
|
|
edcf1aceaa | ||
|
|
d0a0db0f82 | ||
|
|
819f84e5ca | ||
|
|
36f8b87514 | ||
|
|
d06c0d64b3 | ||
|
|
5ab2637485 | ||
|
|
0af639254b | ||
|
|
391a08ac63 | ||
|
|
cd34930e04 | ||
|
|
956b00700f | ||
|
|
91116bba69 | ||
|
|
f4cc44cf17 | ||
|
|
54b90235e5 | ||
|
|
d03d0e0812 | ||
|
|
5b72b6c166 | ||
|
|
d96992cd2d | ||
|
|
925993948a | ||
|
|
09403e18f1 | ||
|
|
c2bc8b7606 | ||
|
|
b1b1aca802 | ||
|
|
2305083c7d | ||
|
|
8413225202 | ||
|
|
559b5ad7f3 | ||
|
|
a21a35f773 | ||
|
|
84e6207891 | ||
|
|
6b57406369 | ||
|
|
53e8bc4cc0 | ||
|
|
54848f9c50 | ||
|
|
2623b787e6 | ||
|
|
ad30ee0c0c | ||
|
|
8575b1e0c3 | ||
|
|
d8aba21d0e | ||
|
|
3f2945eaa4 | ||
|
|
930344656a | ||
|
|
99c9055208 | ||
|
|
f1698d2a46 | ||
|
|
c17e6304e6 | ||
|
|
535260a0c0 | ||
|
|
14988576f3 | ||
|
|
6f03ed9d60 |
@@ -3,3 +3,6 @@ tab_width = 2
|
||||
indent_style = space
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
5
.github/FUNDING.yml
vendored
5
.github/FUNDING.yml
vendored
@@ -1,6 +1,6 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||
github: chenasraf
|
||||
patreon: # Replace with a single Patreon username
|
||||
open_collective: # Replace with a single Open Collective username
|
||||
ko_fi: casraf
|
||||
@@ -9,4 +9,5 @@ community_bridge: # Replace with a single Community Bridge project-name e.g., cl
|
||||
liberapay: # Replace with a single Liberapay username
|
||||
issuehunt: # Replace with a single IssueHunt username
|
||||
otechie: # Replace with a single Otechie username
|
||||
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||
custom:
|
||||
- "https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=TSH3C3ABGQM22¤cy_code=ILS&source=url"
|
||||
|
||||
47
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
47
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: "[BUG] "
|
||||
labels: bug, needs-triage
|
||||
assignees: chenasraf
|
||||
---
|
||||
|
||||
#### Describe the bug
|
||||
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
#### How To Reproduce
|
||||
|
||||
Steps to reproduce the behavior:
|
||||
|
||||
1. Prepare templates:
|
||||
|
||||
```text
|
||||
This is my {{ template }}
|
||||
```
|
||||
|
||||
2. Run with args/config:
|
||||
|
||||
```shell
|
||||
npx simple-scaffold@latest -t input -o output TplName
|
||||
```
|
||||
|
||||
#### Expected behavior\*\*
|
||||
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
#### Logs
|
||||
|
||||
If applicable, paste your logs to help explain your problem. To see more logs, run the scaffold with
|
||||
`-v 1` to enable debug logging.
|
||||
|
||||
#### Desktop (please complete the following information):
|
||||
|
||||
- OS: [e.g. macOS, Windows, Linux]
|
||||
- OS Version: [e.g. Big Sur, 11, Ubuntu 20.04]
|
||||
- Node.js: [e.g. 16.8]
|
||||
- Simple Scaffold Version [e.g. 1.1.2]
|
||||
|
||||
#### Additional context
|
||||
|
||||
Add any other context about the problem here.
|
||||
24
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
24
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: "[FEATURE] "
|
||||
labels: enhancement, needs-triage
|
||||
assignees: chenasraf
|
||||
---
|
||||
|
||||
#### Is your feature request related to a problem? Please describe.
|
||||
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
#### Describe the solution you'd like
|
||||
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
#### Describe alternatives you've considered
|
||||
|
||||
A clear and concise description of any alternative solutions or features you've considered, if
|
||||
applicable.
|
||||
|
||||
#### Additional context
|
||||
|
||||
Add any other context or screenshots about the feature request here.
|
||||
48
.github/workflows/alpha.yml
vendored
48
.github/workflows/alpha.yml
vendored
@@ -1,48 +0,0 @@
|
||||
name: Alpha Releases
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [alpha]
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
if: "!contains(github.event.head_commit.message, '[skip ci]')"
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: "12.x"
|
||||
- run: yarn install --frozen-lockfile
|
||||
- run: yarn build
|
||||
- run: cd ./dist && yarn pack --filename=../release.tgz
|
||||
- uses: Klemensas/action-autotag@stable
|
||||
id: update_tag
|
||||
with:
|
||||
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
||||
tag_prefix: "v"
|
||||
- name: Publish on NPM
|
||||
uses: JS-DevTools/npm-publish@v1
|
||||
if: "!contains(github.event.head_commit.message, '[skip publish]')"
|
||||
with:
|
||||
package: ./dist/package.json
|
||||
token: "${{ secrets.NPM_TOKEN }}"
|
||||
- name: Create Release
|
||||
if: steps.update_tag.outputs.tagname && !contains(github.event.head_commit.message, '[skip publish]')
|
||||
uses: actions/create-release@v1
|
||||
id: create_release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tag_name: ${{ steps.update_tag.outputs.tagname }}
|
||||
release_name: Release ${{ steps.update_tag.outputs.tagname }}
|
||||
- name: Upload Release Asset
|
||||
if: steps.update_tag.outputs.tagname && !contains(github.event.head_commit.message, '[skip publish]')
|
||||
id: upload-release-asset
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: ./package.tgz
|
||||
asset_name: package.tgz
|
||||
asset_content_type: application/tgz
|
||||
20
.github/workflows/docs.yml
vendored
Normal file
20
.github/workflows/docs.yml
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
name: Build Documentation
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master, develop]
|
||||
jobs:
|
||||
docs:
|
||||
runs-on: ubuntu-latest
|
||||
if: "!contains(github.event.head_commit.message, '[skip ci]') && !contains(github.event.head_commit.message, '[skip docs]')"
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: "18.x"
|
||||
- run: yarn install --frozen-lockfile
|
||||
- run: yarn build-docs
|
||||
- uses: peaceiris/actions-gh-pages@v3
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
publish_dir: ./docs
|
||||
47
.github/workflows/main.yml
vendored
47
.github/workflows/main.yml
vendored
@@ -1,47 +0,0 @@
|
||||
name: Releases
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
if: "!contains(github.event.head_commit.message, '[skip ci]')"
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: "12.x"
|
||||
- run: yarn install --frozen-lockfile
|
||||
- run: yarn build
|
||||
- run: cd ./dist && yarn pack --filename=../release.tgz
|
||||
- uses: Klemensas/action-autotag@stable
|
||||
id: update_tag
|
||||
with:
|
||||
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
||||
tag_prefix: "v"
|
||||
- name: Publish on NPM
|
||||
uses: JS-DevTools/npm-publish@v1
|
||||
with:
|
||||
package: ./dist/package.json
|
||||
token: "${{ secrets.NPM_TOKEN }}"
|
||||
- name: Create Release
|
||||
if: steps.update_tag.outputs.tagname && !contains(github.event.head_commit.message, '[skip publish]')
|
||||
uses: actions/create-release@v1
|
||||
id: create_release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tag_name: ${{ steps.update_tag.outputs.tagname }}
|
||||
release_name: Release ${{ steps.update_tag.outputs.tagname }}
|
||||
- name: Upload Release Asset
|
||||
if: steps.update_tag.outputs.tagname && !contains(github.event.head_commit.message, '[skip publish]')
|
||||
id: upload-release-asset
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: ./package.tgz
|
||||
asset_name: package.tgz
|
||||
asset_content_type: application/tgz
|
||||
8
.github/workflows/pull_requests.yml
vendored
8
.github/workflows/pull_requests.yml
vendored
@@ -2,16 +2,16 @@ name: Pull Requests
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [master, alpha, beta]
|
||||
branches: [master, develop]
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
if: "!contains(github.event.head_commit.message, '[skip ci]')"
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v1
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: "12.x"
|
||||
node-version: "18.x"
|
||||
- run: yarn install --frozen-lockfile
|
||||
- run: yarn build
|
||||
- run: yarn test
|
||||
|
||||
22
.github/workflows/release.yml
vendored
Normal file
22
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
name: Semantic Release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master, develop, feat/*, fix/*]
|
||||
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
|
||||
with:
|
||||
node-version: "18.x"
|
||||
- run: yarn install --frozen-lockfile
|
||||
- run: yarn test
|
||||
- run: yarn build
|
||||
- run: yarn semantic-release
|
||||
if: "!contains(github.event.head_commit.message, '[skip publish]')"
|
||||
env:
|
||||
NPM_TOKEN: "${{ secrets.NPM_TOKEN }}"
|
||||
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -42,6 +42,9 @@ typings/
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# NPM
|
||||
.npmrc
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
@@ -60,3 +63,6 @@ typings/
|
||||
examples/test-output/**/*
|
||||
dist/
|
||||
.DS_Store
|
||||
tmp/
|
||||
docs/
|
||||
.nvmrc
|
||||
|
||||
9
.markdownlint.json
Normal file
9
.markdownlint.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"line-length": {
|
||||
"line_length": 100,
|
||||
"tables": false,
|
||||
"code_blocks": false
|
||||
},
|
||||
"no-inline-html": false,
|
||||
"first-line-h1": false
|
||||
}
|
||||
12
.prettierrc
12
.prettierrc
@@ -1,5 +1,15 @@
|
||||
{
|
||||
"semi": false,
|
||||
"trailingComma": "all",
|
||||
"printWidth": 120,
|
||||
"tabWidth": 2
|
||||
"tabWidth": 2,
|
||||
"overrides": [
|
||||
{
|
||||
"files": "*.md",
|
||||
"options": {
|
||||
"printWidth": 100,
|
||||
"proseWrap": "always"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
50
.vscode/launch.json
vendored
50
.vscode/launch.json
vendored
@@ -1,29 +1,27 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Debug Scaffold",
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"protocol": "inspector",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"program": "${workspaceFolder}/test.ts",
|
||||
"outFiles": [
|
||||
"${workspaceRoot}/dist/test.js"
|
||||
],
|
||||
"env": {
|
||||
"NODE_ENV": "develop"
|
||||
},
|
||||
"sourceMaps": true,
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Debug Scaffold",
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"protocol": "inspector",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"program": "${workspaceFolder}/test.ts",
|
||||
"outFiles": ["${workspaceRoot}/dist/test.js"],
|
||||
"env": {
|
||||
"NODE_ENV": "develop"
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "attach",
|
||||
"name": "Attach by Process ID",
|
||||
"processId": "${command:PickProcess}"
|
||||
}
|
||||
]
|
||||
"sourceMaps": true
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "attach",
|
||||
"name": "Attach by Process ID",
|
||||
"processId": "${command:PickProcess}"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
19
.vscode/settings.json
vendored
19
.vscode/settings.json
vendored
@@ -2,6 +2,21 @@
|
||||
"typescript.tsdk": "./node_modules/typescript/lib",
|
||||
"npm.packageManager": "yarn",
|
||||
"cSpell.words": [
|
||||
"massarg"
|
||||
]
|
||||
"massarg",
|
||||
"MYCOMPONENT",
|
||||
"myname",
|
||||
"nobrace",
|
||||
"nocomment",
|
||||
"nodir",
|
||||
"noext",
|
||||
"nonegate",
|
||||
"subdir",
|
||||
"variabletoken"
|
||||
],
|
||||
"[markdown]": {
|
||||
"editor.rulers": [
|
||||
87,
|
||||
100
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
26
.vscode/tasks.json
vendored
26
.vscode/tasks.json
vendored
@@ -5,49 +5,55 @@
|
||||
"script": "build",
|
||||
"label": "build",
|
||||
"type": "npm",
|
||||
"problemMatcher": [],
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"script": "dev",
|
||||
"label": "dev",
|
||||
"type": "npm",
|
||||
"problemMatcher": [],
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"command": "yarn typedoc --watch",
|
||||
"label": "typedoc --watch",
|
||||
"type": "shell",
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"script": "start",
|
||||
"label": "start",
|
||||
"type": "npm",
|
||||
"problemMatcher": [],
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"script": "test",
|
||||
"label": "test",
|
||||
"type": "npm",
|
||||
"problemMatcher": [],
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"command": "yarn test --watchAll",
|
||||
"label": "yarn test --watchAll",
|
||||
"type": "shell",
|
||||
"problemMatcher": [],
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"script": "cmd",
|
||||
"label": "cmd",
|
||||
"type": "npm",
|
||||
"problemMatcher": [],
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"script": "build-test",
|
||||
"label": "build-test",
|
||||
"type": "npm",
|
||||
"problemMatcher": [],
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"script": "build-cmd",
|
||||
"label": "build-cmd",
|
||||
"type": "npm",
|
||||
"problemMatcher": [],
|
||||
},
|
||||
],
|
||||
"problemMatcher": []
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
120
CHANGELOG.md
Normal file
120
CHANGELOG.md
Normal file
@@ -0,0 +1,120 @@
|
||||
# Change Log
|
||||
|
||||
## [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
|
||||
([3c5c2de](https://github.com/chenasraf/simple-scaffold/commit/3c5c2ded02f61ff086e81ea4a7f40529bdff1c9d))
|
||||
|
||||
### 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))
|
||||
|
||||
## [1.1.4](https://github.com/chenasraf/simple-scaffold/compare/v1.1.3...v1.1.4) (2023-03-13)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 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))
|
||||
|
||||
### Misc
|
||||
|
||||
- update typedoc version
|
||||
([c334396](https://github.com/chenasraf/simple-scaffold/commit/c334396d74414cbe0aba305c66dfad7fdeb88669))
|
||||
- update dependencies
|
||||
([20400bd](https://github.com/chenasraf/simple-scaffold/commit/20400bd81dd43d457427675286c9964a8bc0d5f6))
|
||||
- bump version number
|
||||
([8e432bf](https://github.com/chenasraf/simple-scaffold/commit/8e432bfb0b410dc0655c3924031bea2648a42ad0))
|
||||
|
||||
## [1.1.3](https://github.com/chenasraf/simple-scaffold/compare/v1.1.2...v1.1.3) (2023-03-11)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- base path
|
||||
([943717a](https://github.com/chenasraf/simple-scaffold/commit/943717a76998ec0609f2072c886df6b4775f2ea2))
|
||||
- binary files + add tests
|
||||
([e450ad2](https://github.com/chenasraf/simple-scaffold/commit/e450ad242ed70ae928b19964da38cdcb1b6cf659))
|
||||
|
||||
## [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.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.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.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)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- binary files
|
||||
([7c0c347](https://github.com/chenasraf/simple-scaffold/commit/7c0c3470020d7c166ea68a8effa6df65ec38f2c8))
|
||||
|
||||
## [0.6.0](https://github.com/chenasraf/simple-scaffold/compare/v0.5.0...v0.6.0) (2021-02-01)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- support deeper file structure
|
||||
([4afafa5](https://github.com/chenasraf/simple-scaffold/commit/4afafa5a4af2e3f4b0af54f20811ecb2c8d98560))
|
||||
|
||||
## [0.5.0](https://github.com/chenasraf/simple-scaffold/compare/v0.4.5...v0.5.0) (2019-02-27)
|
||||
|
||||
## [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.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.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.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.4](https://github.com/chenasraf/simple-scaffold/compare/v0.1.3...v0.1.4) (2018-01-01)
|
||||
|
||||
## [0.1.3](https://github.com/chenasraf/simple-scaffold/compare/v0.1.2...v0.1.3) (2018-01-01)
|
||||
|
||||
## 0.1.2 (2018-01-01)
|
||||
260
README.md
260
README.md
@@ -1,208 +1,130 @@
|
||||
# simple-scaffold
|
||||
<h1 align="center">Simple Scaffold</h1>
|
||||
|
||||
Simple Scaffold allows you to create your structured files based on templates.
|
||||
<h2 align="center">
|
||||
|
||||
## Install
|
||||
[GitHub](https://github.com/chenasraf/simple-scaffold) |
|
||||
[Documentation](https://casraf.dev/simple-scaffold) |
|
||||
[NPM](https://npmjs.com/package/simple-scaffold) | [casraf.dev](https://casraf.dev)
|
||||
|
||||
You can either use it as a command line tool or import into your own code and run from there.
|
||||

|
||||

|
||||
|
||||
```bash
|
||||
# npm
|
||||
npm install [-g] simple-scaffold
|
||||
# yarn
|
||||
yarn [global] add simple-scaffold
|
||||
# run without installing
|
||||
npx simple-scaffold <...args>
|
||||
```
|
||||
</h2>
|
||||
|
||||
## Use as a command line tool
|
||||
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.
|
||||
|
||||
### Command Line Options
|
||||
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.
|
||||
|
||||
```plaintext
|
||||
Usage: simple-scaffold [options]
|
||||
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!
|
||||
|
||||
Options:
|
||||
<div align="center">
|
||||
|
||||
--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.
|
||||
</div>
|
||||
|
||||
--output|-o Path to output to. If --create-sub-folder is enabled, the
|
||||
subfolder will be created inside this path.
|
||||
---
|
||||
|
||||
--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.
|
||||
## Quick Start
|
||||
|
||||
--overwrite|-w Enable to override output files, even if they already exist.
|
||||
(default: false)
|
||||
The fastest way to get started is to use `npx` to immediately start a scaffold process.
|
||||
|
||||
--data|-d Add custom data to the templates. By default, only your app
|
||||
name is included.
|
||||
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:
|
||||
|
||||
--create-sub-folder|-s Create subfolder with the input name (default:
|
||||
false)
|
||||
`templates/component/{{ pascalName name }}.tsx`
|
||||
|
||||
--quiet|-q Suppress output logs (default:
|
||||
false)
|
||||
```tsx
|
||||
// Created: {{ now | 'yyyy-MM-dd' }}
|
||||
import React from 'react'
|
||||
|
||||
--dry-run|-dr Don't emit actual files. This is good for testing your
|
||||
scaffolds and making sure they don't fail, without having to write
|
||||
actual files. (default: false)
|
||||
```
|
||||
|
||||
You can also add this as a script in your `package.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"scaffold": "yarn simple-scaffold --templates scaffolds/component/**/* --output src/components --data '{\"myProp\": \"propName\", \"myVal\": \"123\"}'"
|
||||
}
|
||||
export default {{pascalCase name}}: React.FC = (props) => {
|
||||
return (
|
||||
<div className="{{camelCase name}}">{{pascalCase name}} Component</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Use in Node.js
|
||||
To generate the template output, run:
|
||||
|
||||
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 constructor, and invoke `run()` when you are ready to start.
|
||||
The config takes similar arguments to the command line:
|
||||
|
||||
```javascript
|
||||
const SimpleScaffold = require("simple-scaffold").default
|
||||
|
||||
const scaffold = new SimpleScaffold({
|
||||
name: "component",
|
||||
templates: [path.join(__dirname, "scaffolds", "component")],
|
||||
output: path.join(__dirname, "src", "components"),
|
||||
createSubFolder: true,
|
||||
locals: {
|
||||
property: "value",
|
||||
},
|
||||
}).run()
|
||||
```shell
|
||||
# generate single component
|
||||
npx simple-scaffold@latest \
|
||||
-t templates/component -o src/components PageWrapper
|
||||
```
|
||||
|
||||
The exception in the config is that `output`, when used in Node directly, may also be passed a
|
||||
function for each input file to output into a dynamic path:
|
||||
This will immediately create the following file: `src/components/PageWrapper.tsx`
|
||||
|
||||
```javascript
|
||||
config.output = (fullPath, baseDir, baseName) => {
|
||||
console.log({ fullPath, baseDir, baseName })
|
||||
return [baseDir, baseName].join(path.sep)
|
||||
```tsx
|
||||
// Created: 2077/01/01
|
||||
import React from 'react'
|
||||
|
||||
export default PageWrapper: React.FC = (props) => {
|
||||
return (
|
||||
<div className="pageWrapper">PageWrapper Component</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Preparing files
|
||||
## Documentation
|
||||
|
||||
### Template files
|
||||
See full documentation [here](https://chenasraf.github.io/simple-scaffold).
|
||||
|
||||
Put your template files anywhere, and fill them with tokens for replacement.
|
||||
- [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)
|
||||
|
||||
### Variable/token replacement
|
||||
## Contributing
|
||||
|
||||
Scaffolding will replace `{{ varName }}` in both the file name and its contents and put the
|
||||
transformed files in the output directory.
|
||||
I am developing this package on my free time, so any support, whether code, issues, or just stars is
|
||||
very helpful to sustaining its life. If you are feeling incredibly generous and would like to donate
|
||||
just a small amount to help sustain this project, I would be very very thankful!
|
||||
|
||||
The data available for the template parser is the data you pass to the `data` config option (or
|
||||
`--data` argument in CLI).
|
||||
<a href='https://ko-fi.com/casraf' target='_blank'>
|
||||
<img height='36' style='border:0px;height:36px;'
|
||||
src='https://cdn.ko-fi.com/cdn/kofi1.png?v=3'
|
||||
alt='Buy Me a Coffee at ko-fi.com' />
|
||||
</a>
|
||||
|
||||
Your `data` will be pre-populated with the following:
|
||||
I welcome any issues or pull requests on GitHub. If you find a bug, or would like a new feature,
|
||||
don't hesitate to open an appropriate issue and I will do my best to reply promptly.
|
||||
|
||||
- `{{Name}}`: PascalCase of the component name
|
||||
- `{{name}}`: raw name of the component
|
||||
If you are a developer and want to contribute code, here are some starting tips:
|
||||
|
||||
> Simple-Scaffold uses [Handlebars.js](https://handlebarsjs.com/) for outputting the file contents,
|
||||
> see their documentation for more information on syntax.
|
||||
> Any `data` you add in the config will be available for use with their names wrapped in
|
||||
> `{{` and `}}`.
|
||||
1. Fork this repository
|
||||
2. Run `yarn install`
|
||||
3. Run `yarn dev` to start file watch mode
|
||||
4. Make any changes you would like
|
||||
5. Create tests for your changes
|
||||
6. Update the relevant documentation (readme, code comments, type comments)
|
||||
7. Create a PR on upstream
|
||||
|
||||
Simple-Scaffold provides some built-in text transformation filters usable by handleBars.
|
||||
Some tips on getting around the code:
|
||||
|
||||
For example, you may use `{{ name | snakeCase }}` inside a template file or filename, and it will
|
||||
replace `My Name` with `my_name` when producing the final value.
|
||||
- Use `yarn dev` for development - it runs TypeScript compile in watch mode, allowing you to make
|
||||
changes and immediately be able to try them using `yarn cmd`.
|
||||
- Use `yarn build` to build the output once
|
||||
- Use `yarn test` to run tests
|
||||
- Use `yarn cmd` to use the CLI feature of Simple Scaffold from within the root directory, enabling
|
||||
you to test different behaviors. See `yarn cmd -h` for more information.
|
||||
|
||||
Here are the built-in helpers available for use:
|
||||
> This requires an updated build, and does not trigger one itself. From here you have several
|
||||
> options:
|
||||
>
|
||||
> - Run `yarn dev` to watch for file changes and build automatically
|
||||
> - Run `yarn build` before running this to trigger a one-time build
|
||||
> - Run `yarn build-cmd` which triggers a build right before running `yarn cmd` automatically with
|
||||
> the rest of the given arguments.
|
||||
|
||||
```plaintext
|
||||
{{ name | camelCase }} => myName
|
||||
{{ name | snakeCase }} => my_name
|
||||
{{ name | startCase }} => My Name
|
||||
{{ name | kebabCase }} => my-name
|
||||
{{ name | hyphenCase }} => my-name
|
||||
{{ name | pascalCase }} => MyName
|
||||
```
|
||||
|
||||
**Note:** These helpers are available for any data property, not exclusive to `name`.
|
||||
|
||||
## Examples
|
||||
|
||||
### Command Example
|
||||
|
||||
```bash
|
||||
simple-scaffold MyComponent \
|
||||
-t project/scaffold/**/* \
|
||||
-o src/components \
|
||||
-d '{"className":"myClassName"}'
|
||||
```
|
||||
|
||||
### Example Scaffold Input
|
||||
|
||||
#### Input Directory structure
|
||||
|
||||
```plaintext
|
||||
- project
|
||||
- scaffold
|
||||
- {{Name}}.js
|
||||
- src
|
||||
- components
|
||||
- ...
|
||||
```
|
||||
|
||||
#### Contents of `project/scaffold/{{Name}}.js`
|
||||
|
||||
```js
|
||||
const React = require('react')
|
||||
|
||||
module.exports = class {{Name}} extends React.Component {
|
||||
render() {
|
||||
<div className="{{className}}">{{Name}} Component</div>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Example Scaffold Output
|
||||
|
||||
### Output directory structure
|
||||
|
||||
```plaintext
|
||||
- project
|
||||
- src
|
||||
- components
|
||||
- MyComponent
|
||||
- MyComponent.js
|
||||
- ...
|
||||
```
|
||||
|
||||
With `createSubfolder = false`:
|
||||
|
||||
```plaintext
|
||||
- project
|
||||
- src
|
||||
- components
|
||||
- MyComponent.js
|
||||
- ...
|
||||
```
|
||||
|
||||
#### Contents of `project/scaffold/MyComponent/MyComponent.js`
|
||||
|
||||
```js
|
||||
const React = require("react")
|
||||
|
||||
module.exports = class MyComponent extends React.Component {
|
||||
render() {
|
||||
<div className="my-component">MyComponent Component</div>
|
||||
}
|
||||
}
|
||||
```
|
||||
- Use `yarn build-docs` to build the documentation once
|
||||
- Use `yarn watch-docs` to start docs in watch mode
|
||||
- To see the documentation, currently you have to serve the directory yourself with a static web
|
||||
server (like node's built in serve, VS code's "Go Live" mode, etc)
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
module.exports = {
|
||||
plugins: ["@babel/plugin-proposal-nullish-coalescing-operator"],
|
||||
}
|
||||
BIN
examples/test-input/Component/button-example.png
Normal file
BIN
examples/test-input/Component/button-example.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.9 KiB |
13
examples/test-input/scaffold.config.js
Normal file
13
examples/test-input/scaffold.config.js
Normal file
@@ -0,0 +1,13 @@
|
||||
/** @type {import('simple-scaffold').ScaffoldConfigFile} */
|
||||
module.exports = {
|
||||
default: {
|
||||
templates: ["examples/test-input/Component"],
|
||||
output: "examples/test-output",
|
||||
data: { property: "myProp", value: "10" },
|
||||
},
|
||||
component: {
|
||||
templates: ["examples/test-input/Component"],
|
||||
output: "examples/test-output",
|
||||
data: { property: "myProp", value: "10" },
|
||||
},
|
||||
}
|
||||
@@ -13,7 +13,7 @@ export default {
|
||||
// The directory where Jest should store its cached dependency information
|
||||
// cacheDirectory: "/private/var/folders/q9/0mns8fgd00b4t5j5lq2wh2yh0000gn/T/jest_dx",
|
||||
|
||||
// Automatically clear mock calls and instances between every test
|
||||
// Automatically clear mock calls, instances, contexts and results before every test
|
||||
clearMocks: true,
|
||||
|
||||
// Indicates whether the coverage information should be collected while executing the test
|
||||
@@ -50,6 +50,11 @@ export default {
|
||||
// Make calling deprecated APIs throw helpful error messages
|
||||
// errorOnDeprecated: false,
|
||||
|
||||
// The default configuration for fake timers
|
||||
// fakeTimers: {
|
||||
// "enableGlobally": false
|
||||
// },
|
||||
|
||||
// Force coverage collection from ignored files using an array of glob patterns
|
||||
// forceCoverageMatch: [],
|
||||
|
||||
@@ -73,6 +78,8 @@ export default {
|
||||
// An array of file extensions your modules use
|
||||
// moduleFileExtensions: [
|
||||
// "js",
|
||||
// "mjs",
|
||||
// "cjs",
|
||||
// "jsx",
|
||||
// "ts",
|
||||
// "tsx",
|
||||
@@ -82,6 +89,11 @@ export default {
|
||||
|
||||
// A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
|
||||
// moduleNameMapper: {},
|
||||
// moduleNameMapper: {
|
||||
// chalk: "<rootDir>/node_modules/chalk/source/index.js",
|
||||
// "#ansi-styles": "<rootDir>/node_modules/chalk/source/vendor/ansi-styles/index.js",
|
||||
// "#supports-color": "<rootDir>/node_modules/chalk/source/vendor/supports-color/index.js",
|
||||
// },
|
||||
|
||||
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
|
||||
modulePathIgnorePatterns: ["<rootDir>/dist"],
|
||||
@@ -101,7 +113,7 @@ export default {
|
||||
// Use this configuration option to add custom reporters to Jest
|
||||
// reporters: undefined,
|
||||
|
||||
// Automatically reset mock state between every test
|
||||
// Automatically reset mock state before every test
|
||||
// resetMocks: false,
|
||||
|
||||
// Reset the module registry before running each individual test
|
||||
@@ -110,7 +122,7 @@ export default {
|
||||
// A path to a custom resolver
|
||||
// resolver: undefined,
|
||||
|
||||
// Automatically restore mock state between every test
|
||||
// Automatically restore mock state and implementation before every test
|
||||
// restoreMocks: false,
|
||||
|
||||
// The root directory that Jest should scan for tests and modules within
|
||||
@@ -137,7 +149,7 @@ export default {
|
||||
// snapshotSerializers: [],
|
||||
|
||||
// The test environment that will be used for testing
|
||||
testEnvironment: "node",
|
||||
// testEnvironment: "jest-environment-node",
|
||||
|
||||
// Options that will be passed to the testEnvironment
|
||||
// testEnvironmentOptions: {},
|
||||
@@ -165,14 +177,8 @@ export default {
|
||||
// This option allows use of a custom test runner
|
||||
// testRunner: "jest-circus/runner",
|
||||
|
||||
// This option sets the URL for the jsdom environment. It is reflected in properties such as location.href
|
||||
// testURL: "http://localhost",
|
||||
|
||||
// Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout"
|
||||
// timers: "real",
|
||||
|
||||
// A map from regular expressions to paths to transformers
|
||||
// transform: {},
|
||||
// transform: undefined,
|
||||
|
||||
// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
|
||||
// transformIgnorePatterns: [
|
||||
@@ -180,6 +186,11 @@ export default {
|
||||
// "\\.pnp\\.[^\\/]+$"
|
||||
// ],
|
||||
|
||||
// transform: {
|
||||
// "^.+\\.ts?$": "ts-jest",
|
||||
// },
|
||||
// transformIgnorePatterns: ["<rootDir>/node_modules/"],
|
||||
|
||||
// An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
|
||||
// unmockedModulePathPatterns: undefined,
|
||||
|
||||
@@ -191,6 +202,4 @@ export default {
|
||||
|
||||
// Whether to use watchman for file crawling
|
||||
// watchman: true,
|
||||
|
||||
// extensionsToTreatAsEsm: [".ts"],
|
||||
}
|
||||
|
||||
BIN
media/intro.gif
Normal file
BIN
media/intro.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 385 KiB |
63
package.json
63
package.json
@@ -1,42 +1,67 @@
|
||||
{
|
||||
"name": "simple-scaffold",
|
||||
"version": "1.0.0-alpha.4",
|
||||
"description": "Create files based on templates",
|
||||
"version": "1.3.1",
|
||||
"description": "A simple command to generate any file structure, from single components to entire app boilerplates.",
|
||||
"homepage": "https://casraf.dev/simple-scaffold",
|
||||
"repository": "https://github.com/chenasraf/simple-scaffold.git",
|
||||
"author": "Chen Asraf <inbox@casraf.com>",
|
||||
"license": "MIT",
|
||||
"main": "index.js",
|
||||
"bin": "cmd.js",
|
||||
"types": "types.d.ts",
|
||||
"keywords": [
|
||||
"javascript",
|
||||
"cli",
|
||||
"template",
|
||||
"files",
|
||||
"typescript",
|
||||
"generator",
|
||||
"scaffold",
|
||||
"file",
|
||||
"scaffolding"
|
||||
],
|
||||
"scripts": {
|
||||
"clean": "rm -rf dist/",
|
||||
"build": "yarn clean && tsc && chmod -R +x ./dist && cp ./package.json ./dist/ && cp ./README.md ./dist/",
|
||||
"clean": "rimraf dist/",
|
||||
"build": "yarn clean && tsc && chmod -R +x ./dist && cp ./package.json ./README.md ./dist/",
|
||||
"dev": "tsc --watch",
|
||||
"start": "node dist/scaffold.js",
|
||||
"test": "jest --verbose",
|
||||
"test": "jest",
|
||||
"cmd": "node --trace-warnings dist/cmd.js",
|
||||
"build-test": "yarn build && yarn test",
|
||||
"build-cmd": "yarn build && yarn cmd"
|
||||
"build-cmd": "yarn build && yarn 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"
|
||||
},
|
||||
"dependencies": {
|
||||
"args": "^5.0.1",
|
||||
"glob": "^7.1.3",
|
||||
"chalk": "^4.1.2",
|
||||
"date-fns": "^2.29.3",
|
||||
"glob": "^9.2.1",
|
||||
"handlebars": "^4.7.7",
|
||||
"lodash": "^4.17.21",
|
||||
"massarg": "^0.1.2",
|
||||
"massarg": "^1.0.7-pre.1",
|
||||
"semantic-release-conventional-commits": "^3.0.0",
|
||||
"util.promisify": "^1.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/args": "^3.0.1",
|
||||
"@types/glob": "^7.1.1",
|
||||
"@types/jest": "^26.0.24",
|
||||
"@knodes/typedoc-plugin-pages": "^0.23.4",
|
||||
"@semantic-release/changelog": "^6.0.3",
|
||||
"@semantic-release/git": "^10.0.1",
|
||||
"@semantic-release/release-notes-generator": "^10.0.3",
|
||||
"@types/jest": "^29.5.1",
|
||||
"@types/lodash": "^4.14.171",
|
||||
"@types/mock-fs": "^4.13.1",
|
||||
"@types/node": "^14.14.22",
|
||||
"jest": "^27.0.6",
|
||||
"mock-fs": "^5.0.0",
|
||||
"ts-jest": "^27.0.3",
|
||||
"ts-node": "^10.1.0",
|
||||
"typescript": "^4.3.5"
|
||||
"@types/node": "^18.16.0",
|
||||
"conventional-changelog": "^3.1.25",
|
||||
"conventional-changelog-cli": "^2.2.2",
|
||||
"conventional-changelog-conventionalcommits": "^5.0.0",
|
||||
"jest": "^29.5.0",
|
||||
"mock-fs": "^5.2.0",
|
||||
"rimraf": "^5.0.0",
|
||||
"semantic-release": "^21.0.1",
|
||||
"ts-jest": "^29.1.0",
|
||||
"ts-node": "^10.9.1",
|
||||
"typedoc": "^0.24.6",
|
||||
"typescript": "^5.0.4"
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
74
pages/cli.md
Normal file
74
pages/cli.md
Normal file
@@ -0,0 +1,74 @@
|
||||
## Available flags
|
||||
|
||||
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`.
|
||||
|
||||
```text
|
||||
Usage: simple-scaffold [options]
|
||||
|
||||
Create structured files based on templates.
|
||||
|
||||
Options:
|
||||
|
||||
--help|-h Display help information
|
||||
|
||||
--name|-n Name to be passed to the generated files. {{name}} and
|
||||
{{Name}} inside contents and file names will be replaced
|
||||
accordingly.
|
||||
|
||||
--config|-c Filename to load config from instead of passing
|
||||
arguments to CLI or using a Node.js script. You may pass a
|
||||
JSON or JS file, with a relative or absolute path.
|
||||
|
||||
--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)
|
||||
```
|
||||
|
||||
> See
|
||||
> [Configuration Files](https://chenasraf.githun.io/simple-scaffold/pages/docs/configuration_files.md)
|
||||
> for organizing multiple scaffold types into easy-to-maintain files
|
||||
|
||||
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}'"
|
||||
}
|
||||
}
|
||||
```
|
||||
86
pages/configuration_files.md
Normal file
86
pages/configuration_files.md
Normal file
@@ -0,0 +1,86 @@
|
||||
If you want to have reusable configurations which are complex and don't fit into command lines
|
||||
easily, or just want to manage your templates easier, you can use configuration files to load your
|
||||
scaffolding configurations.
|
||||
|
||||
## Creating config files
|
||||
|
||||
Configuration files should be valid `.js`/`.json` files that contain valid Scaffold configurations.
|
||||
|
||||
Each file hold multiple scaffolds. Each scaffold is a key, and its value is the configuration. For
|
||||
example:
|
||||
|
||||
```json
|
||||
{
|
||||
"component": {
|
||||
"templates": ["templates/component"],
|
||||
"output": "src/components"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The configuration contents are identical to the
|
||||
[Node.js configuration structure](https://chenasraf.github.io/simple-scaffold/pages/node.md):
|
||||
|
||||
```ts
|
||||
interface ScaffoldConfig {
|
||||
name: string
|
||||
templates: string[]
|
||||
output: FileResponse<string>
|
||||
createSubFolder?: boolean
|
||||
data?: Record<string, any>
|
||||
overwrite?: FileResponse<boolean>
|
||||
quiet?: boolean
|
||||
verbose?: LogLevel
|
||||
dryRun?: boolean
|
||||
helpers?: Record<string, Helper>
|
||||
subFolderNameHelper?: DefaultHelpers | string
|
||||
beforeWrite?(
|
||||
content: Buffer,
|
||||
rawContent: Buffer,
|
||||
outputPath: string,
|
||||
): string | Buffer | undefined | Promise<string | Buffer | undefined>
|
||||
}
|
||||
```
|
||||
|
||||
If you want to supply functions inside the configurations, you must use a `.js` file as JSON does
|
||||
not support non-primitives.
|
||||
|
||||
A `.js` file is just like a `.json` file, make sure to export the final configuration:
|
||||
|
||||
```js
|
||||
/** @type {import('simple-scaffold').ScaffoldConfigFile} */
|
||||
module.exports = {
|
||||
component: {
|
||||
templates: ["templates/component"],
|
||||
output: "src/components",
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Using a config file
|
||||
|
||||
Once your config is created, you can use it by providing the file name to the `--config` (or `-c`
|
||||
for brevity), followed by a colon, then your scaffold config name. 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
|
||||
```
|
||||
25
pages/migration.md
Normal file
25
pages/migration.md
Normal file
@@ -0,0 +1,25 @@
|
||||
In Simple Scaffold v1.0, the entire codebase was overhauled, yet usage remains mostly the same
|
||||
between versions. With these notable exceptions:
|
||||
|
||||
- Some of the argument names have changed
|
||||
- Template syntax has been improved
|
||||
- The command to run Scaffold has been simplified from `new SimpleScaffold(opts).run()` to
|
||||
`SimpleScaffold(opts)`, which now returns a promise that you can await to know when the process
|
||||
has been completed.
|
||||
|
||||
## Argument changes
|
||||
|
||||
- `locals` has been renamed to `data`. The appropriate command line args have been updated as well
|
||||
to `--data` | `-d`.
|
||||
- Additional options have been added to both CLI and Node interfaces. See
|
||||
[Command Line Interface (CLI) usage](https://chenasraf.github.io/simple-scaffold/pages/cli.html)
|
||||
and [Node.js usage](https://chenasraf.github.io/simple-scaffold/pages/node.html) for more
|
||||
information.
|
||||
|
||||
## Template syntax changes
|
||||
|
||||
Simple Scaffold still uses Handlebars.js to handle template content and file names. However, helpers
|
||||
have been added to remove the need for you to pre-process the template data on simple use-cases such
|
||||
as case type manipulation (converting to camel case, snake case, etc)
|
||||
|
||||
See the readme for the full information on how to use these helpers and which are available.
|
||||
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>
|
||||
)
|
||||
}
|
||||
```
|
||||
76
release.config.js
Normal file
76
release.config.js
Normal file
@@ -0,0 +1,76 @@
|
||||
const 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: "Build", release: "patch" },
|
||||
{ type: "perf", section: "Misc", release: "patch" },
|
||||
{ type: "build", section: "Build", release: "patch" },
|
||||
{ type: "chore", section: "Misc", release: "patch" },
|
||||
{ type: "test", section: "Misc", release: "patch" },
|
||||
]
|
||||
|
||||
/** @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 },
|
||||
],
|
||||
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: releaseRules,
|
||||
},
|
||||
],
|
||||
[
|
||||
"@semantic-release/release-notes-generator",
|
||||
{
|
||||
preset: "conventionalcommits",
|
||||
parserOpts: {
|
||||
noteKeywords: ["breaking"],
|
||||
types: releaseRules,
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
"@semantic-release/changelog",
|
||||
{
|
||||
changelogFile: "CHANGELOG.md",
|
||||
changelogTitle: "# Change Log",
|
||||
},
|
||||
],
|
||||
[
|
||||
"@semantic-release/npm",
|
||||
{
|
||||
packageRoot: "dist",
|
||||
},
|
||||
],
|
||||
[
|
||||
"@semantic-release/github",
|
||||
{
|
||||
assets: ["package.tgz"],
|
||||
},
|
||||
],
|
||||
[
|
||||
"@semantic-release/git",
|
||||
{
|
||||
assets: ["CHANGELOG.md", "package.json"],
|
||||
},
|
||||
],
|
||||
],
|
||||
}
|
||||
195
src/cmd.ts
195
src/cmd.ts
@@ -1,69 +1,130 @@
|
||||
import Scaffold from "./scaffold"
|
||||
#!/usr/bin/env node
|
||||
import massarg from "massarg"
|
||||
import { ScaffoldCmdConfig } from "./types"
|
||||
import chalk from "chalk"
|
||||
import { LogLevel, ScaffoldCmdConfig } from "./types"
|
||||
import { Scaffold } from "./scaffold"
|
||||
import path from "path"
|
||||
import fs from "fs/promises"
|
||||
import { parseAppendData, parseConfig } from "./utils"
|
||||
|
||||
massarg<ScaffoldCmdConfig & { help: boolean; extras: string[] }>()
|
||||
.main(Scaffold)
|
||||
.option({
|
||||
name: "name",
|
||||
aliases: ["n"],
|
||||
isDefault: true,
|
||||
description:
|
||||
"Name to be passed to the generated files. {{name}} and {{Name}} inside contents and file names will be replaced accordingly.",
|
||||
})
|
||||
.option({
|
||||
name: "output",
|
||||
aliases: ["o"],
|
||||
description:
|
||||
"Path to output to. If --create-sub-folder is enabled, the subfolder will be created inside this path.",
|
||||
})
|
||||
.option({
|
||||
name: "templates",
|
||||
aliases: ["t"],
|
||||
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.",
|
||||
defaultValue: [],
|
||||
array: true,
|
||||
})
|
||||
.option({
|
||||
aliases: ["w"],
|
||||
name: "overwrite",
|
||||
description: "Enable to override output files, even if they already exist.",
|
||||
defaultValue: false,
|
||||
boolean: true,
|
||||
})
|
||||
.option({
|
||||
aliases: ["d"],
|
||||
name: "data",
|
||||
description: "Add custom data to the templates. By default, only your app name is included.",
|
||||
parse: (v) => JSON.parse(v),
|
||||
})
|
||||
.option({
|
||||
aliases: ["s"],
|
||||
name: "create-sub-folder",
|
||||
description: "Create subfolder with the input name",
|
||||
defaultValue: false,
|
||||
boolean: true,
|
||||
})
|
||||
.option({ aliases: ["q"], name: "quiet", description: "Suppress output logs", defaultValue: false, boolean: true })
|
||||
.option({
|
||||
aliases: ["dr"],
|
||||
name: "dry-run",
|
||||
description:
|
||||
"Don't emit files. This is good for testing your scaffolds and making sure they " +
|
||||
"don't fail, without having to write actual file contents or create directories.",
|
||||
defaultValue: false,
|
||||
boolean: true,
|
||||
})
|
||||
// .example({
|
||||
// input: `yarn cmd -t examples/test-input/Component -o examples/test-output -d '{"property":"myProp","value":"10"}'`,
|
||||
// description: "Usage",
|
||||
// output: "",
|
||||
// })
|
||||
.help({
|
||||
binName: "simple-scaffold",
|
||||
useGlobalColumns: true,
|
||||
usageExample: "[options]",
|
||||
})
|
||||
.parse()
|
||||
export async function parseCliArgs(args = process.argv.slice(2)) {
|
||||
const pkg = JSON.parse((await fs.readFile(path.join(__dirname, "package.json"))).toString())
|
||||
const isConfig = args.includes("--config") || args.includes("-c")
|
||||
|
||||
return (
|
||||
massarg<ScaffoldCmdConfig>()
|
||||
.main((config) => {
|
||||
const _config = parseConfig(config)
|
||||
return Scaffold(_config)
|
||||
})
|
||||
.option({
|
||||
name: "name",
|
||||
aliases: ["n"],
|
||||
description:
|
||||
"Name to be passed to the generated files. {{name}} and {{Name}} inside contents and file names will be replaced accordingly.",
|
||||
isDefault: true,
|
||||
required: true,
|
||||
})
|
||||
.option({
|
||||
name: "config",
|
||||
aliases: ["c"],
|
||||
description:
|
||||
"Filename to load config from instead of passing arguments to CLI or using a Node.js script. You may pass a JSON or JS file, with a relative or absolute path.",
|
||||
})
|
||||
.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: !isConfig,
|
||||
})
|
||||
.option({
|
||||
name: "templates",
|
||||
aliases: ["t"],
|
||||
array: true,
|
||||
description:
|
||||
"Template files to use as input. You may provide multiple files, each of which can be a relative or absolute path, " +
|
||||
"or a glob pattern for multiple file matching easily.",
|
||||
required: !isConfig,
|
||||
})
|
||||
.option({
|
||||
name: "overwrite",
|
||||
aliases: ["w"],
|
||||
boolean: true,
|
||||
defaultValue: false,
|
||||
description: "Enable to override output files, even if they already exist.",
|
||||
})
|
||||
.option({
|
||||
name: "data",
|
||||
aliases: ["d"],
|
||||
description: "Add custom data to the templates. By default, only your app name is included.",
|
||||
parse: (v) => JSON.parse(v),
|
||||
})
|
||||
.option({
|
||||
name: "append-data",
|
||||
aliases: ["D"],
|
||||
description:
|
||||
"Append additional custom data to the templates, which will overwrite --data, using an alternate syntax, which is easier to use with CLI: -D key1=string -D key2:=raw",
|
||||
parse: parseAppendData,
|
||||
})
|
||||
.option({
|
||||
name: "create-sub-folder",
|
||||
aliases: ["s"],
|
||||
boolean: true,
|
||||
defaultValue: false,
|
||||
description: "Create subfolder with the input name",
|
||||
})
|
||||
.option({
|
||||
name: "sub-folder-name-helper",
|
||||
aliases: ["sh"],
|
||||
description: "Default helper to apply to subfolder name when using `--create-sub-folder true`.",
|
||||
})
|
||||
.option({
|
||||
name: "quiet",
|
||||
aliases: ["q"],
|
||||
boolean: true,
|
||||
defaultValue: false,
|
||||
description: "Suppress output logs (Same as --verbose 0)",
|
||||
})
|
||||
.option({
|
||||
name: "verbose",
|
||||
aliases: ["v"],
|
||||
defaultValue: LogLevel.Info,
|
||||
description:
|
||||
"Determine amount of logs to display. The values are: " +
|
||||
`${chalk.bold`0 (none) | 1 (debug) | 2 (info) | 3 (warn) | 4 (error)`}. ` +
|
||||
"The provided level will display messages of the same level or higher.",
|
||||
parse: Number,
|
||||
})
|
||||
.option({
|
||||
name: "dry-run",
|
||||
aliases: ["dr"],
|
||||
boolean: true,
|
||||
defaultValue: false,
|
||||
description:
|
||||
"Don't emit files. This is good for testing your scaffolds and making sure they " +
|
||||
"don't fail, without having to write actual file contents or create directories.",
|
||||
})
|
||||
// .example({
|
||||
// input: `yarn cmd -t examples/test-input/Component -o examples/test-output -d '{"property":"myProp","value":"10"}'`,
|
||||
// description: "Usage",
|
||||
// output: "",
|
||||
// })
|
||||
.help({
|
||||
binName: "simple-scaffold",
|
||||
useGlobalColumns: true,
|
||||
usageExample: "[options]",
|
||||
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`}`,
|
||||
`NPM: ${chalk.underline`https://npmjs.com/package/simple-scaffold`}`,
|
||||
`GitHub: ${chalk.underline`https://github.com/chenasraf/simple-scaffold`}`,
|
||||
].join("\n"),
|
||||
})
|
||||
.parse(args)
|
||||
)
|
||||
}
|
||||
|
||||
parseCliArgs()
|
||||
|
||||
55
src/docs.css
Normal file
55
src/docs.css
Normal file
@@ -0,0 +1,55 @@
|
||||
.tsd-typography table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.tsd-typography table td,
|
||||
.tsd-typography table th {
|
||||
vertical-align: top;
|
||||
border: 1px solid var(--color-accent);
|
||||
padding: 6px;
|
||||
}
|
||||
.tsd-typography h1 + pre,
|
||||
.tsd-typography h2 + pre,
|
||||
.tsd-typography h3 + pre,
|
||||
.tsd-typography h4 + pre,
|
||||
.tsd-typography h5 + pre,
|
||||
.tsd-typography h6 + pre,
|
||||
/* */
|
||||
.tsd-typography h1 + table,
|
||||
.tsd-typography h2 + table,
|
||||
.tsd-typography h3 + table,
|
||||
.tsd-typography h4 + table,
|
||||
.tsd-typography h5 + table,
|
||||
.tsd-typography h6 + table {
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
.tsd-typography pre + a + h1,
|
||||
.tsd-typography pre + a + h2,
|
||||
.tsd-typography pre + a + h3,
|
||||
.tsd-typography pre + a + h4,
|
||||
.tsd-typography pre + a + h5,
|
||||
.tsd-typography pre + a + h6,
|
||||
/* */
|
||||
.tsd-typography table + a + h1,
|
||||
.tsd-typography table + a + h2,
|
||||
.tsd-typography table + a + h3,
|
||||
.tsd-typography table + a + h4,
|
||||
.tsd-typography table + a + h5,
|
||||
.tsd-typography table + a + h6 {
|
||||
margin-top: 2em;
|
||||
}
|
||||
|
||||
.tsd-index-accordion[data-key*="Configuration."] ul.tsd-nested-navigation,
|
||||
.tsd-index-accordion[data-key*="Configuration."] .tsd-accordion-summary > svg,
|
||||
.tsd-index-accordion[data-key="Changelog"] ul.tsd-nested-navigation,
|
||||
.tsd-index-accordion[data-key="Changelog"] .tsd-accordion-summary > svg,
|
||||
.tsd-index-accordion[data-key="Configuration"] li:nth-child(n + 6) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tsd-index-accordion[data-key*="Configuration."],
|
||||
.tsd-index-accordion[data-key="Changelog"] {
|
||||
margin-left: 0;
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from "./scaffold"
|
||||
export * from "./types"
|
||||
import Scaffold from "./scaffold"
|
||||
export default Scaffold
|
||||
|
||||
166
src/scaffold.ts
166
src/scaffold.ts
@@ -1,98 +1,134 @@
|
||||
import { glob } from "glob"
|
||||
/**
|
||||
* @module
|
||||
* Simple Scaffold
|
||||
*
|
||||
* See [readme](README.md)
|
||||
*/
|
||||
import path from "path"
|
||||
import { promisify } from "util"
|
||||
import { promises as fsPromises } from "fs"
|
||||
const { readFile, writeFile } = fsPromises
|
||||
|
||||
import {
|
||||
createDirIfNotExists,
|
||||
getOptionValueForFile,
|
||||
handleErr,
|
||||
handlebarsParse,
|
||||
log,
|
||||
pathExists,
|
||||
pascalCase,
|
||||
isDir,
|
||||
removeGlob,
|
||||
makeRelativePath,
|
||||
registerHelpers,
|
||||
getTemplateGlobInfo,
|
||||
getFileList,
|
||||
getBasePath,
|
||||
copyFileTransformed,
|
||||
getTemplateFileInfo,
|
||||
logInitStep,
|
||||
logInputFile,
|
||||
} from "./utils"
|
||||
import { ScaffoldConfig } from "./types"
|
||||
import { LogLevel, ScaffoldConfig } from "./types"
|
||||
|
||||
export async function Scaffold(config: ScaffoldConfig) {
|
||||
/**
|
||||
* Create a scaffold using given `options`.
|
||||
*
|
||||
* #### Create files
|
||||
* To create a file structure to output, use any directory and file structure you would like.
|
||||
* Inside folder names, file names or file contents, you may place `{{ var }}` where `var` is either
|
||||
* `name` which is the scaffold name you provided or one of the keys you provided in the `data` option.
|
||||
*
|
||||
* The contents and names will be replaced with the transformed values so you can use your original structure as a
|
||||
* boilerplate for other projects, components, modules, or even single files.
|
||||
*
|
||||
* The files will maintain their structure, starting from the directory containing the template (or the template itself
|
||||
* if it is already a directory), and will output from that directory into the directory defined by `config.output`.
|
||||
*
|
||||
* #### Helpers
|
||||
* Helpers are functions you can use to transform your `{{ var }}` contents into other values without having to
|
||||
* pre-define the data and use a duplicated key.
|
||||
*
|
||||
* Any functions you provide in `helpers` option will also be available to you to make custom formatting as you see fit
|
||||
* (for example, formatting a date)
|
||||
*
|
||||
* For available default values, see {@link DefaultHelpers}.
|
||||
*
|
||||
* @param {ScaffoldConfig} config The main configuration object
|
||||
*
|
||||
* @see {@link DefaultHelpers}
|
||||
* @see {@link CaseHelpers}
|
||||
* @see {@link DateHelpers}
|
||||
*
|
||||
* @category Main
|
||||
*/
|
||||
export async function Scaffold(config: ScaffoldConfig): Promise<void> {
|
||||
config.output ??= process.cwd()
|
||||
|
||||
registerHelpers(config)
|
||||
try {
|
||||
const options = { ...config }
|
||||
const data = { name: options.name, Name: pascalCase(options.name), ...options.data }
|
||||
log(options, "Config:", {
|
||||
name: options.name,
|
||||
templates: options.templates,
|
||||
output: options.output,
|
||||
createSubfolder: options.createSubFolder,
|
||||
data: options.data,
|
||||
overwrite: options.overwrite,
|
||||
quiet: options.quiet,
|
||||
})
|
||||
log(options, "Data:", data)
|
||||
for (let template of config.templates) {
|
||||
config.data = { name: config.name, Name: pascalCase(config.name), ...config.data }
|
||||
logInitStep(config)
|
||||
for (let _template of config.templates) {
|
||||
try {
|
||||
const _isDir = await isDir(template)
|
||||
const basePath = path
|
||||
.resolve(process.cwd(), _isDir ? template : path.dirname(template.replace("*", "").replace("//", "/")))
|
||||
.replace(process.cwd(), ".")
|
||||
if (_isDir) {
|
||||
template = template + "/**/*"
|
||||
}
|
||||
const files = await promisify(glob)(template, { dot: true, debug: false })
|
||||
for (const templatePath of files) {
|
||||
if (!(await isDir(templatePath))) {
|
||||
await handleTemplateFile(templatePath, basePath, options, data)
|
||||
const { nonGlobTemplate, origTemplate, isDirOrGlob, isGlob, template } = await getTemplateGlobInfo(
|
||||
config,
|
||||
_template,
|
||||
)
|
||||
const files = await getFileList(config, template)
|
||||
for (const inputFilePath of files) {
|
||||
if (await isDir(inputFilePath)) {
|
||||
continue
|
||||
}
|
||||
const relPath = makeRelativePath(path.dirname(removeGlob(inputFilePath).replace(nonGlobTemplate, "")))
|
||||
const basePath = getBasePath(relPath)
|
||||
logInputFile(config, {
|
||||
origTemplate,
|
||||
relPath,
|
||||
template,
|
||||
inputFilePath,
|
||||
nonGlobTemplate,
|
||||
basePath,
|
||||
isDirOrGlob,
|
||||
isGlob,
|
||||
})
|
||||
await handleTemplateFile(config, {
|
||||
templatePath: inputFilePath,
|
||||
basePath,
|
||||
})
|
||||
}
|
||||
} catch (e: any) {
|
||||
handleErr(e)
|
||||
}
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.error(e)
|
||||
log(config, LogLevel.Error, e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
async function handleTemplateFile(
|
||||
templatePath: string,
|
||||
basePath: string,
|
||||
options: ScaffoldConfig,
|
||||
data: Record<string, string>
|
||||
config: ScaffoldConfig,
|
||||
{ templatePath, basePath }: { templatePath: string; basePath: string },
|
||||
): Promise<void> {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
log(options, `Parsing ${templatePath}`)
|
||||
const inputPath = path.join(process.cwd(), templatePath)
|
||||
const outputPathOpt = getOptionValueForFile(inputPath, data, options.output)
|
||||
const outputDir = path.resolve(
|
||||
process.cwd(),
|
||||
...([outputPathOpt, options.createSubFolder ? options.name : undefined].filter(Boolean) as string[])
|
||||
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`,
|
||||
)
|
||||
const outputPath = path.join(outputDir, handlebarsParse(path.basename(inputPath), data))
|
||||
const overwrite = getOptionValueForFile(inputPath, data, options.overwrite ?? false)
|
||||
const exists = await pathExists(outputPath)
|
||||
|
||||
await createDirIfNotExists(outputDir, options)
|
||||
await createDirIfNotExists(path.dirname(outputPath), config)
|
||||
|
||||
log(options, `Writing to ${outputPath}`)
|
||||
if (!exists || overwrite) {
|
||||
if (exists && overwrite) {
|
||||
log(options, `File ${outputPath} exists, overwriting`)
|
||||
}
|
||||
const templateBuffer = await readFile(inputPath)
|
||||
const outputContents = handlebarsParse(templateBuffer, data)
|
||||
|
||||
if (!options.dryRun) {
|
||||
await writeFile(outputPath, outputContents)
|
||||
} else {
|
||||
log(options, "Content output:")
|
||||
log(options, outputContents)
|
||||
}
|
||||
} else if (exists) {
|
||||
log(options, `File ${outputPath} already exists, skipping`)
|
||||
}
|
||||
log(config, LogLevel.Info, `Writing to ${outputPath}`)
|
||||
await copyFileTransformed(config, { exists, overwrite, outputPath, inputPath })
|
||||
resolve()
|
||||
} catch (e: any) {
|
||||
handleErr(e)
|
||||
|
||||
327
src/types.ts
327
src/types.ts
@@ -1,25 +1,342 @@
|
||||
export type FileResponseFn<T> = (fullPath: string, basedir: string, basename: string) => T
|
||||
|
||||
export type FileResponse<T> = T | FileResponseFn<T>
|
||||
import { HelperDelegate } from "handlebars/runtime"
|
||||
|
||||
/**
|
||||
* The config object for defining a scaffolding group.
|
||||
*
|
||||
* @see https://github.com/chenasraf/simple-scaffold#readme
|
||||
* @see {@link DefaultHelpers}
|
||||
* @see {@link CaseHelpers}
|
||||
* @see {@link DateHelpers}
|
||||
*
|
||||
* @category Config
|
||||
*/
|
||||
export interface ScaffoldConfig {
|
||||
/** The name supplied for the output templates */
|
||||
/**
|
||||
* Name to be passed to the generated files. `{{name}}` and `{{Name}}` inside contents and file names will be replaced
|
||||
* accordingly.
|
||||
*/
|
||||
name: string
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @default Current working directory
|
||||
*/
|
||||
templates: string[]
|
||||
|
||||
/**
|
||||
* Path to output to. If `createSubFolder` is `true`, the subfolder will be created inside this path.
|
||||
*
|
||||
* May also be a {@link FileResponseHandler} which returns a new output path to override the default one.
|
||||
*
|
||||
* @see {@link FileResponse}
|
||||
* @see {@link FileResponseHandler}
|
||||
*/
|
||||
output: FileResponse<string>
|
||||
|
||||
/**
|
||||
* Whether to create subfolder with the input name.
|
||||
*
|
||||
* When `true`, you may also use {@link subFolderNameHelper} to determine a pre-process helper on
|
||||
* the directory name.
|
||||
*
|
||||
* @default `false`
|
||||
*/
|
||||
createSubFolder?: boolean
|
||||
data?: Record<string, string>
|
||||
|
||||
/**
|
||||
* Add custom data to the templates. By default, only your app name is included as `{{name}}` and `{{Name}}`.
|
||||
*
|
||||
* This can be any object that will be usable by Handlebars.
|
||||
*/
|
||||
data?: Record<string, any>
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* May also be a {@link FileResponseHandler} which returns a boolean value per file.
|
||||
*
|
||||
* @see {@link FileResponse}
|
||||
* @see {@link FileResponseHandler}
|
||||
*
|
||||
* @default `false`
|
||||
*/
|
||||
overwrite?: FileResponse<boolean>
|
||||
|
||||
/**
|
||||
* Suppress output logs (Same as `verbose: 0` or `verbose: LogLevel.None`)
|
||||
* @see {@link verbose}
|
||||
*/
|
||||
quiet?: boolean
|
||||
|
||||
/**
|
||||
* Determine amount of logs to display.
|
||||
*
|
||||
* The values are: `0 (none) | 1 (debug) | 2 (info) | 3 (warn) | 4 (error)`. The provided level will display messages
|
||||
* of the same level or higher.
|
||||
*
|
||||
* @see {@link LogLevel}
|
||||
*
|
||||
* @default `2 (info)`
|
||||
*/
|
||||
verbose?: LogLevel
|
||||
|
||||
/**
|
||||
* Don't emit files. This is good for testing your scaffolds and making sure they don't fail, without having to write
|
||||
* actual file contents or create directories.
|
||||
*
|
||||
* @default `false`
|
||||
*/
|
||||
dryRun?: boolean
|
||||
|
||||
/**
|
||||
* Additional helpers to add to the template parser. Provide an object whose keys are the name of the function to add,
|
||||
* and the value is the helper function itself. The signature of helpers is as follows:
|
||||
* ```typescript
|
||||
* (text: string, ...args: any[]) => string
|
||||
* ```
|
||||
*
|
||||
* A full example might be:
|
||||
*
|
||||
* ```typescript
|
||||
* Scaffold({
|
||||
* //...
|
||||
* helpers: {
|
||||
* upperKebabCase: (text) => kebabCase(text).toUpperCase()
|
||||
* }
|
||||
* })
|
||||
* ```
|
||||
*
|
||||
* Which will allow:
|
||||
*
|
||||
* ```
|
||||
* {{ upperKebabCase "my value" }}
|
||||
* ```
|
||||
*
|
||||
* To transform to:
|
||||
*
|
||||
* ```
|
||||
* MY-VALUE
|
||||
* ```
|
||||
*
|
||||
* See {@link DefaultHelpers} for a list of all the built-in available helpers.
|
||||
*
|
||||
* Simple Scaffold uses Handlebars.js, so all the syntax from there is supported. See
|
||||
* [their docs](https://handlebarsjs.com/guide/#custom-helpers) for more information.
|
||||
*
|
||||
* @see {@link DefaultHelpers}
|
||||
* @see {@link CaseHelpers}
|
||||
* @see {@link DateHelpers}
|
||||
* @see https://casraf.dev/simple-scaffold#helpers
|
||||
* @see https://casraf.dev/simple-scaffold#built-in-helpers
|
||||
* @see https://handlebarsjs.com/guide/#custom-helpers
|
||||
*/
|
||||
helpers?: Record<string, Helper>
|
||||
|
||||
/**
|
||||
* Default transformer to apply to subfolder name when using `createSubFolder: true`. Can be one of the default
|
||||
* capitalization helpers, or a custom one you provide to `helpers`. Defaults to `undefined`, which means no
|
||||
* transformation is done.
|
||||
*
|
||||
* @see {@link createSubFolder}
|
||||
* @see {@link CaseHelpers}
|
||||
* @see {@link DefaultHelpers}
|
||||
*/
|
||||
subFolderNameHelper?: DefaultHelpers | string
|
||||
|
||||
/**
|
||||
* This callback runs right before content is being written to the disk. If you supply this function, you may return
|
||||
* a string that represents the final content of your file, you may process the content as you see fit. For example,
|
||||
* you may run formatters on a file, fix output in edge-cases not supported by helpers or data, etc.
|
||||
*
|
||||
* If the return value of this function is `undefined`, the original content will be used.
|
||||
*
|
||||
* @param content The original template after token replacement
|
||||
* @param rawContent The original template before token replacement
|
||||
* @param outputPath The final output path of the processed file
|
||||
*
|
||||
* @returns {Promise<String | Buffer | undefined> | String | Buffer | undefined} The final output of the file
|
||||
* contents-only, after further modifications - or `undefined` to use the original content (i.e. `content.toString()`)
|
||||
*/
|
||||
beforeWrite?(
|
||||
content: Buffer,
|
||||
rawContent: Buffer,
|
||||
outputPath: string,
|
||||
): string | Buffer | undefined | Promise<string | Buffer | undefined>
|
||||
}
|
||||
|
||||
/**
|
||||
* The names of the available helper functions that relate to text capitalization.
|
||||
*
|
||||
* These are available for `subfolderNameHelper`.
|
||||
*
|
||||
* | Helper name | Example code | Example output |
|
||||
* | ------------ | ----------------------- | -------------- |
|
||||
* | [None] | `{{ name }}` | my name |
|
||||
* | `camelCase` | `{{ camelCase name }}` | myName |
|
||||
* | `snakeCase` | `{{ snakeCase name }}` | my_name |
|
||||
* | `startCase` | `{{ startCase name }}` | My Name |
|
||||
* | `kebabCase` | `{{ kebabCase name }}` | my-name |
|
||||
* | `hyphenCase` | `{{ hyphenCase name }}` | my-name |
|
||||
* | `pascalCase` | `{{ pascalCase name }}` | MyName |
|
||||
* | `upperCase` | `{{ upperCase name }}` | MY NAME |
|
||||
* | `lowerCase` | `{{ lowerCase name }}` | my name |
|
||||
*
|
||||
* @see {@link DefaultHelpers}
|
||||
* @see {@link DateHelpers}
|
||||
* @see {@link ScaffoldConfig}
|
||||
* @see {@link ScaffoldConfig.subFolderNameHelper}
|
||||
*
|
||||
* @category Helpers
|
||||
*/
|
||||
export type CaseHelpers =
|
||||
| "camelCase"
|
||||
| "hyphenCase"
|
||||
| "kebabCase"
|
||||
| "lowerCase"
|
||||
| "pascalCase"
|
||||
| "snakeCase"
|
||||
| "startCase"
|
||||
| "upperCase"
|
||||
|
||||
/**
|
||||
* The names of the available helper functions that relate to dates.
|
||||
*
|
||||
* | Helper name | Description | Example code | Example output |
|
||||
* | -------------------------------- | ---------------------------------------------------------------- | ---------------------------------------------------------------- | ------------------ |
|
||||
* | `now` | Current date with format | `{{ now "yyyy-MM-dd HH:mm" }}` | `2042-01-01 15:00` |
|
||||
* | `now` (with offset) | Current date with format, and with offset | `{{ now "yyyy-MM-dd HH:mm" -1 "hours" }}` | `2042-01-01 14:00` |
|
||||
* | `date` | Custom date with format | `{{ date "2042-01-01T15:00:00Z" "yyyy-MM-dd HH:mm" }}` | `2042-01-01 15:00` |
|
||||
* | `date` (with offset) | Custom date with format, and with offset | `{{ date "2042-01-01T15:00:00Z" "yyyy-MM-dd HH:mm" -1 "days" }}` | `2041-31-12 15:00` |
|
||||
* | `date` (with date from `--data`) | Custom date with format, with data from the `data` config option | `{{ date myCustomDate "yyyy-MM-dd HH:mm" }}` | `2042-01-01 12:00` |
|
||||
*
|
||||
* Further details:
|
||||
*
|
||||
* - We use [`date-fns`](https://date-fns.org/docs/) for parsing/manipulating the dates. If you want
|
||||
* more information on the date tokens to use, refer to
|
||||
* [their format documentation](https://date-fns.org/docs/format).
|
||||
*
|
||||
* - The date helper format takes the following arguments:
|
||||
*
|
||||
* ```typescript
|
||||
* (
|
||||
* date: string,
|
||||
* format: string,
|
||||
* offsetAmount?: number,
|
||||
* offsetType?: "years" | "months" | "weeks" | "days" | "hours" | "minutes" | "seconds"
|
||||
* )
|
||||
* ```
|
||||
*
|
||||
* - **The now helper** (for current time) takes the same arguments, minus the first one (`date`) as it is implicitly
|
||||
* the current date.
|
||||
*
|
||||
* @see {@link DefaultHelpers}
|
||||
* @see {@link CaseHelpers}
|
||||
* @see {@link ScaffoldConfig}
|
||||
*
|
||||
* @category Helpers
|
||||
*/
|
||||
export type DateHelpers = "date" | "now"
|
||||
|
||||
/**
|
||||
* The names of all the available helper functions in templates.
|
||||
* Simple-Scaffold provides some built-in text transformation filters usable by Handlebars.js.
|
||||
*
|
||||
* For example, you may use `{{ snakeCase name }}` inside a template file or filename, and it will
|
||||
* replace `My Name` with `my_name` when producing the final value.
|
||||
*
|
||||
* @see {@link CaseHelpers}
|
||||
* @see {@link DateHelpers}
|
||||
* @see {@link ScaffoldConfig}
|
||||
*
|
||||
* @category Helpers
|
||||
*/
|
||||
export type DefaultHelpers = CaseHelpers | DateHelpers
|
||||
|
||||
/**
|
||||
* Helper function, see https://handlebarsjs.com/guide/#custom-helpers
|
||||
*
|
||||
* @category Helpers
|
||||
*/
|
||||
export type Helper = HelperDelegate
|
||||
|
||||
/**
|
||||
* The amount of information to log when generating scaffold.
|
||||
* When not `None`, the selected level will be the lowest level included.
|
||||
*
|
||||
* For example, level `Info` (2) will include `Info`, `Warning` and `Error`, but not `Debug`; and `Warning` will only
|
||||
* show `Warning` and `Error`.
|
||||
*
|
||||
* @default `2 (info)`
|
||||
*
|
||||
* @category Logging
|
||||
*/
|
||||
export enum LogLevel {
|
||||
/** Silent output */
|
||||
None = 0,
|
||||
/** Debugging information. Very verbose and only recommended for troubleshooting. */
|
||||
Debug = 1,
|
||||
/**
|
||||
* The regular level of logging. Major actions are logged to show the scaffold progress.
|
||||
*
|
||||
* @default
|
||||
*/
|
||||
Info = 2,
|
||||
/** Warnings such as when file fails to replace token values properly in template. */
|
||||
Warning = 3,
|
||||
/** Errors, such as missing files, bad replacement token syntax, or un-writable directories. */
|
||||
Error = 4,
|
||||
}
|
||||
|
||||
/**
|
||||
* A function that takes path information about file, and returns a value of type `T`
|
||||
*
|
||||
* @template T The return type for the function
|
||||
* @param {string} fullPath The full path of the current file
|
||||
* @param {string} basedir The directory containing the current file
|
||||
* @param {string} basename The name of the file
|
||||
*
|
||||
* @returns {T} A return value
|
||||
*
|
||||
* @category Config
|
||||
*/
|
||||
export type FileResponseHandler<T> = (fullPath: string, basedir: string, basename: string) => T
|
||||
|
||||
/**
|
||||
* Represents a response for file path information.
|
||||
* Can either be:
|
||||
*
|
||||
* 1. `T` - static value
|
||||
* 2. A function with the following signature which returns `T`:
|
||||
* ```typescript
|
||||
* (fullPath: string, basedir: string, basename: string) => T
|
||||
* ```
|
||||
*
|
||||
* @typedef T The return type
|
||||
*
|
||||
* @see {@link FileResponseHandler}
|
||||
*
|
||||
* @category Config
|
||||
* */
|
||||
export type FileResponse<T> = T | FileResponseHandler<T>
|
||||
|
||||
/** @internal */
|
||||
export interface ScaffoldCmdConfig {
|
||||
name: string
|
||||
templates: string[]
|
||||
output: string
|
||||
createSubFolder: boolean
|
||||
data?: Record<string, string>
|
||||
appendData?: Record<string, string>
|
||||
overwrite: boolean
|
||||
quiet: boolean
|
||||
verbose: LogLevel
|
||||
dryRun: boolean
|
||||
config?: string
|
||||
}
|
||||
|
||||
export type ScaffoldConfigFile = Record<string, ScaffoldConfig>
|
||||
|
||||
364
src/utils.ts
364
src/utils.ts
@@ -1,47 +1,139 @@
|
||||
import path from "path"
|
||||
import { F_OK } from "constants"
|
||||
import { FileResponse, FileResponseFn, ScaffoldConfig } from "./types"
|
||||
import {
|
||||
DefaultHelpers,
|
||||
FileResponse,
|
||||
FileResponseHandler,
|
||||
Helper,
|
||||
LogLevel,
|
||||
ScaffoldCmdConfig,
|
||||
ScaffoldConfig,
|
||||
ScaffoldConfigFile,
|
||||
} 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 helpers = {
|
||||
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,
|
||||
}
|
||||
|
||||
for (const helperName in helpers) {
|
||||
Handlebars.registerHelper(helperName, helpers[helperName as keyof typeof helpers])
|
||||
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 handleErr(err: NodeJS.ErrnoException | null) {
|
||||
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])
|
||||
}
|
||||
}
|
||||
|
||||
export function handleErr(err: NodeJS.ErrnoException | null): void {
|
||||
if (err) throw err
|
||||
}
|
||||
|
||||
export function log(options: ScaffoldConfig, ...obj: any[]) {
|
||||
if (options.quiet) {
|
||||
export function log(config: ScaffoldConfig, level: LogLevel, ...obj: any[]): void {
|
||||
if (config.quiet || config.verbose === LogLevel.None || level < (config.verbose ?? LogLevel.Info)) {
|
||||
return
|
||||
}
|
||||
console["log"](...obj)
|
||||
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 async function createDirIfNotExists(dir: string, options: ScaffoldConfig): Promise<void> {
|
||||
export async function createDirIfNotExists(dir: string, config: ScaffoldConfig): Promise<void> {
|
||||
const parentDir = path.dirname(dir)
|
||||
|
||||
if (!(await pathExists(parentDir))) {
|
||||
await createDirIfNotExists(parentDir, options)
|
||||
await createDirIfNotExists(parentDir, config)
|
||||
}
|
||||
|
||||
if (!(await pathExists(dir))) {
|
||||
try {
|
||||
log(config, LogLevel.Debug, `Creating dir ${dir}`)
|
||||
await mkdir(dir)
|
||||
return
|
||||
} catch (e: any) {
|
||||
@@ -54,25 +146,43 @@ export async function createDirIfNotExists(dir: string, options: ScaffoldConfig)
|
||||
}
|
||||
|
||||
export function getOptionValueForFile<T>(
|
||||
config: ScaffoldConfig,
|
||||
filePath: string,
|
||||
data: Record<string, string>,
|
||||
fn: FileResponse<T>,
|
||||
defaultValue?: T
|
||||
defaultValue?: T,
|
||||
): T {
|
||||
if (typeof fn !== "function") {
|
||||
return defaultValue ?? (fn as T)
|
||||
}
|
||||
return (fn as FileResponseFn<T>)(
|
||||
return (fn as FileResponseHandler<T>)(
|
||||
filePath,
|
||||
path.dirname(handlebarsParse(filePath, data)),
|
||||
path.basename(handlebarsParse(filePath, data))
|
||||
path.dirname(handlebarsParse(config, filePath, { isPath: true }).toString()),
|
||||
path.basename(handlebarsParse(config, filePath, { isPath: true }).toString()),
|
||||
)
|
||||
}
|
||||
|
||||
export function handlebarsParse(templateBuffer: Buffer | string, data: Record<string, string>) {
|
||||
const parser = Handlebars.compile(templateBuffer.toString(), { noEscape: true })
|
||||
const outputContents = parser(data)
|
||||
return outputContents
|
||||
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> {
|
||||
@@ -95,3 +205,219 @@ 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("'"))
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function parseConfig(config: ScaffoldCmdConfig & OptionsBase): ScaffoldConfig {
|
||||
let c: ScaffoldConfig = config
|
||||
|
||||
if (config.config) {
|
||||
const [configFile, template = "default"] = config.config.split(":")
|
||||
const configImport: ScaffoldConfigFile = require(path.resolve(process.cwd(), configFile))
|
||||
if (!configImport[template]) {
|
||||
throw new Error(`Template "${template}" not found in ${configFile}`)
|
||||
}
|
||||
c = {
|
||||
...config,
|
||||
...configImport[template],
|
||||
data: {
|
||||
...configImport[template].data,
|
||||
...config.data,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
c.data = { ...c.data, ...config.appendData }
|
||||
delete config.appendData
|
||||
return c
|
||||
}
|
||||
|
||||
@@ -2,6 +2,11 @@ import mockFs from "mock-fs"
|
||||
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 { join } from "path"
|
||||
import * as dateFns from "date-fns"
|
||||
import crypto from "crypto"
|
||||
|
||||
const fileStructNormal = {
|
||||
input: {
|
||||
@@ -9,6 +14,13 @@ const fileStructNormal = {
|
||||
},
|
||||
output: {},
|
||||
}
|
||||
const fileStructWithBinary = {
|
||||
input: {
|
||||
"{{name}}.txt": "Hello, my app is {{name}}",
|
||||
"{{name}}.bin": crypto.randomBytes(10000),
|
||||
},
|
||||
output: {},
|
||||
}
|
||||
|
||||
const fileStructWithData = {
|
||||
input: {
|
||||
@@ -19,21 +31,60 @@ const fileStructWithData = {
|
||||
|
||||
const fileStructNested = {
|
||||
input: {
|
||||
"{{name}}-1.text": "This should be in root",
|
||||
"{{name}}-1.txt": "This should be in root",
|
||||
"{{Name}}": {
|
||||
"{{name}}-2.txt": "Hello, my value is {{value}}",
|
||||
moreNesting: {
|
||||
"{{name}}-3.txt": "Hi! My value is actually NOT {{value}}!",
|
||||
},
|
||||
},
|
||||
},
|
||||
output: {},
|
||||
}
|
||||
const fileStructSubdirTransformer = {
|
||||
input: {
|
||||
"{{name}}.txt": "Hello, my app is {{name}}",
|
||||
},
|
||||
output: {},
|
||||
}
|
||||
|
||||
const defaultHelperNames = Object.keys(defaultHelpers)
|
||||
const fileStructHelpers = {
|
||||
input: {
|
||||
defaults: defaultHelperNames.reduce<Record<string, string>>(
|
||||
(all, cur) => ({ ...all, [cur + ".txt"]: `{{ ${cur} name }}` }),
|
||||
{},
|
||||
),
|
||||
custom: {
|
||||
"add1.txt": "{{ add1 name }}",
|
||||
},
|
||||
},
|
||||
output: {},
|
||||
}
|
||||
|
||||
const fileStructDates = {
|
||||
input: {
|
||||
"now.txt": "Today is {{ now 'mmm' }}, time is {{ now 'HH:mm' }}",
|
||||
"offset.txt": "Yesterday was {{ now 'mmm' -1 'days' }}, time is {{ now 'HH:mm' -1 'days' }}",
|
||||
"custom.txt": "Custom date is {{ date customDate 'mmm' }}, time is {{ date customDate 'HH:mm' }}",
|
||||
},
|
||||
output: {},
|
||||
}
|
||||
|
||||
function withMock(fileStruct: FileSystem.DirectoryItems, testFn: jest.EmptyFunction): jest.EmptyFunction {
|
||||
return () => {
|
||||
beforeEach(() => {
|
||||
// console.log("Mocking:", fileStruct)
|
||||
console = new Console(process.stdout, process.stderr)
|
||||
|
||||
mockFs(fileStruct)
|
||||
// logMock = jest.spyOn(console, 'log').mockImplementation((...args) => {
|
||||
// logsTemp.push(args)
|
||||
// })
|
||||
})
|
||||
testFn()
|
||||
afterEach(() => {
|
||||
// console.log("Restoring mock")
|
||||
mockFs.restore()
|
||||
})
|
||||
}
|
||||
@@ -48,11 +99,10 @@ describe("Scaffold", () => {
|
||||
name: "app_name",
|
||||
output: "output",
|
||||
templates: ["input"],
|
||||
quiet: true,
|
||||
verbose: 0,
|
||||
})
|
||||
|
||||
const data = readFileSync(process.cwd() + "/output/app_name.txt")
|
||||
expect(data.toString()).toBe("Hello, my app is app_name")
|
||||
const data = readFileSync(join(process.cwd(), "output", "app_name.txt"))
|
||||
expect(data.toString()).toEqual("Hello, my app is app_name")
|
||||
})
|
||||
|
||||
test("should create with config", async () => {
|
||||
@@ -61,13 +111,31 @@ describe("Scaffold", () => {
|
||||
output: "output",
|
||||
templates: ["input"],
|
||||
createSubFolder: true,
|
||||
quiet: true,
|
||||
verbose: 0,
|
||||
})
|
||||
|
||||
const data = readFileSync(process.cwd() + "/output/app_name/app_name.txt")
|
||||
expect(data.toString()).toBe("Hello, my app is app_name")
|
||||
const data = readFileSync(join(process.cwd(), "output", "app_name", "app_name.txt"))
|
||||
expect(data.toString()).toEqual("Hello, my app is app_name")
|
||||
})
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
describe(
|
||||
"binary files",
|
||||
withMock(fileStructWithBinary, () => {
|
||||
test("should copy as-is", async () => {
|
||||
await Scaffold({
|
||||
name: "app_name",
|
||||
output: "output",
|
||||
templates: ["input"],
|
||||
verbose: 0,
|
||||
})
|
||||
const data = readFileSync(join(process.cwd(), "output", "app_name.txt"))
|
||||
expect(data.toString()).toEqual("Hello, my app is app_name")
|
||||
const dataBin = readFileSync(join(process.cwd(), "output", "app_name.bin"))
|
||||
expect(dataBin).toEqual(fileStructWithBinary.input["{{name}}.bin"])
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
describe(
|
||||
@@ -79,7 +147,7 @@ describe("Scaffold", () => {
|
||||
output: "output",
|
||||
templates: ["input"],
|
||||
data: { value: "1" },
|
||||
quiet: true,
|
||||
verbose: 0,
|
||||
})
|
||||
|
||||
await Scaffold({
|
||||
@@ -87,11 +155,11 @@ describe("Scaffold", () => {
|
||||
output: "output",
|
||||
templates: ["input"],
|
||||
data: { value: "2" },
|
||||
quiet: true,
|
||||
verbose: 0,
|
||||
})
|
||||
|
||||
const data = readFileSync(process.cwd() + "/output/app_name.txt")
|
||||
expect(data.toString()).toBe("Hello, my value is 1")
|
||||
const data = readFileSync(join(process.cwd(), "output", "app_name.txt"))
|
||||
expect(data.toString()).toEqual("Hello, my value is 1")
|
||||
})
|
||||
|
||||
test("should overwrite with config", async () => {
|
||||
@@ -100,7 +168,7 @@ describe("Scaffold", () => {
|
||||
output: "output",
|
||||
templates: ["input"],
|
||||
data: { value: "1" },
|
||||
quiet: true,
|
||||
verbose: 0,
|
||||
})
|
||||
|
||||
await Scaffold({
|
||||
@@ -109,21 +177,25 @@ describe("Scaffold", () => {
|
||||
templates: ["input"],
|
||||
data: { value: "2" },
|
||||
overwrite: true,
|
||||
quiet: true,
|
||||
verbose: 0,
|
||||
})
|
||||
|
||||
const data = readFileSync(process.cwd() + "/output/app_name.txt")
|
||||
expect(data.toString()).toBe("Hello, my value is 2")
|
||||
const data = readFileSync(join(process.cwd(), "output", "app_name.txt"))
|
||||
expect(data.toString()).toEqual("Hello, my value is 2")
|
||||
})
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
describe(
|
||||
"errors",
|
||||
withMock(fileStructNormal, () => {
|
||||
let consoleMock: jest.SpyInstance
|
||||
let consoleMock1: jest.SpyInstance
|
||||
beforeAll(() => {
|
||||
consoleMock = jest.spyOn(console, "error").mockImplementation(() => void 0)
|
||||
consoleMock1 = jest.spyOn(console, "error").mockImplementation(() => void 0)
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
consoleMock1.mockRestore()
|
||||
})
|
||||
|
||||
test("should throw for bad input", async () => {
|
||||
@@ -133,17 +205,50 @@ describe("Scaffold", () => {
|
||||
output: "output",
|
||||
templates: ["non-existing-input"],
|
||||
data: { value: "1" },
|
||||
quiet: true,
|
||||
})
|
||||
verbose: 0,
|
||||
}),
|
||||
).rejects.toThrow()
|
||||
|
||||
expect(() => readFileSync(process.cwd() + "/output/app_name.txt")).toThrow()
|
||||
await expect(
|
||||
Scaffold({
|
||||
name: "app_name",
|
||||
output: "output",
|
||||
templates: ["non-existing-input/non-existing-file.txt"],
|
||||
data: { value: "1" },
|
||||
verbose: 0,
|
||||
}),
|
||||
).rejects.toThrow()
|
||||
|
||||
expect(() => readFileSync(join(process.cwd(), "output", "app_name.txt"))).toThrow()
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
describe(
|
||||
"dry run",
|
||||
withMock(fileStructNormal, () => {
|
||||
let consoleMock1: jest.SpyInstance
|
||||
beforeAll(() => {
|
||||
consoleMock1 = jest.spyOn(console, "error").mockImplementation(() => void 0)
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
consoleMock.mockRestore()
|
||||
consoleMock1.mockRestore()
|
||||
})
|
||||
})
|
||||
|
||||
test("should not write to disk", async () => {
|
||||
Scaffold({
|
||||
name: "app_name",
|
||||
output: "output",
|
||||
templates: ["input"],
|
||||
data: { value: "1" },
|
||||
verbose: 0,
|
||||
dryRun: true,
|
||||
})
|
||||
|
||||
expect(() => readFileSync(join(process.cwd(), "output", "app_name.txt"))).toThrow()
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
describe(
|
||||
@@ -152,15 +257,15 @@ describe("Scaffold", () => {
|
||||
test("should allow override function", async () => {
|
||||
await Scaffold({
|
||||
name: "app_name",
|
||||
output: (fullPath, basedir, basename) => `custom-output/${basename.split(".")[0]}`,
|
||||
output: (fullPath, basedir, basename) => join("custom-output", `${basename.split(".")[0]}`),
|
||||
templates: ["input"],
|
||||
data: { value: "1" },
|
||||
quiet: true,
|
||||
verbose: 0,
|
||||
})
|
||||
const data = readFileSync(process.cwd() + "/custom-output/app_name/app_name.txt")
|
||||
expect(data.toString()).toBe("Hello, my app is app_name")
|
||||
const data = readFileSync(join(process.cwd(), "/custom-output/app_name/app_name.txt"))
|
||||
expect(data.toString()).toEqual("Hello, my app is app_name")
|
||||
})
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
describe(
|
||||
@@ -169,15 +274,229 @@ describe("Scaffold", () => {
|
||||
test("should maintain input structure on output", async () => {
|
||||
await Scaffold({
|
||||
name: "app_name",
|
||||
output: "./",
|
||||
output: "output",
|
||||
templates: ["input"],
|
||||
data: { value: "1" },
|
||||
quiet: true,
|
||||
verbose: 0,
|
||||
})
|
||||
|
||||
const dir = readdirSync(process.cwd())
|
||||
const rootDir = readdirSync(join(process.cwd(), "output"))
|
||||
const dir = readdirSync(join(process.cwd(), "output", "AppName"))
|
||||
const nestedDir = readdirSync(join(process.cwd(), "output", "AppName", "moreNesting"))
|
||||
expect(rootDir).toHaveProperty("length")
|
||||
expect(dir).toHaveProperty("length")
|
||||
expect(nestedDir).toHaveProperty("length")
|
||||
|
||||
const rootFile = readFileSync(join(process.cwd(), "output", "app_name-1.txt"))
|
||||
const oneDeepFile = readFileSync(join(process.cwd(), "output", "AppName/app_name-2.txt"))
|
||||
const twoDeepFile = readFileSync(join(process.cwd(), "output", "AppName/moreNesting/app_name-3.txt"))
|
||||
expect(rootFile.toString()).toEqual("This should be in root")
|
||||
expect(oneDeepFile.toString()).toEqual("Hello, my value is 1")
|
||||
expect(twoDeepFile.toString()).toEqual("Hi! My value is actually NOT 1!")
|
||||
})
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
describe(
|
||||
"capitalization helpers",
|
||||
withMock(fileStructHelpers, () => {
|
||||
const _helpers: Record<string, (text: string) => string> = {
|
||||
add1: (text) => text + " 1",
|
||||
}
|
||||
|
||||
test("should work", async () => {
|
||||
await Scaffold({
|
||||
name: "app_name",
|
||||
output: "output",
|
||||
templates: ["input"],
|
||||
verbose: 0,
|
||||
helpers: _helpers,
|
||||
})
|
||||
|
||||
const results = {
|
||||
camelCase: "appName",
|
||||
snakeCase: "app_name",
|
||||
startCase: "App Name",
|
||||
kebabCase: "app-name",
|
||||
hyphenCase: "app-name",
|
||||
pascalCase: "AppName",
|
||||
lowerCase: "app_name",
|
||||
upperCase: "APP_NAME",
|
||||
}
|
||||
for (const key in results) {
|
||||
const file = readFileSync(join(process.cwd(), "output", "defaults", `${key}.txt`))
|
||||
expect(file.toString()).toEqual(results[key as keyof typeof results])
|
||||
}
|
||||
})
|
||||
}),
|
||||
)
|
||||
describe(
|
||||
"date helpers",
|
||||
withMock(fileStructDates, () => {
|
||||
test("should work", async () => {
|
||||
const now = new Date()
|
||||
const yesterday = dateFns.add(new Date(), { days: -1 })
|
||||
const customDate = dateFns.formatISO(dateFns.add(new Date(), { days: -1 }))
|
||||
|
||||
await Scaffold({
|
||||
name: "app_name",
|
||||
output: "output",
|
||||
templates: ["input"],
|
||||
verbose: 0,
|
||||
data: { customDate },
|
||||
})
|
||||
|
||||
const nowFile = readFileSync(join(process.cwd(), "output", "now.txt"))
|
||||
const offsetFile = readFileSync(join(process.cwd(), "output", "offset.txt"))
|
||||
const customFile = readFileSync(join(process.cwd(), "output", "custom.txt"))
|
||||
|
||||
// "now.txt": "Today is {{ now 'mmm' }}, time is {{ now 'HH:mm' }}",
|
||||
// "offset.txt": "Yesterday was {{ now 'mmm' -1 'days' }}, time is {{ now 'HH:mm' -1 'days' }}",
|
||||
// "custom.txt": "Custom date is {{ date customDate 'mmm' }}, time is {{ date customDate 'HH:mm' }}",
|
||||
|
||||
expect(nowFile.toString()).toEqual(
|
||||
`Today is ${dateFns.format(now, "mmm")}, time is ${dateFns.format(now, "HH:mm")}`,
|
||||
)
|
||||
expect(offsetFile.toString()).toEqual(
|
||||
`Yesterday was ${dateFns.format(yesterday, "mmm")}, time is ${dateFns.format(yesterday, "HH:mm")}`,
|
||||
)
|
||||
expect(customFile.toString()).toEqual(
|
||||
`Custom date is ${dateFns.format(dateFns.parseISO(customDate), "mmm")}, time is ${dateFns.format(
|
||||
dateFns.parseISO(customDate),
|
||||
"HH:mm",
|
||||
)}`,
|
||||
)
|
||||
})
|
||||
}),
|
||||
)
|
||||
describe(
|
||||
"custom helpers",
|
||||
withMock(fileStructHelpers, () => {
|
||||
const _helpers: Record<string, (text: string) => string> = {
|
||||
add1: (text) => text + " 1",
|
||||
}
|
||||
test("should work", async () => {
|
||||
await Scaffold({
|
||||
name: "app_name",
|
||||
output: "output",
|
||||
templates: ["input"],
|
||||
verbose: 0,
|
||||
helpers: _helpers,
|
||||
})
|
||||
|
||||
const results = {
|
||||
add1: "app_name 1",
|
||||
}
|
||||
for (const key in results) {
|
||||
const file = readFileSync(join(process.cwd(), "output", "custom", `${key}.txt`))
|
||||
expect(file.toString()).toEqual(results[key as keyof typeof results])
|
||||
}
|
||||
})
|
||||
}),
|
||||
)
|
||||
describe(
|
||||
"transform subfolder",
|
||||
withMock(fileStructSubdirTransformer, () => {
|
||||
test("should work with no helper", async () => {
|
||||
await Scaffold({
|
||||
name: "app_name",
|
||||
output: "output",
|
||||
templates: ["input"],
|
||||
createSubFolder: true,
|
||||
verbose: 0,
|
||||
})
|
||||
|
||||
const data = readFileSync(join(process.cwd(), "output", "app_name", "app_name.txt"))
|
||||
expect(data.toString()).toEqual("Hello, my app is app_name")
|
||||
})
|
||||
|
||||
test("should work with default helper", async () => {
|
||||
await Scaffold({
|
||||
name: "app_name",
|
||||
output: "output",
|
||||
templates: ["input"],
|
||||
createSubFolder: true,
|
||||
verbose: 0,
|
||||
subFolderNameHelper: "upperCase",
|
||||
})
|
||||
|
||||
const data = readFileSync(join(process.cwd(), "output", "APP_NAME", "app_name.txt"))
|
||||
expect(data.toString()).toEqual("Hello, my app is app_name")
|
||||
})
|
||||
|
||||
test("should work with custom helper", async () => {
|
||||
await Scaffold({
|
||||
name: "app_name",
|
||||
output: "output",
|
||||
templates: ["input"],
|
||||
createSubFolder: true,
|
||||
verbose: 0,
|
||||
subFolderNameHelper: "test",
|
||||
helpers: {
|
||||
test: () => "REPLACED",
|
||||
},
|
||||
})
|
||||
|
||||
const data = readFileSync(join(process.cwd(), "output", "REPLACED", "app_name.txt"))
|
||||
expect(data.toString()).toEqual("Hello, my app is app_name")
|
||||
})
|
||||
}),
|
||||
)
|
||||
describe(
|
||||
"before write",
|
||||
withMock(fileStructNormal, () => {
|
||||
test("should work with no callback", async () => {
|
||||
await Scaffold({
|
||||
name: "app_name",
|
||||
output: "output",
|
||||
templates: ["input"],
|
||||
verbose: 0,
|
||||
data: {
|
||||
value: "value",
|
||||
},
|
||||
})
|
||||
|
||||
const data = readFileSync(join(process.cwd(), "output", "app_name.txt"))
|
||||
expect(data.toString()).toEqual("Hello, my app is app_name")
|
||||
})
|
||||
|
||||
test("should work with custom callback", async () => {
|
||||
await Scaffold({
|
||||
name: "app_name",
|
||||
output: "output",
|
||||
templates: ["input"],
|
||||
verbose: 0,
|
||||
data: {
|
||||
value: "value",
|
||||
},
|
||||
beforeWrite: (content, beforeContent, outputPath) =>
|
||||
[content.toString().toUpperCase(), beforeContent, outputPath].join(", "),
|
||||
})
|
||||
|
||||
const data = readFileSync(join(process.cwd(), "output", "app_name.txt"))
|
||||
expect(data.toString()).toEqual(
|
||||
[
|
||||
"Hello, my app is app_name".toUpperCase(),
|
||||
fileStructNormal.input["{{name}}.txt"],
|
||||
join(process.cwd(), "output", "app_name.txt"),
|
||||
].join(", "),
|
||||
)
|
||||
})
|
||||
test("should work with undefined response custom callback", async () => {
|
||||
await Scaffold({
|
||||
name: "app_name",
|
||||
output: "output",
|
||||
templates: ["input"],
|
||||
verbose: 0,
|
||||
data: {
|
||||
value: "value",
|
||||
},
|
||||
beforeWrite: () => undefined,
|
||||
})
|
||||
|
||||
const data = readFileSync(join(process.cwd(), "output", "app_name.txt"))
|
||||
expect(data.toString()).toEqual("Hello, my app is app_name")
|
||||
})
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
124
tests/utils.test.ts
Normal file
124
tests/utils.test.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
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"
|
||||
|
||||
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("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("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" })
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -4,20 +4,13 @@
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"esModuleInterop": true,
|
||||
"lib": [
|
||||
"ES2019",
|
||||
],
|
||||
"lib": ["ES2019"],
|
||||
"declaration": true,
|
||||
"outDir": "dist",
|
||||
"strict": true,
|
||||
"sourceMap": true,
|
||||
"removeComments": false,
|
||||
"removeComments": false
|
||||
},
|
||||
"include": [
|
||||
"src/index.ts",
|
||||
"src/cmd.ts",
|
||||
],
|
||||
"exclude": [
|
||||
"tests/*"
|
||||
]
|
||||
"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("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",
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user