Compare commits

...

156 Commits

Author SHA1 Message Date
semantic-release-bot
0940e843ac chore(release): 1.6.0 [skip ci]
## [1.6.0](https://github.com/chenasraf/simple-scaffold/compare/v1.5.0...v1.6.0) (2023-05-05)

### Features

* node.js function for remote configs ([ce5adbe](ce5adbe0f8))

### Bug Fixes

* move dependency to dev dependency ([d916d88](d916d88384))
2023-05-05 22:33:58 +00:00
Chen Asraf
a880bc9445 Merge pull request #56 from chenasraf/develop
Release 1.6.0
2023-05-06 01:32:25 +03:00
Chen Asraf
6c8eb02cbb build: fix package.json version update 2023-05-04 23:21:16 +03:00
Chen Asraf
08b048845f build: use pnpm instead of yarn 2023-05-04 23:00:24 +03:00
Chen Asraf
77e477e07f chore(release): bump version number 2023-05-04 22:16:48 +03:00
Chen Asraf
5ba6034b2f docs: fix changelog title 2023-05-04 22:12:35 +03:00
Chen Asraf
95dafdf839 docs: update docs 2023-05-04 22:08:39 +03:00
semantic-release-bot
4c3f1c97d1 chore(release): 1.6.0-develop.1 [skip ci]
## [1.6.0-develop.1](https://github.com/chenasraf/simple-scaffold/compare/v1.5.0...v1.6.0-develop.1) (2023-05-04)

### Features

* node.js function for remote configs ([ce5adbe](ce5adbe0f8))

### Bug Fixes

* move dependency to dev dependency ([d916d88](d916d88384))
2023-05-04 19:04:57 +00:00
Chen Asraf
1799971949 docs: update package description 2023-05-04 22:03:08 +03:00
semantic-release-bot
4bf96742b3 chore(release): 1.5.0-develop.3 [skip ci]
## [1.5.0-develop.3](https://github.com/chenasraf/simple-scaffold/compare/v1.5.0-develop.2...v1.5.0-develop.3) (2023-05-04)

### Features

* node.js function for remote configs ([7e9022f](7e9022f433))
2023-05-04 22:00:38 +03:00
Chen Asraf
ce5adbe0f8 feat: node.js function for remote configs 2023-05-04 22:00:38 +03:00
Chen Asraf
9489f14db2 build: separate test & build 2023-05-04 22:00:38 +03:00
Chen Asraf
99318f7654 build: remove unnecessary dependency 2023-05-04 22:00:38 +03:00
semantic-release-bot
81743f1fff chore(release): 1.5.0-develop.2 [skip ci]
## [1.5.0-develop.2](https://github.com/chenasraf/simple-scaffold/compare/v1.5.0-develop.1...v1.5.0-develop.2) (2023-05-03)

### Bug Fixes

* move dependency to dev dependency ([408a940](408a940853))
2023-05-04 22:00:38 +03:00
Chen Asraf
d916d88384 fix: move dependency to dev dependency 2023-05-04 22:00:07 +03:00
Chen Asraf
260ce6a8f3 docs: update docs 2023-05-04 22:00:07 +03:00
Chen Asraf
ba984b6621 docs: fix link 2023-05-04 22:00:07 +03:00
Chen Asraf
7b6260caf8 docs: update docs 2023-05-04 22:00:07 +03:00
Chen Asraf
2ca91a7109 docs: fix cli.md page 2023-05-04 22:00:07 +03:00
semantic-release-bot
7be79fdcaf chore(release): 1.5.0 [skip ci]
## [1.5.0](https://github.com/chenasraf/simple-scaffold/compare/v1.4.0...v1.5.0) (2023-05-02)

### Features

* add github remote templates ([f961c13](f961c13da1))
* support for remote template configs ([05487f4](05487f4d1e))
2023-05-02 07:28:17 +00:00
Chen Asraf
6afbcdc258 Merge pull request #55 from chenasraf/develop
v1.5.0
2023-05-02 10:27:11 +03:00
Chen Asraf
1a3fd3d610 docs: update help text 2023-05-02 10:23:50 +03:00
Chen Asraf
7f10db0d6e chore: bump version number 2023-05-02 10:16:47 +03:00
semantic-release-bot
93f5b4a004 chore(release): 1.5.0-develop.1 [skip ci]
## [1.5.0-develop.1](https://github.com/chenasraf/simple-scaffold/compare/v1.4.0...v1.5.0-develop.1) (2023-05-02)

### Features

* add github remote templates ([f961c13](f961c13da1))
* support for remote template configs ([05487f4](05487f4d1e))
2023-05-02 07:12:11 +00:00
Chen Asraf
b74411ddb8 docs: add docs for remote templates 2023-05-02 10:10:50 +03:00
Chen Asraf
f961c13da1 feat: add github remote templates 2023-05-02 09:46:53 +03:00
Chen Asraf
05487f4d1e feat: support for remote template configs 2023-05-02 09:33:54 +03:00
Chen Asraf
c50518a19c build: fix git/github step order 2023-04-29 17:17:15 +03:00
Chen Asraf
10ea6b4132 chore: fix package version 2023-04-29 17:11:18 +03:00
Chen Asraf
ce399181ab build: always build docs 2023-04-29 17:05:50 +03:00
semantic-release-bot
83d38073f3 chore(release): 1.4.0 [skip ci]
## [1.4.0](https://github.com/chenasraf/simple-scaffold/compare/v1.3.2...v1.4.0) (2023-04-28)

### Features

* add `--key` | `-k` to config loader ([6c5ba0b](6c5ba0bc91))
2023-04-28 23:01:48 +00:00
Chen Asraf
6c5ba0bc91 feat: add --key | -k to config loader 2023-04-29 01:59:39 +03:00
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
Chen Asraf
b2799d0bad chore: use rimraf + add error debug log [skip ci] 2022-04-10 22:53:44 +03:00
Chen Asraf
f4997c6289 docs: Update README.md [skip ci] 2022-04-10 22:53:31 +03:00
Chen Asraf
fe871eb97e Merge pull request #28 from chenasraf/dependabot/npm_and_yarn/minimist-1.2.6
Security fix: Bump minimist from 1.2.5 to 1.2.6
2022-04-10 14:51:02 +03:00
Chen Asraf
dffa81fde1 bump version number 2022-04-10 14:47:06 +03:00
dependabot[bot]
56be5f32cd Bump minimist from 1.2.5 to 1.2.6
Bumps [minimist](https://github.com/substack/minimist) from 1.2.5 to 1.2.6.
- [Release notes](https://github.com/substack/minimist/releases)
- [Commits](https://github.com/substack/minimist/compare/1.2.5...1.2.6)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-04-10 02:49:52 +00:00
Chen Asraf
4a4e024aec Add keywords to package.json [skip ci] 2022-03-15 11:42:53 +02:00
Chen Asraf
86a7a2c063 Update README.md [skip ci] 2022-03-15 11:40:55 +02:00
Chen Asraf
d3259c44aa Update README.md [skip ci] 2022-03-13 11:22:44 +02:00
Chen Asraf
e26a434dba update README.md 2022-03-03 21:53:50 +02:00
Chen Asraf
1783ddf230 remove unnecessary package [skip ci] 2022-03-03 21:53:08 +02:00
Chen Asraf
4575f73d69 Windows compatibility
Fix transform of windows-style paths
2022-03-03 21:49:44 +02:00
Chen Asraf
f07df79e82 improved test 2022-03-03 21:48:13 +02:00
Chen Asraf
cb6e06f1c9 bump version number 2022-03-03 21:43:55 +02:00
Chen Asraf
a043a05bc9 updated tests 2022-03-03 21:42:31 +02:00
Chen Asraf
52cb3e7353 fixed more windows paths, updated tests 2022-03-03 12:25:39 +02:00
Chen Asraf
89d7897f4e refactor handlebarsParse - remove redundant arg 2022-03-03 10:31:30 +02:00
Chen Asraf
d6e1693074 import/file cleanup 2022-03-03 10:25:44 +02:00
Chen Asraf
56f1340093 fix transform of windows-style paths 2022-03-03 10:21:37 +02:00
Chen Asraf
8782f18a73 v1.0.1
Merge pull request #24 from chenasraf/alpha
2022-02-20 14:10:59 +02:00
Chen Asraf
21c4ab6e1a bump version number [skip ci] 2022-02-20 14:06:56 +02:00
42 changed files with 7154 additions and 3685 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"

47
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,47 @@
---
name: Bug report
about: Create a report to help us improve
title: "[BUG] "
labels: bug, needs-triage
assignees: chenasraf
---
#### Describe the bug
A clear and concise description of what the bug is.
#### How To Reproduce
Steps to reproduce the behavior:
1. Prepare templates:
```text
This is my {{ template }}
```
2. Run with args/config:
```shell
npx simple-scaffold@latest -t input -o output TplName
```
#### Expected behavior\*\*
A clear and concise description of what you expected to happen.
#### Logs
If applicable, paste your logs to help explain your problem. To see more logs, run the scaffold with
`-v 1` to enable debug logging.
#### Desktop (please complete the following information):
- OS: [e.g. macOS, Windows, Linux]
- OS Version: [e.g. Big Sur, 11, Ubuntu 20.04]
- Node.js: [e.g. 16.8]
- Simple Scaffold Version [e.g. 1.1.2]
#### Additional context
Add any other context about the problem here.

View File

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

View File

@@ -1,52 +0,0 @@
name: Alpha Releases
on:
push:
branches: [alpha]
jobs:
build:
runs-on: ubuntu-latest
if: "!contains(github.event.head_commit.message, '[skip ci]')"
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: "12.x"
- run: yarn install --frozen-lockfile
- run: yarn 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

21
.github/workflows/docs.yml vendored Normal file
View File

@@ -0,0 +1,21 @@
name: 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]')"
if: "!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

@@ -1,50 +0,0 @@
name: Releases
on:
push:
branches: [master]
jobs:
build:
runs-on: ubuntu-latest
if: "!contains(github.event.head_commit.message, '[skip ci]')"
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: "12.x"
- run: yarn install --frozen-lockfile
- run: yarn 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
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

View File

@@ -2,16 +2,16 @@ name: Pull Requests
on:
pull_request:
branches: [master, alpha, beta]
branches: [master, develop]
jobs:
build:
runs-on: ubuntu-latest
if: "!contains(github.event.head_commit.message, '[skip ci]')"
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: "12.x"
node-version: "18.x"
- run: yarn install --frozen-lockfile
- run: yarn build
- run: yarn test

34
.github/workflows/release.yml vendored Normal file
View File

@@ -0,0 +1,34 @@
name: Test & Build
on:
push:
branches: [ master, develop, feat/*, fix/* ]
jobs:
test:
runs-on: ubuntu-latest
if: "!contains(github.event.head_commit.message, '[skip ci]')"
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: "18.x"
- run: yarn install --frozen-lockfile
- run: yarn test
if: "!contains(github.event.head_commit.message, '[skip test]')"
build:
runs-on: ubuntu-latest
if: "!contains(github.event.head_commit.message, '[skip ci]')"
needs: test
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: "18.x"
- run: yarn install --frozen-lockfile
- run: yarn build
- run: cd ./dist && yarn pack --filename=../package.tgz
- run: yarn semantic-release
if: "!contains(github.event.head_commit.message, '[skip publish]')"
env:
NPM_TOKEN: "${{ secrets.NPM_TOKEN }}"
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"

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

9
.markdownlint.json Normal file
View File

@@ -0,0 +1,9 @@
{
"line-length": {
"line_length": 100,
"tables": false,
"code_blocks": 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}"
}
]
}

13
.vscode/settings.json vendored
View File

@@ -3,11 +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": []
}
]
}

162
CHANGELOG.md Normal file
View File

@@ -0,0 +1,162 @@
# Change Log
## [1.6.0](https://github.com/chenasraf/simple-scaffold/compare/v1.5.0...v1.6.0) (2023-05-05)
### Features
* node.js function for remote configs ([ce5adbe](https://github.com/chenasraf/simple-scaffold/commit/ce5adbe0f898a86db6046d7f66d83dfcaa519ad2))
### Bug Fixes
* move dependency to dev dependency ([d916d88](https://github.com/chenasraf/simple-scaffold/commit/d916d88384054e6c6b40e6299073f1d1acb4d29d))
## Change Log
# [1.5.0](https://github.com/chenasraf/simple-scaffold/compare/v1.6.0-develop.1...v1.5.0) (2023-05-04)
## [1.6.0-develop.1](https://github.com/chenasraf/simple-scaffold/compare/v1.5.0...v1.6.0-develop.1) (2023-05-04)
### Features
- node.js function for remote configs
([ce5adbe](https://github.com/chenasraf/simple-scaffold/commit/ce5adbe0f898a86db6046d7f66d83dfcaa519ad2))
### Bug Fixes
- move dependency to dev dependency
([d916d88](https://github.com/chenasraf/simple-scaffold/commit/d916d88384054e6c6b40e6299073f1d1acb4d29d))
## [1.5.0](https://github.com/chenasraf/simple-scaffold/compare/v1.5.0-develop.1...v1.5.0) (2023-05-02)
## [1.5.0-develop.1](https://github.com/chenasraf/simple-scaffold/compare/v1.4.0...v1.5.0-develop.1) (2023-05-02)
### Features
- add github remote templates
([f961c13](https://github.com/chenasraf/simple-scaffold/commit/f961c13da15320b42540773ed958cdc3f97e4502))
- support for remote template configs
([05487f4](https://github.com/chenasraf/simple-scaffold/commit/05487f4d1e3b05f1d695242bb54427ee2fbdf247))
## [1.4.0](https://github.com/chenasraf/simple-scaffold/compare/v1.3.2...v1.4.0) (2023-04-28)
### Features
- add `--key` | `-k` to config loader
([6c5ba0b](https://github.com/chenasraf/simple-scaffold/commit/6c5ba0bc916fb1d59240d2eaa1abedc74527a974))
## [1.3.2](https://github.com/chenasraf/simple-scaffold/compare/v1.3.1...v1.3.2) (2023-04-28)
### Bug Fixes
- release build
([2c23fa9](https://github.com/chenasraf/simple-scaffold/commit/2c23fa9dbb310cd0a31f09606798f96b95d66779))
- release build asset
([0bef2df](https://github.com/chenasraf/simple-scaffold/commit/0bef2df5f3aa800ad5f1094c0996108db9acce51))
## [1.3.1](https://github.com/chenasraf/simple-scaffold/compare/v1.3.0...v1.3.1) (2023-04-28)
### Bug Fixes
- docs
([6e19a86](https://github.com/chenasraf/simple-scaffold/commit/6e19a86190dd924058a48448aa6463569ef1125f))
- remove old peer-dep
([c7e2ef8](https://github.com/chenasraf/simple-scaffold/commit/c7e2ef862cb658feb1071ac120b185d8b34d6dd3))
## [1.3.0](https://github.com/chenasraf/simple-scaffold/compare/v1.2.0...v1.3.0) (2023-04-25)
### Features
- load scaffold config from files
([c398976](https://github.com/chenasraf/simple-scaffold/commit/c3989769fee445c9183ff5e5b3892c4e9fb66a9e))
### Bug Fixes
- config option should not be mandatory
([3db6a91](https://github.com/chenasraf/simple-scaffold/commit/3db6a918f13d9300efa2fcb4a356d004475ab91c))
- export config file type
([4302eb5](https://github.com/chenasraf/simple-scaffold/commit/4302eb5ce35ed6cf1dc80dfb92790c3fdd96f963))
## [1.2.0](https://github.com/chenasraf/simple-scaffold/compare/v1.1.4...v1.2.0) (2023-04-24)
### Features
- append-data cli flag
([3c5c2de](https://github.com/chenasraf/simple-scaffold/commit/3c5c2ded02f61ff086e81ea4a7f40529bdff1c9d))
### Bug Fixes
- ci node version
([767d34c](https://github.com/chenasraf/simple-scaffold/commit/767d34c684516d4cea865b25e87c27c779bb79ce))
- github action node version
([7c19c53](https://github.com/chenasraf/simple-scaffold/commit/7c19c533376dc6904231e5cc51c7a4b2658c66e0))
- github action node version
([94fec76](https://github.com/chenasraf/simple-scaffold/commit/94fec766165f7540c578dbf2d0aeeb6ea3969ad8))
- semantic-release build dir
([f7956dd](https://github.com/chenasraf/simple-scaffold/commit/f7956ddc786018905c48ccf1f21a3bb4657c3d75))
- support quote wrapping in append-data
([4fecca8](https://github.com/chenasraf/simple-scaffold/commit/4fecca848347312d45d704f82f2bcb3822da9b06))
## [1.1.3](https://github.com/chenasraf/simple-scaffold/compare/v1.1.2...v1.1.3) (2023-03-11)
### 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)

340
README.md
View File

@@ -1,272 +1,123 @@
# simple-scaffold
<h1 align="center">Simple Scaffold</h1>
Simple Scaffold allows you to create your structured files based on templates.
<h2 align="center">
Simply organize your commonly-created files in their original structure, and replace any variable
values (such as component or app name) inside the paths or contents of the files with tokens to be
populated upon scaffolding.
[GitHub](https://github.com/chenasraf/simple-scaffold) |
[Documentation](https://chenasraf.github.io/simple-scaffold) |
[NPM](https://npmjs.com/package/simple-scaffold) | [casraf.dev](https://casraf.dev)
Then, run Simple Scaffold and it will generate your files for you in the desired structure,
with file names and contents that contain your dynamic information.
![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)
It's a simple way to easily create reusable components, common class files to start writing from,
or even entire app structures.
</h2>
## Install
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.
You can either use it as a command line tool or import into your own code and run from there.
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.
```bash
# npm
npm install [-g] simple-scaffold
# yarn
yarn [global] add simple-scaffold
# run without installing
npx simple-scaffold <...args>
```
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!
## Use as a command line tool
<div align="center">
### Command Line Options
![sample animation](media/intro.gif)
```plaintext
Usage: simple-scaffold [options]
</div>
Create structured files based on templates.
---
Options:
## Quick Start
--help|-h Display help information
### Local Templates
--name|-n Name to be passed to the generated files. {{name}} and
{{Name}} inside contents and file names will be replaced
accordingly.
The fastest way to get started is to use `npx` to immediately start a scaffold process.
--output|-o Path to output to. If --create-sub-folder is enabled,
the subfolder will be created inside this path.
(default: current dir)
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:
--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.
`templates/component/{{ pascalName name }}.tsx`
--overwrite|-w Enable to override output files, even if they already
exist. (default: false)
```tsx
// Created: {{ now | 'yyyy-MM-dd' }}
import React from 'react'
--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": "yarn simple-scaffold --templates scaffolds/component/**/* --output src/components --data '{\"myProp\": \"propName\", \"myVal\": \"123\"}'"
}
}
```
## Use in Node.js
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"
locals: {
property: "value",
},
helpers: {
twice: (text) => [text, text].join(" ")
}
}
const scaffold = Scaffold(config)
```
### Additional Node.js options
In addition to all the options available in the command line, there are some JS-specific options
available:
1. When `output` is used in Node directly, it may also be passed a function for each input file to
output into a dynamic path:
```typescript
config.output = (fullPath, baseDir, baseName) => {
console.log({ fullPath, baseDir, baseName })
return path.resolve(baseDir, baseName)
}
```
2. You may add custom `helpers` to your scaffolds. Helpers are simple `(string) => string` 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.
## Preparing files
### Template files
Put your template files anywhere, and fill them with tokens for replacement.
### 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).
Your `data` will be pre-populated with the following:
- `{{Name}}`: PascalCase of the component name
- `{{name}}`: raw name of the component
> Simple-Scaffold uses [Handlebars.js](https://handlebarsjs.com/) for outputting the file contents,
> see their documentation for more information on syntax.
> Any `data` you add in the config will be available for use with their names wrapped in
> `{{` and `}}`.
#### 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.
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 |
> These helpers are available for any data property, not exclusive to `name`.
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(),
}
```
These helpers will also be available to you when using `subFolderNameHelper` or
`--sub-folder-name-helper` 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`
```js
const React = require('react')
module.exports = function {{Name}}(props) {
export default {{pascalCase name}}: React.FC = (props) => {
return (
<div className="{{className}}">{{Name}} Component</div>
<div className="{{camelCase name}}">{{pascalCase name}} Component</div>
)
}
```
### Example Scaffold Output
To generate the template output, run:
### Output directory structure
```plaintext
- project
- src
- components
- MyComponent
- MyComponent.js
- ...
```shell
# generate single component
npx simple-scaffold@latest \
-t templates/component -o src/components PageWrapper
```
With `createSubFolder = false`:
This will immediately create the following file: `src/components/PageWrapper.tsx`
```plaintext
- project
- src
- components
- MyComponent.js
- ...
```
```tsx
// Created: 2077/01/01
import React from 'react'
#### Contents of `project/scaffold/MyComponent/MyComponent.jsx`
```js
const React = require("react")
module.exports = function MyComponent(props) {
export default PageWrapper: React.FC = (props) => {
return (
<div className="myClassName">MyComponent Component</div>
<div className="pageWrapper">PageWrapper Component</div>
)
}
```
### Remote Templates
Another quick way to start is to re-use someone else's (or your own) work using a template
repository.
Here is an example for loading the example component templates in this very repository:
```shell
npx simple-scaffold@latest \
-gh chenasraf/simple-scaffold#examples/test-input/scaffold.config.js:component \
PageWrapper
# equivalent to:
npx simple-scaffold@latest \
-c https://github.com/chenasraf/simple-scaffold.git#examples/test-input/scaffold.config.js:component \
PageWrapper
```
When template name (`:component`) is omitted, `default` is used.
See more at the [CLI documentation](https://chenasraf.github.io/simple-scaffold/pages/cli.html)
## Documentation
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)
## Contributing
I am developing this package on my free time, so any support, whether code, issues, or just stars is
very helpful to sustaining its life. If you are feeling incredibly generous and would like to donate
just a small amount to help sustain this project, I would be very very thankful!
<a href='https://ko-fi.com/casraf' target='_blank'>
<img height='36' style='border:0px;height:36px;'
src='https://cdn.ko-fi.com/cdn/kofi1.png?v=3'
alt='Buy Me a Coffee at ko-fi.com' />
</a>
I welcome any issues or pull requests on GitHub. If you find a bug, or would like a new feature,
don't hesitate to open an appropriate issue and I will do my best to reply promptly.
@@ -284,11 +135,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/component",
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,42 +1,66 @@
{
"name": "simple-scaffold",
"version": "1.0.1-pre.1",
"description": "Create files based on templates",
"version": "1.6.0",
"description": "Generate any file structure - from single components to entire app boilerplates, with a single command.",
"homepage": "https://chenasraf.github.io/simple-scaffold",
"repository": "https://github.com/chenasraf/simple-scaffold.git",
"author": "Chen Asraf <inbox@casraf.com>",
"license": "MIT",
"main": "index.js",
"bin": "cmd.js",
"keywords": [
"javascript",
"cli",
"template",
"files",
"typescript",
"generator",
"scaffold",
"file",
"scaffolding"
],
"scripts": {
"clean": "rm -rf dist/",
"clean": "rimraf dist/",
"build": "yarn clean && tsc && chmod -R +x ./dist && cp ./package.json ./README.md ./dist/",
"dev": "tsc --watch",
"start": "node dist/scaffold.js",
"test": "jest --verbose",
"test": "jest",
"cmd": "node --trace-warnings dist/cmd.js",
"build-test": "yarn build && yarn test",
"build-cmd": "yarn build && yarn cmd"
"build-cmd": "yarn build && yarn cmd",
"build-docs": "typedoc",
"watch-docs": "yarn typedoc --watch",
"audit-fix": "npm_config_yes=true npx yarn-audit-fix --flow=convert",
"changelog": "conventional-changelog -p conventionalcommits -i CHANGELOG.md -s -r 0"
},
"dependencies": {
"args": "^5.0.1",
"chalk": "^4.1.2",
"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",
"util.promisify": "^1.1.1"
"massarg": "^1.0.7-pre.1"
},
"devDependencies": {
"@types/args": "^3.0.1",
"@types/glob": "^7.1.1",
"@types/jest": "^26.0.24",
"@knodes/typedoc-plugin-pages": "^0.23.4",
"@semantic-release/changelog": "^6.0.3",
"@semantic-release/git": "^10.0.1",
"@semantic-release/release-notes-generator": "^10.0.3",
"@types/jest": "^29.5.1",
"@types/lodash": "^4.14.171",
"@types/mock-fs": "^4.13.1",
"@types/node": "^14.14.22",
"jest": "^27.0.6",
"mock-fs": "^5.0.0",
"ts-jest": "^27.0.3",
"ts-node": "^10.1.0",
"typescript": "^4.3.5"
"@types/node": "^18.16.0",
"conventional-changelog": "^3.1.25",
"conventional-changelog-cli": "^2.2.2",
"conventional-changelog-conventionalcommits": "^5.0.0",
"jest": "^29.5.0",
"mock-fs": "^5.2.0",
"rimraf": "^5.0.0",
"semantic-release": "^21.0.1",
"semantic-release-conventional-commits": "^3.0.0",
"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)

79
pages/cli.md Normal file
View File

@@ -0,0 +1,79 @@
## Available flags
```text
Usage: simple-scaffold [options]
```
To see this and more information anytime, add the `-h` or `--help` flag to your call, e.g.
`npx simple-scaffold@latest -h`.
| Command \| alias | |
| --------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--help`\|`-h` | Display help information |
| `--name`\|`-n` | Name to be passed to the generated files. {{name}} and {{Name}} inside contents and file names will be replaced accordingly. |
| `--config`\|`-c` | Filename or HTTPS git URL to load config from instead of passing arguments to CLI or using a Node.js script. |
| `--github`\|`-gh` | GitHub path to load config from instead of passing arguments to CLI or using a Node.js script. |
| `--key`\|`-k` | Key to load inside the config file. This overwrites the config key provided after the colon in --config (e.g. --config scaffold.cmd.js:component) |
| `--output`\|`-o` | Path to output to. If --create-sub-folder is enabled, the subfolder will be created inside this path. (default: current dir) |
| `--templates`\|`-t` | Template files to use as input. You may provide multiple files, each of which can be a relative or absolute path, or a glob pattern for multiple file matching easily. |
| `--overwrite`\|`-w` | Enable to override output files, even if they already exist. (default: false) |
| `--data`\|`-d` | Add custom data to the templates. By default, only your app name is included. |
| `--append-data`\|`-D` | Append additional custom data to the templates, which will overwrite --data, using an alternate syntax, which is easier to use with CLI: -D key1=string -D key2:=raw |
| `--create-sub-folder`\|`-s` | Create subfolder with the input name (default: false) |
| `--sub-folder-name-helper`\|`-sh` | Default helper to apply to subfolder name when using `--create-sub-folder true`. |
| `--quiet`\|`-q` | Suppress output logs (Same as --verbose 0) (default: false) |
| `--verbose`\|`-v` | Determine amount of logs to display. The values are: 0 (none) \| 1 (debug) \| 2 (info) \| 3 (warn) \| 4 (error). The provided level will display messages of the same level or higher. (default: 2) |
| `--dry-run`\|`-dr` | Don't emit files. This is good for testing your scaffolds and making sure they don't fail, without having to write actual file contents or create directories. (default: false) |
## Examples:
> See
> [Configuration Files](https://chenasraf.github.io/simple-scaffold/pages/docs/configuration_files.md)
> for organizing multiple scaffold types into easy-to-maintain files
Usage with config file
```shell
$ simple-scaffold -c scaffold.cmd.js --key component
```
Usage with GitHub config file
```shell
$ simple-scaffold -gh chenasraf/simple-scaffold --key component
```
Usage with https git URL (for non-GitHub)
```shell
$ simple-scaffold -c \
https://example.com/user/template.git#scaffold.cmd.js --key component
```
Full syntax with config path and template key (applicable to all above methods)
```shell
$ simple-scaffold -c scaffold.cmd.js:component MyComponent
```
Excluded template key, assumes 'default' key
```shell
$ simple-scaffold -c scaffold.cmd.js MyComponent
```
Shortest syntax for GitHub, assumes file 'scaffold.cmd.js' and template key 'default'
```shell
$ simple-scaffold -gh chenasraf/simple-scaffold MyComponent
```
You can also add this as a script in your `package.json`:
```json
{
"scripts": {
"scaffold": "npx simple-scaffold@latest -t scaffolds/component/**/* -o src/components -d '{\"myProp\": \"propName\", \"myVal\": 123}'"
}
}
```

View File

@@ -0,0 +1,155 @@
If you want to have reusable configurations which are complex and don't fit into command lines
easily, or just want to manage your templates easier, you can use configuration files to load your
scaffolding configurations.
## Creating config files
Configuration files should be valid `.js`/`.json` files that contain valid Scaffold configurations.
Each file hold multiple scaffolds. Each scaffold is a key, and its value is the configuration. For
example:
```json
{
"component": {
"templates": ["templates/component"],
"output": "src/components"
}
}
```
The configuration contents are identical to the
[Node.js configuration structure](https://chenasraf.github.io/simple-scaffold/pages/node.md):
```ts
interface ScaffoldConfig {
name: string
templates: string[]
output: FileResponse<string>
createSubFolder?: boolean
data?: Record<string, any>
overwrite?: FileResponse<boolean>
quiet?: boolean
verbose?: LogLevel
dryRun?: boolean
helpers?: Record<string, Helper>
subFolderNameHelper?: DefaultHelpers | string
beforeWrite?(
content: Buffer,
rawContent: Buffer,
outputPath: string,
): string | Buffer | undefined | Promise<string | Buffer | undefined>
}
```
If you want to supply functions inside the configurations, you must use a `.js` file as JSON does
not support non-primitives.
A `.js` file is just like a `.json` file, make sure to export the final configuration:
```js
/** @type {import('simple-scaffold').ScaffoldConfigFile} */
module.exports = {
component: {
templates: ["templates/component"],
output: "src/components",
},
}
```
## Using a config file
Once your config is created, you can use it by providing the file name to the `--config` (or `-c`
for brevity), optionally followed by a colon, then your scaffold config name.
```shell
simple-scaffold -c <file>[:<template_key>]
```
For example:
```shell
simple-scaffold -c scaffold.json:component MyComponentName
```
If you don't want to supply a template/config name (e.g. `component`), you can omit the colon and
the name, and it will use the configuration named `default`:
```js
/** @type {import('simple-scaffold').ScaffoldConfigFile} */
module.exports = {
default: {
// ...
},
}
```
And then:
```shell
# will use 'default' template
simple-scaffold -c scaffold.json MyComponentName
```
## Remote Templates
You can load template groups remotely, similar to how you would pass a config normally.
The main difference is the templates will be hosted on a remote location such as a git server, and
not locally in your project. This can be done to easily share & reuse templates.
When passing a git URL to `--config`, you will clone that repo and use the files there as template.
The syntax is as follows:
```shell
simple-scaffold -c <git_url>[#<git_file>][:<template_key>]
```
For example, to use this repository's example as base:
```shell
simple-scaffold -c https://github.com/chenasraf/simple-scaffold.git#examples/test-input/scaffold.config.js:component
```
When the filename is omitted, `/scaffold.config.js` will be used as default.
When the template_key is ommitted, `default` will be used as default.
### GitHub Templates
As a shorter alternative to the above example, you can use `--github` or `-gh` to reference a GitHub
URL without specifying the whole path.
The syntax is as follows:
```shell
simple-scaffold -gh <username>/<project_name>[#<git_file>][:<template_key>]
```
This example is equivalent to the above, just shorter to write:
```shell
simple-scaffold -c chenasraf/simple-scaffold#examples/test-input/scaffold.config.js:component
```
## Use In Node.js
You can also start a scaffold from Node.js with a remote file or URL config.
Just use the `Scaffold.fromConfig` function:
```ts
Scaffold.fromConfig(
"scaffold.config.js", // file or HTTPS git URL
{
// name of the generated component
name: "My Component",
// key to load from the config
key: "component",
},
{
// other config overrides
},
)
```

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>
)
}
```

4764
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

83
release.config.js Normal file
View File

@@ -0,0 +1,83 @@
const releaseRules = [
{ type: "feat", section: "Features", release: "minor" },
{ type: "docs", section: "Build", release: false },
{ type: "fix", section: "Bug Fixes", release: "patch" },
{ type: "refactor", section: "Misc", release: "patch" },
{ type: "perf", section: "Misc", release: "patch" },
{ type: "build", section: "Build", release: "patch" },
{ type: "chore", section: "Misc", release: "patch" },
{ type: "test", section: "Tests", release: false },
]
/** @type {import('semantic-release').Options} */
module.exports = {
branches: [
"+([0-9])?(.{+([0-9]),x}).x",
"master",
"next",
"next-major",
{ name: "develop", prerelease: true },
{ 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", "major"],
types: releaseRules,
},
},
],
[
"@semantic-release/changelog",
{
changelogFile: "CHANGELOG.md",
changelogTitle: "# Change Log",
},
],
[
"@semantic-release/npm",
{
npmPublish: false,
},
],
[
"@semantic-release/npm",
{
npmPublish: true,
pkgRoot: "dist",
},
],
[
"@semantic-release/git",
{
assets: ["CHANGELOG.md", "package.json"],
},
],
[
"@semantic-release/github",
{
assets: ["package.tgz"],
},
],
],
}

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") || args.includes("--github") || args.includes("-gh")
export function parseCliArgs(args = process.argv.slice(2)) {
return (
massarg<ScaffoldCmdConfig & { help: boolean; extras: string[] }>()
.main(Scaffold)
massarg<ScaffoldCmdConfig>()
.main(async (config) => {
const _config = await parseConfig(config)
return Scaffold(_config)
})
.option({
name: "name",
aliases: ["n"],
@@ -16,11 +25,29 @@ export function parseCliArgs(args = process.argv.slice(2)) {
isDefault: true,
required: true,
})
.option({
name: "config",
aliases: ["c"],
description:
"Filename or https git URL to load config from instead of passing arguments to CLI or using a Node.js script. See examples for syntax.",
})
.option({
name: "github",
aliases: ["gh"],
description:
"GitHub path to load config from instead of passing arguments to CLI or using a Node.js script. See examples for syntax.",
})
.option({
name: "key",
aliases: ["k"],
description:
"Key to load inside the config file. This overwrites the config key provided after the colon in --config (e.g. --config scaffold.cmd.js:component)",
})
.option({
name: "output",
aliases: ["o"],
description: `Path to output to. If --create-sub-folder is enabled, the subfolder will be created inside this path. ${chalk.reset`${chalk.white`(default: current dir)`}`}`,
required: true,
required: !isConfig,
})
.option({
name: "templates",
@@ -29,7 +56,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 +71,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 +101,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({
@@ -84,13 +121,41 @@ export function parseCliArgs(args = process.argv.slice(2)) {
// description: "Usage",
// output: "",
// })
.example({
description: "Usage with config file",
input: "simple-scaffold -c scaffold.cmd.js --key component",
})
.example({
description: "Usage with GitHub config file",
input: "simple-scaffold -gh chenasraf/simple-scaffold --key component",
})
.example({
description: "Usage with https git URL (for non-GitHub)",
input: "simple-scaffold -c https://example.com/user/template.git#scaffold.cmd.js --key component",
})
.example({
description: "Full syntax with config path and template key (applicable to all above methods)",
input: "simple-scaffold -c scaffold.cmd.js:component MyComponent",
})
.example({
description: "Excluded template key, assumes 'default' key",
input: "simple-scaffold -c scaffold.cmd.js MyComponent",
})
.example({
description: "Shortest syntax for GitHub, assumes file 'scaffold.cmd.js' and template key 'default'",
input: "simple-scaffold -gh chenasraf/simple-scaffold MyComponent",
})
.help({
binName: "simple-scaffold",
useGlobalColumns: true,
usageExample: "[options]",
header: "Create structured files based on templates.",
printWidth: 100,
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://chenasraf.github.io/simple-scaffold`}`,
`NPM: ${chalk.underline`https://npmjs.com/package/simple-scaffold`}`,
`GitHub: ${chalk.underline`https://github.com/chenasraf/simple-scaffold`}`,
].join("\n"),

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

128
src/scaffold.ts Normal file → Executable file
View File

@@ -1,33 +1,32 @@
import { glob } from "glob"
/**
* @module
* Simple Scaffold
*
* See [readme](README.md)
*/
import path from "path"
import { promisify } from "util"
import { promises as fsPromises } from "fs"
const { readFile, writeFile } = fsPromises
import {
createDirIfNotExists,
getOptionValueForFile,
handleErr,
handlebarsParse,
log,
pathExists,
pascalCase,
isDir,
removeGlob,
makeRelativePath,
registerHelpers,
getTemplateGlobInfo,
ensureFileExists,
getFileList,
getBasePath,
copyFileTransformed,
getTemplateFileInfo,
logInitStep,
logInputFile,
GlobInfo,
OutputFileInfo,
parseConfig,
} from "./utils"
import { FileResponse, LogLevel, ScaffoldConfig } from "./types"
import { LogLevel, ScaffoldCmdConfig, ScaffoldConfig } from "./types"
import { OptionsBase } from "massarg/types"
/**
* Create a scaffold using given `options`.
@@ -40,47 +39,48 @@ import { FileResponse, 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
* @return {Promise<void>} A promise that resolves when the scaffold is complete
*
* @see {@link DefaultHelpers}
* @see {@link CaseHelpers}
* @see {@link DateHelpers}
*
* @category Main
*/
export async function Scaffold({ ...options }: ScaffoldConfig) {
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,
)
await ensureFileExists(template, isDirOrGlob)
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,
@@ -90,32 +90,68 @@ export async function Scaffold({ ...options }: ScaffoldConfig) {
isDirOrGlob,
isGlob,
})
await handleTemplateFile(options, options.data, { templatePath: inputFilePath, basePath })
await handleTemplateFile(config, {
templatePath: inputFilePath,
basePath,
})
}
} catch (e: any) {
handleErr(e)
}
}
} catch (e: any) {
log(options, LogLevel.Error, e)
log(config, LogLevel.Error, e)
throw e
}
}
/**
* Create a scaffold based on a config file or URL.
*
* @param {string} pathOrUrl The path or URL to the config file
* @param {Record<string, string>} config Information needed before loading the config
* @param {Partial<Omit<ScaffoldConfig, 'name'>>} overrides Any overrides to the loaded config
*
* @see {@link Scaffold}
* @category Main
* @return {Promise<void>} A promise that resolves when the scaffold is complete
*/
Scaffold.fromConfig = async function (
pathOrUrl: string,
config: Pick<ScaffoldCmdConfig, "name" | "key">,
overrides?: Partial<Omit<ScaffoldConfig, "name">>,
): Promise<void> {
const _cmdConfig: ScaffoldCmdConfig & OptionsBase = {
dryRun: false,
output: process.cwd(),
verbose: LogLevel.Info,
overwrite: false,
templates: [],
createSubFolder: false,
quiet: false,
help: false,
extras: [],
config: pathOrUrl,
...config,
}
const _config = await parseConfig(_cmdConfig)
return Scaffold({ ..._config, ...overrides })
}
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, data, options.overwrite ?? false)
const overwrite = getOptionValueForFile(config, inputPath, config.overwrite ?? false)
log(
options,
config,
LogLevel.Debug,
`\nParsing ${templatePath}`,
`\nBase path: ${basePath}`,
@@ -123,13 +159,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, data, { 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,29 +1,16 @@
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"
| "snakeCase"
| "startCase"
| "kebabCase"
| "hyphenCase"
| "pascalCase"
| "lowerCase"
| "upperCase"
export type HelperKeys<T> = DefaultHelperKeys | T
export type Helper = (text: string) => string
import { HelperDelegate } from "handlebars/runtime"
/**
* The config object for defining a scaffolding group.
*
* @see {@link https://chenasraf.github.io/simple-scaffold/pages/node.html | Node.js usage}
* @see {@link https://chenasraf.github.io/simple-scaffold/pages/cli.html | CLI usage}
* @see {@link 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
@@ -33,46 +20,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
@@ -80,7 +98,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:
@@ -89,39 +107,253 @@ 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 {@link https://chenasraf.github.io/simple-scaffold/pages/templates.html | Templates}
* */
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
* a string that represents the final content of your file, you may process the content as you see fit. For example,
* you may run formatters on a file, fix output in edge-cases not supported by helpers or data, etc.
*
* If the return value of this function is `undefined`, the original content will be used.
*
* @param content The original template after token replacement
* @param rawContent The original template before token replacement
* @param outputPath The final output path of the processed file
*
* @returns {Promise<String | Buffer | undefined> | String | Buffer | undefined} The final output of the file
* contents-only, after further modifications - or `undefined` to use the original content (i.e. `content.toString()`)
*/
beforeWrite?(
content: Buffer,
rawContent: Buffer,
outputPath: string,
): string | Buffer | undefined | Promise<string | Buffer | undefined>
}
/**
* The names of the available helper functions that relate to text capitalization.
*
* These are available for `subfolderNameHelper`.
*
* | Helper name | Example code | Example output |
* | ------------ | ----------------------- | -------------- |
* | [None] | `{{ name }}` | my name |
* | `camelCase` | `{{ camelCase name }}` | myName |
* | `snakeCase` | `{{ snakeCase name }}` | my_name |
* | `startCase` | `{{ startCase name }}` | My Name |
* | `kebabCase` | `{{ kebabCase name }}` | my-name |
* | `hyphenCase` | `{{ hyphenCase name }}` | my-name |
* | `pascalCase` | `{{ pascalCase name }}` | MyName |
* | `upperCase` | `{{ upperCase name }}` | MY NAME |
* | `lowerCase` | `{{ lowerCase name }}` | my name |
*
* @see {@link DefaultHelpers}
* @see {@link DateHelpers}
* @see {@link ScaffoldConfig}
* @see {@link ScaffoldConfig.subFolderNameHelper}
*
* @category Helpers
*/
export type CaseHelpers =
| "camelCase"
| "hyphenCase"
| "kebabCase"
| "lowerCase"
| "pascalCase"
| "snakeCase"
| "startCase"
| "upperCase"
/**
* The names of the available helper functions that relate to dates.
*
* | Helper name | Description | Example code | Example output |
* | -------------------------------- | ---------------------------------------------------------------- | ---------------------------------------------------------------- | ------------------ |
* | `now` | Current date with format | `{{ now "yyyy-MM-dd HH:mm" }}` | `2042-01-01 15:00` |
* | `now` (with offset) | Current date with format, and with offset | `{{ now "yyyy-MM-dd HH:mm" -1 "hours" }}` | `2042-01-01 14:00` |
* | `date` | Custom date with format | `{{ date "2042-01-01T15:00:00Z" "yyyy-MM-dd HH:mm" }}` | `2042-01-01 15:00` |
* | `date` (with offset) | Custom date with format, and with offset | `{{ date "2042-01-01T15:00:00Z" "yyyy-MM-dd HH:mm" -1 "days" }}` | `2041-31-12 15:00` |
* | `date` (with date from `--data`) | Custom date with format, with data from the `data` config option | `{{ date myCustomDate "yyyy-MM-dd HH:mm" }}` | `2042-01-01 12:00` |
*
* Further details:
*
* - We use [`date-fns`](https://date-fns.org/docs/) for parsing/manipulating the dates. If you want
* more information on the date tokens to use, refer to
* [their format documentation](https://date-fns.org/docs/format).
*
* - The date helper format takes the following arguments:
*
* ```typescript
* (
* date: string,
* format: string,
* offsetAmount?: number,
* offsetType?: "years" | "months" | "weeks" | "days" | "hours" | "minutes" | "seconds"
* )
* ```
*
* - **The now helper** (for current time) takes the same arguments, minus the first one (`date`) as it is implicitly
* the current date.
*
* @see {@link DefaultHelpers}
* @see {@link CaseHelpers}
* @see {@link ScaffoldConfig}
*
* @category Helpers
*/
export type DateHelpers = "date" | "now"
/**
* The names of all the available helper functions in templates.
* Simple-Scaffold provides some built-in text transformation filters usable by Handlebars.js.
*
* For example, you may use `{{ snakeCase name }}` inside a template file or filename, and it will
* replace `My Name` with `my_name` when producing the final value.
*
* @see {@link CaseHelpers}
* @see {@link DateHelpers}
* @see {@link ScaffoldConfig}
*
* @category Helpers
*/
export type DefaultHelpers = CaseHelpers | DateHelpers
/**
* Helper function, see https://handlebarsjs.com/guide/#custom-helpers
*
* @category Helpers
*/
export type Helper = HelperDelegate
/**
* The amount of information to log when generating scaffold.
* When not `None`, the selected level will be the lowest level included.
*
* For example, level `Info` (2) will include `Info`, `Warning` and `Error`, but not `Debug`; and `Warning` will only
* show `Warning` and `Error`.
*
* @default `2 (info)`
*
* @category Logging
*/
export enum LogLevel {
/** Silent output */
None = 0,
/** Debugging information. Very verbose and only recommended for troubleshooting. */
Debug = 1,
/**
* The regular level of logging. Major actions are logged to show the scaffold progress.
*
* @default
*/
Info = 2,
/** Warnings such as when file fails to replace token values properly in template. */
Warning = 3,
/** Errors, such as missing files, bad replacement token syntax, or un-writable directories. */
Error = 4,
}
/**
* A function that takes path information about file, and returns a value of type `T`
*
* @template T The return type for the function
* @param {string} fullPath The full path of the current file
* @param {string} basedir The directory containing the current file
* @param {string} basename The name of the file
*
* @returns {T} A return value
*
* @category Config
*/
export type FileResponseHandler<T> = (fullPath: string, basedir: string, basename: string) => T
/**
* Represents a response for file path information.
* Can either be:
*
* 1. `T` - static value
* 2. A function with the following signature which returns `T`:
* ```typescript
* (fullPath: string, basedir: string, basename: string) => T
* ```
*
* @typedef T The return type
*
* @see {@link FileResponseHandler}
*
* @category Config
* */
export type FileResponse<T> = T | FileResponseHandler<T>
/** @internal */
export interface ScaffoldCmdConfig {
/**
* Name to be passed to the generated files. `{{name}}` and `{{Name}}` inside contents and file names will be replaced
* accordingly.
*/
name: string
templates: string[]
output: string
createSubFolder: boolean
data?: Record<string, string>
appendData?: Record<string, string>
overwrite: boolean
quiet: boolean
verbose: LogLevel
dryRun: boolean
config?: string
/** The key to use for the file which contains the template configurations. */
key?: string
github?: string
}
/**
* A mapping of scaffold template keys to their configurations.
*
* Each configuration is a {@link ScaffoldConfig} object.
*
* The key is the name of the template, and the value is the configuration for that template.
*
* When no template key is provided to the scaffold command, the "default" template is used.
*
* @see {@link ScaffoldConfig}
*/
export type 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"
@@ -9,12 +18,23 @@ import Handlebars from "handlebars"
import { promises as fsPromises } from "fs"
import chalk from "chalk"
const { stat, access, mkdir } = fsPromises
import dtAdd from "date-fns/add"
import dtFormat from "date-fns/format"
import dtParseISO from "date-fns/parseISO"
import { glob, hasMagic } from "glob"
import { OptionsBase } from "massarg/types"
import { spawn } from "node:child_process"
import os from "node:os"
const dateFns = {
add: dtAdd,
format: dtFormat,
parseISO: dtParseISO,
}
import { glob } from "glob"
import { promisify } from "util"
const { readFile, writeFile } = fsPromises
export const defaultHelpers: Record<DefaultHelperKeys, Helper> = {
export const defaultHelpers: Record<DefaultHelpers, Helper> = {
camelCase,
snakeCase,
startCase,
@@ -23,24 +43,72 @@ export const defaultHelpers: Record<DefaultHelperKeys, Helper> = {
pascalCase,
lowerCase: (text) => text.toLowerCase(),
upperCase: (text) => text.toUpperCase(),
now: nowHelper,
date: dateHelper,
}
export function registerHelpers(options: ScaffoldConfig) {
const _helpers = { ...defaultHelpers, ...options.helpers }
export function _dateHelper(date: Date, formatString: string): string
export function _dateHelper(
date: Date,
formatString: string,
durationDifference: number,
durationType: keyof Duration,
): string
export function _dateHelper(
date: Date,
formatString: string,
durationDifference?: number,
durationType?: keyof Duration,
): string {
if (durationType && durationDifference !== undefined) {
return dateFns.format(dateFns.add(date, { [durationType]: durationDifference }), formatString)
}
return dateFns.format(date, formatString)
}
export function nowHelper(formatString: string): string
export function nowHelper(formatString: string, durationDifference: number, durationType: keyof Duration): string
export function nowHelper(formatString: string, durationDifference?: number, durationType?: keyof Duration): string {
return _dateHelper(new Date(), formatString, durationDifference!, durationType!)
}
export function dateHelper(date: string, formatString: string): string
export function dateHelper(
date: string,
formatString: string,
durationDifference: number,
durationType: keyof Duration,
): string
export function dateHelper(
date: string,
formatString: string,
durationDifference?: number,
durationType?: keyof Duration,
): string {
return _dateHelper(dateFns.parseISO(date), formatString, durationDifference!, durationType!)
}
export function registerHelpers(config: ScaffoldConfig): void {
const _helpers = { ...defaultHelpers, ...config.helpers }
for (const helperName in _helpers) {
log(options, LogLevel.Debug, `Registering helper: ${helperName}`)
log(config, LogLevel.Debug, `Registering helper: ${helperName}`)
Handlebars.registerHelper(helperName, _helpers[helperName as keyof typeof _helpers])
}
}
export function handleErr(err: NodeJS.ErrnoException | null) {
export function handleErr(err: NodeJS.ErrnoException | null): void {
if (err) throw err
}
export function log(options: ScaffoldConfig, level: LogLevel, ...obj: any[]) {
if (options.quiet || options.verbose === LogLevel.None || level < (options.verbose ?? LogLevel.Info)) {
/** @internal */
export type LogConfig = Pick<ScaffoldConfig, "quiet" | "verbose">
export function log(config: LogConfig, level: LogLevel, ...obj: any[]): void {
if (config.quiet || config.verbose === LogLevel.None || level < (config.verbose ?? LogLevel.Info)) {
return
}
const levelColor: Record<LogLevel, keyof typeof chalk> = {
[LogLevel.None]: "reset",
[LogLevel.Debug]: "blue",
@@ -48,6 +116,7 @@ export function log(options: ScaffoldConfig, level: LogLevel, ...obj: any[]) {
[LogLevel.Warning]: "yellow",
[LogLevel.Error]: "red",
}
const chalkFn: any = chalk[levelColor[level]]
const key: "log" | "warn" | "error" = level === LogLevel.Error ? "error" : level === LogLevel.Warning ? "warn" : "log"
const logFn: any = console[key]
@@ -57,21 +126,21 @@ export function log(options: ScaffoldConfig, level: LogLevel, ...obj: any[]) {
? 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) {
@@ -84,34 +153,42 @@ export async function createDirIfNotExists(dir: string, options: ScaffoldConfig)
}
export function getOptionValueForFile<T>(
options: ScaffoldConfig,
config: ScaffoldConfig,
filePath: string,
data: Record<string, string>,
fn: FileResponse<T>,
defaultValue?: T
defaultValue?: T,
): T {
if (typeof fn !== "function") {
return defaultValue ?? (fn as T)
}
return (fn as FileResponseFn<T>)(
return (fn as FileResponseHandler<T>)(
filePath,
path.dirname(handlebarsParse(options, filePath, data).toString()),
path.basename(handlebarsParse(options, filePath, data).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,
data: Record<string, string>
) {
{ isPath = false }: { isPath?: boolean } = {},
): Buffer {
const { data } = config
try {
const parser = Handlebars.compile(templateBuffer.toString(), { noEscape: true })
const outputContents = parser(data)
return outputContents
} catch {
log(options, LogLevel.Warning, "Couldn't parse file with handlebars, returning original content")
return templateBuffer
let str = templateBuffer.toString()
if (isPath) {
str = str.replace(/\\/g, "/")
}
const parser = Handlebars.compile(str, { noEscape: true })
let outputContents = parser(data)
if (isPath && path.sep !== "/") {
outputContents = outputContents.replace(/\//g, "\\")
}
return Buffer.from(outputContents)
} catch (e) {
log(config, LogLevel.Debug, e)
log(config, LogLevel.Warning, "Couldn't parse file with handlebars, returning original content")
return Buffer.from(templateBuffer)
}
}
@@ -136,27 +213,29 @@ export async function isDir(path: string): Promise<boolean> {
return tplStat.isDirectory()
}
export function removeGlob(template: string) {
return template.replace(/\*/g, "").replace(/\/\//g, "/")
export function removeGlob(template: string): string {
return template.replace(/\*/g, "").replace(/(\/\/|\\\\)/g, path.sep)
}
export function makeRelativePath(str: string): string {
return str.startsWith("/") ? str.slice(1) : str
return str.startsWith(path.sep) ? str.slice(1) : str
}
export function getBasePath(relPath: string) {
export function getBasePath(relPath: string): string {
return path
.resolve(process.cwd(), relPath)
.replace(process.cwd() + "/", "")
.replace(process.cwd() + path.sep, "")
.replace(process.cwd(), "")
}
export async function getFileList(options: ScaffoldConfig, template: string) {
return await promisify(glob)(template, {
dot: true,
debug: options.verbose === LogLevel.Debug,
nodir: true,
})
export async function getFileList(config: ScaffoldConfig, template: string): Promise<string[]> {
return (
await glob(template, {
dot: true,
nodir: true,
// debug: config.verbose === LogLevel.Debug,
})
).map((f) => f.replace(/\//g, path.sep))
}
export interface GlobInfo {
@@ -167,31 +246,22 @@ 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) {
_template = template + "/**/*"
_template = path.join(template, "**", "*")
}
return { nonGlobTemplate, origTemplate, isDirOrGlob, isGlob, template: _template }
}
export async function ensureFileExists(template: string, isGlob: boolean) {
if (!isGlob && !(await pathExists(template))) {
const err: NodeJS.ErrnoException = new Error(`ENOENT, no such file or directory ${template}`)
err.code = "ENOENT"
err.path = template
err.errno = -2
throw err
}
}
export interface OutputFileInfo {
inputPath: string
outputPathOpt: string
@@ -201,69 +271,71 @@ 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, data, options.output)
const outputDir = getOutputDir(options, data, outputPathOpt, basePath)
const outputPath = handlebarsParse(options, path.join(outputDir, path.basename(inputPath)), data).toString()
const outputPathOpt = getOptionValueForFile(config, inputPath, config.output)
const outputDir = getOutputDir(config, outputPathOpt, basePath)
const outputPath = handlebarsParse(config, path.join(outputDir, path.basename(inputPath)), {
isPath: true,
}).toString()
const exists = await pathExists(outputPath)
return { inputPath, outputPathOpt, outputDir, outputPath, exists }
}
export async function copyFileTransformed(
options: ScaffoldConfig,
data: Record<string, string>,
config: ScaffoldConfig,
{
exists,
overwrite,
outputPath,
inputPath,
}: { exists: boolean; overwrite: boolean; outputPath: string; inputPath: string }
) {
}: {
exists: boolean
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 outputContents = handlebarsParse(options, templateBuffer, data)
const unprocessedOutputContents = handlebarsParse(config, templateBuffer)
const finalOutputContents =
(await config.beforeWrite?.(unprocessedOutputContents, templateBuffer, outputPath)) ?? unprocessedOutputContents
if (!options.dryRun) {
await writeFile(outputPath, outputContents)
log(options, LogLevel.Info, "Done.")
if (!config.dryRun) {
await writeFile(outputPath, finalOutputContents)
log(config, LogLevel.Info, "Done.")
} else {
log(options, LogLevel.Info, "Content output:")
log(options, LogLevel.Info, outputContents)
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,
data: Record<string, string>,
outputPathOpt: string,
basePath: 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 }}`, data)
: 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,
@@ -282,10 +354,10 @@ export function logInputFile(
basePath: string
isDirOrGlob: boolean
isGlob: boolean
}
) {
},
): void {
log(
options,
config,
LogLevel.Debug,
`\nprocess.cwd(): ${process.cwd()}`,
`\norigTemplate: ${origTemplate}`,
@@ -296,24 +368,141 @@ export function logInputFile(
`\nbasePath: ${basePath}`,
`\nisDirOrGlob: ${isDirOrGlob}`,
`\nisGlob: ${isGlob}`,
`\n`
`\n`,
)
}
export function logInitStep(options: ScaffoldConfig) {
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,
subFolderNameHelper: config.subFolderNameHelper,
helpers: Object.keys(config.helpers ?? {}),
verbose: `${config.verbose} (${Object.keys(LogLevel).find(
(k) => (LogLevel[k as any] as unknown as number) === config.verbose!,
)})`,
})
log(options, LogLevel.Info, "Data:", options.data)
dryRun: config.dryRun,
beforeWrite: config.beforeWrite,
} as Record<keyof ScaffoldConfig, unknown>)
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 async function parseConfig(config: ScaffoldCmdConfig & OptionsBase): Promise<ScaffoldConfig> {
let c: ScaffoldConfig = config
if (config.github) {
log(config, LogLevel.Info, `Loading config from github ${config.github}`)
const gitUrl = new URL(`https://github.com/${config.github}`)
if (!gitUrl.pathname.endsWith(".git")) {
gitUrl.pathname += ".git"
}
config.config = gitUrl.toString()
}
if (config.config) {
const isUrl = config.config.includes("://")
const hasColonToken = (!isUrl && config.config.includes(":")) || (isUrl && count(config.config, ":") > 1)
const colonIndex = config.config.lastIndexOf(":")
const [configFile, templateKey = "default"] = hasColonToken
? [config.config.substring(0, colonIndex), config.config.substring(colonIndex + 1)]
: [config.config, undefined]
const key = (config.key ?? templateKey) || "default"
log(config, LogLevel.Info, `Loading config from ${configFile} with key ${key}`)
const configImport = await getConfig({ config: configFile, quiet: config.quiet, verbose: config.verbose })
if (!configImport[key]) {
throw new Error(`Template "${key}" not found in ${configFile}`)
}
c = {
...config,
...configImport[key],
data: {
...configImport[key].data,
...config.data,
},
}
}
c.data = { ...c.data, ...config.appendData }
delete config.appendData
return c
}
/** @internal */
export async function getConfig(
config: Pick<ScaffoldCmdConfig, "quiet" | "verbose" | "config">,
): Promise<ScaffoldConfigFile> {
const { config: configFile, ...logConfig } = config as Required<typeof config>
const url = new URL(configFile)
if (url.protocol === "file:") {
log(logConfig, LogLevel.Info, `Loading config from file ${configFile}`)
const absolutePath = path.resolve(process.cwd(), configFile)
return import(absolutePath)
}
const isHttp = url.protocol === "http:" || url.protocol === "https:"
const isGit = url.protocol === "git:" || (isHttp && url.pathname.endsWith(".git"))
if (isHttp || isGit) {
if (isGit) {
const repoUrl = `${url.protocol}//${url.host}${url.pathname}`
log(logConfig, LogLevel.Info, `Cloning git repo ${repoUrl}`)
const tmpPath = path.resolve(os.tmpdir(), `scaffold-config-${Date.now()}`)
return new Promise((resolve, reject) => {
const clone = spawn("git", ["clone", "--depth", "1", repoUrl, tmpPath])
clone.on("error", reject)
clone.on("close", async (code) => {
if (code === 0) {
log(logConfig, LogLevel.Info, `Loading config from git repo: ${repoUrl}`)
const hashPath = url.hash?.replace("#", "") || "scaffold.config.js"
const absolutePath = path.resolve(tmpPath, hashPath)
const loadedConfig = (await import(absolutePath)).default as ScaffoldConfigFile
log(logConfig, LogLevel.Info, `Loaded config from git`)
log(logConfig, LogLevel.Debug, `Raw config:`, loadedConfig)
const fixedConfig: ScaffoldConfigFile = Object.fromEntries(
Object.entries(loadedConfig).map(([k, v]) => [
k,
// use absolute paths for template as config is necessarily in another directory
{ ...v, templates: v.templates.map((t) => path.resolve(tmpPath, t)) },
]),
)
resolve(fixedConfig)
} else {
reject(new Error(`Git clone failed with code ${code}`))
}
})
})
}
throw new Error(`Unsupported protocol ${url.protocol}`)
}
return import(path.resolve(process.cwd(), configFile))
}
function count(string: string, substring: string): number {
return string.split(substring).length - 1
}

View File

@@ -4,6 +4,9 @@ import Scaffold from "../src/scaffold"
import { readdirSync, readFileSync } from "fs"
import { Console } from "console"
import { defaultHelpers } from "../src/utils"
import { join } from "path"
import * as dateFns from "date-fns"
import crypto from "crypto"
const fileStructNormal = {
input: {
@@ -11,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: {
@@ -43,7 +53,7 @@ const fileStructHelpers = {
input: {
defaults: defaultHelperNames.reduce<Record<string, string>>(
(all, cur) => ({ ...all, [cur + ".txt"]: `{{ ${cur} name }}` }),
{}
{},
),
custom: {
"add1.txt": "{{ add1 name }}",
@@ -51,8 +61,16 @@ const fileStructHelpers = {
},
output: {},
}
// let logsTemp: any = []
// let logMock: any
const fileStructDates = {
input: {
"now.txt": "Today is {{ now 'mmm' }}, time is {{ now 'HH:mm' }}",
"offset.txt": "Yesterday was {{ now 'mmm' -1 'days' }}, time is {{ now 'HH:mm' -1 'days' }}",
"custom.txt": "Custom date is {{ date customDate 'mmm' }}, time is {{ date customDate 'HH:mm' }}",
},
output: {},
}
function withMock(fileStruct: FileSystem.DirectoryItems, testFn: jest.EmptyFunction): jest.EmptyFunction {
return () => {
beforeEach(() => {
@@ -83,8 +101,8 @@ describe("Scaffold", () => {
templates: ["input"],
verbose: 0,
})
const data = readFileSync(process.cwd() + "/output/app_name.txt")
expect(data.toString()).toBe("Hello, my app is app_name")
const data = readFileSync(join(process.cwd(), "output", "app_name.txt"))
expect(data.toString()).toEqual("Hello, my app is app_name")
})
test("should create with config", async () => {
@@ -96,10 +114,28 @@ describe("Scaffold", () => {
verbose: 0,
})
const data = readFileSync(process.cwd() + "/output/app_name/app_name.txt")
expect(data.toString()).toBe("Hello, my app is app_name")
const data = readFileSync(join(process.cwd(), "output", "app_name", "app_name.txt"))
expect(data.toString()).toEqual("Hello, my app is app_name")
})
})
}),
)
describe(
"binary files",
withMock(fileStructWithBinary, () => {
test("should copy as-is", async () => {
await Scaffold({
name: "app_name",
output: "output",
templates: ["input"],
verbose: 0,
})
const data = readFileSync(join(process.cwd(), "output", "app_name.txt"))
expect(data.toString()).toEqual("Hello, my app is app_name")
const dataBin = readFileSync(join(process.cwd(), "output", "app_name.bin"))
expect(dataBin).toEqual(fileStructWithBinary.input["{{name}}.bin"])
})
}),
)
describe(
@@ -122,8 +158,8 @@ describe("Scaffold", () => {
verbose: 0,
})
const data = readFileSync(process.cwd() + "/output/app_name.txt")
expect(data.toString()).toBe("Hello, my value is 1")
const data = readFileSync(join(process.cwd(), "output", "app_name.txt"))
expect(data.toString()).toEqual("Hello, my value is 1")
})
test("should overwrite with config", async () => {
@@ -144,10 +180,10 @@ describe("Scaffold", () => {
verbose: 0,
})
const data = readFileSync(process.cwd() + "/output/app_name.txt")
expect(data.toString()).toBe("Hello, my value is 2")
const data = readFileSync(join(process.cwd(), "output", "app_name.txt"))
expect(data.toString()).toEqual("Hello, my value is 2")
})
})
}),
)
describe(
@@ -170,12 +206,49 @@ describe("Scaffold", () => {
templates: ["non-existing-input"],
data: { value: "1" },
verbose: 0,
})
}),
).rejects.toThrow()
expect(() => readFileSync(process.cwd() + "/output/app_name.txt")).toThrow()
await expect(
Scaffold({
name: "app_name",
output: "output",
templates: ["non-existing-input/non-existing-file.txt"],
data: { value: "1" },
verbose: 0,
}),
).rejects.toThrow()
expect(() => readFileSync(join(process.cwd(), "output", "app_name.txt"))).toThrow()
})
})
}),
)
describe(
"dry run",
withMock(fileStructNormal, () => {
let consoleMock1: jest.SpyInstance
beforeAll(() => {
consoleMock1 = jest.spyOn(console, "error").mockImplementation(() => void 0)
})
afterAll(() => {
consoleMock1.mockRestore()
})
test("should not write to disk", async () => {
Scaffold({
name: "app_name",
output: "output",
templates: ["input"],
data: { value: "1" },
verbose: 0,
dryRun: true,
})
expect(() => readFileSync(join(process.cwd(), "output", "app_name.txt"))).toThrow()
})
}),
)
describe(
@@ -184,15 +257,15 @@ describe("Scaffold", () => {
test("should allow override function", async () => {
await Scaffold({
name: "app_name",
output: (fullPath, basedir, basename) => `custom-output/${basename.split(".")[0]}`,
output: (fullPath, basedir, basename) => join("custom-output", `${basename.split(".")[0]}`),
templates: ["input"],
data: { value: "1" },
verbose: 0,
})
const data = readFileSync(process.cwd() + "/custom-output/app_name/app_name.txt")
expect(data.toString()).toBe("Hello, my app is app_name")
const data = readFileSync(join(process.cwd(), "/custom-output/app_name/app_name.txt"))
expect(data.toString()).toEqual("Hello, my app is app_name")
})
})
}),
)
describe(
@@ -207,76 +280,119 @@ describe("Scaffold", () => {
verbose: 0,
})
const rootDir = readdirSync(process.cwd() + "/output")
const dir = readdirSync(process.cwd() + "/output/AppName")
const nestedDir = readdirSync(process.cwd() + "/output/AppName/moreNesting")
const rootDir = readdirSync(join(process.cwd(), "output"))
const dir = readdirSync(join(process.cwd(), "output", "AppName"))
const nestedDir = readdirSync(join(process.cwd(), "output", "AppName", "moreNesting"))
expect(rootDir).toHaveProperty("length")
expect(dir).toHaveProperty("length")
expect(nestedDir).toHaveProperty("length")
const rootFile = readFileSync(process.cwd() + "/output/app_name-1.txt")
const oneDeepFile = readFileSync(process.cwd() + "/output/AppName/app_name-2.txt")
const twoDeepFile = readFileSync(process.cwd() + "/output/AppName/moreNesting/app_name-3.txt")
const rootFile = readFileSync(join(process.cwd(), "output", "app_name-1.txt"))
const oneDeepFile = readFileSync(join(process.cwd(), "output", "AppName/app_name-2.txt"))
const twoDeepFile = readFileSync(join(process.cwd(), "output", "AppName/moreNesting/app_name-3.txt"))
expect(rootFile.toString()).toEqual("This should be in root")
expect(oneDeepFile.toString()).toEqual("Hello, my value is 1")
expect(twoDeepFile.toString()).toEqual("Hi! My value is actually NOT 1!")
})
})
}),
)
describe(
"helpers",
"capitalization helpers",
withMock(fileStructHelpers, () => {
const _helpers: Record<string, (text: string) => string> = {
add1: (text) => text + " 1",
}
describe("default helpers", () => {
test("should work", async () => {
await Scaffold({
name: "app_name",
output: "output",
templates: ["input"],
verbose: 0,
helpers: _helpers,
})
const results = {
camelCase: "appName",
snakeCase: "app_name",
startCase: "App Name",
kebabCase: "app-name",
hyphenCase: "app-name",
pascalCase: "AppName",
lowerCase: "app_name",
upperCase: "APP_NAME",
}
for (const key in results) {
const file = readFileSync(process.cwd() + `/output/defaults/${key}.txt`)
expect(file.toString()).toEqual(results[key as keyof typeof results])
}
test("should work", async () => {
await Scaffold({
name: "app_name",
output: "output",
templates: ["input"],
verbose: 0,
helpers: _helpers,
})
})
describe("custom helpers", () => {
test("should work", async () => {
await Scaffold({
name: "app_name",
output: "output",
templates: ["input"],
verbose: 0,
helpers: _helpers,
})
const results = {
add1: "app_name 1",
}
for (const key in results) {
const file = readFileSync(process.cwd() + `/output/custom/${key}.txt`)
expect(file.toString()).toEqual(results[key as keyof typeof results])
}
})
const results = {
camelCase: "appName",
snakeCase: "app_name",
startCase: "App Name",
kebabCase: "app-name",
hyphenCase: "app-name",
pascalCase: "AppName",
lowerCase: "app_name",
upperCase: "APP_NAME",
}
for (const key in results) {
const file = readFileSync(join(process.cwd(), "output", "defaults", `${key}.txt`))
expect(file.toString()).toEqual(results[key as keyof typeof results])
}
})
})
}),
)
describe(
"date helpers",
withMock(fileStructDates, () => {
test("should work", async () => {
const now = new Date()
const yesterday = dateFns.add(new Date(), { days: -1 })
const customDate = dateFns.formatISO(dateFns.add(new Date(), { days: -1 }))
await Scaffold({
name: "app_name",
output: "output",
templates: ["input"],
verbose: 0,
data: { customDate },
})
const nowFile = readFileSync(join(process.cwd(), "output", "now.txt"))
const offsetFile = readFileSync(join(process.cwd(), "output", "offset.txt"))
const customFile = readFileSync(join(process.cwd(), "output", "custom.txt"))
// "now.txt": "Today is {{ now 'mmm' }}, time is {{ now 'HH:mm' }}",
// "offset.txt": "Yesterday was {{ now 'mmm' -1 'days' }}, time is {{ now 'HH:mm' -1 'days' }}",
// "custom.txt": "Custom date is {{ date customDate 'mmm' }}, time is {{ date customDate 'HH:mm' }}",
expect(nowFile.toString()).toEqual(
`Today is ${dateFns.format(now, "mmm")}, time is ${dateFns.format(now, "HH:mm")}`,
)
expect(offsetFile.toString()).toEqual(
`Yesterday was ${dateFns.format(yesterday, "mmm")}, time is ${dateFns.format(yesterday, "HH:mm")}`,
)
expect(customFile.toString()).toEqual(
`Custom date is ${dateFns.format(dateFns.parseISO(customDate), "mmm")}, time is ${dateFns.format(
dateFns.parseISO(customDate),
"HH:mm",
)}`,
)
})
}),
)
describe(
"custom helpers",
withMock(fileStructHelpers, () => {
const _helpers: Record<string, (text: string) => string> = {
add1: (text) => text + " 1",
}
test("should work", async () => {
await Scaffold({
name: "app_name",
output: "output",
templates: ["input"],
verbose: 0,
helpers: _helpers,
})
const results = {
add1: "app_name 1",
}
for (const key in results) {
const file = readFileSync(join(process.cwd(), "output", "custom", `${key}.txt`))
expect(file.toString()).toEqual(results[key as keyof typeof results])
}
})
}),
)
describe(
"transform subfolder",
@@ -290,8 +406,8 @@ describe("Scaffold", () => {
verbose: 0,
})
const data = readFileSync(process.cwd() + "/output/app_name/app_name.txt")
expect(data.toString()).toBe("Hello, my app is app_name")
const data = readFileSync(join(process.cwd(), "output", "app_name", "app_name.txt"))
expect(data.toString()).toEqual("Hello, my app is app_name")
})
test("should work with default helper", async () => {
@@ -304,8 +420,8 @@ describe("Scaffold", () => {
subFolderNameHelper: "upperCase",
})
const data = readFileSync(process.cwd() + "/output/APP_NAME/app_name.txt")
expect(data.toString()).toBe("Hello, my app is app_name")
const data = readFileSync(join(process.cwd(), "output", "APP_NAME", "app_name.txt"))
expect(data.toString()).toEqual("Hello, my app is app_name")
})
test("should work with custom helper", async () => {
@@ -321,9 +437,66 @@ describe("Scaffold", () => {
},
})
const data = readFileSync(process.cwd() + "/output/REPLACED/app_name.txt")
expect(data.toString()).toBe("Hello, my app is app_name")
const data = readFileSync(join(process.cwd(), "output", "REPLACED", "app_name.txt"))
expect(data.toString()).toEqual("Hello, my app is app_name")
})
})
}),
)
describe(
"before write",
withMock(fileStructNormal, () => {
test("should work with no callback", async () => {
await Scaffold({
name: "app_name",
output: "output",
templates: ["input"],
verbose: 0,
data: {
value: "value",
},
})
const data = readFileSync(join(process.cwd(), "output", "app_name.txt"))
expect(data.toString()).toEqual("Hello, my app is app_name")
})
test("should work with custom callback", async () => {
await Scaffold({
name: "app_name",
output: "output",
templates: ["input"],
verbose: 0,
data: {
value: "value",
},
beforeWrite: (content, beforeContent, outputPath) =>
[content.toString().toUpperCase(), beforeContent, outputPath].join(", "),
})
const data = readFileSync(join(process.cwd(), "output", "app_name.txt"))
expect(data.toString()).toEqual(
[
"Hello, my app is app_name".toUpperCase(),
fileStructNormal.input["{{name}}.txt"],
join(process.cwd(), "output", "app_name.txt"),
].join(", "),
)
})
test("should work with undefined response custom callback", async () => {
await Scaffold({
name: "app_name",
output: "output",
templates: ["input"],
verbose: 0,
data: {
value: "value",
},
beforeWrite: () => undefined,
})
const data = readFileSync(join(process.cwd(), "output", "app_name.txt"))
expect(data.toString()).toEqual("Hello, my app is app_name")
})
}),
)
})

124
tests/utils.test.ts Normal file
View File

@@ -0,0 +1,124 @@
import { dateHelper, handlebarsParse, nowHelper, parseAppendData } from "../src/utils"
import { ScaffoldCmdConfig, ScaffoldConfig } from "../src/types"
import path from "path"
import * as dateFns from "date-fns"
import { OptionsBase } from "massarg/types"
const blankConf: ScaffoldConfig = {
verbose: 0,
name: "",
output: "",
templates: [],
data: { name: "test" },
}
const blankCliConf: ScaffoldCmdConfig & OptionsBase = {
verbose: 0,
name: "",
output: "",
templates: [],
data: { name: "test" },
overwrite: false,
createSubFolder: false,
dryRun: false,
quiet: false,
extras: [],
help: false,
}
describe("Utils", () => {
describe("handlebarsParse", () => {
let origSep: any
describe("windows paths", () => {
beforeAll(() => {
origSep = path.sep
Object.defineProperty(path, "sep", { value: "\\" })
})
afterAll(() => {
Object.defineProperty(path, "sep", { value: origSep })
})
test("should work for windows paths", async () => {
expect(handlebarsParse(blankConf, "C:\\exports\\{{name}}.txt", { isPath: true })).toEqual(
Buffer.from("C:\\exports\\test.txt"),
)
})
})
describe("non-windows paths", () => {
beforeAll(() => {
origSep = path.sep
Object.defineProperty(path, "sep", { value: "/" })
})
afterAll(() => {
Object.defineProperty(path, "sep", { value: origSep })
})
test("should work for non-windows paths", async () => {
expect(handlebarsParse(blankConf, "/home/test/{{name}}.txt", { isPath: true })).toEqual(
Buffer.from("/home/test/test.txt"),
)
})
})
test("should not do path escaping on non-path compiles", async () => {
expect(
handlebarsParse(
{ ...blankConf, data: { ...blankConf.data, escaped: "value" } },
"/home/test/{{name}} \\{{escaped}}.txt",
{
isPath: false,
},
),
).toEqual(Buffer.from("/home/test/test {{escaped}}.txt"))
})
})
describe("Helpers", () => {
describe("date helpers", () => {
describe("now", () => {
test("should work without extra params", () => {
const now = new Date()
const fmt = "yyyy-MM-dd HH:mm"
expect(nowHelper(fmt)).toEqual(dateFns.format(now, fmt))
})
})
describe("date", () => {
test("should work with no offset params", () => {
const now = new Date()
const fmt = "yyyy-MM-dd HH:mm"
expect(dateHelper(now.toISOString(), fmt)).toEqual(dateFns.format(now, fmt))
})
test("should work with offset params", () => {
const now = new Date()
const fmt = "yyyy-MM-dd HH:mm"
expect(dateHelper(now.toISOString(), fmt, -1, "days")).toEqual(
dateFns.format(dateFns.add(now, { days: -1 }), fmt),
)
expect(dateHelper(now.toISOString(), fmt, 1, "months")).toEqual(
dateFns.format(dateFns.add(now, { months: 1 }), fmt),
)
})
})
})
})
describe("parseAppendData", () => {
test('works for "key=value"', () => {
expect(parseAppendData("key=value", blankCliConf)).toEqual({ key: "value", name: "test" })
})
test('works for "key:=value"', () => {
expect(parseAppendData("key:=123", blankCliConf)).toEqual({ key: 123, name: "test" })
})
test("overwrites existing value", () => {
expect(parseAppendData("name:=123", blankCliConf)).toEqual({ name: 123 })
})
test("works with quotes", () => {
expect(parseAppendData('key="value test"', blankCliConf)).toEqual({ key: "value test", name: "test" })
})
})
})

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

2960
yarn.lock

File diff suppressed because it is too large Load Diff