Compare commits

..

104 Commits

Author SHA1 Message Date
semantic-release-bot
2c4eccd800 chore(release): 1.3.2 [skip ci]
## [1.3.2](https://github.com/chenasraf/simple-scaffold/compare/v1.3.1...v1.3.2) (2023-04-28)

### Bug Fixes

* release build ([2c23fa9](2c23fa9dbb))
* release build asset ([0bef2df](0bef2df5f3))
2023-04-28 22:08:51 +00:00
Chen Asraf
2c23fa9dbb fix: release build 2023-04-29 01:04:01 +03:00
Chen Asraf
0bef2df5f3 fix: release build asset 2023-04-29 00:59:54 +03:00
semantic-release-bot
398a5d723b chore(release): 1.3.1 [skip ci]
## [1.3.1](https://github.com/chenasraf/simple-scaffold/compare/v1.3.0...v1.3.1) (2023-04-28)

### Bug Fixes

* docs ([6e19a86](6e19a86190))
* remove old peer-dep ([c7e2ef8](c7e2ef862c))
2023-04-28 21:50:36 +00:00
Chen Asraf
c7e2ef862c fix: remove old peer-dep 2023-04-29 00:44:49 +03:00
Chen Asraf
36dd27e480 docs: fix doc links 2023-04-29 00:44:49 +03:00
Chen Asraf
6e19a86190 fix: docs 2023-04-29 00:44:49 +03:00
Chen Asraf
eba7897f84 chore: bump version number + fix changelog 2023-04-29 00:44:38 +03:00
semantic-release-bot
dfdbca59a7 chore(release): 1.3.0 [skip ci]
## [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](c3989769fe))

### Bug Fixes

* config option should not be mandatory ([3db6a91](3db6a918f1))
* export config file type ([4302eb5](4302eb5ce3))
2023-04-25 07:27:13 +00:00
Chen Asraf
1d6643b7eb docs: clean up css 2023-04-25 10:26:24 +03:00
Chen Asraf
9b86499a04 docs: use js for typedoc config 2023-04-25 10:26:24 +03:00
Chen Asraf
f41ebfb1d0 docs: reorganize file structure 2023-04-25 10:26:24 +03:00
Chen Asraf
873fa77c9a docs: add changelog to typedoc 2023-04-25 10:26:24 +03:00
Chen Asraf
1ce4a41a97 build: update changelog sections 2023-04-25 10:26:24 +03:00
Chen Asraf
4302eb5ce3 fix: export config file type 2023-04-25 10:26:24 +03:00
Chen Asraf
146086869f docs: move migration to pages, fix urls 2023-04-25 10:26:24 +03:00
Chen Asraf
8a2207bec5 docs: add table of contents 2023-04-25 10:26:24 +03:00
Chen Asraf
42568e0fa1 docs: remove unnecessary nested menus 2023-04-25 10:26:24 +03:00
Chen Asraf
a4498f9539 docs: split into files 2023-04-25 10:26:24 +03:00
Chen Asraf
5a2b187115 docs: update config doc 2023-04-25 10:26:24 +03:00
Chen Asraf
3db6a918f1 fix: config option should not be mandatory 2023-04-25 10:26:24 +03:00
Chen Asraf
c3989769fe feat: load scaffold config from files 2023-04-25 10:26:24 +03:00
Chen Asraf
b3f7912760 build: remove unnecessary yarn pack 2023-04-25 10:26:24 +03:00
Chen Asraf
1e0b73190c build: only generate docs on master 2023-04-25 10:26:24 +03:00
Chen Asraf
0c8e6e7573 docs: fix CHANGELOG.md 2023-04-25 10:26:24 +03:00
Chen Asraf
f28280e815 docs: update readme 2023-04-25 10:26:24 +03:00
semantic-release-bot
7da786a463 chore(release): 1.2.0 [skip ci]
## [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](3c5c2ded02))

### Bug Fixes

* ci node version ([767d34c](767d34c684))
* semantic-release build dir ([f7956dd](f7956ddc78))
* support quote wrapping in append-data ([4fecca8](4fecca8483))
2023-04-24 10:06:10 +00:00
Chen Asraf
4fecca8483 fix: support quote wrapping in append-data 2023-04-24 13:02:29 +03:00
Chen Asraf
222e1a04ca build: update github action versions 2023-04-24 13:02:29 +03:00
Chen Asraf
f7956ddc78 fix: semantic-release build dir 2023-04-24 13:02:29 +03:00
Chen Asraf
029f260a4c chore: bump version number [skip-ci] 2023-04-24 13:02:29 +03:00
Chen Asraf
7a4c0abd33 build: fix docs build 2023-04-24 13:02:29 +03:00
Chen Asraf
ed385ecfee docs: update spacing 2023-04-24 13:02:29 +03:00
Chen Asraf
833ea9d404 docs: update table css 2023-04-24 13:02:29 +03:00
Chen Asraf
8fb508f32f docs: update typedoc & remove custom theme 2023-04-24 13:02:29 +03:00
Chen Asraf
75641e5e20 build: add missing dependencies 2023-04-24 13:02:29 +03:00
Chen Asraf
f4c745bfeb build: add missing dependencies 2023-04-24 13:02:29 +03:00
Chen Asraf
47b4c427ac build: fix build 2023-04-24 13:02:29 +03:00
Chen Asraf
7ef6d583a8 docs: update docs 2023-04-24 13:02:29 +03:00
Chen Asraf
59a46b0455 build: update dependencies & fix build 2023-04-24 13:02:29 +03:00
Chen Asraf
767d34c684 fix: ci node version 2023-04-24 13:02:29 +03:00
Chen Asraf
2050ea38d5 build: semantic-release 2023-04-24 13:02:29 +03:00
Chen Asraf
1bfcafad46 chore: update FUNDING.yml 2023-04-24 13:02:29 +03:00
Chen Asraf
3c5c2ded02 feat: append-data cli flag 2023-04-24 13:02:29 +03:00
Chen Asraf
8f5bee8da6 docs: update domain 2023-04-24 13:02:29 +03:00
Chen Asraf
7d1dc2a808 Merge pull request #50 from chenasraf/develop
v1.1.4
2023-03-13 21:29:42 +02:00
Chen Asraf
8e432bfb0b chore: bump version number 2023-03-13 21:25:30 +02:00
Chen Asraf
7c19c53337 fix: github action node version 2023-03-13 21:23:52 +02:00
Chen Asraf
94fec76616 fix: github action node version 2023-03-13 21:20:43 +02:00
Chen Asraf
20400bd81d chore: update dependencies 2023-03-13 21:16:20 +02:00
Chen Asraf
c334396d74 docs: update typedoc version 2023-03-13 09:53:33 +02:00
dependabot[bot]
78c8e693a8 Merge pull request #47 from chenasraf/dependabot/npm_and_yarn/json5-2.2.3 2023-03-13 07:47:54 +00:00
dependabot[bot]
7865f27471 Merge pull request #49 from chenasraf/dependabot/npm_and_yarn/minimatch-3.1.2 2023-03-13 07:47:43 +00:00
dependabot[bot]
ee4e52c4a0 Bump minimatch from 3.0.4 to 3.1.2
Bumps [minimatch](https://github.com/isaacs/minimatch) from 3.0.4 to 3.1.2.
- [Release notes](https://github.com/isaacs/minimatch/releases)
- [Changelog](https://github.com/isaacs/minimatch/blob/main/changelog.md)
- [Commits](https://github.com/isaacs/minimatch/compare/v3.0.4...v3.1.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-11 21:34:44 +00:00
Chen Asraf
d7d2b13888 chore: bump version number [publish] 2023-03-11 23:34:09 +02:00
Chen Asraf
6b6baefbce Merge pull request #48 from locene/develop
fix: base path
2023-03-11 23:29:23 +02:00
locene
943717a769 fix: base path 2023-03-10 06:01:57 +08:00
dependabot[bot]
e28c4db323 Bump json5 from 2.2.0 to 2.2.3
Bumps [json5](https://github.com/json5/json5) from 2.2.0 to 2.2.3.
- [Release notes](https://github.com/json5/json5/releases)
- [Changelog](https://github.com/json5/json5/blob/main/CHANGELOG.md)
- [Commits](https://github.com/json5/json5/compare/v2.2.0...v2.2.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-01-07 04:02:37 +00:00
Chen Asraf
5308ef9618 Update README.md 2022-12-27 12:45:14 +02:00
Chen Asraf
a8e9f71c11 Update FUNDING.yml 2022-08-29 00:02:20 +03:00
Chen Asraf
6fa2a8b03b Update README.md 2022-08-16 16:42:38 +03:00
Chen Asraf
7c69010e95 update docs + spell checker 2022-05-24 10:09:48 +03:00
Chen Asraf
b569f2b0a1 formatting updates 2022-05-24 00:36:17 +03:00
Chen Asraf
adba6490e8 Update README.md 2022-05-18 01:04:10 +03:00
Chen Asraf
1d0c20cba9 docs: fix typo 2022-05-15 14:32:22 +03:00
Chen Asraf
cd68ab41d4 Update README.md 2022-05-15 14:02:42 +03:00
Chen Asraf
1b37a8eb40 update intro gif 2022-05-06 01:25:34 +03:00
Chen Asraf
576798cfeb update intro gif 2022-05-06 01:24:30 +03:00
Chen Asraf
ac8af8e129 revert docs build in ci 2022-05-03 19:52:23 +03:00
Chen Asraf
96b93d8b60 try fix docs build process 2022-05-03 19:51:12 +03:00
Chen Asraf
11edb0d464 remove unnecessary install 2022-05-03 19:46:00 +03:00
Chen Asraf
35262b5a44 fix docs build process 2022-05-03 19:45:34 +03:00
Chen Asraf
3b77e6938c improve docs build process 2022-05-03 19:41:16 +03:00
Chen Asraf
3cf93598c3 improve docs build process 2022-05-03 19:38:12 +03:00
Chen Asraf
8957b595da improve docs build process 2022-05-03 19:35:36 +03:00
Chen Asraf
2841ebd3d2 update docs + formatting update 2022-05-03 19:24:18 +03:00
Chen Asraf
0364247904 fix build 2022-05-01 14:26:14 +03:00
Chen Asraf
ac2c0d7ffb fix build 2022-05-01 14:24:44 +03:00
Chen Asraf
a002402088 update docs 2022-05-01 14:23:03 +03:00
Chen Asraf
bdc23e7074 Update README.md 2022-05-01 13:56:20 +03:00
Chen Asraf
6160a04009 Update README.md 2022-05-01 13:53:57 +03:00
Chen Asraf
7414baf2ab Merge pull request #42 from chenasraf/master
v1.1.2 back merge
2022-04-29 10:19:26 +03:00
Chen Asraf
c45d870233 v1.1.2
Fix binary file copy
2022-04-29 10:14:48 +03:00
Chen Asraf
8c8cedee48 update help command 2022-04-29 10:10:53 +03:00
Chen Asraf
40606edaee update test desc 2022-04-29 09:56:59 +03:00
Chen Asraf
e450ad242e fix: binary files + add tests 2022-04-29 09:54:06 +03:00
Chen Asraf
df60922154 Merge pull request #39 from chenasraf/master
Merge pull request #38 from chenasraf/develop
2022-04-24 17:41:55 +03:00
Chen Asraf
256d919253 Merge pull request #38 from chenasraf/develop
v1.1.1 - security fix
2022-04-24 17:30:10 +03:00
Chen Asraf
f8c31a1ecf Merge branch 'master' into develop 2022-04-24 17:28:39 +03:00
Chen Asraf
5571aba8dc npm audit fix 2022-04-24 17:27:03 +03:00
Chen Asraf
12f8bca7b5 fix doc deps 2022-04-23 11:29:09 +03:00
Chen Asraf
643431d333 fix gtag 2022-04-23 11:24:02 +03:00
Chen Asraf
0042c12ee7 fix docs build 2022-04-23 11:19:41 +03:00
Chen Asraf
869911bba0 update docs GA 2022-04-23 11:18:23 +03:00
Chen Asraf
9bd6219aa5 add gaid to docs 2022-04-23 09:45:48 +03:00
Chen Asraf
27a6ba4dda update docs 2022-04-22 23:00:11 +03:00
Chen Asraf
923b5316be update docs 2022-04-21 15:37:44 +03:00
Chen Asraf
bf10fb8ec3 update docs 2022-04-21 15:34:03 +03:00
Chen Asraf
4e2fa01bed update docs [skip publish] 2022-04-21 14:45:58 +03:00
Chen Asraf
af65ecadb9 try to fix docs [skip publish] 2022-04-21 13:07:19 +03:00
Chen Asraf
0d359b1917 fix workflow [skip publish] 2022-04-21 12:37:01 +03:00
Chen Asraf
f2a75c9fb0 Typdoc (#37) [skip publish]
* update package.json

* generate typedocs

* update docs
2022-04-21 12:35:32 +03:00
Chen Asraf
3c57638903 update package.json 2022-04-21 03:04:08 +03:00
Chen Asraf
a3deda2d80 v1.1 (#35)
v1.1

### New features

- Enable custom transformations with `beforeWrite`
- Add built-in date formatting helpers

### Commit log

* add develop branch

* chore: remove unneeded dep

* Update issue templates [skip ci]

* update PR workflow

* implementation of before-write callback + tests

* clean up + update docs

* add explicit function return types

* bump version number

* docs: add table of contents

* correct version number

* update workflows + release alpha

* update README.md [skip ci]

* update README.md [skip ci]

* implement now & custom date helpers

* docs + bump version number

* docs cleanup

* update docs

* accept async beforeWrite callback

* fix docs

* remove redundent check

* fix type import

* update README.md [skip ci]

* bump version number [skip ci]

* update docs [skip ci]
2022-04-21 02:57:52 +03:00
39 changed files with 6025 additions and 1868 deletions

View File

@@ -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
View File

@@ -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&currency_code=ILS&source=url"

View File

@@ -1,37 +1,47 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
title: "[BUG] "
labels: bug, needs-triage
assignees: chenasraf
---
**Describe the bug**
#### Describe the bug
A clear and concise description of what the bug is.
**To Reproduce**
#### How To Reproduce
Steps to reproduce the behavior:
1. Prepare templates:
```
template contents
```
```text
This is my {{ template }}
```
2. Run with args/config:
```
npx simple-scaffold@latest -t ... -o output TplName
```
**Expected behavior**
```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.
#### Logs
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
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.0.4]
- Simple Scaffold Version [e.g. 1.1.2]
#### Additional context
**Additional context**
Add any other context about the problem here.

View File

@@ -1,20 +1,24 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
title: "[FEATURE] "
labels: enhancement, needs-triage
assignees: chenasraf
---
**Is your feature request related to a problem? Please describe.**
#### Is your feature request related to a problem? Please describe.
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
#### Describe the solution you'd like
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
#### Describe alternatives you've considered
A clear and concise description of any alternative solutions or features you've considered, if
applicable.
#### Additional context
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@@ -1,52 +0,0 @@
name: Alpha Releases
on:
push:
branches: [develop]
jobs:
build:
runs-on: ubuntu-latest
if: "!contains(github.event.head_commit.message, '[skip ci]')"
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: "12.x"
- run: yarn install --frozen-lockfile
- run: yarn test
- run: yarn build
- run: cd ./dist && yarn pack --filename=../package.tgz
if: "!contains(github.event.head_commit.message, '[skip publish]')"
- uses: Klemensas/action-autotag@stable
if: "!contains(github.event.head_commit.message, '[skip publish]')"
id: update_tag
with:
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
tag_prefix: "v"
- name: Publish on NPM
uses: JS-DevTools/npm-publish@v1
if: "!contains(github.event.head_commit.message, '[skip publish]')"
with:
package: ./dist/package.json
token: "${{ secrets.NPM_TOKEN }}"
- name: Create Release
if: steps.update_tag.outputs.tagname && !contains(github.event.head_commit.message, '[skip publish]')
uses: actions/create-release@v1
id: create_release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
prerelease: true
tag_name: ${{ steps.update_tag.outputs.tagname }}
release_name: Release ${{ steps.update_tag.outputs.tagname }}
- name: Upload Release Asset
if: steps.update_tag.outputs.tagname && !contains(github.event.head_commit.message, '[skip publish]')
id: upload-release-asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./package.tgz
asset_name: simple-scaffold ${{ steps.update_tag.outputs.tagname }}.tgz
asset_content_type: application/tgz

20
.github/workflows/docs.yml vendored Normal file
View 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

View File

@@ -8,10 +8,10 @@ jobs:
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

View File

@@ -1,50 +1,23 @@
name: Release
name: Semantic Release
on:
push:
branches: [master]
branches: [ master, develop, feat/*, fix/* ]
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 test
- run: yarn build
- run: cd ./dist && yarn pack --filename=../package.tgz
- run: yarn semantic-release
if: "!contains(github.event.head_commit.message, '[skip publish]')"
- uses: Klemensas/action-autotag@stable
if: "!contains(github.event.head_commit.message, '[skip publish]')"
id: update_tag
with:
env:
NPM_TOKEN: "${{ secrets.NPM_TOKEN }}"
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: simple-scaffold ${{ steps.update_tag.outputs.tagname }}.tgz
asset_content_type: application/tgz

5
.gitignore vendored
View File

@@ -42,6 +42,9 @@ typings/
# Optional npm cache directory
.npm
# NPM
.npmrc
# Optional eslint cache
.eslintcache
@@ -61,3 +64,5 @@ examples/test-output/**/*
dist/
.DS_Store
tmp/
docs/
.nvmrc

View File

@@ -1,8 +1,9 @@
{
"MD013": {
"line-length": {
"line_length": 100,
"tables": false,
"code_blocks": false
},
"MD033": false
"no-inline-html": false,
"first-line-h1": false
}

View File

@@ -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
View File

@@ -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}"
}
]
}

12
.vscode/settings.json vendored
View File

@@ -3,12 +3,20 @@
"npm.packageManager": "yarn",
"cSpell.words": [
"massarg",
"MYCOMPONENT",
"myname",
"nobrace",
"nocomment",
"nodir",
"noext",
"nonegate",
"subdir"
]
"subdir",
"variabletoken"
],
"[markdown]": {
"editor.rulers": [
87,
100
],
},
}

26
.vscode/tasks.json vendored
View File

@@ -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": []
}
]
}

128
CHANGELOG.md Normal file
View File

@@ -0,0 +1,128 @@
# Change Log
## [1.3.2](https://github.com/chenasraf/simple-scaffold/compare/v1.3.1...v1.3.2) (2023-04-28)
### Bug Fixes
* release build ([2c23fa9](https://github.com/chenasraf/simple-scaffold/commit/2c23fa9dbb310cd0a31f09606798f96b95d66779))
* release build asset ([0bef2df](https://github.com/chenasraf/simple-scaffold/commit/0bef2df5f3aa800ad5f1094c0996108db9acce51))
## [1.3.1](https://github.com/chenasraf/simple-scaffold/compare/v1.3.0...v1.3.1) (2023-04-28)
### Bug Fixes
* docs ([6e19a86](https://github.com/chenasraf/simple-scaffold/commit/6e19a86190dd924058a48448aa6463569ef1125f))
* remove old peer-dep ([c7e2ef8](https://github.com/chenasraf/simple-scaffold/commit/c7e2ef862cb658feb1071ac120b185d8b34d6dd3))
## [1.3.0](https://github.com/chenasraf/simple-scaffold/compare/v1.2.0...v1.3.0) (2023-04-25)
### Features
- load scaffold config from files
([c398976](https://github.com/chenasraf/simple-scaffold/commit/c3989769fee445c9183ff5e5b3892c4e9fb66a9e))
### Bug Fixes
- config option should not be mandatory
([3db6a91](https://github.com/chenasraf/simple-scaffold/commit/3db6a918f13d9300efa2fcb4a356d004475ab91c))
- export config file type
([4302eb5](https://github.com/chenasraf/simple-scaffold/commit/4302eb5ce35ed6cf1dc80dfb92790c3fdd96f963))
## [1.2.0](https://github.com/chenasraf/simple-scaffold/compare/v1.1.4...v1.2.0) (2023-04-24)
### Features
- append-data cli flag
([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)

398
README.md
View File

@@ -1,361 +1,92 @@
# Simple Scaffold
<h1 align="center">Simple Scaffold</h1>
Simple Scaffold allows you to generate any set of files in the easiest way possible with simple commands.
<h2 align="center">
It is completely framework agnostic so you can use it for anything from a few simple files to an
entire app boilerplate setup.
[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)
Simply organize your commonly-created files in their original structure, and running Simple Scaffold
will copy the files to the output path, while replacing values (such as component or app name, or
other custom data) inside the paths or contents of the files using Handlebars.js syntax.
![version](https://img.shields.io/github/package-json/v/chenasraf/simple-scaffold/master?label=version)
![build](https://img.shields.io/github/actions/workflow/status/chenasraf/simple-scaffold/release.yml?branch=master)
<br />
</h2>
<details>
<summary>Table of contents</summary>
Looking to streamline your workflow and get your projects up and running quickly? Look no further
than Simple Scaffold - the easy-to-use NPM package that simplifies the process of organizing and
copying your commonly-created files.
- [Simple Scaffold](#simple-scaffold)
- [Install](#install)
- [Use as a command line tool](#use-as-a-command-line-tool)
- [Command Line Options](#command-line-options)
- [Use in Node.js](#use-in-nodejs)
- [Node-specific options](#node-specific-options)
- [Preparing files](#preparing-files)
- [Template files](#template-files)
- [Variable/token replacement](#variabletoken-replacement)
- [Built-in Helpers](#built-in-helpers)
- [Capitalization Helpers](#capitalization-helpers)
- [Date helpers](#date-helpers)
- [Custom Helpers](#custom-helpers)
- [Examples](#examples)
- [Command Example](#command-example)
- [Example Scaffold Input](#example-scaffold-input)
- [Input Directory structure](#input-directory-structure)
- [Contents of `project/scaffold/{{Name}}.jsx`](#contents-of-projectscaffoldnamejsx)
- [Example Scaffold Output](#example-scaffold-output)
- [Output directory structure](#output-directory-structure)
- [Contents of `project/scaffold/MyComponent/MyComponent.jsx`](#contents-of-projectscaffoldmycomponentmycomponentjsx)
- [Contributing](#contributing)
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.
</details>
Don't waste any more time manually copying and pasting files - let Simple Scaffold do the heavy
lifting for you and start building your projects faster and more efficiently today!
<div align="center">
![sample animation](media/intro.gif)
</div>
---
## Install
## Quick Start
You can either use it as a command line tool or import into your own code and run from there.
The fastest way to get started is to use `npx` to immediately start a scaffold process.
```bash
# npm
npm install [-g] simple-scaffold
# yarn
yarn [global] add simple-scaffold
# run without installing
npx simple-scaffold@latest <...args>
```
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:
## Use as a command line tool
`templates/component/{{ pascalName name }}.tsx`
### Command Line Options
```tsx
// Created: {{ now | 'yyyy-MM-dd' }}
import React from 'react'
```plaintext
Usage: simple-scaffold [options]
Create structured files based on templates.
Options:
--help|-h Display help information
--name|-n Name to be passed to the generated files. {{name}} and
{{Name}} inside contents and file names will be replaced
accordingly.
--output|-o Path to output to. If --create-sub-folder is enabled,
the subfolder will be created inside this path.
(default: current dir)
--templates|-t Template files to use as input. You may provide multiple
files, each of which can be a relative or absolute path, or a
glob pattern for multiple file matching easily.
--overwrite|-w Enable to override output files, even if they already
exist. (default: false)
--data|-d Add custom data to the templates. By default, only your
app name is included.
--create-sub-folder|-s Create subfolder with the input name
(default: false)
--sub-folder-name-helper|-sh Default helper to apply to subfolder name when using
`--create-sub-folder true`.
--quiet|-q Suppress output logs (Same as --verbose 0)
(default: false)
--verbose|-v Determine amount of logs to display. The values are:
0 (none) | 1 (debug) | 2 (info) | 3 (warn) | 4
(error). The provided level will display messages of
the same level or higher. (default:
2)
--dry-run|-dr Don't emit files. This is good for testing your
scaffolds and making sure they don't fail, without having to
write actual file contents or create directories.
(default: false)
```
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}'"
}
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 Scaffold function when you are ready to start.
The config takes similar arguments to the command line:
```typescript
import Scaffold from "simple-scaffold"
const config = {
name: "component",
templates: [path.join(__dirname, "scaffolds", "component")],
output: path.join(__dirname, "src", "components"),
createSubFolder: true,
subFolderNameHelper: "upperCase"
data: {
property: "value",
},
helpers: {
twice: (text) => [text, text].join(" ")
},
beforeWrite: (content, rawContent, outputPath) => content.toString().toUpperCase()
}
const scaffold = Scaffold(config)
```
### Node-specific options
In addition to all the options available in the command line, there are some Node/JS-specific
options available:
| Option | Type | Description |
| ------------- | -------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `output` | | In addition to being passed the same as CLI, it may also be passed a function for each input file to output into a dynamic path: `{ output: (fullPath, baseDir, baseName) => path.resolve(baseDir, baseName) }` |
| `helpers` | `Record<string, (string) => string>` | Helpers are simple functions that transform your `data` variables into other values. See [Helpers](#helpers) for the list of default helpers, or add your own to be loaded into the template parser. |
| `beforeWrite` | `(content: Buffer, rawContent: Buffer, outputPath: string) => Promise<String \| Buffer \| undefined> \| String \| Buffer \| undefined` | Supply this function to override the final output contents of each of your files. The return value of this function will replace the output content of the respective file, which you may discriminate (if needed) using the `outputPath` argument. |
## Preparing files
### Template files
Put your template files anywhere, and fill them with tokens for replacement.
Each template (not file) in the config array is parsed individually, and copied to the output
directory. If a single template path contains multiple files (e.g. if you use a folder path or a
glob pattern), the first directory up the tree of that template will become the base inside the
defined output path for that template, while copying files recursively and maintaining their
relative structure.
Examples:
> In the following examples, the config `name` is `AppName`, and the config `output` is `src`.
| Input template | Output path(s) |
| ---------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------ |
| `./templates/{{ name }}.txt` | `src/AppName.txt` |
| `./templates/directory` <br /><br /> Directory contents:<br /> <ol><li>`outer/{{name}}.txt`</li></li><li>`outer2/inner/{{name.txt}}`</li></ol> | `src/outer/AppName.txt`,<br />`src/outer2/inner/AppName.txt` |
| `./templates/others/**/*.txt` <br /><br /> Directory contents:<br /> <ol><li>`outer/{{name}}.jpg`</li></li><li>`outer2/inner/{{name.txt}}`</li></ol> | `src/outer2/inner/AppName.txt` |
### Variable/token replacement
Scaffolding will replace `{{ varName }}` in both the file name and its contents and put the
transformed files in the output directory.
The data available for the template parser is the data you pass to the `data` config option (or
`--data` argument in CLI).
For example, using the following command:
```bash
```shell
# generate single component
npx simple-scaffold@latest \
--templates templates/components/{{name}}.jsx \
--output src/components \
-create-sub-folder true \
MyComponent
-t templates/component -o src/components PageWrapper
```
Will output a file with the path:
This will immediately create the following file: `src/components/PageWrapper.tsx`
```plaintext
<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.
### Built-in Helpers
Simple-Scaffold provides some built-in text transformation filters usable by handleBars.
For example, you may use `{{ snakeCase name }}` inside a template file or filename, and it will
replace `My Name` with `my_name` when producing the final value.
#### Capitalization Helpers
| Helper name | Example code | Example output |
| ------------ | ----------------------- | -------------- |
| [None] | `{{ name }}` | my name |
| `camelCase` | `{{ camelCase name }}` | myName |
| `snakeCase` | `{{ snakeCase name }}` | my_name |
| `startCase` | `{{ startCase name }}` | My Name |
| `kebabCase` | `{{ kebabCase name }}` | my-name |
| `hyphenCase` | `{{ hyphenCase name }}` | my-name |
| `pascalCase` | `{{ pascalCase name }}` | MyName |
| `upperCase` | `{{ upperCase name }}` | MY NAME |
| `lowerCase` | `{{ lowerCase name }}` | my name |
#### Date helpers
| Helper name | Description | Example code | Example output |
| -------------------------------- | ---------------------------------------------------------------- | ---------------------------------------------------------------- | ---------------- |
| `now` | Current date with format | `{{ now "yyyy-MM-dd HH:mm" }}` | 2042-01-01 15:00 |
| `now` (with offset) | Current date with format, and with offset | `{{ now "yyyy-MM-dd HH:mm" -1 "hours" }}` | 2042-01-01 14:00 |
| `date` | Custom date with format | `{{ date "2042-01-01T15:00:00Z" "yyyy-MM-dd HH:mm" }}` | 2042-01-01 15:00 |
| `date` (with offset) | Custom date with format, and with offset | `{{ date "2042-01-01T15:00:00Z" "yyyy-MM-dd HH:mm" -1 "days" }}` | 2041-31-12 15:00 |
| `date` (with date from `--data`) | Custom date with format, with data from the `data` config option | `{{ date myCustomDate "yyyy-MM-dd HH:mm" }}` | 2042-01-01 12:00 |
Further details:
- We use [`date-fns`](https://date-fns.org/docs/) for parsing/manipulating the dates.
If you want more information on the date tokens to use, refer to
[their format documentation](https://date-fns.org/docs/format).
- The date helper format takes the following arguments:
```typescript
(
date: string,
format: string,
offsetAmount?: number,
offsetType?: "years" | "months" | "weeks" | "days" | "hours" | "minutes" | "seconds"
)
```
- **The now helper** (for current time) takes the same arguments, minus the first one (`date`) as
it is implicitly the current date.
### Custom Helpers
You may also add your own custom helpers using the `helpers` options when using the JS API (rather
than the CLI). The `helpers` option takes an object whose keys are helper names, and values are
the transformation functions. For example, `upperCase` is implemented like so:
```typescript
config.helpers = {
upperCase: (text) => text.toUpperCase(),
}
```
All of the above helpers (built in and custom) will also be available to you when using
`subFolderNameHelper` (`--sub-folder-name-helper`/`-sh`) as a possible value.
## Examples
### Command Example
```bash
simple-scaffold MyComponent \
-t project/scaffold/**/* \
-o src/components \
-d '{"className": "myClassName"}'
MyComponent
```
### Example Scaffold Input
#### Input Directory structure
```plaintext
- project
- scaffold
- {{Name}}.js
- src
- components
- ...
```
#### Contents of `project/scaffold/{{Name}}.jsx`
```typescriptreact
```tsx
// Created: 2077/01/01
import React from 'react'
export default {{camelCase ame}}: React.FC = (props) => {
export default PageWrapper: React.FC = (props) => {
return (
<div className="{{className}}">{{camelCase name}} Component</div>
<div className="pageWrapper">PageWrapper Component</div>
)
}
```
### Example Scaffold Output
## Documentation
### Output directory structure
See full documentation [here](https://chenasraf.github.io/simple-scaffold).
```plaintext
- project
- src
- components
- MyComponent
- MyComponent.js
- ...
```
With `createSubFolder = false`:
```plaintext
- project
- src
- components
- MyComponent.js
- ...
```
#### Contents of `project/scaffold/MyComponent/MyComponent.jsx`
```typescriptreact
import React from 'react'
export default MyComponent: React.FC = (props) => {
return (
<div className="myClassName">MyComponent Component</div>
)
}
```
- [Command Line Interface (CLI) usage](https://chenasraf.github.io/simple-scaffold/pages/cli.html)
- [Node.js usage](https://chenasraf.github.io/simple-scaffold/pages/node.html)
- [Templates](https://chenasraf.github.io/simple-scaffold/pages/templates.html)
- [Configuration Files](https://chenasraf.github.io/simple-scaffold/pages/configuration_files.html)
- [Migrating v0.x to v1.x](https://chenasraf.github.io/simple-scaffold/pages/migration.html)
## Contributing
I am developing this package on my free time, so any support, whether code, issues, or just stars
is very helpful to sustaining its life. If you would like to donate a bit to help keep the project
alive, I would be very thankful!
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!
<a href='https://ko-fi.com/casraf' target='_blank'>
<img height='36' style='border:0px;height:36px;'
@@ -380,11 +111,20 @@ Some tips on getting around the code:
- Use `yarn dev` for development - it runs TypeScript compile in watch mode, allowing you to make
changes and immediately be able to try them using `yarn cmd`.
- Use `yarn build` to build the output
- 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.
- Use `yarn cmd` to use the CLI feature of Simple Scaffold from within the root directory, enabling
you to test different behaviors. See `yarn cmd -h` for more information.
> This requires an updated build, and does not trigger one itself. Either use `yarn dev` to watch
> for changes and build, or `yarn build` before running this, or use `yarn build-cmd` instead,
> which triggers a build right before running the command with the rest of the given arguments.
> This requires an updated build, and does not trigger one itself. From here you have several
> options:
>
> - Run `yarn dev` to watch for file changes and build automatically
> - Run `yarn build` before running this to trigger a one-time build
> - Run `yarn build-cmd` which triggers a build right before running `yarn cmd` automatically with
> the rest of the given arguments.
- Use `yarn build-docs` to build the documentation once
- Use `yarn watch-docs` to start docs in watch mode
- To see the documentation, currently you have to serve the directory yourself with a static web
server (like node's built in serve, VS code's "Go Live" mode, etc)

View File

@@ -1,3 +0,0 @@
module.exports = {
plugins: ["@babel/plugin-proposal-nullish-coalescing-operator"],
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

View 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" },
},
}

View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 385 KiB

View File

@@ -1,7 +1,8 @@
{
"name": "simple-scaffold",
"version": "1.1.0-alpha.6",
"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",
@@ -23,31 +24,44 @@
"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": {
"chalk": "^4.1.2",
"date-fns": "^2.28.0",
"glob": "^7.1.3",
"date-fns": "^2.29.3",
"glob": "^9.2.1",
"handlebars": "^4.7.7",
"lodash": "^4.17.21",
"massarg": "^1.0.5",
"massarg": "^1.0.7-pre.1",
"semantic-release-conventional-commits": "^3.0.0",
"util.promisify": "^1.1.1"
},
"devDependencies": {
"@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",
"rimraf": "^3.0.2",
"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
View 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
View 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}'"
}
}
```

View 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
```

View File

@@ -1,16 +1,20 @@
# Migrating from 0.x to 1.0
In Simple Scaffold v1.0, the entire codebase was overhauled, yet usage remains mostly the same
between versions. With these notable exceptions:
- Some of the argument names have changed
- Template syntax has been improved
- The command to run Scaffold has been simplified from `new SimpleScaffold(opts).run()` to `SimpleScaffold(opts)`, which now returns a promise that you can await to know when the process has been completed.
- 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`.
- `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

53
pages/node.md Normal file
View 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
View 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
View 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",
{
pkgRoot: "dist",
},
],
[
"@semantic-release/github",
{
assets: ["package.tgz"],
},
],
[
"@semantic-release/git",
{
assets: ["CHANGELOG.md", "package.json"],
},
],
],
}

View File

@@ -3,11 +3,20 @@ import massarg from "massarg"
import chalk from "chalk"
import { LogLevel, ScaffoldCmdConfig } from "./types"
import { Scaffold } from "./scaffold"
import path from "path"
import fs from "fs/promises"
import { parseAppendData, parseConfig } from "./utils"
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")
export function parseCliArgs(args = process.argv.slice(2)) {
return (
massarg<ScaffoldCmdConfig & { help: boolean; extras: string[] }>()
.main(Scaffold)
massarg<ScaffoldCmdConfig>()
.main((config) => {
const _config = parseConfig(config)
return Scaffold(_config)
})
.option({
name: "name",
aliases: ["n"],
@@ -16,11 +25,17 @@ export function parseCliArgs(args = process.argv.slice(2)) {
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: true,
required: !isConfig,
})
.option({
name: "templates",
@@ -29,7 +44,7 @@ export function parseCliArgs(args = process.argv.slice(2)) {
description:
"Template files to use as input. You may provide multiple files, each of which can be a relative or absolute path, " +
"or a glob pattern for multiple file matching easily.",
required: true,
required: !isConfig,
})
.option({
name: "overwrite",
@@ -44,6 +59,13 @@ export function parseCliArgs(args = process.argv.slice(2)) {
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"],
@@ -67,7 +89,10 @@ export function parseCliArgs(args = process.argv.slice(2)) {
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.`,
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({
@@ -88,9 +113,12 @@ export function parseCliArgs(args = process.argv.slice(2)) {
binName: "simple-scaffold",
useGlobalColumns: true,
usageExample: "[options]",
header: "Create structured files based on templates.",
header: [`Create structured files based on templates.`].join("\n"),
footer: [
`Copyright © Chen Asraf 2021`,
`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"),

55
src/docs.css Normal file
View 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;
}

View File

@@ -1,3 +1,9 @@
/**
* @module
* Simple Scaffold
*
* See [readme](README.md)
*/
import path from "path"
import {
@@ -31,46 +37,47 @@ import { LogLevel, ScaffoldConfig } from "./types"
* 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. Common cases are transforming name-case format
* (e.g. `MyName` &rarr; `my_name`), so these have been provided as defaults:
*
* | Helper name | Example code | Example output |
* | ----------- | ----------------------- | -------------- |
* | camelCase | `{{ camelCase name }}` | myName |
* | snakeCase | `{{ snakeCase name }}` | my_name |
* | startCase | `{{ startCase name }}` | My Name |
* | kebabCase | `{{ kebabCase name }}` | my-name |
* | hyphenCase | `{{ hyphenCase name }}` | my-name |
* | pascalCase | `{{ pascalCase name }}` | MyName |
* | upperCase | `{{ upperCase name }}` | MYNAME |
* | lowerCase | `{{ lowerCase name }}` | myname |
* 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({ ...options }: ScaffoldConfig): Promise<void> {
options.output ??= process.cwd()
export async function Scaffold(config: ScaffoldConfig): Promise<void> {
config.output ??= process.cwd()
registerHelpers(options)
registerHelpers(config)
try {
options.data = { name: options.name, Name: pascalCase(options.name), ...options.data }
logInitStep(options)
for (let _template of options.templates) {
config.data = { name: config.name, Name: pascalCase(config.name), ...config.data }
logInitStep(config)
for (let _template of config.templates) {
try {
const { nonGlobTemplate, origTemplate, isDirOrGlob, isGlob, template } = await getTemplateGlobInfo(
options,
_template
config,
_template,
)
const files = await getFileList(options, 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(options, {
logInputFile(config, {
origTemplate,
relPath,
template,
@@ -80,7 +87,7 @@ export async function Scaffold({ ...options }: ScaffoldConfig): Promise<void> {
isDirOrGlob,
isGlob,
})
await handleTemplateFile(options, options.data, {
await handleTemplateFile(config, {
templatePath: inputFilePath,
basePath,
})
@@ -90,25 +97,24 @@ export async function Scaffold({ ...options }: ScaffoldConfig): Promise<void> {
}
}
} catch (e: any) {
log(options, LogLevel.Error, e)
log(config, LogLevel.Error, e)
throw e
}
}
async function handleTemplateFile(
options: ScaffoldConfig,
data: Record<string, string>,
{ templatePath, basePath }: { templatePath: string; basePath: string }
config: ScaffoldConfig,
{ templatePath, basePath }: { templatePath: string; basePath: string },
): Promise<void> {
return new Promise(async (resolve, reject) => {
try {
const { inputPath, outputPathOpt, outputDir, outputPath, exists } = await getTemplateFileInfo(options, data, {
const { inputPath, outputPathOpt, outputDir, outputPath, exists } = await getTemplateFileInfo(config, {
templatePath,
basePath,
})
const overwrite = getOptionValueForFile(options, inputPath, options.overwrite ?? false)
const overwrite = getOptionValueForFile(config, inputPath, config.overwrite ?? false)
log(
options,
config,
LogLevel.Debug,
`\nParsing ${templatePath}`,
`\nBase path: ${basePath}`,
@@ -116,13 +122,13 @@ async function handleTemplateFile(
`\nOutput Path Opt: ${outputPathOpt}`,
`\nFull output dir: ${outputDir}`,
`\nFull output path: ${outputPath}`,
`\n`
`\n`,
)
await createDirIfNotExists(path.dirname(outputPath), options)
await createDirIfNotExists(path.dirname(outputPath), config)
log(options, LogLevel.Info, `Writing to ${outputPath}`)
await copyFileTransformed(options, { exists, overwrite, outputPath, inputPath })
log(config, LogLevel.Info, `Writing to ${outputPath}`)
await copyFileTransformed(config, { exists, overwrite, outputPath, inputPath })
resolve()
} catch (e: any) {
handleErr(e)

View File

@@ -1,33 +1,15 @@
import { HelperDelegate } from "handlebars/runtime"
export enum LogLevel {
None = 0,
Debug = 1,
Info = 2,
Warning = 3,
Error = 4,
}
export type FileResponseFn<T> = (fullPath: string, basedir: string, basename: string) => T
export type FileResponse<T> = T | FileResponseFn<T>
export type DefaultHelperKeys =
| "camelCase"
| "date"
| "hyphenCase"
| "kebabCase"
| "lowerCase"
| "now"
| "pascalCase"
| "snakeCase"
| "startCase"
| "upperCase"
export type HelperKeys<T> = DefaultHelperKeys | T
export type Helper = HelperDelegate
/**
* 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 {
/**
* Name to be passed to the generated files. `{{name}}` and `{{Name}}` inside contents and file names will be replaced
@@ -37,46 +19,77 @@ export interface ScaffoldConfig {
/**
* Template files to use as input. You may provide multiple files, each of which can be a relative or absolute path,
* or a glob pattern for multiple file matching easily. (default: current working directory)
* 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. */
/**
* 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>
/**
* Create subfolder with the input name (default: `false`)
* 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
/**
* 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, string>
data?: Record<string, any>
/**
* Enable to override output files, even if they already exist. (default: `false`)
* 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`) */
/**
* 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. (default: `2 (info)`)
* @see LogLevel
* 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`)
* actual file contents or create directories.
*
* @default `false`
*/
dryRun?: boolean
@@ -84,7 +97,7 @@ export interface ScaffoldConfig {
* Additional helpers to add to the template parser. Provide an object whose keys are the name of the function to add,
* and the value is the helper function itself. The signature of helpers is as follows:
* ```typescript
* (text: string) => string
* (text: string, ...args: any[]) => string
* ```
*
* A full example might be:
@@ -93,30 +106,47 @@ export interface ScaffoldConfig {
* Scaffold({
* //...
* helpers: {
* upperCamelCase: (text) => camelCase(text).toUpperCase()
* upperKebabCase: (text) => kebabCase(text).toUpperCase()
* }
* })
* ```
*
* Here are the built-in helpers available for use:
* | Helper name | Example code | Example output |
* | ----------- | ----------------------- | -------------- |
* | camelCase | `{{ camelCase name }}` | myName |
* | snakeCase | `{{ snakeCase name }}` | my_name |
* | startCase | `{{ startCase name }}` | My Name |
* | kebabCase | `{{ kebabCase name }}` | my-name |
* | hyphenCase | `{{ hyphenCase name }}` | my-name |
* | pascalCase | `{{ pascalCase name }}` | MyName |
* | upperCase | `{{ upperCase name }}` | MYNAME |
* | lowerCase | `{{ lowerCase name }}` | myname |
* 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
* helpers, or a custom one you provide to `helpers`. Defaults to `undefined`, which means no transformation is done.
* 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?: DefaultHelperKeys | string
subFolderNameHelper?: DefaultHelpers | string
/**
* This callback runs right before content is being written to the disk. If you supply this function, you may return
@@ -129,23 +159,184 @@ export interface ScaffoldConfig {
* @param rawContent The original template before token replacement
* @param outputPath The final output path of the processed file
*
* @returns `String | Buffer | undefined` The final output of the file contents-only, after further modifications -
* or `undefined` to use the original content (i.e. `content.toString()`)
* @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
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>

View File

@@ -1,6 +1,15 @@
import path from "path"
import { F_OK } from "constants"
import { DefaultHelperKeys, FileResponse, FileResponseFn, Helper, LogLevel, 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"
@@ -12,6 +21,8 @@ const { stat, access, mkdir } = fsPromises
import dtAdd from "date-fns/add"
import dtFormat from "date-fns/format"
import dtParseISO from "date-fns/parseISO"
import { glob, hasMagic } from "glob"
import { OptionsBase } from "massarg/types"
const dateFns = {
add: dtAdd,
@@ -19,11 +30,9 @@ const dateFns = {
parseISO: dtParseISO,
}
import { glob } from "glob"
import { promisify } from "util"
const { readFile, writeFile } = fsPromises
export const defaultHelpers: Record<DefaultHelperKeys, Helper> = {
export const defaultHelpers: Record<DefaultHelpers, Helper> = {
camelCase,
snakeCase,
startCase,
@@ -41,13 +50,13 @@ export function _dateHelper(
date: Date,
formatString: string,
durationDifference: number,
durationType: keyof Duration
durationType: keyof Duration,
): string
export function _dateHelper(
date: Date,
formatString: string,
durationDifference?: number,
durationType?: keyof Duration
durationType?: keyof Duration,
): string {
if (durationType && durationDifference !== undefined) {
return dateFns.format(dateFns.add(date, { [durationType]: durationDifference }), formatString)
@@ -66,22 +75,22 @@ export function dateHelper(
date: string,
formatString: string,
durationDifference: number,
durationType: keyof Duration
durationType: keyof Duration,
): string
export function dateHelper(
date: string,
formatString: string,
durationDifference?: number,
durationType?: keyof Duration
durationType?: keyof Duration,
): string {
return _dateHelper(dateFns.parseISO(date), formatString, durationDifference!, durationType!)
}
export function registerHelpers(options: ScaffoldConfig): void {
const _helpers = { ...defaultHelpers, ...options.helpers }
export function registerHelpers(config: ScaffoldConfig): void {
const _helpers = { ...defaultHelpers, ...config.helpers }
for (const helperName in _helpers) {
log(options, LogLevel.Debug, `Registering helper: ${helperName}`)
log(config, LogLevel.Debug, `Registering helper: ${helperName}`)
Handlebars.registerHelper(helperName, _helpers[helperName as keyof typeof _helpers])
}
}
@@ -90,8 +99,8 @@ export function handleErr(err: NodeJS.ErrnoException | null): void {
if (err) throw err
}
export function log(options: ScaffoldConfig, level: LogLevel, ...obj: any[]): void {
if (options.quiet || options.verbose === LogLevel.None || level < (options.verbose ?? LogLevel.Info)) {
export function log(config: ScaffoldConfig, level: LogLevel, ...obj: any[]): void {
if (config.quiet || config.verbose === LogLevel.None || level < (config.verbose ?? LogLevel.Info)) {
return
}
const levelColor: Record<LogLevel, keyof typeof chalk> = {
@@ -110,21 +119,21 @@ export function log(options: ScaffoldConfig, level: LogLevel, ...obj: any[]): vo
? chalkFn(i, JSON.stringify(i, undefined, 1), i.stack)
: typeof i === "object"
? chalkFn(JSON.stringify(i, undefined, 1))
: chalkFn(i)
)
: 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(options, LogLevel.Debug, `Creating dir ${dir}`)
log(config, LogLevel.Debug, `Creating dir ${dir}`)
await mkdir(dir)
return
} catch (e: any) {
@@ -137,27 +146,27 @@ export async function createDirIfNotExists(dir: string, options: ScaffoldConfig)
}
export function getOptionValueForFile<T>(
options: ScaffoldConfig,
config: ScaffoldConfig,
filePath: 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(options, filePath, { isPath: true }).toString()),
path.basename(handlebarsParse(options, filePath, { isPath: true }).toString())
path.dirname(handlebarsParse(config, filePath, { isPath: true }).toString()),
path.basename(handlebarsParse(config, filePath, { isPath: true }).toString()),
)
}
export function handlebarsParse(
options: ScaffoldConfig,
config: ScaffoldConfig,
templateBuffer: Buffer | string,
{ isPath = false }: { isPath?: boolean } = {}
{ isPath = false }: { isPath?: boolean } = {},
): Buffer {
const { data } = options
const { data } = config
try {
let str = templateBuffer.toString()
if (isPath) {
@@ -170,8 +179,8 @@ export function handlebarsParse(
}
return Buffer.from(outputContents)
} catch (e) {
log(options, LogLevel.Debug, e)
log(options, LogLevel.Warning, "Couldn't parse file with handlebars, returning original content")
log(config, LogLevel.Debug, e)
log(config, LogLevel.Warning, "Couldn't parse file with handlebars, returning original content")
return Buffer.from(templateBuffer)
}
}
@@ -212,12 +221,12 @@ export function getBasePath(relPath: string): string {
.replace(process.cwd(), "")
}
export async function getFileList(options: ScaffoldConfig, template: string): Promise<string[]> {
export async function getFileList(config: ScaffoldConfig, template: string): Promise<string[]> {
return (
await promisify(glob)(template, {
await glob(template, {
dot: true,
debug: options.verbose === LogLevel.Debug,
nodir: true,
// debug: config.verbose === LogLevel.Debug,
})
).map((f) => f.replace(/\//g, path.sep))
}
@@ -230,13 +239,14 @@ export interface GlobInfo {
template: string
}
export async function getTemplateGlobInfo(options: ScaffoldConfig, template: string): Promise<GlobInfo> {
const isGlob = glob.hasMagic(template)
log(options, LogLevel.Debug, "before isDir", "isGlob:", isGlob, template)
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
const nonGlobTemplate = isGlob ? removeGlob(template) : template
let nonGlobTemplate = isGlob ? removeGlob(template) : template
nonGlobTemplate = path.normalize(nonGlobTemplate)
const isDirOrGlob = isGlob ? true : await isDir(template)
log(options, LogLevel.Debug, "after isDir", isDirOrGlob)
log(config, LogLevel.Debug, "after isDir", isDirOrGlob)
const _shouldAddGlob = !isGlob && isDirOrGlob
const origTemplate = template
if (_shouldAddGlob) {
@@ -254,14 +264,13 @@ export interface OutputFileInfo {
}
export async function getTemplateFileInfo(
options: ScaffoldConfig,
data: Record<string, string>,
{ templatePath, basePath }: { templatePath: string; basePath: string }
config: ScaffoldConfig,
{ templatePath, basePath }: { templatePath: string; basePath: string },
): Promise<OutputFileInfo> {
const inputPath = path.resolve(process.cwd(), templatePath)
const outputPathOpt = getOptionValueForFile(options, inputPath, options.output)
const outputDir = getOutputDir(options, outputPathOpt, basePath)
const outputPath = handlebarsParse(options, path.join(outputDir, path.basename(inputPath)), {
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)
@@ -269,7 +278,7 @@ export async function getTemplateFileInfo(
}
export async function copyFileTransformed(
options: ScaffoldConfig,
config: ScaffoldConfig,
{
exists,
overwrite,
@@ -280,47 +289,46 @@ export async function copyFileTransformed(
overwrite: boolean
outputPath: string
inputPath: string
}
},
): Promise<void> {
if (!exists || overwrite) {
if (exists && overwrite) {
log(options, LogLevel.Info, `File ${outputPath} exists, overwriting`)
log(config, LogLevel.Info, `File ${outputPath} exists, overwriting`)
}
const templateBuffer = await readFile(inputPath)
const unprocessedOutputContents = handlebarsParse(options, templateBuffer)
const finalOutputContents = (
(await options.beforeWrite?.(unprocessedOutputContents, templateBuffer, outputPath)) ?? unprocessedOutputContents
).toString()
const unprocessedOutputContents = handlebarsParse(config, templateBuffer)
const finalOutputContents =
(await config.beforeWrite?.(unprocessedOutputContents, templateBuffer, outputPath)) ?? unprocessedOutputContents
if (!options.dryRun) {
if (!config.dryRun) {
await writeFile(outputPath, finalOutputContents)
log(options, LogLevel.Info, "Done.")
log(config, LogLevel.Info, "Done.")
} else {
log(options, LogLevel.Info, "Content output:")
log(options, LogLevel.Info, finalOutputContents)
log(config, LogLevel.Info, "Content output:")
log(config, LogLevel.Info, finalOutputContents)
}
} else if (exists) {
log(options, LogLevel.Info, `File ${outputPath} already exists, skipping`)
log(config, LogLevel.Info, `File ${outputPath} already exists, skipping`)
}
}
export function getOutputDir(options: ScaffoldConfig, outputPathOpt: string, basePath: string): string {
export function getOutputDir(config: ScaffoldConfig, outputPathOpt: string, basePath: string): string {
return path.resolve(
process.cwd(),
...([
outputPathOpt,
basePath,
options.createSubFolder
? options.subFolderNameHelper
? handlebarsParse(options, `{{ ${options.subFolderNameHelper} name }}`).toString()
: options.name
config.createSubFolder
? config.subFolderNameHelper
? handlebarsParse(config, `{{ ${config.subFolderNameHelper} name }}`).toString()
: config.name
: undefined,
].filter(Boolean) as string[])
].filter(Boolean) as string[]),
)
}
export function logInputFile(
options: ScaffoldConfig,
config: ScaffoldConfig,
{
origTemplate,
relPath,
@@ -339,10 +347,10 @@ export function logInputFile(
basePath: string
isDirOrGlob: boolean
isGlob: boolean
}
},
): void {
log(
options,
config,
LogLevel.Debug,
`\nprocess.cwd(): ${process.cwd()}`,
`\norigTemplate: ${origTemplate}`,
@@ -353,24 +361,63 @@ export function logInputFile(
`\nbasePath: ${basePath}`,
`\nisDirOrGlob: ${isDirOrGlob}`,
`\nisGlob: ${isGlob}`,
`\n`
`\n`,
)
}
export function logInitStep(options: ScaffoldConfig): void {
log(options, LogLevel.Debug, "Full config:", {
name: options.name,
templates: options.templates,
output: options.output,
createSubfolder: options.createSubFolder,
data: options.data,
overwrite: options.overwrite,
quiet: options.quiet,
subFolderTransformHelper: options.subFolderNameHelper,
helpers: Object.keys(options.helpers ?? {}),
verbose: `${options.verbose} (${Object.keys(LogLevel).find(
(k) => (LogLevel[k as any] as unknown as number) === options.verbose!
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(options, LogLevel.Info, "Data:", options.data)
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
}

View File

@@ -6,6 +6,7 @@ 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: {
@@ -13,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: {
@@ -45,7 +53,7 @@ const fileStructHelpers = {
input: {
defaults: defaultHelperNames.reduce<Record<string, string>>(
(all, cur) => ({ ...all, [cur + ".txt"]: `{{ ${cur} name }}` }),
{}
{},
),
custom: {
"add1.txt": "{{ add1 name }}",
@@ -109,7 +117,25 @@ describe("Scaffold", () => {
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(
@@ -157,7 +183,7 @@ describe("Scaffold", () => {
const data = readFileSync(join(process.cwd(), "output", "app_name.txt"))
expect(data.toString()).toEqual("Hello, my value is 2")
})
})
}),
)
describe(
@@ -180,7 +206,7 @@ describe("Scaffold", () => {
templates: ["non-existing-input"],
data: { value: "1" },
verbose: 0,
})
}),
).rejects.toThrow()
await expect(
@@ -190,12 +216,12 @@ describe("Scaffold", () => {
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(
@@ -222,7 +248,7 @@ describe("Scaffold", () => {
expect(() => readFileSync(join(process.cwd(), "output", "app_name.txt"))).toThrow()
})
})
}),
)
describe(
@@ -239,7 +265,7 @@ describe("Scaffold", () => {
const data = readFileSync(join(process.cwd(), "/custom-output/app_name/app_name.txt"))
expect(data.toString()).toEqual("Hello, my app is app_name")
})
})
}),
)
describe(
@@ -268,7 +294,7 @@ describe("Scaffold", () => {
expect(oneDeepFile.toString()).toEqual("Hello, my value is 1")
expect(twoDeepFile.toString()).toEqual("Hi! My value is actually NOT 1!")
})
})
}),
)
describe(
@@ -302,7 +328,7 @@ describe("Scaffold", () => {
expect(file.toString()).toEqual(results[key as keyof typeof results])
}
})
})
}),
)
describe(
"date helpers",
@@ -329,19 +355,19 @@ describe("Scaffold", () => {
// "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")}`
`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")}`
`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"
)}`
"HH:mm",
)}`,
)
})
})
}),
)
describe(
"custom helpers",
@@ -366,7 +392,7 @@ describe("Scaffold", () => {
expect(file.toString()).toEqual(results[key as keyof typeof results])
}
})
})
}),
)
describe(
"transform subfolder",
@@ -414,7 +440,7 @@ describe("Scaffold", () => {
const data = readFileSync(join(process.cwd(), "output", "REPLACED", "app_name.txt"))
expect(data.toString()).toEqual("Hello, my app is app_name")
})
})
}),
)
describe(
"before write",
@@ -453,7 +479,7 @@ describe("Scaffold", () => {
"Hello, my app is app_name".toUpperCase(),
fileStructNormal.input["{{name}}.txt"],
join(process.cwd(), "output", "app_name.txt"),
].join(", ")
].join(", "),
)
})
test("should work with undefined response custom callback", async () => {
@@ -471,6 +497,6 @@ describe("Scaffold", () => {
const data = readFileSync(join(process.cwd(), "output", "app_name.txt"))
expect(data.toString()).toEqual("Hello, my app is app_name")
})
})
}),
)
})

View File

@@ -1,7 +1,8 @@
import { dateHelper, handlebarsParse, nowHelper } from "../src/utils"
import { ScaffoldConfig } from "../src/types"
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,
@@ -11,6 +12,20 @@ const blankConf: ScaffoldConfig = {
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
@@ -24,7 +39,7 @@ describe("Utils", () => {
})
test("should work for windows paths", async () => {
expect(handlebarsParse(blankConf, "C:\\exports\\{{name}}.txt", { isPath: true })).toEqual(
Buffer.from("C:\\exports\\test.txt")
Buffer.from("C:\\exports\\test.txt"),
)
})
})
@@ -38,7 +53,7 @@ describe("Utils", () => {
})
test("should work for non-windows paths", async () => {
expect(handlebarsParse(blankConf, "/home/test/{{name}}.txt", { isPath: true })).toEqual(
Buffer.from("/home/test/test.txt")
Buffer.from("/home/test/test.txt"),
)
})
})
@@ -49,8 +64,8 @@ describe("Utils", () => {
"/home/test/{{name}} \\{{escaped}}.txt",
{
isPath: false,
}
)
},
),
).toEqual(Buffer.from("/home/test/test {{escaped}}.txt"))
})
})
@@ -79,13 +94,31 @@ describe("Utils", () => {
const fmt = "yyyy-MM-dd HH:mm"
expect(dateHelper(now.toISOString(), fmt, -1, "days")).toEqual(
dateFns.format(dateFns.add(now, { days: -1 }), fmt)
dateFns.format(dateFns.add(now, { days: -1 }), fmt),
)
expect(dateHelper(now.toISOString(), fmt, 1, "months")).toEqual(
dateFns.format(dateFns.add(now, { months: 1 }), fmt)
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" })
})
})
})

View File

@@ -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
View 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",
},
],
},
}

5563
yarn.lock

File diff suppressed because it is too large Load Diff