Compare commits

...

33 Commits

Author SHA1 Message Date
Chen Asraf
db6177c200 chore(develop): release 2.3.0 2024-09-18 00:12:00 +03:00
ae64db846f chore: add coverage script 2024-09-18 00:12:00 +03:00
89dc43c73d fix: exclude globs
refactor: split main file iteration to 2 steps
2024-09-17 23:57:48 +03:00
2c43dc4daf build: update target 2024-09-17 23:57:48 +03:00
f4c907e6c9 ci: fix build 2024-09-17 23:57:48 +03:00
a275e688d4 ci: use release-please 2024-09-17 23:57:48 +03:00
ff4ebf0a5b test: add color tests 2024-09-17 23:57:48 +03:00
ab9322e1ab feat: remove chalk dependency 2024-09-17 23:57:48 +03:00
semantic-release-bot
35f0d014d9 chore(release): 2.2.2 [skip ci]
## [2.2.2](https://github.com/chenasraf/simple-scaffold/compare/v2.2.1...v2.2.2) (2024-08-27)

### Bug Fixes

* homepage url ([daaefaf](daaefaf54e))
2024-08-27 13:07:31 +00:00
8ad8cb4be1 chore: update dependencies 2024-08-27 16:06:56 +03:00
daaefaf54e fix: homepage url 2024-08-27 16:06:56 +03:00
aefba4b773 docs: fix property names 2024-08-27 16:06:56 +03:00
dependabot[bot]
8457f0996a build(deps): bump ws
Bumps the npm_and_yarn group with 1 update in the /docs directory: [ws](https://github.com/websockets/ws).


Updates `ws` from 7.5.9 to 7.5.10
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/compare/7.5.9...7.5.10)

---
updated-dependencies:
- dependency-name: ws
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-27 16:06:56 +03:00
semantic-release-bot
adc95809ba chore(release): 2.2.1 [skip ci]
## [2.2.1](https://github.com/chenasraf/simple-scaffold/compare/v2.2.0...v2.2.1) (2024-04-21)

### Bug Fixes

* beforeWrite from config files ([98b326c](98b326c843))
* use console.info for handlebars parse warning ([19e7b0f](19e7b0f0c3))
2024-04-21 20:38:09 +00:00
98b326c843 fix: beforeWrite from config files 2024-04-21 23:37:35 +03:00
ddc115a037 chore: update dependencies 2024-04-21 23:37:35 +03:00
19e7b0f0c3 fix: use console.info for handlebars parse warning 2024-04-21 23:37:35 +03:00
dependabot[bot]
f883571daa build(deps): bump express from 4.18.2 to 4.19.2 in /docs
Bumps [express](https://github.com/expressjs/express) from 4.18.2 to 4.19.2.
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/master/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.18.2...4.19.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-21 23:37:35 +03:00
Chen Asraf
be3068a533 ci: update doc workflow permissions 2024-04-21 23:37:35 +03:00
dependabot[bot]
8acc660dea build(deps): bump the npm_and_yarn group across 1 directory with 2 updates
Bumps the npm_and_yarn group with 2 updates in the /docs directory: [follow-redirects](https://github.com/follow-redirects/follow-redirects) and [webpack-dev-middleware](https://github.com/webpack/webpack-dev-middleware).


Updates `follow-redirects` from 1.15.5 to 1.15.6
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.5...v1.15.6)

Updates `webpack-dev-middleware` from 5.3.3 to 5.3.4
- [Release notes](https://github.com/webpack/webpack-dev-middleware/releases)
- [Changelog](https://github.com/webpack/webpack-dev-middleware/blob/v5.3.4/CHANGELOG.md)
- [Commits](https://github.com/webpack/webpack-dev-middleware/compare/v5.3.3...v5.3.4)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-type: indirect
  dependency-group: npm_and_yarn-security-group
- dependency-name: webpack-dev-middleware
  dependency-type: indirect
  dependency-group: npm_and_yarn-security-group
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-21 23:37:35 +03:00
semantic-release-bot
df6c351cb0 chore(release): 2.2.0 [skip ci]
# [2.2.0](https://github.com/chenasraf/simple-scaffold/compare/v2.1.0...v2.2.0) (2024-02-23)

### Features

* `list` command ([d579c09](d579c09c11))
* add `--before-write` cli option ([#89](https://github.com/chenasraf/simple-scaffold/issues/89)) ([5f810e2](5f810e2116))
2024-02-23 22:42:32 +00:00
Chen Asraf
5f810e2116 feat: add --before-write cli option (#89)
* feat: add `--before-write` cli option

* refactor: cleanup before write wrapper

* docs: add before-write docs
2024-02-24 00:41:56 +02:00
d579c09c11 feat: list command 2024-02-24 00:41:56 +02:00
3765398ab9 ci: attemp to fix pack cmd 2024-02-24 00:41:56 +02:00
semantic-release-bot
9be8a8b71b chore(release): 2.1.0 [skip ci]
# [2.1.0](https://github.com/chenasraf/simple-scaffold/compare/v2.0.2...v2.1.0) (2024-02-12)

### Features

* support directory in --config flag ([e48b832](e48b832e0b))
* support providing name in config ([4e7ac34](4e7ac34db9))
2024-02-12 23:39:35 +00:00
5cb8c3c081 docs: update navbar logo 2024-02-13 01:39:04 +02:00
3b52c255f0 chore: dry 2024-02-13 01:39:04 +02:00
80cf2076b0 chore: update dependencies 2024-02-13 01:39:04 +02:00
4fd710b763 test: fix tests 2024-02-13 01:39:04 +02:00
4e7ac34db9 feat: support providing name in config 2024-02-13 01:39:04 +02:00
e48b832e0b feat: support directory in --config flag 2024-02-13 01:39:04 +02:00
06ffa656ae docs: update templates page 2024-02-13 01:39:04 +02:00
919fd54ebb docs: usage page order 2024-02-13 01:39:04 +02:00
32 changed files with 8630 additions and 8443 deletions

33
.github/workflows/develop.yml vendored Normal file
View File

@@ -0,0 +1,33 @@
name: Test
on:
pull_request:
branches:
- master
- develop
permissions:
contents: write
pull-requests: write
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm i -g pnpm
- run: pnpm run ci
- run: pnpm test
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm i -g pnpm
- run: pnpm run ci
- run: pnpm build

View File

@@ -2,7 +2,12 @@ name: Documentation
on:
push:
branches: [master, pre, develop]
branches:
- master
- develop
permissions:
contents: write
jobs:
docs:
@@ -10,22 +15,15 @@ jobs:
runs-on: ubuntu-latest
# if: "contains(github.event.head_commit.message, 'chore(release)')"
steps:
- name: Checkout
uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: "20.x"
- name: Install PNPM
run: npm i -g pnpm
- name: Install dependencies
run: |
node-version: 20
- run: npm i -g pnpm
- run: |
pnpm install --frozen-lockfile
cd docs && pnpm install --frozen-lockfile
- name: Build Docs
run: pnpm docs:build
- run: pnpm docs:build
- name: Deploy on GitHub Pages
uses: peaceiris/actions-gh-pages@v3
with:

View File

@@ -1,26 +0,0 @@
name: Pull Requests
on:
pull_request:
branches: [master, pre, develop]
jobs:
build:
name: Test & Build PR
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: "20.x"
- name: Install PNPM
run: npm i -g pnpm
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Run Tests
run: pnpm test
- name: Build Package
run: pnpm build

View File

@@ -2,37 +2,63 @@ name: Release
on:
push:
branches: [master, pre, develop]
branches:
- master
permissions:
contents: read # for checkout
contents: write
pull-requests: write
jobs:
release:
name: Release
test:
runs-on: ubuntu-latest
permissions:
contents: write # to be able to publish a GitHub release
issues: write # to be able to comment on released issues
pull-requests: write # to be able to comment on released pull requests
id-token: write # to enable use of OIDC for npm provenance
steps:
- name: Checkout
uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v3
node-version: 20
- run: npm i -g pnpm
- run: pnpm run ci
- run: pnpm test
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20.x"
- name: Install PNPM
run: npm i -g pnpm
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Run Tests
run: pnpm test
- name: Build Package
run: pnpm build
- name: Semantic Release
run: npx semantic-release
node-version: 20
- run: npm i -g pnpm
- run: pnpm run ci
- run: pnpm build
release:
needs:
- build
- test
runs-on: ubuntu-latest
outputs:
release_created: ${{ steps.release.outputs.release_created }}
steps:
- uses: googleapis/release-please-action@v4
id: release
with:
token: ${{ secrets.RELEASE_PLEASE_TOKEN }}
release-type: node
target-branch: master
publish:
needs: release
runs-on: ubuntu-latest
if: ${{ needs.release.outputs.release_created }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
registry-url: "https://registry.npmjs.org"
- run: npm i -g pnpm
- run: pnpm run ci
- run: pnpm build
- run: cd dist && npm publish
env:
NPM_TOKEN: "${{ secrets.NPM_TOKEN }}"
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

View File

@@ -1,5 +1,48 @@
# Change Log
## [2.3.0](https://github.com/chenasraf/simple-scaffold/compare/v2.2.2...v2.3.0) (2024-09-17)
### Features
* remove chalk dependency ([ba96ca6](https://github.com/chenasraf/simple-scaffold/commit/ba96ca64d1e02beb16bd127fc889da3ef016b7d5))
### Bug Fixes
* exclude globs ([a2788e7](https://github.com/chenasraf/simple-scaffold/commit/a2788e7c7d27f46d55cf4810e1a8193b5d403568))
## [2.2.2](https://github.com/chenasraf/simple-scaffold/compare/v2.2.1...v2.2.2) (2024-08-27)
### Bug Fixes
* homepage url ([daaefaf](https://github.com/chenasraf/simple-scaffold/commit/daaefaf54e8c8887e6f210d02fd5f96c6ff4aa21))
## [2.2.1](https://github.com/chenasraf/simple-scaffold/compare/v2.2.0...v2.2.1) (2024-04-21)
### Bug Fixes
* beforeWrite from config files ([98b326c](https://github.com/chenasraf/simple-scaffold/commit/98b326c84346162f379af46bc5aefb69df8be515))
* use console.info for handlebars parse warning ([19e7b0f](https://github.com/chenasraf/simple-scaffold/commit/19e7b0f0c35c1b79a98781bdec9f54354123d8e0))
# [2.2.0](https://github.com/chenasraf/simple-scaffold/compare/v2.1.0...v2.2.0) (2024-02-23)
### Features
* `list` command ([d579c09](https://github.com/chenasraf/simple-scaffold/commit/d579c09c11f2149fe7bb4515297c1287fa67083e))
* add `--before-write` cli option ([#89](https://github.com/chenasraf/simple-scaffold/issues/89)) ([5f810e2](https://github.com/chenasraf/simple-scaffold/commit/5f810e21160816bc683cc0f375de318ff874871c))
# [2.1.0](https://github.com/chenasraf/simple-scaffold/compare/v2.0.2...v2.1.0) (2024-02-12)
### Features
* support directory in --config flag ([e48b832](https://github.com/chenasraf/simple-scaffold/commit/e48b832e0b72a084d33fa2cbcca332e8209a734f))
* support providing name in config ([4e7ac34](https://github.com/chenasraf/simple-scaffold/commit/4e7ac34db9bf67d012bbd1c06c1a26bc5ac93559))
## [2.0.2](https://github.com/chenasraf/simple-scaffold/compare/v2.0.1...v2.0.2) (2024-02-04)

View File

@@ -50,7 +50,6 @@ 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.
@@ -110,7 +109,15 @@ Further details:
```
- **The now helper** (for current time) takes the same arguments, minus the first one (`date`) as it
is implicitly the current date.
is implicitly the current date:
```typescript
(
format: string,
offsetAmount?: number,
offsetType?: "years" | "months" | "weeks" | "days" | "hours" | "minutes" | "seconds"
)
```
### Custom Helpers

View File

@@ -23,48 +23,11 @@ module.exports = {
}
```
The configuration contents are identical to the
[Node.js configuration structure](https://chenasraf.github.io/simple-scaffold/docs/usage/node):
```ts
interface ScaffoldConfig {
name: string
templates: string[]
output: FileResponse<string>
subdir?: boolean
git?: string
config?: string
key?: string
data?: Record<string, any>
overwrite?: FileResponse<boolean>
quiet?: boolean
verbose?: LogLevel
dryRun?: boolean
helpers?: Record<string, Helper>
subdirHelper?: DefaultHelpers | string
beforeWrite?(
content: Buffer,
rawContent: Buffer,
outputPath: string,
): string | Buffer | undefined | Promise<string | Buffer | undefined>
}
```
For the full configuration options, see [ScaffoldConfigFile](../api/modules#scaffoldconfigfile).
If you want to supply functions inside the configurations, you must use a `.js`/`.cjs`/`.mjs` file
as JSON does not support non-primitives.
A `.js` file can be just like a `.json` file, make sure to export the final configuration:
```js
/** @type {import('simple-scaffold').ScaffoldConfigFile} */
module.exports = {
component: {
templates: ["templates/component"],
output: "src/components",
},
}
```
Another feature of using a JS file is you can export a function which will be loaded with the CMD
config provided to Simple Scaffold. The `extra` key contains any values not consumed by built-in
flags, so you can pre-process your args before outputting a config:
@@ -82,6 +45,12 @@ module.exports = (config) => {
}
```
If you want to provide templates that need no name (such as common config files which are easily
portable between projects), you may provide the `name` property in the config object.
You will always be able to override it using `--name NewName`, but it will be given a value by
default and therefore it will no longer be required in the CLI arguments.
## Using a config file
Once your config is created, you can use it by providing the file name to the `--config` (or `-c`
@@ -116,7 +85,7 @@ And then:
simple-scaffold -c scaffold.json MyComponentName
```
- When the filename is omitted, the following files will be tried in order:
- When the a directory is given, the following files in the given directory will be tried in order:
- `scaffold.config.*`
- `scaffold.*`
@@ -162,6 +131,12 @@ simple-scaffold -g git://gitlab.com/<username>/<project_name> [-c <filename>] [-
simple-scaffold -g https://gitlab.com/<username>/<project_name>.git [-c <filename>] [-k <template_key>]
```
When a config file path is omitted, the files given in the list above will be tried on the root
directory of the git repository.
**Note:** The repository will be cloned to a temporary directory and removed after the scaffolding
has been done.
## Use In Node.js
You can also start a scaffold from Node.js with a remote file or URL config.

130
docs/docs/usage/03-cli.md Normal file
View File

@@ -0,0 +1,130 @@
---
title: CLI Usage
---
## 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`.
Options:
| Option/flag \| Alias | Description |
| ------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--name` \| `-n` | Name to be passed to the generated files. `{{name}}` and other data parameters inside contents and file names will be replaced accordingly. You may omit the `--name` or `-n` for this specific option. |
| `--config` \| `-c` | Filename or directory to load config from |
| `--git` \| `-g` | Git URL or GitHub path to load a template from. |
| `--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 `--subdir` is enabled, the subdir will be created inside this path. Default is current working directory. |
| `--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` \| `--no-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` |
| `--subdir` \| `-s` \| `--no-subdir` \| `-S` | Create a parent directory with the input name (and possibly `--subdir-helper` (default: false) |
| `--subdir-helper` \| `-H` | Default helper to apply to subdir name when using `--subdir`. |
| `--quiet` \| `-q` | Suppress output logs (Same as `--log-level none`)(default: false) |
| `--log-level` \| `-l` | Determine amount of logs to display. The values are: `none, debug, info, warn, error`. The provided level will display messages of the same level or higher. (default: info) |
| `--before-write` \| `-B` | Run a script before writing the files. This can be a command or a path to a file. A temporary file path will be passed to the given command and the command should return a string for the final output. |
| `--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) |
| `--version` \| `-v` | Display version. |
### Before Write option
This option allows you to preprocess a file before it is being written, such as running a formatter,
linter or other commands.
To use this option, pass it the command you would like to run. The following tokens will be replaced
in your string:
- `{{path}}` - the temporary file path for you to read from
- `{{rawpath}}` - a different file path containing the raw file contents **before** they were
handled by Handlebars.js.
If none of these tokens are found, the regular (non-raw) path will be appended to the end of the
command.
```shell
simple-scaffold -c . --before-write prettier
# command: prettier /tmp/somefile
simple-scaffold -c . --before-write 'cat {{path}} | my-linter'
# command: cat /tmp/somefile | my-linter
```
The command should return the string to write to the file through standard output (stdout), and not
re-write the tmp file as it is not used for writing. Returning an empty string (after trimming) will
discard the result and write the original file contents.
See
[beforeWrite](https://chenasraf.github.io/simple-scaffold/docs/api/interfaces/ScaffoldConfig#beforewrite)
Node.js API for more details. Instead of returning `undefined` to keep the default behavior, you can
output `''` for the same effect.
## Available Commands:
| Command \| Alias | Description |
| ---------------- | ------------------------------------------------------------------------------------ |
| `list` \| `ls` | List all available templates for a given config. See `list -h` for more information. |
## Examples:
> See
> [Configuration Files](https://chenasraf.github.io/simple-scaffold/docs/usage/configuration_files)
> for organizing multiple scaffold types into easy-to-maintain files
Usage with config file
```shell
$ simple-scaffold -c scaffold.cmd.js -k component MyComponent
```
Usage with GitHub config file
```shell
$ simple-scaffold -g chenasraf/simple-scaffold -k component MyComponent
```
Usage with https git URL (for non-GitHub)
```shell
$ simple-scaffold \
-g https://example.com/user/template.git \
-c scaffold.cmd.js \
-k component \
MyComponent
```
Full syntax with config path and template key (applicable to all above methods)
```shell
$ simple-scaffold -c scaffold.cmd.js -k 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 -g chenasraf/simple-scaffold MyComponent
```
You can also add this as a script in your `package.json`:
```json
{
"scripts": {
"scaffold-cfg": "npx simple-scaffold -c scaffold.cmd.js -k component",
"scaffold-gh": "npx simple-scaffold -g chenasraf/simple-scaffold -k component",
"scaffold": "npx simple-scaffold@latest -t scaffolds/component/**/* -o src/components -d '{\"myProp\": \"propName\", \"myVal\": 123}'"
"scaffold-component": "npx simple-scaffold -c scaffold.cmd.js -k"
}
}
```

View File

@@ -2,6 +2,8 @@
title: Node.js Usage
---
## Overview
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.
@@ -17,14 +19,14 @@ interface ScaffoldConfig {
name: string
templates: string[]
output: FileResponse<string>
createSubFolder?: boolean
subdir?: boolean
data?: Record<string, any>
overwrite?: FileResponse<boolean>
quiet?: boolean
verbose?: LogLevel
dryRun?: boolean
helpers?: Record<string, Helper>
subFolderNameHelper?: DefaultHelpers | string
subdirHelper?: DefaultHelpers | string
beforeWrite?(
content: Buffer,
rawContent: Buffer,
@@ -33,6 +35,19 @@ interface ScaffoldConfig {
}
```
### Before Write option
This option allows you to preprocess a file before it is being written, such as running a formatter,
linter or other commands.
To use this option, you can run any async/blocking command, and return a string as the final output
to be used as the file contents.
Returning `undefined` will keep the file contents as-is, after normal Handlebars.js procesing by
Simple Scaffold.
## Example
This is an example of loading a complete scaffold via Node.js:
```typescript
@@ -42,15 +57,17 @@ const config = {
name: "component",
templates: [path.join(__dirname, "scaffolds", "component")],
output: path.join(__dirname, "src", "components"),
createSubFolder: true,
subFolderNameHelper: "upperCase"
subdir: true,
subdirHelper: "upperCase",
data: {
property: "value",
},
helpers: {
twice: (text) => [text, text].join(" ")
twice: (text) => [text, text].join(" "),
},
beforeWrite: (content, rawContent, outputPath) => content.toString().toUpperCase()
// return a string to replace the final file contents after pre-processing, or `undefined`
// to keep it as-is
beforeWrite: (content, rawContent, outputPath) => content.toString().toUpperCase(),
}
const scaffold = Scaffold(config)

View File

@@ -1,90 +0,0 @@
---
title: CLI Usage
---
## 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 | |
| ----------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--name` \| `-n` | Name to be passed to the generated files. `{{name}}` and other data parameters inside contents and file names will be replaced accordingly. You may omit the `--name` or `-n` for this specific option. |
| `--config`\|`-c` | Filename to load config from instead of passing arguments to CLI or using a Node.js script. See examples for syntax. This can also work in conjunction with `--git` or `--github` to point to remote files, and with `--key` to denote which key to select from the file., |
| `--git`\|`-g` | Git URL to load config from instead of passing arguments to CLI or using a Node.js script. See examples for syntax. |
| `--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 is current working directory. |
| `--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. |
| `--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 |
| `--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 `--log-level none`) |
| `--log-level` \| `-l` | Determine amount of logs to display. The values are: `none \| debug \| info \| warn \| error`. The provided level will display messages of the same level or higher. |
| `--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. |
| `--help` \| `-h` | Show this help message |
| `--version` \| `-v` | Display version. |
## Examples:
> See
> [Configuration Files](https://chenasraf.github.io/simple-scaffold/docs/usage/configuration_files)
> for organizing multiple scaffold types into easy-to-maintain files
Usage with config file
```shell
$ simple-scaffold -c scaffold.cmd.js -k component MyComponent
```
Usage with GitHub config file
```shell
$ simple-scaffold -g chenasraf/simple-scaffold -k component MyComponent
```
Usage with https git URL (for non-GitHub)
```shell
$ simple-scaffold \
-g https://example.com/user/template.git \
-c scaffold.cmd.js \
-k component \
MyComponent
```
Full syntax with config path and template key (applicable to all above methods)
```shell
$ simple-scaffold -c scaffold.cmd.js -k 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 -g chenasraf/simple-scaffold MyComponent
```
You can also add this as a script in your `package.json`:
```json
{
"scripts": {
"scaffold-cfg": "npx simple-scaffold -c scaffold.cmd.js -k component",
"scaffold-gh": "npx simple-scaffold -g chenasraf/simple-scaffold -k component",
"scaffold": "npx simple-scaffold@latest -t scaffolds/component/**/* -o src/components -d '{\"myProp\": \"propName\", \"myVal\": 123}'"
"scaffold-component": "npx simple-scaffold -c scaffold.cmd.js -k"
}
}
```

View File

@@ -83,7 +83,7 @@ const config: Config = {
title: "Simple Scaffold",
logo: {
alt: "Simple Scaffold",
src: "img/logo.svg",
src: "img/favicon.svg",
},
items: [
{

10289
docs/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
import * as React from "react"
import * as css from "./{{pascalCae name}}.css"
import * as css from "./{{pascalCase name}}.css"
class {{pascalCae name}} extends React.Component<any> {
class {{pascalCase name}} extends React.Component<any> {
private {{ property }}
constructor(props: any) {
@@ -10,8 +10,8 @@ class {{pascalCae name}} extends React.Component<any> {
}
public render() {
return <div className={ css.{{pascalCae name}} } />
return <div className={ css.{{pascalCase name}} } />
}
}
export default {pascalCae nName}}
export default {{pascalCase name}}

View File

@@ -88,7 +88,6 @@ 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",
// },

View File

@@ -1,8 +1,8 @@
{
"name": "simple-scaffold",
"version": "2.0.2",
"version": "2.3.0",
"description": "Generate any file structure - from single components to entire app boilerplates, with a single command.",
"homepage": "https: //chenasraf.github.io/simple-scaffold",
"homepage": "https://chenasraf.github.io/simple-scaffold",
"repository": {
"type": "git",
"url": "https://github.com/chenasraf/simple-scaffold.git"
@@ -13,7 +13,7 @@
"bin": {
"simple-scaffold": "cmd.js"
},
"packageManager": "pnpm@8.15.1",
"packageManager": "pnpm@9.9.0",
"keywords": [
"javascript",
"cli",
@@ -26,41 +26,33 @@
"scaffolding"
],
"scripts": {
"clean": "rm -rf dist/",
"clean": "rimraf dist",
"build": "pnpm clean && tsc && chmod -R +x ./dist && cp ./package.json ./README.md ./dist/",
"dev": "tsc --watch",
"start": "ts-node src/scaffold.ts",
"test": "jest",
"coverage": "open coverage/lcov-report/index.html",
"cmd": "ts-node src/cmd.ts",
"docs:build": "cd docs && pnpm build",
"docs:watch": "cd docs && pnpm start",
"audit-fix": "pnpm audit --fix",
"changelog": "conventional-changelog -i CHANGELOG.md -s -r 0; echo \"# Change Log\n\n$(cat CHANGELOG.md)\" > CHANGELOG.md"
"ci": "pnpm install --frozen-lockfile"
},
"dependencies": {
"chalk": "^4.1.2",
"date-fns": "^3.3.1",
"glob": "^10.3.10",
"date-fns": "^4.0.0",
"glob": "^11.0.0",
"handlebars": "^4.7.8",
"massarg": "2.0.0"
"massarg": "2.0.1"
},
"devDependencies": {
"@semantic-release/changelog": "^6.0.3",
"@semantic-release/exec": "^6.0.3",
"@semantic-release/git": "^10.0.1",
"@semantic-release/release-notes-generator": "^12.1.0",
"@types/jest": "^29.5.11",
"@types/jest": "^29.5.13",
"@types/mock-fs": "^4.13.4",
"@types/node": "^20.11.14",
"@types/semantic-release": "^20.0.6",
"conventional-changelog": "^5.1.0",
"conventional-changelog-cli": "^4.1.0",
"@types/node": "^22.5.5",
"jest": "^29.7.0",
"mock-fs": "^5.2.0",
"semantic-release": "^23.0.0",
"semantic-release-conventional-commits": "^3.0.0",
"ts-jest": "^29.1.2",
"rimraf": "^6.0.1",
"ts-jest": "^29.2.5",
"ts-node": "^10.9.2",
"typescript": "^5.3.3"
"typescript": "^5.6.2"
}
}

5524
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,69 +0,0 @@
const ref = process.env.GITHUB_REF || ""
const branch = ref.split("/").pop()
/**
* @type {import('semantic-release').GlobalConfig}
*/
module.exports = {
branches: ["master", { name: "pre", prerelease: true }],
plugins: [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
[
"@semantic-release/npm",
{
// only update the pkg version on root, don't publish
npmPublish: false,
},
],
// [
// '@semantic-release/npm',
// {
// // only update the pkg version on doc, don't publish
// npmPublish: false,
// pkgRoot: 'doc',
// },
// ]
[
"@semantic-release/exec",
{
publish: "cd ./dist && pnpm pack --pack-destination=../",
},
],
[
"@semantic-release/npm",
{
// publish from dist dir instead of root
pkgRoot: "dist",
},
],
[
"@semantic-release/github",
{
assets: ["*.tgz"],
},
],
branch === "master"
? [
"@semantic-release/changelog",
{
changelogFile: "CHANGELOG.md",
changelogTitle: "# Change Log",
},
]
: undefined,
[
"@semantic-release/git",
{
assets: ["package.json", "CHANGELOG.md"].filter(Boolean),
},
],
//
// [
// '@semantic-release/exec',
// {
// verifyReleaseCmd: 'echo ${nextRelease.version} > .VERSION',
// },
// ],
].filter(Boolean),
}

View File

@@ -1,4 +1,5 @@
/** @type {import('simple-scaffold').ScaffoldConfigFile} */
// @ts-check
/** @type {import('./dist').ScaffoldConfigFile} */
module.exports = (conf) => {
console.log("Config:", conf)
return {
@@ -12,5 +13,10 @@ module.exports = (conf) => {
output: "examples/test-output/component",
data: { property: "myProp", value: "10" },
},
configs: {
templates: ["examples/test-input/**/.*"],
output: "examples/test-output/configs",
name: "---",
},
}
}

View File

@@ -1,22 +1,24 @@
#!/usr/bin/env node
import os from "node:os"
import { massarg } from "massarg"
import chalk from "chalk"
import { LogLevel, ScaffoldCmdConfig } from "./types"
import { Scaffold } from "./scaffold"
import path from "node:path"
import fs from "node:fs/promises"
import { parseAppendData, parseConfigFile } from "./config"
import { massarg } from "massarg"
import { ListCommandCliOptions, LogLevel, ScaffoldCmdConfig } from "./types"
import { Scaffold } from "./scaffold"
import { getConfigFile, parseAppendData, parseConfigFile } from "./config"
import { log } from "./logger"
import { MassargCommand } from "massarg/command"
import { getUniqueTmpPath as generateUniqueTmpPath } from "./file"
import { colorize } from "./utils"
export async function parseCliArgs(args = process.argv.slice(2)) {
const isProjectRoot = Boolean(await fs.stat(path.join(__dirname, "package.json")).catch(() => false))
const pkgFile = await fs.readFile(path.resolve(__dirname, isProjectRoot ? "." : "..", "package.json"))
const pkg = JSON.parse(pkgFile.toString())
const isVersionFlag = args.includes("--version") || args.includes("-v")
const isConfigProvided =
args.includes("--config") || args.includes("-c") || args.includes("--git") || args.includes("-g") || isVersionFlag
const isConfigFileProvided = args.includes("--config") || args.includes("-c")
const isGitProvided = args.includes("--git") || args.includes("-g")
const isConfigProvided = isConfigFileProvided || isGitProvided || isVersionFlag
return massarg<ScaffoldCmdConfig>({
name: pkg.name,
@@ -28,11 +30,14 @@ export async function parseCliArgs(args = process.argv.slice(2)) {
return
}
log(config, LogLevel.info, `Simple Scaffold v${pkg.version}`)
const tmpPath = path.resolve(os.tmpdir(), `scaffold-config-${Date.now()}`)
const tmpPath = generateUniqueTmpPath()
try {
log(config, LogLevel.debug, "Parsing config file...", config)
const parsed = await parseConfigFile(config, tmpPath)
await Scaffold(parsed)
} catch (e) {
const message = "message" in (e as any) ? (e as any).message : e?.toString()
log(config, LogLevel.error, message)
} finally {
log(config, LogLevel.debug, "Cleaning up temporary files...", tmpPath)
await fs.rm(tmpPath, { recursive: true, force: true })
@@ -43,24 +48,20 @@ export async function parseCliArgs(args = process.argv.slice(2)) {
aliases: ["n"],
description:
"Name to be passed to the generated files. `{{name}}` and other data parameters inside " +
"contents and file names will be replaced accordingly. You may omit the `--name` or `-n` for this specific option.",
"contents and file names will be replaced accordingly. You may omit the `--name` or `-n` " +
"for this specific option.",
isDefault: true,
required: !isVersionFlag,
required: !isConfigProvided,
})
.option({
name: "config",
aliases: ["c"],
description:
"Filename to load config from instead of passing arguments to CLI or using a Node.js " +
"script. See examples for syntax. This can also work in conjunction with `--git` or `--github` to point " +
"to remote files, and with `--key` to denote which key to select from the file.",
description: "Filename or directory to load config from",
})
.option({
name: "git",
aliases: ["g"],
description:
"Git URL to load config from instead of passing arguments to CLI or using a Node.js script. See " +
"examples for syntax.",
description: "Git URL or GitHub path to load a template from.",
})
.option({
name: "key",
@@ -73,7 +74,7 @@ export async function parseCliArgs(args = process.argv.slice(2)) {
name: "output",
aliases: ["o"],
description:
"Path to output to. If `--subdir` is enabled, the subfolder will be created inside " +
"Path to output to. If `--subdir` is enabled, the subdir will be created inside " +
"this path. Default is current working directory.",
required: !isConfigProvided,
})
@@ -119,7 +120,7 @@ export async function parseCliArgs(args = process.argv.slice(2)) {
.option({
name: "subdir-helper",
aliases: ["H"],
description: "Default helper to apply to subfolder name when using `--subdir`.",
description: "Default helper to apply to subdir name when using `--subdir`.",
})
.flag({
name: "quiet",
@@ -133,7 +134,7 @@ export async function parseCliArgs(args = process.argv.slice(2)) {
defaultValue: LogLevel.info,
description:
"Determine amount of logs to display. The values are: " +
`${chalk.bold`\`none | debug | info | warn | error\``}. ` +
`${colorize.bold`\`none | debug | info | warn | error\``}. ` +
"The provided level will display messages of the same level or higher.",
parse: (v) => {
const val = v.toLowerCase()
@@ -143,6 +144,14 @@ export async function parseCliArgs(args = process.argv.slice(2)) {
return val
},
})
.option({
name: "before-write",
aliases: ["B"],
description:
"Run a script before writing the files. This can be a command or a path to a" +
" file. A temporary file path will be passed to the given command and the command should " +
"return a string for the final output.",
})
.flag({
name: "dry-run",
aliases: ["dr"],
@@ -156,6 +165,67 @@ export async function parseCliArgs(args = process.argv.slice(2)) {
aliases: ["v"],
description: "Display version.",
})
.command(
new MassargCommand<ListCommandCliOptions>({
name: "list",
aliases: ["ls"],
description: "List all available templates for a given config. See `list -h` for more information.",
run: async (_config) => {
const tmpPath = generateUniqueTmpPath()
const config = {
templates: [],
name: "",
version: false,
output: "",
subdir: false,
overwrite: false,
dryRun: false,
..._config,
config: _config.config ?? (!_config.git ? process.cwd() : undefined),
}
try {
const file = await getConfigFile(config, tmpPath)
console.log(colorize.underline`Available templates:\n`)
console.log(Object.keys(file).join("\n"))
} catch (e) {
const message = "message" in (e as any) ? (e as any).message : e?.toString()
log(config, LogLevel.error, message)
} finally {
log(config, LogLevel.debug, "Cleaning up temporary files...", tmpPath)
await fs.rm(tmpPath, { recursive: true, force: true })
}
},
})
.option({
name: "config",
aliases: ["c"],
description: "Filename or directory to load config from. Defaults to current working directory.",
})
.option({
name: "git",
aliases: ["g"],
description: "Git URL or GitHub path to load a template from.",
})
.option({
name: "log-level",
aliases: ["l"],
defaultValue: LogLevel.none,
description:
"Determine amount of logs to display. The values are: " +
`${colorize.bold`\`none | debug | info | warn | error\``}. ` +
"The provided level will display messages of the same level or higher.",
parse: (v) => {
const val = v.toLowerCase()
if (!(val in LogLevel)) {
throw new Error(`Invalid log level: ${val}, must be one of: ${Object.keys(LogLevel).join(", ")}`)
}
return val
},
})
.help({
bindOption: true,
}),
)
.example({
description: "Usage with config file",
input: "simple-scaffold -c scaffold.cmd.js --key component",
@@ -181,7 +251,7 @@ export async function parseCliArgs(args = process.argv.slice(2)) {
bindOption: true,
lineLength: 100,
useGlobalTableColumns: true,
usageText: [chalk.yellow`simple-scaffold`, chalk.gray`[options]`, chalk.cyan`<name>`].join(" "),
usageText: [colorize.yellow`simple-scaffold`, colorize.gray`[options]`, colorize.cyan`<name>`].join(" "),
optionOptions: {
displayNegations: true,
},
@@ -189,9 +259,9 @@ export async function parseCliArgs(args = process.argv.slice(2)) {
`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`}`,
`Documentation: ${colorize.underline`https://chenasraf.github.io/simple-scaffold`}`,
`NPM: ${colorize.underline`https://npmjs.com/package/simple-scaffold`}`,
`GitHub: ${colorize.underline`https://github.com/chenasraf/simple-scaffold`}`,
].join("\n"),
})
.parse(args)

View File

@@ -1,4 +1,5 @@
import path from "node:path"
import fs from "node:fs/promises"
import {
ConfigLoadConfig,
FileResponse,
@@ -9,11 +10,14 @@ import {
ScaffoldCmdConfig,
ScaffoldConfig,
ScaffoldConfigFile,
ScaffoldConfigMap,
} from "./types"
import { handlebarsParse } from "./parser"
import { log } from "./logger"
import { resolve, wrapNoopResolver } from "./utils"
import { getGitConfig } from "./git"
import { createDirIfNotExists, getUniqueTmpPath, isDir, pathExists } from "./file"
import { exec, spawn } from "node:child_process"
/** @internal */
export function getOptionValueForFile<T>(
@@ -48,57 +52,73 @@ function isWrappedWithQuotes(string: string): boolean {
}
/** @internal */
export async function parseConfigFile(config: ScaffoldCmdConfig, tmpPath: string): Promise<ScaffoldConfig> {
let output: ScaffoldConfig = config
if (config.quiet) {
config.logLevel = LogLevel.none
}
export async function getConfigFile(config: ScaffoldCmdConfig, tmpPath: string): Promise<ScaffoldConfigMap> {
if (config.git && !config.git.includes("://")) {
log(config, LogLevel.info, `Loading config from GitHub ${config.git}`)
config.git = githubPartToUrl(config.git)
}
const shouldLoadConfig = config.config || config.git
const isGit = Boolean(config.git)
const configFilename = config.config
const configPath = isGit ? config.git : configFilename
log(config, LogLevel.info, `Loading config from file ${configFilename}`)
const configPromise = await (isGit
? getRemoteConfig({ git: configPath, config: configFilename, logLevel: config.logLevel, tmpPath })
: getLocalConfig({ config: configFilename, logLevel: config.logLevel }))
// resolve the config
let configImport = await resolve(configPromise, config)
// If the config is a function or promise, return the output
if (typeof configImport.default === "function" || configImport.default instanceof Promise) {
log(config, LogLevel.debug, "Config is a function or promise, resolving...")
configImport = await resolve(configImport.default, config)
}
return configImport
}
/** @internal */
export async function parseConfigFile(config: ScaffoldCmdConfig, tmpPath: string): Promise<ScaffoldConfig> {
let output: ScaffoldConfig = { ...config, beforeWrite: undefined }
if (config.quiet) {
config.logLevel = LogLevel.none
}
const shouldLoadConfig = Boolean(config.config || config.git)
if (shouldLoadConfig) {
const isGit = Boolean(config.git)
const key = config.key ?? "default"
const configFilename = config.config
const configPath = isGit ? config.git : configFilename
log(config, LogLevel.info, `Loading config from ${configFilename} with key ${key}`)
const configPromise = await (isGit
? getRemoteConfig({ git: configPath, config: configFilename, logLevel: config.logLevel, tmpPath })
: getLocalConfig({ config: configFilename, logLevel: config.logLevel }))
// resolve the config
let configImport = await resolve(configPromise, config)
// If the config is a function or promise, return the output
if (typeof configImport.default === "function" || configImport.default instanceof Promise) {
configImport = await resolve(configImport.default, config)
}
const configImport = await getConfigFile(config, tmpPath)
if (!configImport[key]) {
throw new Error(`Template "${key}" not found in ${configFilename}`)
throw new Error(`Template "${key}" not found in ${config.config}`)
}
const importedKey = configImport[key]
const imported = configImport[key]
log(config, LogLevel.debug, "Imported result", imported)
output = {
...config,
...importedKey,
...imported,
beforeWrite: undefined,
data: {
...(importedKey as any).data,
...(imported as any).data,
...config.data,
},
}
}
output.data = { ...output.data, ...config.appendData }
delete config.appendData
const cmdBeforeWrite = config.beforeWrite ? wrapBeforeWrite(config, config.beforeWrite) : undefined
output.beforeWrite = cmdBeforeWrite ?? output.beforeWrite
if (!output.name) {
throw new Error("simple-scaffold: Missing required option: name")
}
log(output, LogLevel.debug, "Parsed config", output)
return output
}
@@ -115,8 +135,22 @@ export function githubPartToUrl(part: string): string {
export async function getLocalConfig(config: ConfigLoadConfig & Partial<LogConfig>): Promise<ScaffoldConfigFile> {
const { config: configFile, ...logConfig } = config as Required<typeof config>
log(logConfig, LogLevel.info, `Loading config from file ${configFile}`)
const absolutePath = path.resolve(process.cwd(), configFile)
const _isDir = await isDir(absolutePath)
if (_isDir) {
log(logConfig, LogLevel.debug, `Resolving config file from directory ${absolutePath}`)
const file = await findConfigFile(absolutePath)
const exists = await pathExists(file)
if (!exists) {
throw new Error(`Could not find config file in directory ${absolutePath}`)
}
log(logConfig, LogLevel.info, `Loading config from: ${path.resolve(absolutePath, file)}`)
return wrapNoopResolver(import(path.resolve(absolutePath, file)))
}
log(logConfig, LogLevel.info, `Loading config from: ${absolutePath}`)
return wrapNoopResolver(import(absolutePath))
}
@@ -138,3 +172,88 @@ export async function getRemoteConfig(
return getGitConfig(url, configFile, tmpPath, logConfig)
}
/** @internal */
export async function findConfigFile(root: string): Promise<string> {
const allowed = ["mjs", "cjs", "js", "json"].reduce((acc, ext) => {
acc.push(`scaffold.config.${ext}`)
acc.push(`scaffold.${ext}`)
return acc
}, [] as string[])
for (const file of allowed) {
const exists = await pathExists(path.resolve(root, file))
if (exists) {
return file
}
}
throw new Error(`Could not find config file in git repo`)
}
function wrapBeforeWrite(
config: LogConfig & Pick<ScaffoldConfig, "dryRun">,
beforeWrite: string,
): ScaffoldConfig["beforeWrite"] {
return async (content, rawContent, outputFile) => {
const tmpPath = path.join(getUniqueTmpPath(), path.basename(outputFile))
await createDirIfNotExists(path.dirname(tmpPath), config)
const ext = path.extname(outputFile)
const rawTmpPath = tmpPath.replace(ext, ".raw" + ext)
try {
log(config, LogLevel.debug, "Parsing beforeWrite command", beforeWrite)
let cmd = await prepareBeforeWriteCmd({ beforeWrite, tmpPath, content, rawTmpPath, rawContent })
const result = await new Promise<string | undefined>((resolve, reject) => {
log(config, LogLevel.debug, "Running parsed beforeWrite command:", cmd)
const proc = exec(cmd)
proc.stdout!.on("data", (data) => {
if (data.trim()) {
resolve(data.toString())
} else {
resolve(undefined)
}
})
proc.stderr!.on("data", (data) => {
reject(data.toString())
})
})
return result
} catch (e) {
log(config, LogLevel.debug, e)
log(config, LogLevel.warning, "Error running beforeWrite command, returning original content")
return undefined
} finally {
await fs.rm(tmpPath, { force: true })
await fs.rm(rawTmpPath, { force: true })
}
}
}
async function prepareBeforeWriteCmd({
beforeWrite,
tmpPath,
content,
rawTmpPath,
rawContent,
}: {
beforeWrite: string
tmpPath: string
content: Buffer
rawTmpPath: string
rawContent: Buffer
}): Promise<string> {
let cmd: string = ""
const pathReg = /\{\{\s*path\s*\}\}/gi
const rawPathReg = /\{\{\s*rawpath\s*\}\}/gi
if (pathReg.test(beforeWrite)) {
await fs.writeFile(tmpPath, content)
cmd = beforeWrite.replaceAll(pathReg, tmpPath)
}
if (rawPathReg.test(beforeWrite)) {
await fs.writeFile(rawTmpPath, rawContent)
cmd = beforeWrite.replaceAll(rawPathReg, rawTmpPath)
}
if (!cmd) {
await fs.writeFile(tmpPath, content)
cmd = [beforeWrite, tmpPath].join(" ")
}
return cmd
}

View File

@@ -1,7 +1,8 @@
import os from "node:os"
import path from "node:path"
import { F_OK } from "node:constants"
import { LogLevel, ScaffoldConfig } from "./types"
import fs from "node:fs/promises"
import { F_OK } from "node:constants"
import { LogConfig, LogLevel, ScaffoldConfig } from "./types"
import { glob, hasMagic } from "glob"
import { log } from "./logger"
import { getOptionValueForFile } from "./config"
@@ -10,7 +11,10 @@ import { handleErr } from "./utils"
const { stat, access, mkdir, readFile, writeFile } = fs
export async function createDirIfNotExists(dir: string, config: ScaffoldConfig): Promise<void> {
export async function createDirIfNotExists(
dir: string,
config: LogConfig & Pick<ScaffoldConfig, "dryRun">,
): Promise<void> {
if (config.dryRun) {
log(config, LogLevel.info, `Dry Run. Not creating dir ${dir}`)
return
@@ -67,11 +71,10 @@ export function getBasePath(relPath: string): string {
.replace(process.cwd(), "")
}
export async function getFileList(_config: ScaffoldConfig, template: string): Promise<string[]> {
template = template.replaceAll(/[\\]+/g, "/")
log(_config, LogLevel.debug, `Getting file list for ${template}`)
export async function getFileList(config: ScaffoldConfig, templates: string[]): Promise<string[]> {
log(config, LogLevel.debug, `Getting file list for glob list: ${templates}`)
return (
await glob(template, {
await glob(templates, {
dot: true,
nodir: true,
})
@@ -142,6 +145,7 @@ export async function copyFileTransformed(
if (exists && overwrite) {
log(config, LogLevel.info, `File ${outputPath} exists, overwriting`)
}
log(config, LogLevel.debug, `Processing file ${inputPath}`)
const templateBuffer = await readFile(inputPath)
const unprocessedOutputContents = handlebarsParse(config, templateBuffer)
const finalOutputContents =
@@ -149,7 +153,6 @@ export async function copyFileTransformed(
if (!config.dryRun) {
await writeFile(outputPath, finalOutputContents)
log(config, LogLevel.info, "Done.")
} else {
log(config, LogLevel.info, "Dry Run. Output should be:")
log(config, LogLevel.info, finalOutputContents.toString())
@@ -157,6 +160,7 @@ export async function copyFileTransformed(
} else if (exists) {
log(config, LogLevel.info, `File ${outputPath} already exists, skipping`)
}
log(config, LogLevel.info, "Done.")
}
export function getOutputDir(config: ScaffoldConfig, outputPathOpt: string, basePath: string): string {
@@ -209,3 +213,8 @@ export async function handleTemplateFile(
}
})
}
/** @internal */
export function getUniqueTmpPath(): string {
return path.resolve(os.tmpdir(), `scaffold-config-${Date.now()}-${Math.random().toString(36).slice(2)}`)
}

View File

@@ -1,9 +1,9 @@
import path from "node:path"
import fs from "node:fs/promises"
import { log } from "./logger"
import { AsyncResolver, LogConfig, LogLevel, ScaffoldCmdConfig, ScaffoldConfigMap } from "./types"
import { spawn } from "node:child_process"
import { resolve, wrapNoopResolver } from "./utils"
import { findConfigFile } from "./config"
export async function getGitConfig(
url: URL,
@@ -60,22 +60,3 @@ export async function loadGitConfig({
}
return wrapNoopResolver(fixedConfig)
}
/** @internal */
export async function findConfigFile(root: string): Promise<string> {
const allowed = ["mjs", "cjs", "js", "json"].reduce((acc, ext) => {
acc.push(`scaffold.config.${ext}`)
acc.push(`scaffold.${ext}`)
return acc
}, [] as string[])
for (const file of allowed) {
const exists = await fs
.stat(path.resolve(root, file))
.then(() => true)
.catch(() => false)
if (exists) {
return file
}
}
throw new Error(`Could not find config file in git repo`)
}

View File

@@ -1,5 +1,6 @@
import util from "util"
import { LogConfig, LogLevel, ScaffoldConfig } from "./types"
import chalk from "chalk"
import { colorize, TermColor } from "./utils"
export function log(config: LogConfig, level: LogLevel, ...obj: any[]): void {
const priority: Record<LogLevel, number> = {
@@ -14,7 +15,7 @@ export function log(config: LogConfig, level: LogLevel, ...obj: any[]): void {
return
}
const levelColor: Record<keyof typeof LogLevel, keyof typeof chalk> = {
const levelColor: Record<keyof typeof LogLevel, TermColor> = {
[LogLevel.none]: "reset",
[LogLevel.debug]: "blue",
[LogLevel.info]: "dim",
@@ -22,16 +23,16 @@ export function log(config: LogConfig, level: LogLevel, ...obj: any[]): void {
[LogLevel.error]: "red",
}
const chalkFn: any = chalk[levelColor[level]]
const colorFn = colorize[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)
? colorFn(i, JSON.stringify(i, undefined, 1), i.stack)
: typeof i === "object"
? chalkFn(JSON.stringify(i, undefined, 1))
: chalkFn(i),
? util.inspect(i, { depth: null, colors: true })
: colorFn(i),
),
)
}

View File

@@ -16,6 +16,7 @@ import {
getFileList,
getBasePath,
handleTemplateFile,
GlobInfo,
} from "./file"
import { LogLevel, MinimalConfig, Resolver, ScaffoldCmdConfig, ScaffoldConfig } from "./types"
import { registerHelpers } from "./parser"
@@ -61,39 +62,45 @@ export async function Scaffold(config: ScaffoldConfig): Promise<void> {
try {
config.data = { name: config.name, ...config.data }
logInitStep(config)
for (let _template of config.templates) {
const excludes = config.templates.filter((t) => t.startsWith("!"))
const includes = config.templates.filter((t) => !t.startsWith("!"))
const templates: GlobInfo[] = []
for (let _template of includes) {
try {
const { nonGlobTemplate, origTemplate, isDirOrGlob, isGlob, template } = await getTemplateGlobInfo(
config,
_template,
)
const files = await getFileList(config, template)
log(config, LogLevel.debug, "Iterating files", { files, template })
for (const inputFilePath of files) {
if (await isDir(inputFilePath)) {
continue
}
const relPath = makeRelativePath(path.dirname(removeGlob(inputFilePath).replace(nonGlobTemplate, "")))
const basePath = getBasePath(relPath)
logInputFile(config, {
originalTemplate: origTemplate,
relativePath: relPath,
parsedTemplate: template,
inputFilePath,
nonGlobTemplate,
basePath,
isDirOrGlob,
isGlob,
})
await handleTemplateFile(config, {
templatePath: inputFilePath,
basePath,
})
}
templates.push({ nonGlobTemplate, origTemplate, isDirOrGlob, isGlob, template })
} catch (e: any) {
handleErr(e)
}
}
for (const tpl of templates) {
const files = await getFileList(config, [tpl.template, ...excludes])
for (const file of files) {
if (await isDir(file)) {
continue
}
log(config, LogLevel.debug, "Iterating files", { files, file })
const relPath = makeRelativePath(path.dirname(removeGlob(file).replace(tpl.nonGlobTemplate, "")))
const basePath = getBasePath(relPath)
logInputFile(config, {
originalTemplate: tpl.origTemplate,
relativePath: relPath,
parsedTemplate: tpl.template,
inputFilePath: file,
nonGlobTemplate: tpl.nonGlobTemplate,
basePath,
isDirOrGlob: tpl.isDirOrGlob,
isGlob: tpl.isGlob,
})
await handleTemplateFile(config, {
templatePath: file,
basePath,
})
}
}
} catch (e: any) {
log(config, LogLevel.error, e)
throw e
@@ -111,7 +118,7 @@ export async function Scaffold(config: ScaffoldConfig): Promise<void> {
* @category Main
* @return {Promise<void>} A promise that resolves when the scaffold is complete
*/
Scaffold.fromConfig = async function (
Scaffold.fromConfig = async function(
/** The path or URL to the config file */
pathOrUrl: string,
/** Information needed before loading the config */

View File

@@ -22,12 +22,17 @@ 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.
*
* You may omit files from output by prepending a `!` to their glob pattern.
*
* For example, `["components/**", "!components/README.md"]` will include everything in the directory `components`
* except the `README.md` file inside.
*
* @default Current working directory
*/
templates: string[]
/**
* Path to output to. If `subdir` is `true`, the subfolder will be created inside this path.
* Path to output to. If `subdir` is `true`, the subdir will be created inside this path.
*
* May also be a {@link FileResponseHandler} which returns a new output path to override the default one.
*
@@ -37,7 +42,7 @@ export interface ScaffoldConfig {
output: FileResponse<string>
/**
* Whether to create subfolder with the input name.
* Whether to create subdir with the input name.
*
* When `true`, you may also use {@link subdirHelper} to determine a pre-process helper on
* the directory name.
@@ -131,7 +136,7 @@ export interface ScaffoldConfig {
helpers?: Record<string, Helper>
/**
* Default transformer to apply to subfolder name when using `subdir: true`. Can be one of the default
* Default transformer to apply to subdir name when using `subdir: true`. Can be one of the default
* capitalization helpers, or a custom one you provide to `helpers`. Defaults to `undefined`, which means no
* transformation is done.
*
@@ -165,7 +170,7 @@ export interface ScaffoldConfig {
/**
* The names of the available helper functions that relate to text capitalization.
*
* These are available for `subfolderNameHelper`.
* These are available for `subdirHelper`.
*
* | Helper name | Example code | Example output |
* | ------------ | ----------------------- | -------------- |
@@ -331,7 +336,9 @@ export type FileResponse<T> = T | FileResponseHandler<T>
/**
* The Scaffold config for CLI
* Contains less and more specific options than {@link ScaffoldConfig}
* Contains less and more specific options than {@link ScaffoldConfig}.
*
* For more information about each option, see {@link ScaffoldConfig}.
*/
export type ScaffoldCmdConfig = {
/** The name of the scaffold template to use. */
@@ -340,9 +347,9 @@ export type ScaffoldCmdConfig = {
templates: string[]
/** The output path to write to */
output: string
/** Whether to create subfolder with the input name */
/** Whether to create subdir with the input name */
subdir: boolean
/** Default transformer to apply to subfolder name when using `subdir: true` */
/** Default transformer to apply to subdir name when using `subdir: true` */
subdirHelper?: string
/** Add custom data to the templates */
data?: Record<string, string>
@@ -368,6 +375,8 @@ export type ScaffoldCmdConfig = {
git?: string
/** Display version */
version: boolean
/** Run a script before writing the files. This can be a command or a path to a file. The file contents will be passed to the given command. */
beforeWrite?: string
}
/**
@@ -413,3 +422,5 @@ export type RemoteConfigLoadConfig = LogConfig & Pick<ScaffoldCmdConfig, "config
/** @internal */
export type MinimalConfig = Pick<ScaffoldCmdConfig, "name" | "key">
export type ListCommandCliOptions = Pick<ScaffoldCmdConfig, "config" | "git" | "logLevel" | "quiet">

View File

@@ -15,3 +15,65 @@ export function wrapNoopResolver<T, R = T>(value: Resolver<T, R>): Resolver<T, R
return (_) => value
}
const colorMap = {
reset: 0,
dim: 2,
bold: 1,
italic: 3,
underline: 4,
red: 31,
green: 32,
yellow: 33,
blue: 34,
magenta: 35,
cyan: 36,
white: 37,
gray: 90,
} as const
export type TermColor = keyof typeof colorMap
function _colorize(text: string, color: TermColor): string {
const c = colorMap[color]!
let r = 0
if (c > 1 && c < 30) {
r = c + 20
} else if (c === 1) {
r = 23
} else {
r = 0
}
return `\x1b[${c}m${text}\x1b[${r}m`
}
function isTemplateStringArray(template: TemplateStringsArray | unknown): template is TemplateStringsArray {
return Array.isArray(template) && typeof template[0] === "string"
}
const createColorize =
(color: TermColor) =>
(template: TemplateStringsArray | unknown, ...params: unknown[]): string => {
return isTemplateStringArray(template)
? _colorize(
(template as TemplateStringsArray).reduce((acc, str, i) => acc + str + (params[i] ?? ""), ""),
color,
)
: _colorize(String(template), color)
}
type TemplateStringsFn = ReturnType<typeof createColorize> & ((text: string) => string)
type TemplateStringsFns = { [key in TermColor]: TemplateStringsFn }
export const colorize: typeof _colorize & TemplateStringsFns = Object.assign(
_colorize,
Object.entries(colorMap).reduce(
(acc, [key]) => {
acc[key as TermColor] = createColorize(key as TermColor)
return acc
},
{} as Record<TermColor, TemplateStringsFn>,
),
)

View File

@@ -6,7 +6,7 @@ import * as config from "../src/config"
import { resolve } from "../src/utils"
// @ts-ignore
import * as configFile from "../scaffold.config"
import { findConfigFile } from "../src/git"
import { findConfigFile } from "../src/config"
jest.mock("../src/git", () => {
return {
@@ -72,16 +72,18 @@ describe("config", () => {
await parseConfigFile(
{
...blankCliConf,
name: "-",
},
`/tmp/scaffold-config-${Date.now()}`,
),
).toEqual(blankCliConf)
).toEqual({ ...blankCliConf, name: "-" })
})
describe("appendData", () => {
test("appends", async () => {
const result = await parseConfigFile(
{
...blankCliConf,
name: "-",
appendData: { key: "value" },
},
`/tmp/scaffold-config-${Date.now()}`,
@@ -92,6 +94,7 @@ describe("config", () => {
const result = await parseConfigFile(
{
...blankCliConf,
name: "-",
data: { num: "123" },
appendData: { num: "1234" },
},

View File

@@ -71,6 +71,14 @@ const fileStructDates = {
output: {},
}
const fileStructExcludes = {
input: {
"include.txt": "This file should be included",
"exclude.txt": "This file should be excluded",
},
output: {},
}
function withMock(fileStruct: FileSystem.DirectoryItems, testFn: jest.EmptyFunction): jest.EmptyFunction {
return () => {
beforeEach(() => {
@@ -92,7 +100,7 @@ function withMock(fileStruct: FileSystem.DirectoryItems, testFn: jest.EmptyFunct
describe("Scaffold", () => {
describe(
"create subfolder",
"create subdir",
withMock(fileStructNormal, () => {
test("should not create by default", async () => {
await Scaffold({
@@ -268,8 +276,7 @@ describe("Scaffold", () => {
}),
)
describe(
"output structure",
describe("output structure", () => {
withMock(fileStructNested, () => {
test("should maintain input structure on output", async () => {
await Scaffold({
@@ -294,8 +301,23 @@ describe("Scaffold", () => {
expect(oneDeepFile.toString()).toEqual("Hello, my value is 1")
expect(twoDeepFile.toString()).toEqual("Hi! My value is actually NOT 1!")
})
}),
)
})
withMock(fileStructExcludes, () => {
test("should exclude files", async () => {
await Scaffold({
name: "app_name",
output: "output",
templates: ["input", "!exclude.txt"],
data: { value: "1" },
logLevel: "none",
})
const includeFile = readFileSync(join(process.cwd(), "output", "app_name.txt"))
expect(includeFile.toString()).toEqual("This file should be included")
expect(() => readFileSync(join(process.cwd(), "output", "exclude.txt"))).toThrow()
})
})
})
describe(
"capitalization helpers",
@@ -395,7 +417,7 @@ describe("Scaffold", () => {
}),
)
describe(
"transform subfolder",
"transform subdir",
withMock(fileStructSubdirTransformer, () => {
test("should work with no helper", async () => {
await Scaffold({

View File

@@ -1,5 +1,4 @@
import { handleErr, resolve } from "../src/utils"
import { handleErr, resolve, colorize, TermColor } from "../src/utils"
describe("utils", () => {
describe("resolve", () => {
test("should resolve function", () => {
@@ -19,3 +18,51 @@ describe("utils", () => {
})
})
})
describe("colorize", () => {
it("should colorize text with red color", () => {
const result = colorize("Hello", "red")
expect(result).toBe("\x1b[31mHello\x1b[0m")
})
it("should colorize text with bold", () => {
const result = colorize("Hello", "bold")
expect(result).toBe("\x1b[1mHello\x1b[23m")
})
it("should reset color", () => {
const result = colorize("Hello", "reset")
expect(result).toBe("\x1b[0mHello\x1b[0m")
})
it("should have all color functions", () => {
const colors: TermColor[] = [
"reset",
"dim",
"bold",
"italic",
"underline",
"red",
"green",
"yellow",
"blue",
"magenta",
"cyan",
"white",
"gray",
]
colors.forEach((color) => {
expect(typeof colorize[color]).toBe("function")
})
})
it("should colorize text using colorize.red", () => {
const result = colorize.red("Hello")
expect(result).toBe("\x1b[31mHello\x1b[0m")
})
it("should colorize text using template strings with colorize.blue", () => {
const result = colorize.blue`Hello ${"World"}`
expect(result).toBe("\x1b[34mHello World\x1b[0m")
})
})

View File

@@ -1,20 +1,29 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
"target": "ES2022",
"target": "ESNext",
"module": "commonjs",
"moduleResolution": "node",
"esModuleInterop": true,
"lib": ["ES2022"],
"lib": [
"ESNext"
],
"declaration": true,
"outDir": "dist",
"strict": true,
"sourceMap": true,
"removeComments": false,
"paths": {
"@/*": ["./src/*"],
"@/*": [
"./src/*"
],
},
},
"include": ["src/index.ts", "src/cmd.ts"],
"exclude": ["tests/*"],
"include": [
"src/index.ts",
"src/cmd.ts"
],
"exclude": [
"tests/*"
],
}