Compare commits

...

122 Commits

Author SHA1 Message Date
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
Chen Asraf
d797e5b640 bump alpha version number 2022-02-17 19:19:07 +02:00
Chen Asraf
c3835a7b04 refactoring - code cleanup 2022-02-17 19:18:48 +02:00
Chen Asraf
81ba5f50fd add subFolderNameHelper arg 2022-02-17 19:12:17 +02:00
Chen Asraf
edcf1aceaa Update README.md 2022-02-14 23:34:59 +02:00
Chen Asraf
d0a0db0f82 Update MIGRATION.md [skip ci] 2022-02-14 23:31:52 +02:00
Chen Asraf
819f84e5ca Merge pull request #23 from chenasraf/dependabot/npm_and_yarn/ansi-regex-5.0.1
Bump ansi-regex from 5.0.0 to 5.0.1
2022-02-14 23:29:41 +02:00
dependabot[bot]
36f8b87514 Bump ansi-regex from 5.0.0 to 5.0.1
Bumps [ansi-regex](https://github.com/chalk/ansi-regex) from 5.0.0 to 5.0.1.
- [Release notes](https://github.com/chalk/ansi-regex/releases)
- [Commits](https://github.com/chalk/ansi-regex/compare/v5.0.0...v5.0.1)

---
updated-dependencies:
- dependency-name: ansi-regex
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-02-14 21:28:17 +00:00
Chen Asraf
d06c0d64b3 bump version: v1.0.0 2022-02-14 23:26:48 +02:00
Chen Asraf
5ab2637485 bump version number 2022-02-14 23:26:48 +02:00
Chen Asraf
0af639254b fail handlebars parse silently 2022-02-14 23:26:48 +02:00
Chen Asraf
391a08ac63 update README.md, default output fix 2022-02-14 23:26:48 +02:00
Chen Asraf
cd34930e04 fix cmd 2022-02-14 23:26:48 +02:00
Chen Asraf
956b00700f update workflows [skip publish] 2022-02-14 23:26:48 +02:00
Chen Asraf
91116bba69 update alpha workflow 2022-02-14 23:26:48 +02:00
Chen Asraf
f4cc44cf17 helpers fix 2022-02-14 23:26:48 +02:00
Chen Asraf
54b90235e5 fix copyright 2022-02-14 23:26:48 +02:00
Chen Asraf
d03d0e0812 added custom helpers 2022-02-14 23:26:48 +02:00
Chen Asraf
5b72b6c166 update deps + add MIGRATION.md 2022-02-14 23:26:48 +02:00
Chen Asraf
d96992cd2d update deps + update cmd requirements 2022-02-14 23:26:48 +02:00
Chen Asraf
925993948a update readme [skip ci] 2022-02-14 23:26:48 +02:00
Chen Asraf
09403e18f1 update readme [skip ci] 2022-02-14 23:26:48 +02:00
Chen Asraf
c2bc8b7606 update docs [skip publish] 2022-02-14 23:26:48 +02:00
Chen Asraf
b1b1aca802 fix basename in some cases 2022-02-14 23:26:48 +02:00
Chen Asraf
2305083c7d run tests on ci [skip publish] 2022-02-14 23:26:48 +02:00
Chen Asraf
8413225202 fix errors, fix nested output 2022-02-14 23:26:48 +02:00
Chen Asraf
559b5ad7f3 remove types from package.json 2022-02-14 23:26:48 +02:00
Chen Asraf
a21a35f773 fix yarn.lock 2022-02-14 23:26:48 +02:00
Chen Asraf
84e6207891 fix log level 0 2022-02-14 23:26:48 +02:00
Chen Asraf
6b57406369 add export for cmd_util 2022-02-14 23:26:48 +02:00
Chen Asraf
53e8bc4cc0 bump version number 2022-02-14 23:26:48 +02:00
Chen Asraf
54848f9c50 update workflows [skip publish] 2022-02-14 23:26:48 +02:00
Chen Asraf
2623b787e6 fixes + add log level [skip publish] 2022-02-14 23:26:48 +02:00
Chen Asraf
ad30ee0c0c fix readme [skip publish] 2022-02-14 23:26:48 +02:00
Chen Asraf
8575b1e0c3 try fix release upload 2022-02-14 23:26:48 +02:00
Chen Asraf
d8aba21d0e try new release version 2022-02-14 23:26:48 +02:00
Chen Asraf
3f2945eaa4 fixed release tarball file location 2022-02-14 23:26:48 +02:00
Chen Asraf
930344656a fixed release tarball file location 2022-02-14 23:26:48 +02:00
Chen Asraf
99c9055208 fix build output files 2022-02-14 23:26:48 +02:00
Chen Asraf
f1698d2a46 add build step 2022-02-14 23:26:48 +02:00
Chen Asraf
c17e6304e6 try to fix workflow 2022-02-14 23:26:48 +02:00
Chen Asraf
535260a0c0 publish: debug mode off, try to fix workflow 2022-02-14 23:26:48 +02:00
Chen Asraf
14988576f3 publish: debug mode on 2022-02-14 23:26:48 +02:00
Chen Asraf
6f03ed9d60 try fix workflow 2022-02-14 23:26:48 +02:00
Chen Asraf
c7749a8d33 update workflows 2021-11-18 13:42:28 +02:00
Chen Asraf
a59f29d71d fix build/publish cmd 2021-11-18 13:38:16 +02:00
Chen Asraf
bc224d93e1 support node 12 for fs package 2021-11-18 13:16:10 +02:00
Chen Asraf
cf923d8889 support node 12 for fs package 2021-11-18 13:14:53 +02:00
Chen Asraf
01e458ee0c update jest config 2021-11-18 13:12:37 +02:00
Chen Asraf
93853712f5 use node 12 2021-11-18 13:08:54 +02:00
Chen Asraf
474a3dcc1f try fix workflow 2021-11-18 13:04:19 +02:00
Chen Asraf
27e84d1093 update workflow 2021-11-18 12:55:07 +02:00
Chen Asraf
a6f25facc0 update workflows 2021-11-18 12:51:16 +02:00
Chen Asraf
3ee66b24f5 build: update workflow 2021-11-18 12:45:37 +02:00
Chen Asraf
0ce19a7e1a build: add workflow 2021-11-18 12:45:37 +02:00
Chen Asraf
7273538d79 fix main field in package.json 2021-11-18 12:45:37 +02:00
Chen Asraf
43b64965e3 v0.7.4 2021-11-18 12:45:37 +02:00
Chen Asraf
4f81654e05 added --quiet flag 2021-11-18 12:45:37 +02:00
Chen Asraf
8fcc7a6dc6 chore: cleanup 2021-11-18 12:45:31 +02:00
Chen Asraf
b4b0de6f2f docs: update README 2021-11-18 12:45:31 +02:00
Chen Asraf
2d5626ca70 improve tests 2021-11-18 12:45:26 +02:00
Chen Asraf
564e821419 maintain directory structure 2021-11-18 12:45:26 +02:00
Chen Asraf
a52f9a0b84 migrate cmd to massarg + update tests 2021-11-18 12:45:26 +02:00
Chen Asraf
aeddd44778 add dry run option 2021-11-18 12:45:26 +02:00
Chen Asraf
7cdf5e461b add cmd args 2021-11-18 12:45:26 +02:00
Chen Asraf
c42a58c2f2 add tests 2021-11-18 12:45:26 +02:00
Chen Asraf
d0c0152717 remove excess files 2021-11-18 12:45:26 +02:00
Chen Asraf
208ee307c8 code splitting 2021-11-18 12:45:16 +02:00
Chen Asraf
54834909b9 major refactor 2021-11-18 12:45:16 +02:00
Chen Asraf
40b592072e Create FUNDING.yml 2021-10-11 18:20:18 +03:00
Chen Asraf
3cb9a6fcd8 fix main field in package.json 2021-09-26 11:42:52 +03:00
Chen Asraf
12974b5561 v0.7.4 2021-09-26 11:37:02 +03:00
Chen Asraf
7f98d469a3 added --quiet flag 2021-09-26 11:28:38 +03:00
Chen Asraf
cd25b04886 Update README.md 2021-05-20 11:03:19 +03:00
Chen Asraf
5cd637f41f Merge pull request #4 from chenasraf/dependabot/npm_and_yarn/handlebars-4.7.7
Bump handlebars from 4.7.6 to 4.7.7
2021-05-20 10:55:41 +03:00
Chen Asraf
0a4467ae5f Merge pull request #5 from chenasraf/dependabot/npm_and_yarn/url-parse-1.5.1
Bump url-parse from 1.4.7 to 1.5.1
2021-05-20 10:55:33 +03:00
Chen Asraf
713a0ed44f Merge pull request #6 from chenasraf/dependabot/npm_and_yarn/lodash-4.17.21
Bump lodash from 4.17.20 to 4.17.21
2021-05-20 10:55:18 +03:00
Chen Asraf
edec2d1c26 Merge pull request #7 from chenasraf/dependabot/npm_and_yarn/hosted-git-info-2.8.9
Bump hosted-git-info from 2.8.8 to 2.8.9
2021-05-20 10:55:03 +03:00
dependabot[bot]
2e12907412 Bump hosted-git-info from 2.8.8 to 2.8.9
Bumps [hosted-git-info](https://github.com/npm/hosted-git-info) from 2.8.8 to 2.8.9.
- [Release notes](https://github.com/npm/hosted-git-info/releases)
- [Changelog](https://github.com/npm/hosted-git-info/blob/v2.8.9/CHANGELOG.md)
- [Commits](https://github.com/npm/hosted-git-info/compare/v2.8.8...v2.8.9)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-09 23:31:02 +00:00
dependabot[bot]
5b7e0e30f1 Bump lodash from 4.17.20 to 4.17.21
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.20 to 4.17.21.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.20...4.17.21)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-08 04:50:00 +00:00
dependabot[bot]
09238300cd Bump url-parse from 1.4.7 to 1.5.1
Bumps [url-parse](https://github.com/unshiftio/url-parse) from 1.4.7 to 1.5.1.
- [Release notes](https://github.com/unshiftio/url-parse/releases)
- [Commits](https://github.com/unshiftio/url-parse/compare/1.4.7...1.5.1)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-07 10:20:13 +00:00
dependabot[bot]
552614ca3f Bump handlebars from 4.7.6 to 4.7.7
Bumps [handlebars](https://github.com/wycats/handlebars.js) from 4.7.6 to 4.7.7.
- [Release notes](https://github.com/wycats/handlebars.js/releases)
- [Changelog](https://github.com/handlebars-lang/handlebars.js/blob/master/release-notes.md)
- [Commits](https://github.com/wycats/handlebars.js/compare/v4.7.6...v4.7.7)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-07 02:51:26 +00:00
Chen Asraf
813f706cf0 update readme 2021-04-19 22:30:22 +03:00
Chen Asraf
1bc2221472 update readme 2021-04-19 22:28:22 +03:00
Chen Asraf
f07affa124 add basename to output config function (fixes #3) 2021-04-19 22:24:21 +03:00
Chen Asraf
ce22a2c34c disable overwriting files + parse JSON for locals 2021-02-28 01:38:51 +02:00
Chen Asraf
7c0c347002 fix: binary files 2021-02-01 15:24:57 +02:00
Chen Asraf
977288ae5a build: upgrade packages 2021-02-01 15:24:26 +02:00
Chen Asraf
4afafa5a4a fix: support deeper file structure 2021-02-01 15:23:50 +02:00
Chen Asraf
7bee2a51c7 0.5.0 2019-02-27 16:53:42 +02:00
Chen Asraf
d4c049baaf v0.5.0 2019-02-27 16:53:33 +02:00
Chen Asraf
06590c4b6e Fixed output argument + updated README 2019-02-27 16:20:33 +02:00
Chen Asraf
c4f2dfb04f v0.4.5 2019-02-27 16:20:12 +02:00
Chen Asraf
a410b79195 Improved docs 2019-02-27 16:14:56 +02:00
Chen Asraf
71d544aff4 v0.4.4 2019-02-27 16:14:22 +02:00
Chen Asraf
20389d7b33 v0.4.3 2019-02-27 16:12:43 +02:00
Chen Asraf
d7a4362725 mapfile 2019-02-25 19:54:12 +02:00
Chen Asraf
0a2d7c08f3 v0.4.2 2019-02-25 18:17:15 +02:00
Chen Asraf
a92c471243 bugfixes 2019-02-25 18:17:03 +02:00
Chen Asraf
07b1c4b1f0 mapfile 2019-02-25 18:01:10 +02:00
Chen Asraf
ec91fbf639 v0.4.1 2019-02-25 18:00:39 +02:00
Chen Asraf
85aa9f953b Bugfix: dotfiles 2019-02-25 18:00:23 +02:00
Chen Asraf
d6195c6c1d added 'createSubFolder' option, cleaned up CMD file 2019-02-25 17:49:03 +02:00
Chen Asraf
b14e3d2d76 Update README.md 2018-01-15 02:25:29 +02:00
Chen Asraf
fa2ddca57e v0.3.1 2018-01-15 02:23:14 +02:00
Chen Asraf
686b0bf227 Update README.md 2018-01-15 02:23:05 +02:00
Chen Asraf
0be29dd89e v0.3.0 2018-01-15 02:19:23 +02:00
Chen Asraf
4f29a612a3 cleanups 2018-01-15 02:18:39 +02:00
Chen Asraf
14b60ffc79 output is optional 2018-01-15 02:16:51 +02:00
Chen Asraf
1275743764 Update README.md 2018-01-07 02:25:27 +02:00
Chen Asraf
b09299b432 Uodate README.md 2018-01-05 04:53:41 +02:00
Chen Asraf
45e8de3034 Rename 2018-01-05 04:52:52 +02:00
Chen Asraf
a3a77e2ab5 Update README.md 2018-01-05 04:52:26 +02:00
39 changed files with 4425 additions and 3094 deletions

5
.editorconfig Normal file
View File

@@ -0,0 +1,5 @@
[*]
tab_width = 2
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true

12
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,12 @@
# These are supported funding model platforms
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: casraf
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
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']

52
.github/workflows/alpha.yml vendored Normal file
View File

@@ -0,0 +1,52 @@
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

50
.github/workflows/main.yml vendored Normal file
View File

@@ -0,0 +1,50 @@
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

17
.github/workflows/pull_requests.yml vendored Normal file
View File

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

5
.gitignore vendored
View File

@@ -57,4 +57,7 @@ typings/
# dotenv environment variables file
.env
examples/test-output
examples/test-output/**/*
dist/
.DS_Store
tmp/

5
.prettierrc Normal file
View File

@@ -0,0 +1,5 @@
{
"semi": false,
"printWidth": 120,
"tabWidth": 2
}

12
.vscode/settings.json vendored
View File

@@ -1,3 +1,13 @@
{
"typescript.tsdk": "./node_modules/typescript/lib"
"typescript.tsdk": "./node_modules/typescript/lib",
"npm.packageManager": "yarn",
"cSpell.words": [
"massarg",
"nobrace",
"nocomment",
"nodir",
"noext",
"nonegate",
"subdir"
]
}

56
.vscode/tasks.json vendored
View File

@@ -1,7 +1,53 @@
{
"version": "0.1.0",
"command": "webpack",
"isShellCommand": true,
"args": [],
"showOutput": "always"
"version": "2.0.0",
"tasks": [
{
"script": "build",
"label": "build",
"type": "npm",
"problemMatcher": [],
},
{
"script": "dev",
"label": "dev",
"type": "npm",
"problemMatcher": [],
},
{
"script": "start",
"label": "start",
"type": "npm",
"problemMatcher": [],
},
{
"script": "test",
"label": "test",
"type": "npm",
"problemMatcher": [],
},
{
"command": "yarn test --watchAll",
"label": "yarn test --watchAll",
"type": "shell",
"problemMatcher": [],
},
{
"script": "cmd",
"label": "cmd",
"type": "npm",
"problemMatcher": [],
},
{
"script": "build-test",
"label": "build-test",
"type": "npm",
"problemMatcher": [],
},
{
"script": "build-cmd",
"label": "build-cmd",
"type": "npm",
"problemMatcher": [],
},
],
}

21
MIGRATION.md Normal file
View File

@@ -0,0 +1,21 @@
# Migrating from 0.x to 1.0
In Simple Scaffold v1.0, the entire codebase was overhauled, yet usage remains mostly the same
between versions. With these notable exceptions:
- Some of the argument names have changed
- Template syntax has been improved
- The command to run Scaffold has been simplified from `new SimpleScaffold(opts).run()` to `SimpleScaffold(opts)`, which now returns a promise that you can await to know when the process has been completed.
## Argument changes
- `locals` has been renamed to `data`. The appropriate command line args have been updated as
well to `--data` | `-d`.
## Template syntax changes
Simple Scaffold still uses Handlebars.js to handle template content and file names. However, helpers
have been added to remove the need for you to pre-process the template data on simple use-cases such
as case type manipulation (converting to camel case, snake case, etc)
See the readme for the full information on how to use these helpers and which are available.

305
README.md
View File

@@ -1,47 +1,294 @@
# scaffolder
Scaffolder allows you to create your structured files based on templates.
# simple-scaffold
Simple Scaffold allows you to create your structured files based on templates.
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.
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.
It's a simple way to easily create reusable components, common class files to start writing from,
or even entire app structures.
## Install
You can either use it as a command line tool or import into your own code and run from there.
You can install simple-scaffold globally like so:
You can either use it as a command line tool or import into your own code and run from there.
```bash
# npm
npm install -g simple-scaffold
npm install [-g] simple-scaffold
# yarn
yarn global add simple-scaffold
yarn [global] add simple-scaffold
# run without installing
npx simple-scaffold <...args>
```
## Use as a command line tool
#### Command line options
### Command Line Options
```plaintext
Usage: simple-scaffold [options]
Create structured files based on templates.
Options:
--help|-h Display help information
--name|-n Name to be passed to the generated files. {{name}} and
{{Name}} inside contents and file names will be replaced
accordingly.
--output|-o Path to output to. If --create-sub-folder is enabled,
the subfolder will be created inside this path.
(default: current dir)
--templates|-t Template files to use as input. You may provide multiple
files, each of which can be a relative or absolute path, or a
glob pattern for multiple file matching easily.
--overwrite|-w Enable to override output files, even if they already
exist. (default: false)
--data|-d Add custom data to the templates. By default, only your
app name is included.
--create-sub-folder|-s Create subfolder with the input name
(default: false)
--sub-folder-name-helper|-sh Default helper to apply to subfolder name when using
`--create-sub-folder true`.
--quiet|-q Suppress output logs (Same as --verbose 0)
(default: false)
--verbose|-v Determine amount of logs to display. The values are:
0 (none) | 1 (debug) | 2 (info) | 3 (warn) | 4
(error). The provided level will display messages of
the same level or higher. (default:
2)
--dry-run|-dr Don't emit files. This is good for testing your
scaffolds and making sure they don't fail, without having to
write actual file contents or create directories.
(default: false)
```
You can also add this as a script in your `package.json`:
```json
{
...
"scripts": {
...
"scaffold": "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 --template scaffolds/component \
--output src/components \
--locals myProp="propname",myVal=123
simple-scaffold MyComponent \
-t project/scaffold/**/* \
-o src/components \
-d '{"className": "myClassName"}'
MyComponent
```
##### `--template`
A glob pattern of template files to load.
### Example Scaffold Input
#### Input Directory structure
A template file may be of any type and extension, and supports [Handlebars](https://handlebarsjs.com) as a parsing engine for the file names and contents, so you may customize both with variables from your configuration.
You may load more than one template list by simple adding more `--template` arguments.
##### `--output`
The output directory to put the new files in. They will attempt to maintain their regular structure as they are found, if possible.
Your new scaffold will be placed under a directory with the scaffold name from the argumemts.
You may also pass a function to transform the output path for each file individually.
This function takes 2 arguments: filename, and base glob path
##### `--locals`
You may set local variables to be pushed into the template and replaced.
The format for the value of this argument is:
```plaintext
- project
- scaffold
- {{Name}}.js
- src
- components
- ...
```
property=value[,property=value[,...]]
#### Contents of `project/scaffold/{{Name}}.jsx`
```js
const React = require('react')
module.exports = function {{Name}}(props) {
return (
<div className="{{className}}">{{Name}} Component</div>
)
}
```
### Example Scaffold Output
### Output directory structure
```plaintext
- project
- src
- components
- MyComponent
- MyComponent.js
- ...
```
With `createSubFolder = false`:
```plaintext
- project
- src
- components
- MyComponent.js
- ...
```
#### Contents of `project/scaffold/MyComponent/MyComponent.jsx`
```js
const React = require("react")
module.exports = function MyComponent(props) {
return (
<div className="myClassName">MyComponent Component</div>
)
}
```
## Contributing
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.
If you are a developer and want to contribute code, here are some starting tips:
1. Fork this repository
2. Run `yarn install`
3. Run `yarn dev` to start file watch mode
4. Make any changes you would like
5. Create tests for your changes
6. Update the relevant documentation (readme, code comments, type comments)
7. Create a PR on upstream
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 test` to run tests
- Use `yarn cmd` to use the CLI feature of Simple Scaffold from within the root directory,
enabling you to test different behaviors. See `yarn cmd -h` for more information.
> This requires an updated build, and does not trigger one itself. Either use `yarn dev` to watch
> for changes and build, or `yarn build` before running this, or use `yarn build-cmd` instead,
> which triggers a build right before running the command with the rest of the given arguments.

3
babel.config.js Normal file
View File

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

86
cmd.ts
View File

@@ -1,86 +0,0 @@
import SimpleScaffold from './scaffold'
import * as fs from 'fs'
import IScaffold from './index'
const args = process.argv.slice(2)
class ScaffoldCmd {
private config: IScaffold.IConfig
constructor() {
this.config = this.getOptionsFromArgs()
}
private getOptionsFromArgs(): IScaffold.IConfig {
let skipNext = false
const options = {} as any
args.forEach((arg, i) => {
if (skipNext) {
skipNext = false
return
}
if (arg.slice(0, 2) == '--') {
skipNext = true
let value: string
if (arg.indexOf('=') >= 0) {
value = arg.split('=').slice(1).join('')
} else if (args.length >= i + 1 && args[i + 1] && args[i + 1].slice(0, 2) !== '--') {
value = args[i + 1]
} else {
value = 'true'
}
const argName = arg.slice(2)
options[argName] = this.getArgValue(argName, value, options)
} else {
if (!options.name) {
options.name = arg
} else {
throw new TypeError(`Invalid argument: ${arg}`)
}
}
})
if (!['name', 'templates', 'output'].every(o => options[o] !== undefined)) {
throw new Error(`Config is missing keys: ${JSON.stringify(options)}`)
}
return options
}
private getArgValue(arg: string, value: string, options: IScaffold.IConfig) {
switch (arg) {
case 'templates':
return (options.templates || []).concat([value])
case 'output':
return value
case 'locals':
const split = value.split(',')
const locals = options.locals || {}
for (const item of split) {
const [k, v] = item.split('=')
locals[k] = v
}
return locals
default:
throw TypeError(`arguments invalid for config: arg=\`${arg}\`, value=\`${value}\``)
}
}
public run() {
const config: IScaffold.IConfig = this.config
console.info('Config:', config)
const scf = new SimpleScaffold({
name: config.name,
templates: config.templates,
output: config.output,
locals: config.locals,
}).run()
}
}
new ScaffoldCmd().run()

3
dist/cmd.js vendored

File diff suppressed because one or more lines are too long

1
dist/cmd.js.map vendored

File diff suppressed because one or more lines are too long

View File

@@ -1,13 +0,0 @@
import IScaffold from './index';
declare class SimpleScaffold {
private config;
private locals;
constructor(config: IScaffold.IConfig);
private parseLocals(text);
private fileList(input);
private getFileContents(filePath);
private getOutputPath(file, basePath);
private writeFile(filePath, fileContents);
run(): void;
}
export default SimpleScaffold;

0
dist/dist/test.d.ts vendored
View File

2
dist/scaffold.js vendored
View File

@@ -1,2 +0,0 @@
!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.library=t():e.library=t()}(this,function(){return function(e){function t(o){if(n[o])return n[o].exports;var r=n[o]={i:o,l:!1,exports:{}};return e[o].call(r.exports,r,r.exports,t),r.l=!0,r.exports}var n={};return t.m=e,t.c=n,t.d=function(e,n,o){t.o(e,n)||Object.defineProperty(e,n,{configurable:!1,enumerable:!0,get:o})},t.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(n,"a",n),n},t.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},t.p="",t(t.s=1)}([function(e,t){e.exports=require("path")},function(e,t,n){"use strict";var o=this&&this.__generator||function(e,t){function n(e){return function(t){return o([e,t])}}function o(n){if(r)throw new TypeError("Generator is already executing.");for(;c;)try{if(r=1,i&&(s=i[2&n[0]?"return":n[0]?"throw":"next"])&&!(s=s.call(i,n[1])).done)return s;switch(i=0,s&&(n=[0,s.value]),n[0]){case 0:case 1:s=n;break;case 4:return c.label++,{value:n[1],done:!1};case 5:c.label++,i=n[1],n=[0];continue;case 7:n=c.ops.pop(),c.trys.pop();continue;default:if(s=c.trys,!(s=s.length>0&&s[s.length-1])&&(6===n[0]||2===n[0])){c=0;continue}if(3===n[0]&&(!s||n[1]>s[0]&&n[1]<s[3])){c.label=n[1];break}if(6===n[0]&&c.label<s[1]){c.label=s[1],s=n;break}if(s&&c.label<s[2]){c.label=s[2],c.ops.push(n);break}s[2]&&c.ops.pop(),c.trys.pop();continue}n=t.call(e,c)}catch(e){n=[6,e],i=0}finally{r=s=0}if(5&n[0])throw n[1];return{value:n[0]?n[1]:void 0,done:!0}}var r,i,s,a,c={label:0,sent:function(){if(1&s[0])throw s[1];return s[1]},trys:[],ops:[]};return a={next:n(0),throw:n(1),return:n(2)},"function"==typeof Symbol&&(a[Symbol.iterator]=function(){return this}),a};Object.defineProperty(t,"__esModule",{value:!0});var r=n(2),i=n(0),s=n(3),a=n(4),c=function(){function e(e){this.locals={};var t={name:"scaffold",templates:[],output:process.cwd()};this.config=Object.assign({},t,e);var n={Name:this.config.name[0].toUpperCase()+this.config.name.slice(1),name:this.config.name[0].toLowerCase()+this.config.name.slice(1)};this.locals=Object.assign({},n,e.locals)}return e.prototype.parseLocals=function(e){return a.compile(e,{noEscape:!0})(this.locals)},e.prototype.fileList=function(e){var t,n,r,a,c,u,l,f,p;return o(this,function(o){switch(o.label){case 0:t=0,n=e,o.label=1;case 1:if(!(t<n.length))return[3,6];r=n[t],a=s.sync(r).map(function(e){return"/"==e[0]?e:i.join(process.cwd(),e)}),c=r.indexOf("*"),u=r,c>=0&&(u=r.slice(0,c-1)),l=0,f=a,o.label=2;case 2:return l<f.length?(p=f[l],[4,{base:u,file:p}]):[3,5];case 3:o.sent(),o.label=4;case 4:return l++,[3,2];case 5:return t++,[3,1];case 6:return[2]}})},e.prototype.getFileContents=function(e){return r.readFileSync(e).toString()},e.prototype.getOutputPath=function(e,t){var n;if("function"==typeof this.config.output)n=this.config.output(e,t);else{var o=this.config.output+"/"+this.config.name+"/",r=e.indexOf(t),i=e;r>=0&&(i=e.slice(r+t.length+1)),n=o+i}return this.parseLocals(n)},e.prototype.writeFile=function(e,t){r.existsSync(i.dirname(e))||r.mkdirSync(i.dirname(e)),console.info("Writing file:",e),r.writeFileSync(e,t,{encoding:"utf-8"})},e.prototype.run=function(){console.log("Generating scaffold: "+this.config.name+"...");for(var e,t=this.fileList(this.config.templates),n=0;e=t.next().value;){n++;var o=e.file,r=e.base,i=this.getOutputPath(o,r),s=this.getFileContents(o),a=this.parseLocals(s);this.writeFile(i,a),console.info("Parsing:",{file:o,base:r,outputPath:i,outputContents:a.replace("\n","\\n")})}if(!n)throw new Error("No files to scaffold!");console.log("Done")},e}();t.default=c},function(e,t){e.exports=require("fs")},function(e,t){e.exports=require("glob")},function(e,t){e.exports=require("handlebars")}])});
//# sourceMappingURL=scaffold.js.map

File diff suppressed because one or more lines are too long

2
dist/test.js vendored
View File

@@ -1,2 +0,0 @@
!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.library=t():e.library=t()}(this,function(){return function(e){function t(o){if(n[o])return n[o].exports;var r=n[o]={i:o,l:!1,exports:{}};return e[o].call(r.exports,r,r.exports,t),r.l=!0,r.exports}var n={};return t.m=e,t.c=n,t.d=function(e,n,o){t.o(e,n)||Object.defineProperty(e,n,{configurable:!1,enumerable:!0,get:o})},t.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(n,"a",n),n},t.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},t.p="",t(t.s=5)}([function(e,t){e.exports=require("path")},function(e,t,n){"use strict";var o=this&&this.__generator||function(e,t){function n(e){return function(t){return o([e,t])}}function o(n){if(r)throw new TypeError("Generator is already executing.");for(;u;)try{if(r=1,i&&(s=i[2&n[0]?"return":n[0]?"throw":"next"])&&!(s=s.call(i,n[1])).done)return s;switch(i=0,s&&(n=[0,s.value]),n[0]){case 0:case 1:s=n;break;case 4:return u.label++,{value:n[1],done:!1};case 5:u.label++,i=n[1],n=[0];continue;case 7:n=u.ops.pop(),u.trys.pop();continue;default:if(s=u.trys,!(s=s.length>0&&s[s.length-1])&&(6===n[0]||2===n[0])){u=0;continue}if(3===n[0]&&(!s||n[1]>s[0]&&n[1]<s[3])){u.label=n[1];break}if(6===n[0]&&u.label<s[1]){u.label=s[1],s=n;break}if(s&&u.label<s[2]){u.label=s[2],u.ops.push(n);break}s[2]&&u.ops.pop(),u.trys.pop();continue}n=t.call(e,u)}catch(e){n=[6,e],i=0}finally{r=s=0}if(5&n[0])throw n[1];return{value:n[0]?n[1]:void 0,done:!0}}var r,i,s,a,u={label:0,sent:function(){if(1&s[0])throw s[1];return s[1]},trys:[],ops:[]};return a={next:n(0),throw:n(1),return:n(2)},"function"==typeof Symbol&&(a[Symbol.iterator]=function(){return this}),a};Object.defineProperty(t,"__esModule",{value:!0});var r=n(2),i=n(0),s=n(3),a=n(4),u=function(){function e(e){this.locals={};var t={name:"scaffold",templates:[],output:process.cwd()};this.config=Object.assign({},t,e);var n={Name:this.config.name[0].toUpperCase()+this.config.name.slice(1),name:this.config.name[0].toLowerCase()+this.config.name.slice(1)};this.locals=Object.assign({},n,e.locals)}return e.prototype.parseLocals=function(e){return a.compile(e,{noEscape:!0})(this.locals)},e.prototype.fileList=function(e){var t,n,r,a,u,c,l,f,p;return o(this,function(o){switch(o.label){case 0:t=0,n=e,o.label=1;case 1:if(!(t<n.length))return[3,6];r=n[t],a=s.sync(r).map(function(e){return"/"==e[0]?e:i.join(process.cwd(),e)}),u=r.indexOf("*"),c=r,u>=0&&(c=r.slice(0,u-1)),l=0,f=a,o.label=2;case 2:return l<f.length?(p=f[l],[4,{base:c,file:p}]):[3,5];case 3:o.sent(),o.label=4;case 4:return l++,[3,2];case 5:return t++,[3,1];case 6:return[2]}})},e.prototype.getFileContents=function(e){return r.readFileSync(e).toString()},e.prototype.getOutputPath=function(e,t){var n;if("function"==typeof this.config.output)n=this.config.output(e,t);else{var o=this.config.output+"/"+this.config.name+"/",r=e.indexOf(t),i=e;r>=0&&(i=e.slice(r+t.length+1)),n=o+i}return this.parseLocals(n)},e.prototype.writeFile=function(e,t){r.existsSync(i.dirname(e))||r.mkdirSync(i.dirname(e)),console.info("Writing file:",e),r.writeFileSync(e,t,{encoding:"utf-8"})},e.prototype.run=function(){console.log("Generating scaffold: "+this.config.name+"...");for(var e,t=this.fileList(this.config.templates),n=0;e=t.next().value;){n++;var o=e.file,r=e.base,i=this.getOutputPath(o,r),s=this.getFileContents(o),a=this.parseLocals(s);this.writeFile(i,a),console.info("Parsing:",{file:o,base:r,outputPath:i,outputContents:a.replace("\n","\\n")})}if(!n)throw new Error("No files to scaffold!");console.log("Done")},e}();t.default=u},function(e,t){e.exports=require("fs")},function(e,t){e.exports=require("glob")},function(e,t){e.exports=require("handlebars")},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var o=n(1),r=n(0),i=r.join(process.cwd(),"examples");new o.default({templates:[i+"/test-input/Component/**/*"],output:i+"/test-output",locals:{property:"myProp",value:'"value"'}}).run()}])});
//# sourceMappingURL=test.js.map

1
dist/test.js.map vendored

File diff suppressed because one or more lines are too long

View File

View File

@@ -0,0 +1 @@
{{name}}

View File

@@ -1,18 +1,16 @@
import * as React from 'react'
import * as css from './{{Name}}.css'
import * as React from "react"
import * as css from "./{{Name}}.css"
class {{Name}} extends React.Component<any> {
private {{property}}
private {{ property }}
constructor(props: any) {
super(props)
this.{{property}} = {{value}}
this.{{ property }} = {{ value }}
}
public render() {
return (
<div className={ css.{{Name}} } />
)
return <div className={ css.{{Name}} } />
}
}

22
index.d.ts vendored
View File

@@ -1,22 +0,0 @@
declare namespace IScaffold {
export interface IConfig {
name?: string
templates: string[]
output: string | ((path: string, base: string) => string)
locals?: any
}
export interface IReplacement {
find: string | RegExp
replace(): string
[other: string]: any
}
export interface IFileRepr {
base: string
file: string
}
}
export default IScaffold

196
jest.config.ts Normal file
View File

@@ -0,0 +1,196 @@
/*
* For a detailed explanation regarding each configuration property and type check, visit:
* https://jestjs.io/docs/configuration
*/
export default {
// All imported modules in your tests should be mocked automatically
// automock: false,
// Stop running tests after `n` failures
// bail: 0,
// The directory where Jest should store its cached dependency information
// cacheDirectory: "/private/var/folders/q9/0mns8fgd00b4t5j5lq2wh2yh0000gn/T/jest_dx",
// Automatically clear mock calls and instances between every test
clearMocks: true,
// Indicates whether the coverage information should be collected while executing the test
collectCoverage: true,
// An array of glob patterns indicating a set of files for which coverage information should be collected
// collectCoverageFrom: undefined,
// The directory where Jest should output its coverage files
coverageDirectory: "coverage",
// An array of regexp pattern strings used to skip coverage collection
// coveragePathIgnorePatterns: [
// "/node_modules/"
// ],
// Indicates which provider should be used to instrument code for coverage
coverageProvider: "v8",
// A list of reporter names that Jest uses when writing coverage reports
// coverageReporters: [
// "json",
// "text",
// "lcov",
// "clover"
// ],
// An object that configures minimum threshold enforcement for coverage results
// coverageThreshold: undefined,
// A path to a custom dependency extractor
// dependencyExtractor: undefined,
// Make calling deprecated APIs throw helpful error messages
// errorOnDeprecated: false,
// Force coverage collection from ignored files using an array of glob patterns
// forceCoverageMatch: [],
// A path to a module which exports an async function that is triggered once before all test suites
// globalSetup: undefined,
// A path to a module which exports an async function that is triggered once after all test suites
// globalTeardown: undefined,
// A set of global variables that need to be available in all test environments
// globals: {},
// The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers.
// maxWorkers: "50%",
// An array of directory names to be searched recursively up from the requiring module's location
// moduleDirectories: [
// "node_modules"
// ],
// An array of file extensions your modules use
// moduleFileExtensions: [
// "js",
// "jsx",
// "ts",
// "tsx",
// "json",
// "node"
// ],
// A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
// moduleNameMapper: {},
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
modulePathIgnorePatterns: ["<rootDir>/dist"],
// Activates notifications for test results
// notify: false,
// An enum that specifies notification mode. Requires { notify: true }
// notifyMode: "failure-change",
// A preset that is used as a base for Jest's configuration
preset: "ts-jest",
// Run tests from one or more projects
// projects: undefined,
// Use this configuration option to add custom reporters to Jest
// reporters: undefined,
// Automatically reset mock state between every test
// resetMocks: false,
// Reset the module registry before running each individual test
// resetModules: false,
// A path to a custom resolver
// resolver: undefined,
// Automatically restore mock state between every test
// restoreMocks: false,
// The root directory that Jest should scan for tests and modules within
// rootDir: undefined,
// A list of paths to directories that Jest should use to search for files in
// roots: [
// "<rootDir>"
// ],
// Allows you to use a custom runner instead of Jest's default test runner
// runner: "jest-runner",
// The paths to modules that run some code to configure or set up the testing environment before each test
// setupFiles: [],
// A list of paths to modules that run some code to configure or set up the testing framework before each test
// setupFilesAfterEnv: [],
// The number of seconds after which a test is considered as slow and reported as such in the results.
// slowTestThreshold: 5,
// A list of paths to snapshot serializer modules Jest should use for snapshot testing
// snapshotSerializers: [],
// The test environment that will be used for testing
testEnvironment: "node",
// Options that will be passed to the testEnvironment
// testEnvironmentOptions: {},
// Adds a location field to test results
// testLocationInResults: false,
// The glob patterns Jest uses to detect test files
// testMatch: [
// "**/__tests__/**/*.[jt]s?(x)",
// "**/?(*.)+(spec|test).[tj]s?(x)"
// ],
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
// testPathIgnorePatterns: [
// "/node_modules/"
// ],
// The regexp pattern or array of patterns that Jest uses to detect test files
// testRegex: [],
// This option allows the use of a custom results processor
// testResultsProcessor: undefined,
// This option allows use of a custom test runner
// testRunner: "jest-circus/runner",
// This option sets the URL for the jsdom environment. It is reflected in properties such as location.href
// testURL: "http://localhost",
// Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout"
// timers: "real",
// A map from regular expressions to paths to transformers
// transform: {},
// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
// transformIgnorePatterns: [
// "/node_modules/",
// "\\.pnp\\.[^\\/]+$"
// ],
// An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
// unmockedModulePathPatterns: undefined,
// Indicates whether each individual test should be reported during the run
// verbose: undefined,
// An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
// watchPathIgnorePatterns: [],
// Whether to use watchman for file crawling
// watchman: true,
// extensionsToTreatAsEsm: [".ts"],
}

View File

@@ -1,32 +1,42 @@
{
"name": "simple-scaffold",
"version": "0.2.0",
"version": "1.0.2",
"description": "Create files based on templates",
"repository": "https://github.com/chenasraf/simple-scaffold.git",
"author": "Chen Asraf <inbox@casraf.com>",
"license": "MIT",
"main": "dist/scaffold.js",
"bin": "dist/cmd.js",
"types": "index.d.ts",
"main": "index.js",
"bin": "cmd.js",
"scripts": {
"build": "NODE_ENV=${NODE_ENV:-production} webpack -p && chmod -R +x ./dist",
"dev": "webpack --watch",
"clean": "rm -rf dist/",
"build": "yarn clean && tsc && chmod -R +x ./dist && cp ./package.json ./README.md ./dist/",
"dev": "tsc --watch",
"start": "node dist/scaffold.js",
"test": "node dist/test.js",
"cmd": "dist/cmd.js",
"test": "jest --verbose",
"cmd": "node --trace-warnings dist/cmd.js",
"build-test": "yarn build && yarn test",
"build-cmd": "yarn build && yarn cmd"
},
"dependencies": {
"@types/glob": "^5.0.34",
"@types/handlebars": "^4.0.36",
"@types/node": "^8.0.50",
"glob": "^7.1.2",
"handlebars": "^4.0.11",
"ts-loader": "^3.1.1",
"typescript": "^2.6.1",
"webpack": "^3.8.1",
"webpack-dev-server": "^2.9.4",
"webpack-node-externals": "^1.6.0"
"args": "^5.0.1",
"chalk": "^4.1.2",
"glob": "^7.1.3",
"handlebars": "^4.7.7",
"lodash": "^4.17.21",
"massarg": "^1.0.5",
"util.promisify": "^1.1.1"
},
"devDependencies": {
"@types/args": "^3.0.1",
"@types/glob": "^7.1.1",
"@types/jest": "^26.0.24",
"@types/lodash": "^4.14.171",
"@types/mock-fs": "^4.13.1",
"@types/node": "^14.14.22",
"jest": "^27.0.6",
"mock-fs": "^5.0.0",
"ts-jest": "^27.0.3",
"ts-node": "^10.1.0",
"typescript": "^4.3.5"
}
}

View File

@@ -1,103 +0,0 @@
import * as fs from 'fs'
import * as path from 'path'
import IScaffold from './index'
import * as glob from 'glob'
import * as handlebars from 'handlebars'
class SimpleScaffold {
private config: IScaffold.IConfig
private locals = {} as any
constructor(config: IScaffold.IConfig) {
const DefaultConfig: IScaffold.IConfig = {
name: 'scaffold',
templates: [],
output: process.cwd(),
}
this.config = (Object as any).assign({}, DefaultConfig, config)
const DefaultLocals = {
Name: this.config.name![0].toUpperCase() + this.config.name!.slice(1),
name: this.config.name![0].toLowerCase() + this.config.name!.slice(1)
}
this.locals = (Object as any).assign({}, DefaultLocals, config.locals)
}
private parseLocals(text: string): string {
const template = handlebars.compile(text, {
noEscape: true
})
return template(this.locals)
}
private *fileList(input: string[]): IterableIterator<IScaffold.IFileRepr> {
for (const checkPath of input) {
const files = glob.sync(checkPath).map(g => g[0] == '/' ? g : path.join(process.cwd(), g))
const idx = checkPath.indexOf('*')
let cleanCheckPath = checkPath
if (idx >= 0) {
cleanCheckPath = checkPath.slice(0, idx - 1)
}
for (const file of files) {
yield {base: cleanCheckPath, file}
}
}
}
private getFileContents(filePath: string): string {
return fs.readFileSync(filePath).toString()
}
private getOutputPath(file: string, basePath: string): string {
let out
if (typeof this.config.output === 'function') {
out = this.config.output(file, basePath)
} else {
const outputDir = this.config.output + `/${this.config.name}/`
const idx = file.indexOf(basePath)
let relativeFilePath = file
if (idx >= 0) {
relativeFilePath = file.slice(idx + basePath.length + 1)
}
out = outputDir + relativeFilePath
}
return this.parseLocals(out)
}
private writeFile(filePath: string, fileContents: string): void {
if (!fs.existsSync(path.dirname(filePath))) {
fs.mkdirSync(path.dirname(filePath))
}
console.info('Writing file:', filePath)
fs.writeFileSync(filePath, fileContents, { encoding: 'utf-8' })
}
public run(): void {
console.log(`Generating scaffold: ${this.config.name}...`)
const templates = this.fileList(this.config.templates)
let fileConf, count = 0
while (fileConf = templates.next().value) {
count++
const {file, base} = fileConf
const outputPath = this.getOutputPath(file, base)
const contents = this.getFileContents(file)
const outputContents = this.parseLocals(contents)
this.writeFile(outputPath, outputContents)
console.info('Parsing:', {file, base, outputPath, outputContents: outputContents.replace("\n", "\\n")})
}
if (!count) {
throw new Error('No files to scaffold!')
}
console.log('Done')
}
}
export default SimpleScaffold

102
src/cmd.ts Normal file
View File

@@ -0,0 +1,102 @@
#!/usr/bin/env node
import massarg from "massarg"
import chalk from "chalk"
import { LogLevel, ScaffoldCmdConfig } from "./types"
import { Scaffold } from "./scaffold"
export function parseCliArgs(args = process.argv.slice(2)) {
return (
massarg<ScaffoldCmdConfig & { help: boolean; extras: string[] }>()
.main(Scaffold)
.option({
name: "name",
aliases: ["n"],
description:
"Name to be passed to the generated files. {{name}} and {{Name}} inside contents and file names will be replaced accordingly.",
isDefault: true,
required: true,
})
.option({
name: "output",
aliases: ["o"],
description: `Path to output to. If --create-sub-folder is enabled, the subfolder will be created inside this path. ${chalk.reset`${chalk.white`(default: current dir)`}`}`,
required: true,
})
.option({
name: "templates",
aliases: ["t"],
array: true,
description:
"Template files to use as input. You may provide multiple files, each of which can be a relative or absolute path, " +
"or a glob pattern for multiple file matching easily.",
required: true,
})
.option({
name: "overwrite",
aliases: ["w"],
boolean: true,
defaultValue: false,
description: "Enable to override output files, even if they already exist.",
})
.option({
name: "data",
aliases: ["d"],
description: "Add custom data to the templates. By default, only your app name is included.",
parse: (v) => JSON.parse(v),
})
.option({
name: "create-sub-folder",
aliases: ["s"],
boolean: true,
defaultValue: false,
description: "Create subfolder with the input name",
})
.option({
name: "sub-folder-name-helper",
aliases: ["sh"],
description: "Default helper to apply to subfolder name when using `--create-sub-folder true`.",
})
.option({
name: "quiet",
aliases: ["q"],
boolean: true,
defaultValue: false,
description: "Suppress output logs (Same as --verbose 0)",
})
.option({
name: "verbose",
aliases: ["v"],
defaultValue: LogLevel.Info,
description: `Determine amount of logs to display. The values are: ${chalk.bold`0 (none) | 1 (debug) | 2 (info) | 3 (warn) | 4 (error)`}. The provided level will display messages of the same level or higher.`,
parse: Number,
})
.option({
name: "dry-run",
aliases: ["dr"],
boolean: true,
defaultValue: false,
description:
"Don't emit files. This is good for testing your scaffolds and making sure they " +
"don't fail, without having to write actual file contents or create directories.",
})
// .example({
// input: `yarn cmd -t examples/test-input/Component -o examples/test-output -d '{"property":"myProp","value":"10"}'`,
// description: "Usage",
// output: "",
// })
.help({
binName: "simple-scaffold",
useGlobalColumns: true,
usageExample: "[options]",
header: "Create structured files based on templates.",
footer: [
`Copyright © Chen Asraf 2021`,
`NPM: ${chalk.underline`https://npmjs.com/package/simple-scaffold`}`,
`GitHub: ${chalk.underline`https://github.com/chenasraf/simple-scaffold`}`,
].join("\n"),
})
.parse(args)
)
}
parseCliArgs()

4
src/index.ts Normal file
View File

@@ -0,0 +1,4 @@
export * from "./scaffold"
export * from "./types"
import Scaffold from "./scaffold"
export default Scaffold

133
src/scaffold.ts Normal file
View File

@@ -0,0 +1,133 @@
import path from "path"
import {
createDirIfNotExists,
getOptionValueForFile,
handleErr,
log,
pascalCase,
isDir,
removeGlob,
makeRelativePath,
registerHelpers,
getTemplateGlobInfo,
ensureFileExists,
getFileList,
getBasePath,
copyFileTransformed,
getTemplateFileInfo,
logInitStep,
logInputFile,
} from "./utils"
import { LogLevel, ScaffoldConfig } from "./types"
/**
* Create a scaffold using given `options`.
*
* #### Create files
* To create a file structure to output, use any directory and file structure you would like.
* Inside folder names, file names or file contents, you may place `{{ var }}` where `var` is either
* `name` which is the scaffold name you provided or one of the keys you provided in the `data` option.
*
* The contents and names will be replaced with the transformed values so you can use your original structure as a
* boilerplate for other projects, components, modules, or even single files.
*
* #### 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 |
*
* 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)
*/
export async function Scaffold({ ...options }: ScaffoldConfig) {
options.output ??= process.cwd()
registerHelpers(options)
try {
options.data = { name: options.name, Name: pascalCase(options.name), ...options.data }
logInitStep(options)
for (let _template of options.templates) {
try {
const { nonGlobTemplate, origTemplate, isDirOrGlob, isGlob, template } = await getTemplateGlobInfo(
options,
_template
)
await ensureFileExists(template, isDirOrGlob)
const files = await getFileList(options, template)
for (const inputFilePath of files) {
if (await isDir(inputFilePath)) {
continue
}
const relPath = makeRelativePath(path.dirname(removeGlob(inputFilePath).replace(nonGlobTemplate, "")))
const basePath = getBasePath(relPath)
logInputFile(options, {
origTemplate,
relPath,
template,
inputFilePath,
nonGlobTemplate,
basePath,
isDirOrGlob,
isGlob,
})
await handleTemplateFile(options, options.data, { templatePath: inputFilePath, basePath })
}
} catch (e: any) {
handleErr(e)
}
}
} catch (e: any) {
log(options, LogLevel.Error, e)
throw e
}
}
async function handleTemplateFile(
options: ScaffoldConfig,
data: Record<string, string>,
{ 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, {
templatePath,
basePath,
})
const overwrite = getOptionValueForFile(options, inputPath, data, options.overwrite ?? false)
log(
options,
LogLevel.Debug,
`\nParsing ${templatePath}`,
`\nBase path: ${basePath}`,
`\nFull input path: ${inputPath}`,
`\nOutput Path Opt: ${outputPathOpt}`,
`\nFull output dir: ${outputDir}`,
`\nFull output path: ${outputPath}`,
`\n`
)
await createDirIfNotExists(path.dirname(outputPath), options)
log(options, LogLevel.Info, `Writing to ${outputPath}`)
await copyFileTransformed(options, data, { exists, overwrite, outputPath, inputPath })
resolve()
} catch (e: any) {
handleErr(e)
reject(e)
}
})
}
export default Scaffold

127
src/types.ts Normal file
View File

@@ -0,0 +1,127 @@
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
export interface ScaffoldConfig {
/**
* Name to be passed to the generated files. `{{name}}` and `{{Name}}` inside contents and file names will be replaced
* accordingly.
*/
name: string
/**
* Template files to use as input. You may provide multiple files, each of which can be a relative or absolute path,
* or a glob pattern for multiple file matching easily. (default: current working directory)
*/
templates: string[]
/** Path to output to. If `createSubFolder` is `true`, the subfolder will be created inside this path. */
output: FileResponse<string>
/**
* Create subfolder with the input name (default: `false`)
*/
createSubFolder?: boolean
/**
* Add custom data to the templates. By default, only your app name is included as `{{name}}` and `{{Name}}`.
*/
data?: Record<string, string>
/**
* Enable to override output files, even if they already exist. (default: `false`)
*
* You may supply a function to this option, which can take the arguments `(fullPath, baseDir, baseName)` and returns
* a string, to return a dynamic path for each file.
*/
overwrite?: FileResponse<boolean>
/** Suppress output logs (Same as `verbose: 0` or `verbose: LogLevel.None`) */
quiet?: boolean
/**
* Determine amount of logs to display.
*
* The values are: `0 (none) | 1 (debug) | 2 (info) | 3 (warn) | 4 (error)`. The provided level will display messages
* of the same level or higher. (default: `2 (info)`)
* @see LogLevel
*/
verbose?: LogLevel
/**
* Don't emit files. This is good for testing your scaffolds and making sure they don't fail, without having to write
* actual file contents or create directories. (default: `false`)
*/
dryRun?: boolean
/**
* Additional helpers to add to the template parser. Provide an object whose keys are the name of the function to add,
* and the value is the helper function itself. The signature of helpers is as follows:
* ```typescript
* (text: string) => string
* ```
*
* A full example might be:
*
* ```typescript
* Scaffold({
* //...
* helpers: {
* upperCamelCase: (text) => camelCase(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 |
*/
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.
*/
subFolderNameHelper?: DefaultHelperKeys | string
}
export interface ScaffoldCmdConfig {
name: string
templates: string[]
output: string
createSubFolder: boolean
data?: Record<string, string>
overwrite: boolean
quiet: boolean
verbose: LogLevel
dryRun: boolean
}

331
src/utils.ts Normal file
View File

@@ -0,0 +1,331 @@
import path from "path"
import { F_OK } from "constants"
import { DefaultHelperKeys, FileResponse, FileResponseFn, Helper, LogLevel, ScaffoldConfig } from "./types"
import camelCase from "lodash/camelCase"
import snakeCase from "lodash/snakeCase"
import kebabCase from "lodash/kebabCase"
import startCase from "lodash/startCase"
import Handlebars from "handlebars"
import { promises as fsPromises } from "fs"
import chalk from "chalk"
const { stat, access, mkdir } = fsPromises
import { glob } from "glob"
import { promisify } from "util"
const { readFile, writeFile } = fsPromises
export const defaultHelpers: Record<DefaultHelperKeys, Helper> = {
camelCase,
snakeCase,
startCase,
kebabCase,
hyphenCase: kebabCase,
pascalCase,
lowerCase: (text) => text.toLowerCase(),
upperCase: (text) => text.toUpperCase(),
}
export function registerHelpers(options: ScaffoldConfig) {
const _helpers = { ...defaultHelpers, ...options.helpers }
for (const helperName in _helpers) {
log(options, LogLevel.Debug, `Registering helper: ${helperName}`)
Handlebars.registerHelper(helperName, _helpers[helperName as keyof typeof _helpers])
}
}
export function handleErr(err: NodeJS.ErrnoException | null) {
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)) {
return
}
const levelColor: Record<LogLevel, keyof typeof chalk> = {
[LogLevel.None]: "reset",
[LogLevel.Debug]: "blue",
[LogLevel.Info]: "dim",
[LogLevel.Warning]: "yellow",
[LogLevel.Error]: "red",
}
const chalkFn: any = chalk[levelColor[level]]
const key: "log" | "warn" | "error" = level === LogLevel.Error ? "error" : level === LogLevel.Warning ? "warn" : "log"
const logFn: any = console[key]
logFn(
...obj.map((i) =>
i instanceof Error
? chalkFn(i, JSON.stringify(i, undefined, 1), i.stack)
: typeof i === "object"
? chalkFn(JSON.stringify(i, undefined, 1))
: chalkFn(i)
)
)
}
export async function createDirIfNotExists(dir: string, options: ScaffoldConfig): Promise<void> {
const parentDir = path.dirname(dir)
if (!(await pathExists(parentDir))) {
await createDirIfNotExists(parentDir, options)
}
if (!(await pathExists(dir))) {
try {
log(options, LogLevel.Debug, `Creating dir ${dir}`)
await mkdir(dir)
return
} catch (e: any) {
if (e.code !== "EEXIST") {
throw e
}
return
}
}
}
export function getOptionValueForFile<T>(
options: ScaffoldConfig,
filePath: string,
data: Record<string, string>,
fn: FileResponse<T>,
defaultValue?: T
): T {
if (typeof fn !== "function") {
return defaultValue ?? (fn as T)
}
return (fn as FileResponseFn<T>)(
filePath,
path.dirname(handlebarsParse(options, filePath, { isPath: true }).toString()),
path.basename(handlebarsParse(options, filePath, { isPath: true }).toString())
)
}
export function handlebarsParse(
options: ScaffoldConfig,
templateBuffer: Buffer | string,
{ isPath = false }: { isPath?: boolean } = {}
) {
const { data } = options
try {
let str = templateBuffer.toString()
if (isPath) {
str = str.replace(/\\/g, "/")
}
const parser = Handlebars.compile(str, { noEscape: true })
let outputContents = parser(data)
if (isPath && path.sep !== "/") {
outputContents = outputContents.replace(/\//g, "\\")
}
return outputContents
} catch {
log(options, LogLevel.Warning, "Couldn't parse file with handlebars, returning original content")
return templateBuffer
}
}
export async function pathExists(filePath: string): Promise<boolean> {
try {
await access(filePath, F_OK)
return true
} catch (e: any) {
if (e.code === "ENOENT") {
return false
}
throw e
}
}
export function pascalCase(s: string): string {
return startCase(s).replace(/\s+/g, "")
}
export async function isDir(path: string): Promise<boolean> {
const tplStat = await stat(path)
return tplStat.isDirectory()
}
export function removeGlob(template: string) {
return template.replace(/\*/g, "").replace(/(\/\/|\\\\)/g, path.sep)
}
export function makeRelativePath(str: string): string {
return str.startsWith(path.sep) ? str.slice(1) : str
}
export function getBasePath(relPath: string) {
return path
.resolve(process.cwd(), relPath)
.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,
})
).map((f) => f.replace(/\//g, path.sep))
}
export interface GlobInfo {
nonGlobTemplate: string
origTemplate: string
isDirOrGlob: boolean
isGlob: boolean
template: string
}
export async function getTemplateGlobInfo(options: ScaffoldConfig, template: string): Promise<GlobInfo> {
const isGlob = glob.hasMagic(template)
log(options, LogLevel.Debug, "before isDir", "isGlob:", isGlob, template)
let _template = template
const nonGlobTemplate = isGlob ? removeGlob(template) : template
const isDirOrGlob = isGlob ? true : await isDir(template)
log(options, LogLevel.Debug, "after isDir", isDirOrGlob)
const _shouldAddGlob = !isGlob && isDirOrGlob
const origTemplate = template
if (_shouldAddGlob) {
_template = path.join(template, "**", "*")
}
return { nonGlobTemplate, origTemplate, isDirOrGlob, isGlob, template: _template }
}
export 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
outputDir: string
outputPath: string
exists: boolean
}
export async function getTemplateFileInfo(
options: ScaffoldConfig,
data: Record<string, string>,
{ templatePath, basePath }: { templatePath: string; basePath: string }
): Promise<OutputFileInfo> {
const inputPath = path.resolve(process.cwd(), templatePath)
const outputPathOpt = getOptionValueForFile(options, inputPath, data, options.output)
const outputDir = getOutputDir(options, data, outputPathOpt, basePath)
const outputPath = handlebarsParse(options, path.join(outputDir, path.basename(inputPath)), {
isPath: true,
}).toString()
const exists = await pathExists(outputPath)
return { inputPath, outputPathOpt, outputDir, outputPath, exists }
}
export async function copyFileTransformed(
options: ScaffoldConfig,
data: Record<string, string>,
{
exists,
overwrite,
outputPath,
inputPath,
}: { exists: boolean; overwrite: boolean; outputPath: string; inputPath: string }
) {
if (!exists || overwrite) {
if (exists && overwrite) {
log(options, LogLevel.Info, `File ${outputPath} exists, overwriting`)
}
const templateBuffer = await readFile(inputPath)
const outputContents = handlebarsParse(options, templateBuffer)
if (!options.dryRun) {
await writeFile(outputPath, outputContents)
log(options, LogLevel.Info, "Done.")
} else {
log(options, LogLevel.Info, "Content output:")
log(options, LogLevel.Info, outputContents)
}
} else if (exists) {
log(options, LogLevel.Info, `File ${outputPath} already exists, skipping`)
}
}
export function getOutputDir(
options: ScaffoldConfig,
data: Record<string, string>,
outputPathOpt: string,
basePath: string
) {
return path.resolve(
process.cwd(),
...([
outputPathOpt,
basePath,
options.createSubFolder
? options.subFolderNameHelper
? handlebarsParse(options, `{{ ${options.subFolderNameHelper} name }}`)
: options.name
: undefined,
].filter(Boolean) as string[])
)
}
export function logInputFile(
options: ScaffoldConfig,
{
origTemplate,
relPath,
template,
inputFilePath,
nonGlobTemplate,
basePath,
isDirOrGlob,
isGlob,
}: {
origTemplate: string
relPath: string
template: string
inputFilePath: string
nonGlobTemplate: string
basePath: string
isDirOrGlob: boolean
isGlob: boolean
}
) {
log(
options,
LogLevel.Debug,
`\nprocess.cwd(): ${process.cwd()}`,
`\norigTemplate: ${origTemplate}`,
`\nrelPath: ${relPath}`,
`\ntemplate: ${template}`,
`\ninputFilePath: ${inputFilePath}`,
`\nnonGlobTemplate: ${nonGlobTemplate}`,
`\nbasePath: ${basePath}`,
`\nisDirOrGlob: ${isDirOrGlob}`,
`\nisGlob: ${isGlob}`,
`\n`
)
}
export function logInitStep(options: ScaffoldConfig) {
log(options, LogLevel.Debug, "Full config:", {
name: options.name,
templates: options.templates,
output: options.output,
createSubfolder: options.createSubFolder,
data: options.data,
overwrite: options.overwrite,
quiet: options.quiet,
subFolderTransformHelper: options.subFolderNameHelper,
helpers: Object.keys(options.helpers ?? {}),
verbose: `${options.verbose} (${Object.keys(LogLevel).find(
(k) => (LogLevel[k as any] as unknown as number) === options.verbose!
)})`,
})
log(options, LogLevel.Info, "Data:", options.data)
}

13
test.ts
View File

@@ -1,13 +0,0 @@
import SimpleScaffold from './scaffold'
import * as path from 'path'
const templateDir = path.join(process.cwd(), 'examples')
new SimpleScaffold({
templates: [templateDir + '/test-input/Component/**/*'],
output: templateDir + '/test-output',
locals: {
property: 'myProp',
value: '"value"'
}
}).run()

330
tests/scaffold.test.ts Normal file
View File

@@ -0,0 +1,330 @@
import mockFs from "mock-fs"
import FileSystem from "mock-fs/lib/filesystem"
import Scaffold from "../src/scaffold"
import { readdirSync, readFileSync } from "fs"
import { Console } from "console"
import { defaultHelpers } from "../src/utils"
import { join } from "path"
const fileStructNormal = {
input: {
"{{name}}.txt": "Hello, my app is {{name}}",
},
output: {},
}
const fileStructWithData = {
input: {
"{{name}}.txt": "Hello, my value is {{value}}",
},
output: {},
}
const fileStructNested = {
input: {
"{{name}}-1.txt": "This should be in root",
"{{Name}}": {
"{{name}}-2.txt": "Hello, my value is {{value}}",
moreNesting: {
"{{name}}-3.txt": "Hi! My value is actually NOT {{value}}!",
},
},
},
output: {},
}
const fileStructSubdirTransformer = {
input: {
"{{name}}.txt": "Hello, my app is {{name}}",
},
output: {},
}
const defaultHelperNames = Object.keys(defaultHelpers)
const fileStructHelpers = {
input: {
defaults: defaultHelperNames.reduce<Record<string, string>>(
(all, cur) => ({ ...all, [cur + ".txt"]: `{{ ${cur} name }}` }),
{}
),
custom: {
"add1.txt": "{{ add1 name }}",
},
},
output: {},
}
// let logsTemp: any = []
// let logMock: any
function withMock(fileStruct: FileSystem.DirectoryItems, testFn: jest.EmptyFunction): jest.EmptyFunction {
return () => {
beforeEach(() => {
// console.log("Mocking:", fileStruct)
console = new Console(process.stdout, process.stderr)
mockFs(fileStruct)
// logMock = jest.spyOn(console, 'log').mockImplementation((...args) => {
// logsTemp.push(args)
// })
})
testFn()
afterEach(() => {
// console.log("Restoring mock")
mockFs.restore()
})
}
}
describe("Scaffold", () => {
describe(
"create subfolder",
withMock(fileStructNormal, () => {
test("should not create by default", 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()).toBe("Hello, my app is app_name")
})
test("should create with config", async () => {
await Scaffold({
name: "app_name",
output: "output",
templates: ["input"],
createSubFolder: true,
verbose: 0,
})
const data = readFileSync(join(process.cwd(), "output", "app_name", "app_name.txt"))
expect(data.toString()).toBe("Hello, my app is app_name")
})
})
)
describe(
"overwrite",
withMock(fileStructWithData, () => {
test("should not overwrite by default", async () => {
await Scaffold({
name: "app_name",
output: "output",
templates: ["input"],
data: { value: "1" },
verbose: 0,
})
await Scaffold({
name: "app_name",
output: "output",
templates: ["input"],
data: { value: "2" },
verbose: 0,
})
const data = readFileSync(join(process.cwd(), "output", "app_name.txt"))
expect(data.toString()).toBe("Hello, my value is 1")
})
test("should overwrite with config", async () => {
await Scaffold({
name: "app_name",
output: "output",
templates: ["input"],
data: { value: "1" },
verbose: 0,
})
await Scaffold({
name: "app_name",
output: "output",
templates: ["input"],
data: { value: "2" },
overwrite: true,
verbose: 0,
})
const data = readFileSync(join(process.cwd(), "output", "app_name.txt"))
expect(data.toString()).toBe("Hello, my value is 2")
})
})
)
describe(
"errors",
withMock(fileStructNormal, () => {
let consoleMock1: jest.SpyInstance
beforeAll(() => {
consoleMock1 = jest.spyOn(console, "error").mockImplementation(() => void 0)
})
afterAll(() => {
consoleMock1.mockRestore()
})
test("should throw for bad input", async () => {
await expect(
Scaffold({
name: "app_name",
output: "output",
templates: ["non-existing-input"],
data: { value: "1" },
verbose: 0,
})
).rejects.toThrow()
expect(() => readFileSync(join(process.cwd(), "output", "app_name.txt"))).toThrow()
})
})
)
describe(
"outputPath override",
withMock(fileStructNormal, () => {
test("should allow override function", async () => {
await Scaffold({
name: "app_name",
output: (fullPath, basedir, basename) => join("custom-output", `${basename.split(".")[0]}`),
templates: ["input"],
data: { value: "1" },
verbose: 0,
})
const data = readFileSync(join(process.cwd(), "/custom-output/app_name/app_name.txt"))
expect(data.toString()).toBe("Hello, my app is app_name")
})
})
)
describe(
"output structure",
withMock(fileStructNested, () => {
test("should maintain input structure on output", async () => {
await Scaffold({
name: "app_name",
output: "output",
templates: ["input"],
data: { value: "1" },
verbose: 0,
})
const rootDir = readdirSync(join(process.cwd(), "output"))
const dir = readdirSync(join(process.cwd(), "output", "AppName"))
const nestedDir = readdirSync(join(process.cwd(), "output", "AppName", "moreNesting"))
expect(rootDir).toHaveProperty("length")
expect(dir).toHaveProperty("length")
expect(nestedDir).toHaveProperty("length")
const rootFile = readFileSync(join(process.cwd(), "output", "app_name-1.txt"))
const oneDeepFile = readFileSync(join(process.cwd(), "output", "AppName/app_name-2.txt"))
const twoDeepFile = readFileSync(join(process.cwd(), "output", "AppName/moreNesting/app_name-3.txt"))
expect(rootFile.toString()).toEqual("This should be in root")
expect(oneDeepFile.toString()).toEqual("Hello, my value is 1")
expect(twoDeepFile.toString()).toEqual("Hi! My value is actually NOT 1!")
})
})
)
describe(
"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(join(process.cwd(), "output", "defaults", `${key}.txt`))
expect(file.toString()).toEqual(results[key as keyof typeof results])
}
})
})
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(join(process.cwd(), "output", "custom", `${key}.txt`))
expect(file.toString()).toEqual(results[key as keyof typeof results])
}
})
})
})
)
describe(
"transform subfolder",
withMock(fileStructSubdirTransformer, () => {
test("should work with no helper", async () => {
await Scaffold({
name: "app_name",
output: "output",
templates: ["input"],
createSubFolder: true,
verbose: 0,
})
const data = readFileSync(join(process.cwd(), "output", "app_name/app_name.txt"))
expect(data.toString()).toBe("Hello, my app is app_name")
})
test("should work with default helper", async () => {
await Scaffold({
name: "app_name",
output: "output",
templates: ["input"],
createSubFolder: true,
verbose: 0,
subFolderNameHelper: "upperCase",
})
const data = readFileSync(join(process.cwd(), "output", "APP_NAME/app_name.txt"))
expect(data.toString()).toBe("Hello, my app is app_name")
})
test("should work with custom helper", async () => {
await Scaffold({
name: "app_name",
output: "output",
templates: ["input"],
createSubFolder: true,
verbose: 0,
subFolderNameHelper: "test",
helpers: {
test: () => "REPLACED",
},
})
const data = readFileSync(join(process.cwd(), "output", "REPLACED/app_name.txt"))
expect(data.toString()).toBe("Hello, my app is app_name")
})
})
)
})

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

@@ -0,0 +1,54 @@
import { handlebarsParse } from "../src/utils"
import { ScaffoldConfig } from "../src/types"
import path from "path"
const blankConf: ScaffoldConfig = {
verbose: 0,
name: "",
output: "",
templates: [],
data: { name: "test" },
}
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(
"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("/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("/home/test/test {{escaped}}.txt")
})
})
})

View File

@@ -1,14 +1,23 @@
{
"compilerOptions": {
"target": "es5",
"target": "ES2019",
"module": "commonjs",
"lib": ["es2017", "es6"],
"moduleResolution": "node",
"esModuleInterop": true,
"lib": [
"ES2019",
],
"declaration": true,
"outDir": "dist",
"strict": true,
"sourceMap": true
"sourceMap": true,
"removeComments": false,
},
"include": [
"src/index.ts",
"src/cmd.ts",
],
"exclude": [
"examples"
"tests/*"
]
}

View File

@@ -1,43 +0,0 @@
const path = require('path')
const webpack = require('webpack')
const nodeExternals = require('webpack-node-externals')
module.exports = {
devtool: process.env.NODE_ENV === 'develop' ? 'inline-source-map' : 'source-map',
target: 'node',
entry: {
scaffold: './scaffold.ts',
test: './test.ts',
cmd: './cmd.ts',
},
output: {
filename: '[name].js',
path: path.resolve(__dirname, 'dist'),
devtoolModuleFilenameTemplate: '[absolute-resource-path]',
library: 'library',
libraryTarget: 'umd',
},
resolve: {
extensions: ['.ts']
},
externals: [nodeExternals()],
module: {
rules: [
{
test: /\.tsx?$/,
loader: 'ts-loader',
exclude: ['./examples', 'node_modules']
}
]
},
plugins: [
new webpack.DefinePlugin({
'__dirname': '__dirname'
}),
new webpack.BannerPlugin({
banner: '#!/usr/bin/env node',
raw: true,
include: /cmd\.js/,
})
],
}

5333
yarn.lock

File diff suppressed because it is too large Load Diff